#!/usr/bin/env python3
"""Fidelity gate: reconcile a reuse-map mapping against the implemented screen.

Checks that every mapped newKey is wired in the screen code, that every key
the code wires is mapped, and that no row carries an empty or invented value.
Exit 2 when a hard issue is found (blocks publishing), 0 otherwise.
"""
import argparse
import json
import os
import re
import sys

from _shared import (
    ACCESSOR_EXTENSIONS, DEFAULT_KEY_TYPES, LITERAL_KEY, WEB_DOLLAR_T, WEB_T, clean_key, is_component, is_dynamic,
    key_type_warning, key_types, lsk_keys, normalize_key, raw_key, source_files,
)


def scan_code(root, types=DEFAULT_KEY_TYPES):
    keys = set()
    corpus = []
    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
        scanned += 1
        corpus.append(text.lower())
        if os.path.splitext(path)[1] in ACCESSOR_EXTENSIONS:
            accessor_texts.append(text)
        keys.update(key.lower() for key in lsk_keys(text, types))
        for pattern, group in ((LITERAL_KEY, 1), (WEB_T, 2), (WEB_DOLLAR_T, 2)):
            keys.update(hit.group(group).lower() for hit in pattern.finditer(text))
    return keys, "\n".join(corpus), scanned, key_type_warning(accessor_texts, types, mapping_hint=True)


def filled(value):
    return bool(str(value or "").strip())


def suggested_exists(root, key):
    name = normalize_key(key) + ".json"
    candidates = [
        os.path.join(root, "Localization", "Suggested", name),
        os.path.join(root, "Resources", "Localization", "LocalizationStrings", "Sources", "Suggested", name),
    ]
    return any(os.path.isfile(p) for p in candidates)


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--mapping", required=True, help="the reuse-map JSON")
    parser.add_argument("--screen-path", required=True, help="folder or single file of the built screen")
    parser.add_argument("--resources-root", help="resources checkout or snapshot; turns on the Suggested-file check")
    parser.add_argument("--out", help="JSON report destination; stdout when omitted")
    parser.add_argument(
        "--key-type", action="append", metavar="NAME",
        help="type whose member chains are keys; repeatable or comma list. Falls back to the mapping's "
             f"keyType, then {', '.join(DEFAULT_KEY_TYPES)}",
    )
    args = parser.parse_args()

    with open(args.mapping, encoding="utf-8") as stream:
        mapping = json.load(stream)
    rows = mapping.get("rows") or []
    from_mapping = mapping.get("keyType") or []
    if isinstance(from_mapping, str):
        from_mapping = [from_mapping]
    elif not (isinstance(from_mapping, list) and all(isinstance(name, str) for name in from_mapping)):
        parser.error(f"keyType in the mapping must be a string or a list of strings, got {from_mapping!r}")
    try:
        types = key_types(args.key_type or from_mapping)
    except ValueError as exc:
        parser.error(str(exc))
    code_keys, corpus, scanned, type_warning = scan_code(args.screen_path, types)

    verified = 0
    exempt = 0
    weak, unverified = [], []
    gaps, untranslated = [], []
    map_keys = set()

    for row in rows:
        key = clean_key(raw_key(row))
        component = is_component(row)
        dynamic = is_dynamic(row, key)
        if key:
            map_keys.add(key.lower())

        if component or dynamic:
            exempt += 1
        elif key.lower() in code_keys:
            verified += 1
        else:
            leaf = key.rsplit(".", 1)[-1].lower()
            whole_word = r"\b%s\b" % re.escape(leaf)
            if leaf and re.search(whole_word, corpus):
                weak.append(key)
            else:
                unverified.append({"key": key, "element": row.get("element")})

        verdict = row.get("verdict")
        new = row.get("new") or {}
        legacy = row.get("legacy") or {}
        if verdict != "new" and not filled(new.get("tr")) and not dynamic:
            gaps.append({"key": key, "issue": "new.tr empty"})
        if verdict in ("reuse", "review") and not (filled(legacy.get("en")) or filled(legacy.get("tr"))):
            gaps.append({"key": key, "issue": f"verdict={verdict} but no legacy value"})
        if filled(new.get("tr")) and new.get("tr") == new.get("en"):
            untranslated.append(key)
        if args.resources_root and not component and not dynamic and key and not suggested_exists(args.resources_root, key):
            gaps.append({"key": key, "issue": f"no Suggested/{normalize_key(key)}.json"})

    not_in_map = sorted(code_keys - map_keys)
    report = {
        "screen": mapping.get("screen"),
        "rows": len(rows),
        "filesScanned": scanned,
        "newKeyInCode": {
            "verified": verified,
            "weak": weak,
            "unverified": unverified,
            "component_or_dynamic": exempt,
        },
        "codeKeysNotInMap": not_in_map,
        "valueGaps": gaps,
        "untranslated": untranslated,
    }
    if type_warning:
        report["keyTypeWarning"] = type_warning
    text = json.dumps(report, indent=2, ensure_ascii=False) + "\n"
    if args.out:
        with open(args.out, "w", encoding="utf-8") as sink:
            sink.write(text)
    else:
        sys.stdout.write(text)

    no_legacy = [g for g in gaps if "no legacy value" in g["issue"]]
    hard = len(unverified) + len(not_in_map) + len(no_legacy)
    print(
        f"# rows {len(rows)}, files {scanned}: verified {verified}, weak {len(weak)}, "
        f"UNVERIFIED {len(unverified)}, component/dynamic {exempt}, wired-but-unmapped {len(not_in_map)}, "
        f"value gaps {len(gaps)} ({len(no_legacy)} hard), untranslated {len(untranslated)}",
        file=sys.stderr,
    )
    if type_warning:
        print(f"# {type_warning}", file=sys.stderr)
    for item in unverified[:12]:
        print(f"#   UNVERIFIED {item['key']}  ({item['element']})", file=sys.stderr)
    for key in not_in_map[:12]:
        print(f"#   wired but unmapped: {key}", file=sys.stderr)
    if hard:
        print(f"# FAIL: {hard} hard issue(s); fix them or record a conscious waiver before publishing", file=sys.stderr)
        sys.exit(2)
    print("# PASS, but see the key type WARNING above" if type_warning else "# PASS", file=sys.stderr)


if __name__ == "__main__":
    main()
