#!/usr/bin/env bash
#
# fetch-figma-annotations.sh
#
# Tier-2 (Figma REST) fallback for /multi-agent:analysis annotation ingestion.
# Fetches Dev Mode annotations for one or more nodes and emits them as the
# authoritative copy for each node (the visible text layer is a placeholder).
#
# Tier 1 (Figma MCP get_design_context) is preferred and handled in the skill;
# this script is the no-MCP fallback. Generic: no project file keys, no
# language codes, and no corporate transport are baked in - the caller passes
# the file key, node ids, and the recognized language prefixes.
#
# Usage:
#   fetch-figma-annotations.sh --file-key <key> --node-id <id>[,<id>...] \
#                              [--lang-prefixes TR,EN] [--batch 4]
#   fetch-figma-annotations.sh --url <figma-url> [--lang-prefixes TR,EN]
#
# Token source: figma-token.sh, which resolves the canonical `figma` logical key
# through credential-store.sh (falling back to the legacy `figma_pat` key, then
# to the FIGMA_PAT environment variable). No literal keychain service name is
# embedded here.
#
# Label parsing (config-driven via --lang-prefixes, ordered):
#   prefixed  : lines like "TR: ...", "EN: ..." win regardless of order
#   two-line  : line 1 -> prefixes[0], line 2 -> prefixes[1]
#   single    : one line -> prefixes[0]
#   empty     : no label text
#
# Output (stdout, single JSON object):
#   { "ok": true, "source": "figma-rest", "fileKey": "...", "fetchedAt": "...",
#     "count": N, "withBase": M,
#     "annotations": [ {nodeId, name, designText, raw, parsed:{<lang>:val}, mode} ] }
# On missing/rejected token: { "ok": false, "reason": "missing-token" } and exit 2.
#
# Exit codes: 0 success (incl. zero annotations), 1 usage, 2 auth,
#   4 node not found, 5 rate limited, 6 network/other fatal.

set -uo pipefail

# Locate the resolver with an existence check, not a `.`-chain: sourcing a missing file
# aborts the shell under `set -e`, `||` included, so a chain skips both its later
# candidates and its trailing `|| true`. A missing store is tolerated here - CRED_STORE
# simply stays empty and the caller falls back to an env-supplied token.
for _cred_resolver in \
  "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
  "$HOME/.claude/lib/credential-store-resolver.sh" \
  "$HOME/.copilot/lib/credential-store-resolver.sh" \
  "$HOME/.codex/lib/credential-store-resolver.sh"; do
  [ -f "$_cred_resolver" ] || continue
  # shellcheck source=/dev/null
  . "$_cred_resolver" 2>/dev/null || true
  # `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
  # 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
  # candidate that does not resolve - the very case the loop exists to survive.
  if [ -n "${CRED_STORE:-}" ]; then break; fi
done
unset _cred_resolver
CRED_STORE="${CRED_STORE:-}"

# Tier 2 token lookup lives in one file so it cannot drift per fetcher. Same
# existence-check discipline as the resolver loop above, and the same tolerance:
# without it, resolution still works through the FIGMA_PAT env fallback below.
for _figma_token_lib in \
  "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/figma-token.sh" \
  "$HOME/.claude/lib/figma-token.sh" \
  "$HOME/.copilot/lib/figma-token.sh" \
  "$HOME/.codex/lib/figma-token.sh"; do
  [ -f "$_figma_token_lib" ] || continue
  # shellcheck source=/dev/null
  . "$_figma_token_lib" 2>/dev/null || true
  if command -v resolve_figma_token >/dev/null 2>&1; then break; fi
done
unset _figma_token_lib
if ! command -v resolve_figma_token >/dev/null 2>&1; then
  resolve_figma_token() { [ -n "${FIGMA_PAT:-}" ] && printf '%s' "$FIGMA_PAT"; }
  figma_token_remediation() { printf 'figma-token.sh not found - reinstall the pipeline, or export FIGMA_PAT.'; }
fi

FIGMA_API="https://api.figma.com/v1"
HTTP_TIMEOUT=60

FILE_KEY=""
NODE_IDS=""
URL=""
LANG_PREFIXES="TR,EN"
BATCH=4

log() { printf '%s\n' "$*" >&2; }
die() { local code="$1"; shift; log "fetch-figma-annotations: $*"; exit "$code"; }

while [ $# -gt 0 ]; do
  case "$1" in
    --file-key) FILE_KEY="${2:-}"; shift 2 ;;
    --node-id) NODE_IDS="${2:-}"; shift 2 ;;
    --url) URL="${2:-}"; shift 2 ;;
    --lang-prefixes) LANG_PREFIXES="${2:-TR,EN}"; shift 2 ;;
    --batch) BATCH="${2:-4}"; shift 2 ;;
    -h|--help) grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'; exit 0 ;;
    *) die 1 "unknown argument: $1" ;;
  esac
done

# Parse fileKey + nodeId from a Figma URL when given.
if [ -n "$URL" ]; then
  parsed_key=$(printf '%s' "$URL" | sed -nE 's#.*figma\.com/(design|file)/([A-Za-z0-9]+).*#\2#p')
  parsed_node=$(printf '%s' "$URL" | sed -nE 's#.*[?&]node-id=([^&]+).*#\1#p' | sed 's/-/:/')
  [ -z "$FILE_KEY" ] && FILE_KEY="$parsed_key"
  [ -z "$NODE_IDS" ] && NODE_IDS="$parsed_node"
fi

[ -z "$FILE_KEY" ] && die 1 "missing --file-key (or a --url to parse it from)"
[ -z "$NODE_IDS" ] && die 1 "missing --node-id (or a --url to parse it from)"

FIGMA_TOKEN=$(resolve_figma_token || true)
if [ -z "$FIGMA_TOKEN" ]; then
  printf '{"ok":false,"reason":"missing-token","source":"figma-rest","fileKey":"%s"}\n' "$FILE_KEY"
  die 2 "$(figma_token_remediation)"
fi

TMPDIR_A=$(mktemp -d "${TMPDIR:-/tmp}/figma-annot.XXXXXX")
trap 'rm -rf "$TMPDIR_A"' EXIT INT TERM

# curl one /nodes batch (token via -K config so it never lands in argv).
curl_nodes() {
  local out="$1" ids="$2" code
  # curl already writes "000" via -w on a total connection failure (and
  # still exits non-zero for it, which the inner `|| echo "000"` used to
  # catch too - so `code` became the literal string "000000", never
  # matching a plain "000" comparison anywhere downstream). `|| true` keeps
  # a failing curl from tripping any caller's `set -e` without re-printing
  # into $code; `:-000` only fills in when curl fails before -w ever runs.
  code=$(curl -sS -o "$out" -w '%{http_code}' \
            --max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
            -K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
            "$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids") || true
  code="${code:-000}"
  printf '%s' "$code"
}

# Split comma ids into batches of $BATCH, fetch each into TMPDIR_A/part-N.json.
OLD_IFS="$IFS"; IFS=','; set -- $NODE_IDS; IFS="$OLD_IFS"
part=0; batch_ids=""; n=0
flush_batch() {
  [ -z "$batch_ids" ] && return 0
  part=$((part + 1))
  local code
  code=$(curl_nodes "$TMPDIR_A/part-$part.json" "$batch_ids")
  case "$code" in
    2*) : ;;
    401|403) die 2 "auth rejected (HTTP $code)" ;;
    404) die 4 "node(s) not found (HTTP 404)" ;;
    429) die 5 "rate limited (HTTP 429)" ;;
    *) die 6 "figma call failed (HTTP $code)" ;;
  esac
  batch_ids=""; n=0
}
for id in "$@"; do
  [ -z "$id" ] && continue
  if [ -z "$batch_ids" ]; then batch_ids="$id"; else batch_ids="$batch_ids,$id"; fi
  n=$((n + 1))
  [ "$n" -ge "$BATCH" ] && flush_batch
done
flush_batch

# Walk all node docs, collect + parse annotations. Python only parses local
# response files; the token is never passed to it.
LANG_PREFIXES="$LANG_PREFIXES" FILE_KEY="$FILE_KEY" python3 - "$TMPDIR_A" <<'PY'
import json, os, sys, glob, datetime

parts_dir = sys.argv[1]
prefixes = [p.strip() for p in os.environ.get("LANG_PREFIXES", "TR,EN").split(",") if p.strip()]
base = (prefixes[0].lower() if prefixes else "tr")

def parse_label(raw):
    if not raw or not raw.strip():
        return {}, "empty"
    lines = [ln.strip() for ln in raw.splitlines() if ln.strip()]
    parsed = {}
    # prefixed: "TR: ...", "EN: ..."
    prefixed = False
    for ln in lines:
        for p in prefixes:
            lp = p.lower()
            low = ln.lower()
            if low.startswith(lp + ":") or low.startswith(lp + " :"):
                parsed[lp] = ln.split(":", 1)[1].strip()
                prefixed = True
    if prefixed:
        return parsed, "prefixed"
    if len(lines) >= 2 and len(prefixes) >= 2:
        return {prefixes[0].lower(): lines[0], prefixes[1].lower(): lines[1]}, "two-line"
    if len(lines) == 1 and prefixes:
        return {prefixes[0].lower(): lines[0]}, "single"
    # fallback: join everything under the base language
    return {base: " ".join(lines)}, "single"

annotations = []

def walk(node):
    if not isinstance(node, dict):
        return
    anns = node.get("annotations")
    if isinstance(anns, list) and anns:
        raw_parts = []
        for a in anns:
            if not isinstance(a, dict):
                continue
            lbl = a.get("labelMarkdown") or a.get("label") or ""
            if lbl:
                raw_parts.append(lbl)
        raw = "\n".join(raw_parts)
        parsed, mode = parse_label(raw)
        design_text = node.get("characters")
        annotations.append({
            "nodeId": node.get("id"),
            "name": node.get("name"),
            "designText": design_text if isinstance(design_text, str) else None,
            "raw": raw or None,
            "parsed": parsed,
            "mode": mode,
        })
    for child in node.get("children", []) or []:
        walk(child)

for pf in sorted(glob.glob(os.path.join(parts_dir, "part-*.json"))):
    try:
        doc = json.load(open(pf))
    except Exception:
        continue
    nodes = doc.get("nodes", {})
    if isinstance(nodes, dict):
        for _, entry in nodes.items():
            if isinstance(entry, dict) and isinstance(entry.get("document"), dict):
                walk(entry["document"])

with_base = sum(1 for a in annotations if a["parsed"].get(base))
# Flag base-present-but-a-target-missing to stderr (kick back to content team).
for a in annotations:
    if a["parsed"].get(base):
        missing = [p.lower() for p in prefixes if not a["parsed"].get(p.lower())]
        if missing:
            sys.stderr.write("WARN: node %s has %s but missing %s\n" % (
                a.get("nodeId"), base, ",".join(missing)))

out = {
    "ok": True,
    "source": "figma-rest",
    "fileKey": os.environ.get("FILE_KEY"),
    "fetchedAt": datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
    "count": len(annotations),
    "withBase": with_base,
    "annotations": annotations,
}
print(json.dumps(out, ensure_ascii=False, indent=2))
PY
