#!/usr/bin/env python3
"""Gather the "Final UX Writing" Dev Mode annotations of a Figma screen into _annotations.json.

The REST API is tried first; a saved MCP dump (--from-mcp) or the tree.json of
a design export kept in the repo (--local) can stand in for it. The TR and EN
copy of each annotation is separated. Nothing is written back to Figma.
"""
import argparse
import json
import os
import re
import ssl
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone

from _shared import add_token_flag, reject_token_flag

API = "https://api.figma.com/v1"
BATCH = 4
SEP = "[:\\-\u2013]"
TR_RE = re.compile("^(?:tr|tr[kc]e|t[\u00fcu]rk[\u00e7c]e)\\s*" + SEP + "\\s*(.+)$", re.IGNORECASE)
EN_RE = re.compile("^(?:en|eng|ing|[i\u0130]ngilizce)\\s*" + SEP + "\\s*(.+)$", re.IGNORECASE)


URL_NODE_ID = re.compile(r"^\d+-\d+$")


def canon_id(node_id):
    text = "" if node_id is None else str(node_id).strip()
    return text.replace("-", ":") if URL_NODE_ID.match(text) else text


def split_lang(text):
    picked = {"tr": None, "en": None}
    loose = []
    for line in filter(None, (piece.strip() for piece in str(text or "").splitlines())):
        tr_hit, en_hit = TR_RE.match(line), EN_RE.match(line)
        if tr_hit and picked["tr"] is None:
            picked["tr"] = tr_hit.group(1).strip()
        elif en_hit and picked["en"] is None:
            picked["en"] = en_hit.group(1).strip()
        else:
            loose.append(line)
    if picked["tr"] is None and picked["en"] is None:
        padded = loose + [None, None]
        style = {0: "empty", 1: "single"}.get(len(loose), "two-line")
        return {"tr": padded[0], "en": padded[1], "mode": style}
    spare = iter(loose)
    for lang in ("tr", "en"):
        if picked[lang] is None:
            picked[lang] = next(spare, None)
    return dict(picked, mode="prefixed")


def label_text(annotation):
    if isinstance(annotation, dict):
        return annotation.get("label") or annotation.get("labelMarkdown") or ""
    return str(annotation or "")


def pick_label(labels):
    for label in labels:
        parsed = split_lang(label)
        if parsed["tr"] or parsed["en"]:
            return label, parsed
    first = labels[0] if labels else ""
    return first, split_lang(first)


def record(node_id, name, node_type, design_text, labels):
    raw, parsed = pick_label(labels)
    return {
        "nodeId": canon_id(node_id) or None,
        "name": name,
        "type": node_type,
        "designText": design_text,
        "raw": raw,
        "tr": parsed["tr"],
        "en": parsed["en"],
        "mode": parsed["mode"],
    }


def walk_nodes(root):
    stack = [root]
    while stack:
        node = stack.pop()
        if not isinstance(node, dict):
            continue
        yield node
        stack.extend(reversed(node.get("children") or []))


def annotated(root):
    found = []
    for node in walk_nodes(root):
        notes = node.get("annotations")
        if isinstance(notes, list) and notes:
            node_type = node.get("type")
            design = node.get("characters") if node_type == "TEXT" else node.get("name")
            node_id = node.get("id") or node.get("nodeId")
            found.append(record(node_id, node.get("name"), node_type, design, [label_text(a) for a in notes]))
    return found


def keychain_secret(service):
    try:
        lookup = subprocess.run(
            ["security", "find-generic-password", "-s", service, "-w"],
            capture_output=True, text=True, timeout=10,
        )
    except (OSError, subprocess.SubprocessError):
        return None
    secret = lookup.stdout.strip()
    return secret if lookup.returncode == 0 and secret else None


def figma_token():
    from_env = next((os.environ[v] for v in ("FIGMA_ACCESS_TOKEN", "FIGMA_TOKEN") if os.environ.get(v)), None)
    return from_env or keychain_secret("FIGMA_ACCESS_TOKEN")


def get_json(url, token, ca_file, timeout=60):
    tls = ssl.create_default_context(cafile=ca_file) if ca_file else None
    call = urllib.request.Request(url, headers={"X-Figma-Token": token})
    try:
        with urllib.request.urlopen(call, timeout=timeout, context=tls) as response:
            return json.loads(response.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        if exc.code == 429:
            sys.exit("Figma is rate limiting (HTTP 429). Dump the annotations with Figma MCP instead and pass that file via --from-mcp <raw.json>.")
        snippet = exc.read()[:200].decode("utf-8", "replace")
        sys.exit(f"Figma HTTP {exc.code} for {url}: {snippet}")
    except urllib.error.URLError as exc:
        why = str(exc.reason)
        if any(marker in why.upper() for marker in ("CERTIFICATE", "SSL")):
            sys.exit(
                "Could not verify TLS, most likely because a proxy signs traffic with its own root. "
                "Write the system roots out with `security find-certificate -a -p > roots.pem` and add --ca-file roots.pem."
            )
        sys.exit(f"Could not reach Figma: {why}")


def from_rest(file_key, node_ids, token, ca_file):
    wanted = list(map(canon_id, node_ids))
    found = []
    for offset in range(0, len(wanted), BATCH):
        chunk = wanted[offset:offset + BATCH]
        joined = urllib.parse.quote(",".join(chunk), safe="")
        reply = get_json(f"{API}/files/{file_key}/nodes?ids={joined}", token, ca_file)
        by_id = reply.get("nodes") or {}
        for node_id in chunk:
            document = (by_id.get(node_id) or {}).get("document")
            if document:
                found.extend(annotated(document))
    return found, [file_key], wanted


def read_json(path):
    with open(path, encoding="utf-8") as stream:
        return json.load(stream)


def from_mcp(path):
    data = read_json(path)
    if isinstance(data, dict):
        items = data.get("annotations") or data.get("items") or []
    else:
        items = data
    found, files, audited = [], [], []
    for item in items:
        if not isinstance(item, dict):
            continue
        labels = item.get("labels")
        if not isinstance(labels, list):
            labels = [item.get("raw")] if item.get("raw") else []
        found.append(record(item.get("nodeId"), item.get("name"), item.get("type"), item.get("designText"), [label_text(l) for l in labels]))
        if item.get("fileKey") and item["fileKey"] not in files:
            files.append(item["fileKey"])
        if item.get("nodeId"):
            audited.append(canon_id(item["nodeId"]))
    return found, files, audited


def from_local(directory):
    tree = os.path.join(directory, "tree.json")
    if not os.path.isfile(tree):
        return [], [], []
    data = read_json(tree)
    frames = [data] if not isinstance(data, list) else data
    found = [item for frame in frames for item in annotated(frame)]
    ids = [frame.get("nodeId") or frame.get("id") for frame in frames if isinstance(frame, dict)]
    return found, [], [canon_id(i) for i in ids if i]


def pick_source(opts, cli, file_key, nodes):
    if opts.from_mcp or opts.source == "mcp":
        if not opts.from_mcp:
            cli.error("--source mcp needs --from-mcp <raw.json>")
        return ("mcp",) + from_mcp(opts.from_mcp)
    if opts.source == "local":
        if not opts.local:
            cli.error("--source local needs --local <design-export-screen-dir>")
        return ("local",) + from_local(opts.local)
    have_target = bool(file_key and nodes)
    if opts.source == "rest":
        if not have_target:
            cli.error("--source rest needs a file key and node-ids (--file/--nodes or --mapping)")
        token = figma_token()
        if not token:
            sys.exit("No Figma token found. Export FIGMA_ACCESS_TOKEN or FIGMA_TOKEN, or keep it in the keychain under FIGMA_ACCESS_TOKEN.")
        return ("rest",) + from_rest(file_key, nodes, token, opts.ca_file)
    token = figma_token() if have_target else None
    if have_target and token:
        try:
            return ("rest",) + from_rest(file_key, nodes, token, opts.ca_file)
        except SystemExit as exc:
            if not opts.local:
                raise
            print(f"# REST fetch did not work ({exc}); using --local instead", file=sys.stderr)
            return ("local",) + from_local(opts.local)
    if opts.local:
        return ("local",) + from_local(opts.local)
    cli.error("nothing to do: give --file/--nodes (or --mapping) with a token, --from-mcp, or --local")


def main():
    cli = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    cli.add_argument("--file", help="key of the Figma file")
    cli.add_argument("--nodes", help="frame node-ids, separated by commas")
    cli.add_argument("--mapping", help="mapping JSON; its figmaFileKey and figmaNodes fill in for missing flags")
    cli.add_argument("--from-mcp", help="JSON dump of annotations taken through MCP")
    cli.add_argument("--local", help="screen folder of a design export that contains tree.json")
    cli.add_argument("--source", default="auto", choices=["auto", "rest", "mcp", "local"])
    add_token_flag(cli)
    cli.add_argument("--ca-file", help="PEM roots to trust when a proxy intercepts TLS")
    cli.add_argument("--out", help="file to write; stdout when omitted")
    opts = cli.parse_args()
    reject_token_flag(cli, opts, "export FIGMA_ACCESS_TOKEN (or FIGMA_TOKEN), or keep it in the macOS keychain under FIGMA_ACCESS_TOKEN")

    file_key = opts.file
    nodes = [piece for piece in (opts.nodes or "").split(",") if piece.strip()]
    if opts.mapping:
        mapping = read_json(opts.mapping)
        file_key = file_key or mapping.get("figmaFileKey")
        nodes = nodes or list(mapping.get("figmaNodes") or [])

    source, found, files, audited = pick_source(opts, cli, file_key, nodes)

    unique, seen = [], set()
    for item in found:
        if item["nodeId"]:
            if item["nodeId"] in seen:
                continue
            seen.add(item["nodeId"])
        unique.append(item)

    def having(lang):
        return sum(1 for entry in unique if entry[lang])

    result = {
        "generatedAt": datetime.now(timezone.utc).isoformat(),
        "source": source,
        "files": files,
        "auditedNodes": audited,
        "count": len(unique),
        "withTr": having("tr"),
        "withEn": having("en"),
        "annotations": unique,
    }
    text = json.dumps(result, indent=2, ensure_ascii=False) + "\n"
    if not opts.out:
        sys.stdout.write(text)
    else:
        with open(opts.out, "w", encoding="utf-8") as sink:
            sink.write(text)
        print(f"# {result['count']} annotations ({result['withTr']} TR, {result['withEn']} EN) from {source} -> {opts.out}", file=sys.stderr)

    missing_en = [entry["nodeId"] for entry in unique if entry["tr"] and not entry["en"]]
    if missing_en:
        print(f"# {len(missing_en)} annotation(s) carry TR copy without EN; return them to the content team:", file=sys.stderr)
        for node_id in missing_en:
            print(f"#   {node_id}", file=sys.stderr)


if __name__ == "__main__":
    main()
