#!/usr/bin/env python3
"""Find the localization keys a built screen references and sort them into categories.

This is a pattern match over source text; it changes nothing. A key written
out literally is "static", unless its line looks like an alert, validation or
error path, in which case it is "error". A key assembled at runtime, which no
static cross-check can resolve, is "dynamic". Swift, Kotlin, Android layout
XML and web t() / $t() calls are understood.
"""
import argparse
import json
import os
import re
import sys

from _shared import (
    ACCESSOR_EXTENSIONS, DEFAULT_KEY_TYPES, IDENT, WEB_DOLLAR_T, WEB_T, WORD, is_interpolated, key_type_alternation,
    key_type_warning, key_types, lsk_keys, source_files,
)

ANDROID = {".kt", ".kts", ".xml"}
WEB = {".ts", ".tsx", ".js", ".jsx", ".vue"}

ERROR_WORDS = ("toast", "snackbar", "alert", "warning", "failure", "exception", "showerror", "errormessage", "error", "validat")
ERROR_CONTEXT = re.compile("|".join(ERROR_WORDS), re.IGNORECASE)

LITERAL_LOCALIZED = re.compile(r"\"([^\"\\\n]+)\"\.localized\b")
R_STRING = re.compile(rf"\bR\.string\.([{WORD}]+)")
XML_STRING = re.compile(rf"@string/([{WORD}.]+)")
WEB_T_DYNAMIC = re.compile(r"(?<![\w$])t\(\s*[`$A-Za-z_]")
WEB_DOLLAR_T_DYNAMIC = re.compile(r"\$t\(\s*[`$A-Za-z_]")
FORMAT_DYNAMIC = re.compile(r"String\(format:")
GETSTRING_DYNAMIC = re.compile(r"\bgetString\(\s*[A-Za-z_]")
INTERPOLATED_LOCALIZED = re.compile(r"\"([^\"\n]*(?:\\\(|\$\{|\$[A-Za-z_])[^\"\n]*)\"\.localized\b")
RECEIVER_LOCALIZED = re.compile(rf"(?<![\w.\"])({IDENT}(?:\.{IDENT})*)\.localized\b")


def platform_of(path):
    ext = os.path.splitext(path)[1]
    if ext in ANDROID:
        return "android"
    if ext in WEB:
        return "web"
    return "ios"


def dynamic_key_pattern(types):
    return re.compile(rf"({key_type_alternation(types)})\(")


def hits_in_line(line, ext, types=DEFAULT_KEY_TYPES):
    static, dynamic = [], []
    if ext == ".xml":
        static.extend(m.group(1) for m in XML_STRING.finditer(line))
        return static, dynamic
    if ext in WEB:
        static.extend(m.group(2) for m in WEB_T.finditer(line))
        static.extend(m.group(2) for m in WEB_DOLLAR_T.finditer(line))
        if WEB_T_DYNAMIC.search(line):
            dynamic.append("(dynamic: t(...))")
        if WEB_DOLLAR_T_DYNAMIC.search(line):
            dynamic.append("(dynamic: $t(...))")
        return static, dynamic
    static.extend(lsk_keys(line, types))
    static.extend(m.group(1) for m in LITERAL_LOCALIZED.finditer(line) if not is_interpolated(m.group(1)))
    static.extend(m.group(1) for m in R_STRING.finditer(line))
    constructed = dynamic_key_pattern(types).search(line)
    if constructed:
        dynamic.append(f"(dynamic: {constructed.group(1)}(...))")
    mentions_key = any(marker in line for marker in (".localized", *types))
    if mentions_key and FORMAT_DYNAMIC.search(line):
        dynamic.append("(dynamic: String(format:...))")
    if "R.string." not in line and GETSTRING_DYNAMIC.search(line):
        dynamic.append("(dynamic: getString(var))")
    for match in INTERPOLATED_LOCALIZED.finditer(line):
        dynamic.append(f'(dynamic: "{match.group(1)}".localized)')
    for match in RECEIVER_LOCALIZED.finditer(line):
        receiver = match.group(1)
        if not receiver.startswith(tuple(types)):
            dynamic.append(f"(dynamic: {receiver}.localized)")
    return static, dynamic


def scan(root, types=DEFAULT_KEY_TYPES):
    keys = []
    seen = set()
    accessor_texts = []
    scanned = 0
    for path in source_files(root):
        try:
            with open(path, encoding="utf-8") as stream:
                text = stream.read()
        except (OSError, UnicodeDecodeError):
            continue
        lines = text.splitlines()
        scanned += 1
        ext = os.path.splitext(path)[1]
        if ext in ACCESSOR_EXTENSIONS:
            accessor_texts.append(text)
        platform = platform_of(path)
        for number, line in enumerate(lines, 1):
            static, dynamic = hits_in_line(line, ext, types)
            in_error = bool(ERROR_CONTEXT.search(line))
            found = [(k, "error" if in_error else "static") for k in static] + [(k, "dynamic") for k in dynamic]
            for key, category in found:
                marker = (key, category, path, number)
                if marker in seen:
                    continue
                seen.add(marker)
                keys.append({
                    "key": key,
                    "category": category,
                    "platform": platform,
                    "file": path,
                    "line": number,
                    "snippet": line.strip()[:200],
                })
    return scanned, keys, key_type_warning(accessor_texts, types)


CATEGORIES = ("static", "error", "dynamic")


def main():
    cli = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    cli.add_argument("--screen-path", required=True, help="folder or single file of the built screen")
    cli.add_argument("--out", help="JSON destination; stdout when omitted")
    cli.add_argument("--category", default="all", choices=["all", "error", "dynamic", "static"])
    cli.add_argument(
        "--key-type", action="append", metavar="NAME",
        help="type whose member chains are keys, e.g. the project's generated key enum; repeatable or comma list "
             f"(default {', '.join(DEFAULT_KEY_TYPES)})",
    )
    opts = cli.parse_args()

    if not os.path.exists(opts.screen_path):
        cli.error(f"--screen-path not found: {opts.screen_path}")
    try:
        types = key_types(opts.key_type)
    except ValueError as exc:
        cli.error(str(exc))

    scanned, keys, type_warning = scan(opts.screen_path, types)
    if opts.category != "all":
        keys = [entry for entry in keys if entry["category"] == opts.category]
    keys.sort(key=lambda entry: (entry["category"], entry["file"], entry["line"]))
    per_category = [entry["category"] for entry in keys]
    counts = {name: per_category.count(name) for name in CATEGORIES}

    report = {"screenPath": opts.screen_path, "filesScanned": scanned, "counts": counts, "keys": keys}
    if type_warning:
        report["keyTypeWarning"] = type_warning
    text = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
    if opts.out:
        with open(opts.out, "w", encoding="utf-8") as sink:
            sink.write(text)
    else:
        sys.stdout.write(text)
    summary = ", ".join(f"{counts[name]} {name}" for name in CATEGORIES)
    print(f"# {scanned} file(s) read: {summary}", file=sys.stderr)
    if type_warning:
        print(f"# {type_warning}", file=sys.stderr)


if __name__ == "__main__":
    main()
