#!/usr/bin/env python3
"""Runs on THIS host (the orchestrating machine), never on the target.

Bridges the two remote calls in scripts/10-generate-app-icons.sh:

  plan   Reads the target's `icon_remote.py resolve` output (one JSON
         object per line) plus the local manifest, and decides which icon
         names actually need the expensive remote trace step -- new names,
         names whose resolved source file changed, names whose style guide
         changed, or anything passed via --force/--force-all. Writes that
         subset as a jobs.json for `icon_remote.py trace` to consume on the
         target.

  commit After the driver has pulled generated_icons/silhouettes/ back from
         the target, updates generated_icons/manifest.json for every job
         whose silhouette file actually landed (a missing file means that
         icon's trace failed remotely and is left for the next run/--force).
"""
import argparse
import datetime
import json
import sys
from pathlib import Path


def load_json(path, default):
    p = Path(path)
    return json.loads(p.read_text()) if p.is_file() else default


def cmd_plan(args):
    manifest = load_json(args.manifest, {})
    style_guide_hash = Path(args.style_guide_hash).read_text().strip()
    force_all = args.force_all
    force_names = set(args.force or [])

    resolved = []
    missing = []
    for line in Path(args.resolve_output).read_text().splitlines():
        line = line.strip()
        if not line:
            continue
        obj = json.loads(line)
        if "error" in obj:
            missing.append(obj["name"])
            continue
        resolved.append(obj)

    jobs = []
    unchanged = 0
    for obj in resolved:
        name = obj["name"]
        entry = manifest.get(name)
        silhouette_exists = (Path(args.silhouette_dir) / f"{name}.svg").is_file()
        needs = (
            force_all
            or name in force_names
            or not entry
            or not silhouette_exists
            or entry.get("source_hash") != obj["source_hash"]
            or entry.get("style_guide_hash") != style_guide_hash
        )
        if needs:
            jobs.append({
                "name": name,
                "source_path": obj["source_path"],
                "source_hash": obj["source_hash"],
                "is_status": obj["is_status"],
                "source_theme": obj.get("source_theme"),
                "source_relpath": obj.get("source_relpath"),
            })
        else:
            unchanged += 1

    Path(args.jobs_out).write_text(json.dumps(jobs, indent=2))
    # Full resolved set (incl. unchanged) -- "commit" needs is_status/hash
    # for entries that weren't (re)traced too, to keep the manifest's
    # is_status flag in sync even when the icon itself didn't change.
    Path(args.resolved_out).write_text(json.dumps(resolved, indent=2))

    print(f">>> {len(jobs)} to (re)generate, {unchanged} unchanged, {len(missing)} unresolved")
    if missing:
        print(">>> Unresolved (no icon file found on target for this name):")
        for n in missing[:40]:
            print(f"    - {n}")
        if len(missing) > 40:
            print(f"    ... and {len(missing) - 40} more")


def cmd_commit(args):
    manifest = load_json(args.manifest, {})
    style_guide_hash = Path(args.style_guide_hash).read_text().strip()
    jobs = json.loads(Path(args.jobs).read_text())
    now = datetime.datetime.now(datetime.timezone.utc).isoformat()

    ok, failed = 0, []
    for job in jobs:
        name = job["name"]
        if (Path(args.silhouette_dir) / f"{name}.svg").is_file():
            manifest[name] = {
                "source_path": job["source_path"],
                "source_hash": job["source_hash"],
                "style_guide_hash": style_guide_hash,
                "is_status": job["is_status"],
                "source_theme": job.get("source_theme"),
                "source_relpath": job.get("source_relpath"),
                "generated_at": now,
            }
            ok += 1
        else:
            failed.append(name)

    Path(args.manifest).write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
    print(f">>> manifest updated: {ok} committed, {len(failed)} failed")
    if failed:
        for n in failed:
            print(f"    - FAILED: {n}")


def main():
    ap = argparse.ArgumentParser()
    sub = ap.add_subparsers(dest="mode", required=True)

    p_plan = sub.add_parser("plan")
    p_plan.add_argument("--resolve-output", required=True)
    p_plan.add_argument("--manifest", required=True)
    p_plan.add_argument("--style-guide-hash", required=True)
    p_plan.add_argument("--silhouette-dir", required=True)
    p_plan.add_argument("--jobs-out", required=True)
    p_plan.add_argument("--resolved-out", required=True)
    p_plan.add_argument("--force", action="append")
    p_plan.add_argument("--force-all", action="store_true")
    p_plan.set_defaults(func=cmd_plan)

    p_commit = sub.add_parser("commit")
    p_commit.add_argument("--jobs", required=True)
    p_commit.add_argument("--manifest", required=True)
    p_commit.add_argument("--style-guide-hash", required=True)
    p_commit.add_argument("--silhouette-dir", required=True)
    p_commit.set_defaults(func=cmd_commit)

    args = ap.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
