# pyright: reportAttributeAccessIssue=false, reportArgumentType=false

from __future__ import annotations

import ast
import json
from pathlib import Path

HERE = Path(__file__).parent
CORE = HERE.parent.parent
CORE_SRC = CORE / "src"
KEY_PREFIX = ""

MAPPING_FILE = HERE / "mapping_upy.json"
IGNORE_FILE = HERE / "ignore_upy.json"

if IGNORE_FILE.exists():
    content = json.loads(IGNORE_FILE.read_text())
    IGNORE_SET: set[str] = set(content.keys())
else:
    IGNORE_SET = set()


def find_all_strings(filename: str | Path) -> list[str]:
    with open(filename, "r") as file:
        file_content = file.read()

    tree = ast.parse(file_content)
    strings: list[str] = []

    class StringVisitor(ast.NodeVisitor):

        def visit_Constant(self, node: ast.Constant) -> None:
            if isinstance(node.value, str):
                strings.append(node.value)

        def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
            for value in node.values:
                if isinstance(value, ast.Constant) and isinstance(value.value, str):
                    strings.append(value.value)

    visitor = StringVisitor()
    visitor.visit(tree)

    return strings


def find_strings_to_ignore(filename: str | Path) -> list[str]:
    with open(filename, "r") as file:
        file_content = file.read()

    tree = ast.parse(file_content)
    strings: list[str] = []

    def ignore_func(func_name: str) -> bool:
        if not func_name:
            return True
        substrs = ["Error", "Exception", "wire.", "log.", "ensure", "mem_trace"]
        if any(substr in func_name for substr in substrs):
            return True
        if func_name in (
            "_log",
            "info",
            "warning",
            "debug",
            "Success",
            "SdCardUnavailable",
            "NotInitialized",
            "ActionCancelled",
            "UnexpectedMessage",
            "NotEnoughFunds",
            "Failure",
            "PinCancelled",
            "TypeVar",
            "getattr",
            "_validate_public_key",
            "check_mem",
            "halt",
            "pack",
            "mem_trace",
        ):
            return True
        return False

    def get_final_attribute_name(node: ast.AST) -> str:
        """Recursively extracts the final attribute name from a nested attribute expression."""
        if isinstance(node, ast.Name):
            return node.id
        elif isinstance(node, ast.Attribute):
            return get_final_attribute_name(node.value) + "." + node.attr
        return ""

    def include_all_strings(arg: ast.expr) -> None:
        if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
            strings.append(arg.value)
        elif isinstance(arg, ast.JoinedStr):
            for value in arg.values:
                if isinstance(value, ast.Constant) and isinstance(value.value, str):
                    strings.append(value.value)
                elif isinstance(value, ast.FormattedValue):
                    # This part is an expression inside an f-string
                    expr_as_str = ast.dump(value.value, annotate_fields=False)
                    strings.append(expr_as_str)

    class IgnoreStringVisitor(ast.NodeVisitor):

        def visit_Call(self, node: ast.Call) -> None:
            func_name = get_final_attribute_name(node.func)
            if ignore_func(func_name):
                for arg in node.args + [kw.value for kw in node.keywords]:
                    include_all_strings(arg)
            # Continue visiting the children of this node (!!!Necessary!!!)
            self.generic_visit(node)

        def visit_Assert(self, node: ast.Assert) -> None:
            error_message = node.msg
            if error_message:
                include_all_strings(error_message)
            self.generic_visit(node)

        def visit_Assign(self, node: ast.Assign) -> None:
            ignore_variables = [
                "msg_wire",
                "msg_type",
            ]
            for target in node.targets:
                if isinstance(target, ast.Name) and target.id in ignore_variables:
                    value = node.value
                    include_all_strings(value)
            self.generic_visit(node)

        def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
            for arg in node.args.args:
                annotation = arg.annotation
                if annotation:
                    include_all_strings(annotation)
            return_annotation = node.returns
            if return_annotation:
                include_all_strings(return_annotation)
            self.generic_visit(node)

        def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
            for arg in node.args.args:
                annotation = arg.annotation
                if annotation:
                    include_all_strings(annotation)
            return_annotation = node.returns
            if return_annotation:
                include_all_strings(return_annotation)
            self.generic_visit(node)

        def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
            annotation = node.annotation
            include_all_strings(annotation)
            self.generic_visit(node)

    visitor = IgnoreStringVisitor()
    visitor.visit(tree)

    # get all the top-level string comments
    for node in tree.body:
        if isinstance(node, ast.Expr) and isinstance(
            node.value, (ast.Str, ast.JoinedStr)
        ):
            strings.append(node.value.s)

    return strings


def find_docstrings(filename: str | Path) -> list[str]:
    with open(filename, "r") as file:
        file_content = file.read()

    tree = ast.parse(file_content)

    functions = [
        f
        for f in ast.walk(tree)
        if isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef))
    ]
    function_docs = [ast.get_docstring(f) for f in functions]

    classes = [c for c in ast.walk(tree) if isinstance(c, ast.ClassDef)]
    class_docs = [ast.get_docstring(c) for c in classes]

    all_docstrings = function_docs + class_docs

    module_docstring = ast.get_docstring(tree)
    if module_docstring:
        all_docstrings.append(module_docstring)

    return [doc for doc in all_docstrings if doc]


def check_file(file: str | Path) -> list[str]:
    all_strings = find_all_strings(file)

    def is_docstring(string: str) -> bool:
        return "\n " in string or (string.startswith("\n") and string.endswith("\n"))

    all_strings = [string for string in all_strings if not is_docstring(string)]
    ignore_strings = find_strings_to_ignore(file)
    docstrings = find_docstrings(file)

    # Remove duplicates
    all_strings = list(set(all_strings))
    ignore_strings = set(ignore_strings)
    docstrings = set(docstrings)

    to_ignore = ignore_strings | docstrings

    # Remove strings that are passed to error and other non-translatable functions
    return [s for s in all_strings if s not in to_ignore]


def check_file_report(file: str | Path) -> None:
    all_files = {str(file): check_file(file)}
    report_all_files(all_files)


def check_folder_resursive_report(
    folder: str | Path, ignore_files: list[str] | None = None
) -> None:
    if ignore_files is None:
        ignore_files = []
    all_files: dict[str, list[str]] = {}
    for file in Path(folder).rglob("*.py"):
        if file.name in ignore_files:
            continue
        file_strings = check_file(file)
        all_files[str(file)] = file_strings
    report_all_files(all_files)


def report_all_files(all_files: dict[str, list[str]]) -> None:
    str_mapping: dict[str, str] = {}
    for _file, strings in all_files.items():
        for string in strings:
            if "_" in string:
                continue
            str_id = (
                string.lower()
                .strip()
                .replace(" ", "_")
                .replace("-", "_")
                .replace(":", "")
                .replace("?", "")
            )
            if KEY_PREFIX:
                str_id = f"{KEY_PREFIX}__{str_id}"
            if str_id in IGNORE_SET:
                continue
            str_mapping[str_id] = string
    MAPPING_FILE.write_text(json.dumps(str_mapping, indent=4))


if __name__ == "__main__":
    ignore_files = [
        "coininfo.py",
        "nem_mosaics.py",
        "knownapps.py",
        "networks.py",
        "tokens.py",
        "workflow_handlers.py",
        "messages.py",
        "errors.py",
    ]

    # folder = CORE_SRC / "apps"
    # folder = CORE_SRC / "trezor"
    folder = CORE_SRC
    check_folder_resursive_report(folder, ignore_files=ignore_files)

    # file = CORE_SRC / "trezor/ui/layouts/tt_v2/reset.py"
    # KEY_PREFIX = "TR.reset"
    # check_file_report(file)
