"""Translation marker management for Custom Fields.

Custom Field labels and descriptions defined in `custom_fields.py` are bare
Python strings inside `dict(label='...', description='...')` literals. Frappe's
gettext extractor (`bench generate-pot-file`) scans .py files only for
`__()`/`_()`/`_lt()` calls — it cannot see bare strings inside dict literals.
Result: those labels never make it into `jey_erp.pot`, then into `.po`, then
into runtime translations — even though Frappe DOES wrap them in `__()` when
rendering Custom Field labels at form render time.

This module ASTs `custom_fields.py`, harvests every string value of the
`label` and `description` keyword arguments, and writes them out to
`translation_markers.py` as `_lt(...)` calls. `_lt` is a lazy translation
proxy — never rendered, never called for real — but the gettext extractor
recognizes it the same as `_`/`__`, so the strings show up in the POT.

Triggered automatically at the top of `after_migrate_combined()` in `hooks.py`,
or manually via:

    bench execute jey_erp.translate.sync_translation_markers
"""

import ast
import json
import os


SOURCE_FILE = "custom_fields.py"
MARKER_FILE = "translation_markers.py"
EXTRACTED_KWARGS = ("label", "description")

_HEADER = '''\
# AUTO-GENERATED FILE — DO NOT EDIT BY HAND.
#
# Regenerated from {source} by jey_erp.translate.sync_translation_markers,
# invoked at the top of the after_migrate hook in hooks.py.
#
# Purpose: expose Custom Field labels/descriptions to bench generate-pot-file.
# The extractor scans .py files for _lt()/_()/__() calls only; it cannot see
# bare strings inside dict(label='...') literals. _lt() is a lazy translation
# proxy with no runtime effect — these calls exist purely so gettext picks
# the strings up.

from frappe import _lt

'''


def sync_translation_markers():
    """Regenerate translation_markers.py from labels in custom_fields.py.

    Idempotent: skips writing if the regenerated content matches what's already
    on disk. Safe to call repeatedly.
    """
    app_dir = os.path.dirname(os.path.abspath(__file__))
    source_path = os.path.join(app_dir, SOURCE_FILE)
    marker_path = os.path.join(app_dir, MARKER_FILE)

    if not os.path.isfile(source_path):
        print(f"[jey_erp] sync_translation_markers: {source_path} not found, skipping")
        return

    try:
        with open(source_path, encoding="utf-8") as f:
            tree = ast.parse(f.read(), filename=SOURCE_FILE)
    except SyntaxError as e:
        print(f"[jey_erp] sync_translation_markers: failed to parse {SOURCE_FILE}: {e}")
        return

    labels = _extract_strings(tree)
    new_content = _render(labels)

    if os.path.exists(marker_path):
        try:
            with open(marker_path, encoding="utf-8") as f:
                if f.read() == new_content:
                    print(
                        f"[jey_erp] sync_translation_markers: {MARKER_FILE} up to date "
                        f"({len(labels)} markers)"
                    )
                    return
        except OSError as e:
            print(f"[jey_erp] sync_translation_markers: could not read existing {MARKER_FILE}: {e}")

    try:
        with open(marker_path, "w", encoding="utf-8") as f:
            f.write(new_content)
    except OSError as e:
        print(f"[jey_erp] sync_translation_markers: failed to write {MARKER_FILE}: {e}")
        return

    print(
        f"[jey_erp] sync_translation_markers: regenerated {MARKER_FILE} "
        f"({len(labels)} markers)"
    )


def _extract_strings(tree):
    """Walk the AST and harvest string values for `label=` / `description=`.

    Handles both `dict(label='X', ...)` constructor form and `{'label': 'X', ...}`
    literal form. Skips non-string values (e.g. f-strings, variables).
    """
    found = set()

    for node in ast.walk(tree):
        # dict(label='X', description='Y', ...)
        if (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id == "dict"
        ):
            for kw in node.keywords:
                if kw.arg in EXTRACTED_KWARGS and _is_str_constant(kw.value):
                    val = kw.value.value.strip()
                    if val:
                        found.add(val)

        # {"label": "X", "description": "Y", ...}
        elif isinstance(node, ast.Dict):
            for key, value in zip(node.keys, node.values):
                if (
                    isinstance(key, ast.Constant)
                    and isinstance(key.value, str)
                    and key.value in EXTRACTED_KWARGS
                    and _is_str_constant(value)
                ):
                    val = value.value.strip()
                    if val:
                        found.add(val)

    return sorted(found)


def _is_str_constant(node):
    return isinstance(node, ast.Constant) and isinstance(node.value, str)


def _render(labels):
    lines = [_HEADER.format(source=SOURCE_FILE)]
    for label in labels:
        # json.dumps produces a valid Python string literal: handles quotes,
        # backslashes, and newlines correctly. ensure_ascii=False keeps
        # Cyrillic / Azerbaijani / etc. readable in the generated file.
        lines.append(f"_lt({json.dumps(label, ensure_ascii=False)})")
    lines.append("")
    return "\n".join(lines)
