#!/usr/bin/env python3
"""Resolve the legacy translation of each requested key, per language.

Sources, in order of preference: the in-repo flat snapshot
(<root>/<lang>.json), bundled language.plist files, or the live label
endpoint. Values are never invented: a key the source lacks comes back null.
Prints {key: {lang: value|null}} as JSON.
"""
import argparse
import json
import os
import plistlib
import sys
import urllib.request

from _shared import parse_langs



def split_csv(value):
    return [part.strip() for part in value.split(",") if part.strip()]


def present_langs(snapshot_root, plist_root):
    found = []
    if snapshot_root and os.path.isdir(snapshot_root):
        found += [name[:-5] for name in os.listdir(snapshot_root) if name.endswith(".json")]
    if plist_root and os.path.isdir(plist_root):
        for name in os.listdir(plist_root):
            if name.endswith(".lproj") and os.path.isfile(os.path.join(plist_root, name, "language.plist")):
                found.append(name[: -len(".lproj")])
            elif name.endswith(".plist") and name != "language.plist":
                found.append(name[: -len(".plist")])
    return found


def parse_pairs(pairs, flag):
    result = {}
    for pair in pairs or []:
        if "=" not in pair:
            raise SystemExit(f"{flag} expects K=V, got {pair!r}")
        key, value = pair.split("=", 1)
        result[key.strip()] = value.strip()
    return result


def build_headers(args):
    headers = {"Accept": "application/json"}
    if args.headers_file:
        with open(args.headers_file, encoding="utf-8") as handle:
            extra = json.load(handle)
        if not isinstance(extra, dict):
            raise SystemExit(f"--headers-file must hold a JSON object: {args.headers_file}")
        headers.update({str(k): str(v) for k, v in extra.items()})
    headers.update(parse_pairs(args.header, "--header"))
    return headers


def largest_string_map(data):
    best = {}
    stack = [data]
    while stack:
        node = stack.pop()
        if isinstance(node, dict):
            if node and all(isinstance(k, str) and isinstance(v, str) for k, v in node.items()):
                if len(node) > len(best):
                    best = node
            stack.extend(node.values())
        elif isinstance(node, list):
            stack.extend(node)
    return best


def fetch_labels(endpoint, lang, env, headers, status_field, fail_status, timeout):
    url = endpoint.format(lang=lang, env=env)
    request_headers = dict(headers)
    request_headers.setdefault("Accept-Language", lang)
    request = urllib.request.Request(url, headers=request_headers)
    with urllib.request.urlopen(request, timeout=timeout) as response:
        data = json.loads(response.read().decode("utf-8"))
    if isinstance(data, dict) and status_field in data:
        code = str(data[status_field])
        if code in fail_status:
            raise RuntimeError(f"{url}: {fail_status[code]} (status {code})")
    labels = largest_string_map(data)
    if not labels:
        raise RuntimeError(f"the response for '{lang}' carried no label map ({url})")
    return labels


def lookup(table, key, prefix):
    if prefix and prefix + key in table:
        return table[prefix + key]
    return table.get(key)


def from_snapshot(root, keys, langs, prefix):
    result = {key: {} for key in keys}
    for lang in langs:
        path = os.path.join(root, f"{lang}.json")
        table = {}
        if os.path.isfile(path):
            with open(path, encoding="utf-8") as handle:
                table = json.load(handle)
        else:
            print(f"# no snapshot file for '{lang}': {path}", file=sys.stderr)
        for key in keys:
            result[key][lang] = lookup(table, key, prefix)
    return result


def from_plist(root, keys, langs, prefix):
    result = {key: {} for key in keys}
    for lang in langs:
        table = {}
        candidates = [os.path.join(root, f"{lang}.lproj", "language.plist"), os.path.join(root, f"{lang}.plist")]
        found = next((p for p in candidates if os.path.isfile(p)), None)
        if found:
            with open(found, "rb") as handle:
                table = plistlib.load(handle)
        else:
            print(f"# no plist for '{lang}' under {root}", file=sys.stderr)
        for key in keys:
            result[key][lang] = lookup(table, key, prefix)
    return result


def from_live(args, keys, langs):
    if not args.endpoint:
        raise RuntimeError("--live needs --endpoint (no label endpoint is built in)")
    headers = build_headers(args)
    fail_status = parse_pairs(args.fail_status, "--fail-status")
    result = {key: {} for key in keys}
    for lang in langs:
        table = fetch_labels(args.endpoint, lang, args.env, headers, args.status_field, fail_status, args.timeout)
        for key in keys:
            result[key][lang] = lookup(table, key, args.prefix)
    return result


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--keys", required=True, help="legacy keys separated by commas; leave off the backend prefix")
    parser.add_argument("--langs", default="all", help="comma list, or 'all' for every language the snapshot or plist root holds")
    parser.add_argument("--snapshot-root", help="directory holding <lang>.json flat snapshots")
    parser.add_argument("--live", action="store_true", help="query the label endpoint instead of the snapshot")
    parser.add_argument("--endpoint", help="URL template with {lang} and optional {env}")
    parser.add_argument("--header", action="append", help="extra request header K=V (repeatable)")
    parser.add_argument("--headers-file", help="JSON object of request headers; keeps secrets off argv")
    parser.add_argument("--env", default="", help="value substituted for {env}")
    parser.add_argument("--status-field", default="statusCode", help="response field carrying a status code")
    parser.add_argument("--fail-status", action="append", help="CODE=message treated as a failure (repeatable)")
    parser.add_argument("--timeout", type=int, default=30)
    parser.add_argument("--plist-root", help="last resort: directory of bundled <lang>.lproj/language.plist")
    parser.add_argument("--prefix", default="", help="backend key prefix the app strips, e.g. App-")
    args = parser.parse_args()

    keys = split_csv(args.keys)
    langs = parse_langs(args.langs, present_langs(args.snapshot_root, args.plist_root))
    if not (args.snapshot_root or args.plist_root or args.endpoint):
        sys.exit("no source given: pass --snapshot-root, --plist-root, or --live with --endpoint")

    if args.snapshot_root and not args.live:
        result = from_snapshot(args.snapshot_root, keys, langs, args.prefix)
    elif args.plist_root and not args.live:
        print("# reading bundled plists: a stale subset; trust the snapshot and the service over them", file=sys.stderr)
        result = from_plist(args.plist_root, keys, langs, args.prefix)
    else:
        try:
            result = from_live(args, keys, langs)
        except Exception as exc:
            print(f"# live label fetch failed ({exc})", file=sys.stderr)
            if args.snapshot_root:
                result = from_snapshot(args.snapshot_root, keys, langs, args.prefix)
            elif args.plist_root:
                result = from_plist(args.plist_root, keys, langs, args.prefix)
            else:
                sys.exit("no --snapshot-root or --plist-root to fall back on")

    json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
    sys.stdout.write("\n")

    empty = [key for key, values in result.items() if not any(values.values())]
    if empty:
        print(f"# {len(empty)} key(s) with no legacy value in any requested language:", file=sys.stderr)
        for key in empty:
            print(f"#   {key}", file=sys.stderr)


if __name__ == "__main__":
    main()
