#!/usr/bin/env python3
"""Resolve LEGACY translation values for a list of keys.

PRIMARY source  -  the in-repo legacy snapshot (`--snapshot-root`): one flat
`<lang>.json` of `{key: value}` per language, refreshed offline by
`fetch-legacy-labels.py`. No network per run.

`--live` re-reads the project's label endpoint directly, to refresh or to verify the
snapshot. That endpoint is project-specific, so nothing about it is baked in  -  supply
it the same way `fetch-legacy-labels.py` takes it:

    resolve-legacy-values.py --keys Continue,Cancel --langs all --live \\
      --endpoint 'https://labels.example.internal/{env}/labels/{lang}' \\
      --headers-file .secrets/label-headers.json

Prefer --headers-file (JSON object) over repeated --header for anything secret.

`--prefix` covers backends that store keys under a namespace prefix the app strips
(e.g. a stored `Mobile-Continue` for the app's `Continue`); pass keys WITHOUT it.

LAST-RESORT offline fallback (`--plist-root`): a legacy iOS app's shipped
`<lang>.lproj/language.plist`. It is typically a stale subset  -  the snapshot and the
service are authoritative; a plist hit deserves a note in the map.

Output: `{key: {lang: value|null}}`. Never invent a value  -  `null` is a valid answer.
"""
import argparse
import json
import os
import sys
import urllib.request

LANGS_ALL = ["en", "tr", "ar", "de", "es", "fr", "it", "ru"]


def _largest_str_map(obj):
    best, stack = {}, [obj]
    while stack:
        cur = stack.pop()
        if isinstance(cur, dict):
            strs = {k: v for k, v in cur.items() if isinstance(v, str)}
            if len(strs) > len(best):
                best = strs
            stack.extend(cur.values())
        elif isinstance(cur, list):
            stack.extend(cur)
    return best


def _parse_headers(a):
    headers = {"Accept": "application/json"}
    if a.headers_file:
        with open(a.headers_file, encoding="utf-8") as fh:
            loaded = json.load(fh)
        if not isinstance(loaded, dict):
            raise SystemExit(f"--headers-file must be a JSON object, got {type(loaded).__name__}")
        headers.update({str(k): str(v) for k, v in loaded.items()})
    for pair in a.header or []:
        if "=" not in pair:
            raise SystemExit(f"--header expects K=V, got {pair!r}")
        k, v = pair.split("=", 1)
        headers[k.strip()] = v.strip()
    return headers


def _parse_fail_statuses(values):
    out = {}
    for pair in values or []:
        if "=" not in pair:
            raise SystemExit(f"--fail-status expects CODE=message, got {pair!r}")
        code, message = pair.split("=", 1)
        out[code.strip()] = message.strip()
    return out


def fetch(lang, a):
    if not a.endpoint:
        raise RuntimeError("--live needs --endpoint (no label endpoint is built in)")
    url = a.endpoint.format(env=a.env, lang=lang)
    headers = _parse_headers(a)
    headers.setdefault("Accept-Language", lang)
    req = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(req, timeout=a.timeout) as r:
        data = json.loads(r.read().decode("utf-8"))
    fail_statuses = _parse_fail_statuses(a.fail_status)
    if isinstance(data, dict) and a.status_field in data:
        code = str(data.get(a.status_field))
        if code in fail_statuses:
            raise RuntimeError(f"{url}: {fail_statuses[code]} (status {code})")
    m = _largest_str_map(data)
    if not m:
        raise RuntimeError(f"no label map found in the response for '{lang}' ({url})")
    return m


def resolve_api(keys, langs, a):
    out = {k: {} for k in keys}
    for lang in langs:
        m = fetch(lang, a)
        for k in keys:
            out[k][lang] = m.get(a.prefix + k, m.get(k))
    return out


def resolve_snapshot(keys, langs, root, prefix):
    """Read the in-repo legacy snapshot (<root>/<lang>.json, flat {key: value})  -  
    offline, no network. Refresh it with fetch-legacy-labels.py."""
    maps = {}
    for l in langs:
        p = os.path.join(root, f"{l}.json")
        if os.path.isfile(p):
            with open(p, encoding="utf-8") as fh:
                maps[l] = json.load(fh)
        else:
            sys.stderr.write(f"# no legacy snapshot for '{l}' under {root}\n")
    return {k: {l: maps.get(l, {}).get(prefix + k, maps.get(l, {}).get(k)) for l in langs} for k in keys}


def resolve_plist(keys, langs, root, prefix):
    import plistlib
    pl = {}
    for l in langs:
        for cand in (os.path.join(root, f"{l}.lproj", "language.plist"), os.path.join(root, f"{l}.plist")):
            if os.path.isfile(cand):
                with open(cand, "rb") as fh:
                    pl[l] = plistlib.load(fh)
                break
        else:
            sys.stderr.write(f"# no plist for '{l}' under {root}\n")
    return {k: {l: pl.get(l, {}).get(prefix + k, pl.get(l, {}).get(k)) for l in langs} for k in keys}


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--keys", required=True,
                    help="comma-separated legacy keys, WITHOUT the --prefix")
    ap.add_argument("--langs", default="en,tr", help="comma-separated langs, or 'all'")
    ap.add_argument("--snapshot-root", help="PRIMARY: in-repo legacy snapshot dir  -  offline, no network")
    ap.add_argument("--live", action="store_true",
                    help="bypass the snapshot and read --endpoint live (to refresh / verify)")
    ap.add_argument("--endpoint", help="URL template for the legacy label map; may contain {lang} and {env}")
    ap.add_argument("--header", action="append",
                    help="extra request header as K=V (repeatable); avoid for secrets")
    ap.add_argument("--headers-file", help="JSON object of request headers  -  use this for tokens")
    ap.add_argument("--env", default="", help="value substituted for {env} in --endpoint")
    ap.add_argument("--status-field", default="statusCode",
                    help="response field carrying a provider status code")
    ap.add_argument("--fail-status", action="append",
                    help="CODE=message for a provider error code that must abort (repeatable)")
    ap.add_argument("--timeout", type=int, default=30)
    ap.add_argument("--plist-root", help="last-resort offline fallback: dir with <lang>.lproj/language.plist")
    ap.add_argument("--prefix", default="", help="prefix the backend stores keys under (default none)")
    a = ap.parse_args()

    langs = LANGS_ALL if a.langs.strip() == "all" else [l.strip() for l in a.langs.split(",") if l.strip()]
    keys = [k.strip() for k in a.keys.split(",") if k.strip()]

    # Preference: in-repo snapshot (offline) → live endpoint → plist.
    if a.snapshot_root and not a.live:
        out = resolve_snapshot(keys, langs, a.snapshot_root, a.prefix)
    elif a.plist_root and not a.live and not a.snapshot_root:
        sys.stderr.write("# OFFLINE plist fallback  -  the snapshot/service are the source of truth.\n")
        out = resolve_plist(keys, langs, a.plist_root, a.prefix)
    else:
        try:
            out = resolve_api(keys, langs, a)
        except Exception as exc:
            sys.stderr.write(f"# live label fetch failed ({exc}).\n")
            if a.snapshot_root:
                sys.stderr.write("# falling back to --snapshot-root.\n")
                out = resolve_snapshot(keys, langs, a.snapshot_root, a.prefix)
            elif a.plist_root:
                sys.stderr.write("# falling back to --plist-root.\n")
                out = resolve_plist(keys, langs, a.plist_root, a.prefix)
            else:
                raise

    json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
    sys.stdout.write("\n")
    absent = [k for k, v in out.items() if not any(v.values())]
    if absent:
        sys.stderr.write(f"\n# {len(absent)} key(s) with no value: " + ", ".join(absent) + "\n")


if __name__ == "__main__":
    main()
