#!/usr/bin/env python3

import ast
import json
from pathlib import Path
from typing import Dict, Set


BASE_DIR = Path(__file__).resolve().parent / "bouquin"
LOCALES_DIR = BASE_DIR / "locales"

DEFAULT_LOCALE = "en"


def load_json_keys(locale: str = DEFAULT_LOCALE) -> Set[str]:
    """Load all keys from the given locale JSON file."""
    path = LOCALES_DIR / f"{locale}.json"
    with path.open(encoding="utf-8") as f:
        data = json.load(f)
    return set(data.keys())


class KeyParamFinder(ast.NodeVisitor):
    """
    First pass:
    For each function/method, figure out which parameters are later passed
    into _(), translated(), or strings._().

    Example: in your _prompt_name, it discovers that title_key and label_key
    are translation-key parameters.
    """

    def __init__(self) -> None:
        # func_name -> {"param_positions": {param: arg_index}, "key_param_positions": set[arg_index]}
        self.func_info: Dict[str, dict] = {}
        self.current_func_name_stack: list[str] = []
        self.current_param_positions_stack: list[Dict[str, int]] = []
        self.current_class_stack: list[str] = []

    # Track when we're inside a class so we can treat "self" specially
    def visit_ClassDef(self, node: ast.ClassDef) -> None:
        self.current_class_stack.append(node.name)
        self.generic_visit(node)
        self.current_class_stack.pop()

    def _enter_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
        funcname = node.name
        params = [arg.arg for arg in node.args.args]

        # If we're inside a class and there is at least one param,
        # assume the first one is "self"/"cls" and is implicit at call sites.
        is_method = bool(self.current_class_stack) and len(params) > 0

        param_positions: Dict[str, int] = {}
        for i, name in enumerate(params):
            if is_method and i == 0:
                # skip self/cls; it doesn't correspond to an explicit arg in calls like self.method(...)
                continue
            call_index = i - 1 if is_method else i
            param_positions[name] = call_index

        self.current_func_name_stack.append(funcname)
        self.current_param_positions_stack.append(param_positions)

        self.func_info.setdefault(
            funcname,
            {
                "param_positions": param_positions,
                "key_param_positions": set(),
            },
        )
        # If the function name is reused, last definition wins
        self.func_info[funcname]["param_positions"] = param_positions

    def _exit_function(self) -> None:
        self.current_func_name_stack.pop()
        self.current_param_positions_stack.pop()

    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
        self._enter_function(node)
        self.generic_visit(node)
        self._exit_function()

    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
        self._enter_function(node)
        self.generic_visit(node)
        self._exit_function()

    def visit_Call(self, node: ast.Call) -> None:
        # Only care about calls *inside* functions
        if not self.current_func_name_stack:
            return self.generic_visit(node)

        func = node.func
        func_name: str | None = None

        if isinstance(func, ast.Name):
            func_name = func.id
        elif isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
            # e.g. strings._(...)
            func_name = f"{func.value.id}.{func.attr}"

        # Is this a translation call?
        if func_name in {"_", "translated", "strings._"}:
            cur_name = self.current_func_name_stack[-1]
            param_positions = self.current_param_positions_stack[-1]

            # Positional first arg
            if node.args:
                first = node.args[0]
                if isinstance(first, ast.Name):
                    pname = first.id
                    if pname in param_positions:
                        idx = param_positions[pname]
                        self.func_info[cur_name]["key_param_positions"].add(idx)

            # Keyword args, e.g. strings._(key=title_key)
            for kw in node.keywords or []:
                if isinstance(kw.value, ast.Name):
                    pname = kw.value.id
                    if pname in param_positions:
                        idx = param_positions[pname]
                        self.func_info[cur_name]["key_param_positions"].add(idx)

        self.generic_visit(node)


class UsedKeyCollector(ast.NodeVisitor):
    """
    Second pass:
    - Collect string literals passed directly to _()/translated()/strings._()
    - Collect string literals passed into parameters that we know are
      "translation-key parameters" of wrapper functions/methods.
    """

    def __init__(self, func_info: Dict[str, dict]) -> None:
        self.func_info = func_info
        self.used_keys: Set[str] = set()

    def visit_Call(self, node: ast.Call) -> None:
        func = node.func

        def full_name(f: ast.expr) -> str | None:
            if isinstance(f, ast.Name):
                return f.id
            if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name):
                return f"{f.value.id}.{f.attr}"
            return None

        func_full = full_name(func)

        # 1) Direct translation calls like _("key") or strings._("key")
        if func_full in {"_", "translated", "strings._"}:
            if node.args:
                first = node.args[0]
                if isinstance(first, ast.Constant) and isinstance(first.value, str):
                    self.used_keys.add(first.value)
            for kw in node.keywords or []:
                if isinstance(kw.value, ast.Constant) and isinstance(
                    kw.value.value, str
                ):
                    self.used_keys.add(kw.value.value)

        # 2) Wrapper calls: functions whose params we know are translation-key params
        called_base_name: str | None = None
        if isinstance(func, ast.Name):
            called_base_name = func.id
        elif isinstance(func, ast.Attribute):
            called_base_name = func.attr  # e.g. self._prompt_name -> "_prompt_name"

        if called_base_name in self.func_info:
            info = self.func_info[called_base_name]
            param_positions: Dict[str, int] = info["param_positions"]
            key_positions: Set[int] = info["key_param_positions"]

            # positional args
            for idx, arg in enumerate(node.args):
                if (
                    idx in key_positions
                    and isinstance(arg, ast.Constant)
                    and isinstance(arg.value, str)
                ):
                    self.used_keys.add(arg.value)

            # keyword args
            for kw in node.keywords or []:
                if kw.arg is None:
                    continue  # **kwargs, ignore
                param_name = kw.arg
                if param_name in param_positions:
                    idx = param_positions[param_name]
                    if idx in key_positions:
                        val = kw.value
                        if isinstance(val, ast.Constant) and isinstance(val.value, str):
                            self.used_keys.add(val.value)

        self.generic_visit(node)


def collect_used_keys() -> Set[str]:
    """Parse all .py files and collect all translation keys used."""
    trees: list[ast.AST] = []

    # Read and parse all Python files in this folder
    for path in BASE_DIR.glob("*.py"):
        # Optionally skip this script itself
        if path.name == Path(__file__).name:
            continue
        src = path.read_text(encoding="utf-8")
        tree = ast.parse(src, filename=str(path))
        trees.append(tree)

    # First pass: find which parameters are translation-key params
    finder = KeyParamFinder()
    for tree in trees:
        finder.visit(tree)

    # Second pass: collect string literals passed to those parameters
    collector = UsedKeyCollector(finder.func_info)
    for tree in trees:
        collector.visit(tree)

    return collector.used_keys


def main() -> None:
    json_keys = load_json_keys()
    used_keys = collect_used_keys()

    unused_keys = sorted(json_keys - used_keys)
    missing_in_json = sorted(used_keys - json_keys)

    print("=== Unused keys in JSON (present in locales but never used in code) ===")
    if unused_keys:
        for k in unused_keys:
            print("  ", k)
    else:
        print("  (none)")

    print("\n=== Keys used in code but missing from JSON ===")
    if missing_in_json:
        for k in missing_in_json:
            print("  ", k)
    else:
        print("  (none)")


if __name__ == "__main__":
    main()
