#!/usr/bin/env python3
"""
Extract Claude Design "standalone" HTML exports.

Claude Design packs the whole React app as base64+gzip blobs inside
<script type="__bundler/manifest"> tags. This script decompresses them
and writes each asset (JSX, CSS, fonts) to a target directory.

Usage:
    python3 extract_design_bundle.py <path/to/export.html> <output_dir>

The output dir will contain:
    data.jsx              (if detected by header comment)
    fn_library_emu.jsx    (lib emulation)
    charts_emu.jsx        (charts emulation)
    app.jsx               (main tree)
    <uuid>.<ext>          (anything else — fonts, unknown js)
    manifest.json         (summary of all assets: uuid, mime, bytes, filename)

JSX files are named heuristically from their leading comment. If names
cannot be inferred from headers, they keep their uuid prefix.
"""
from __future__ import annotations

import base64
import gzip
import json
import pathlib
import re
import sys


MIME_TO_EXT = {
    "text/javascript": "js",
    "application/javascript": "js",
    "text/babel": "jsx",
    "application/json": "json",
    "text/css": "css",
    "image/svg+xml": "svg",
    "font/woff2": "woff2",
    "font/woff": "woff",
    "text/html": "html",
}

# Order matters: first matching hint wins. Put MORE SPECIFIC patterns first.
HEADER_HINTS = [
    ("charts_emu.jsx",     [r"Emulaci(ó|o)n de @fn_library/\{", r"LineChart, AreaChart, BarChart"]),
    ("fn_library_emu.jsx", [r"Emulaci(ó|o)n visual de @fn_library"]),
    ("data.jsx",           [r"mock data \(determinista\)", r"window\.\w+Data\s*="]),
    ("app.jsx",            [r"ReactDOM\.createRoot", r"arbol principal", r"function App\s*\("]),
]


def pick_name(content: str, used_names: set[str]) -> str | None:
    head = content[:2000]
    for name, patterns in HEADER_HINTS:
        if name in used_names:
            continue
        if any(re.search(p, head, re.IGNORECASE) for p in patterns):
            return name
    return None


def grab_script(html: str, kind: str) -> str | None:
    m = re.search(
        r'<script type="__bundler/' + kind + r'">\s*(.*?)\s*</script>',
        html, re.DOTALL,
    )
    return m.group(1) if m else None


def extract(html_path: pathlib.Path, out_dir: pathlib.Path) -> dict:
    html = html_path.read_text(encoding="utf-8")

    manifest_raw = grab_script(html, "manifest")
    if not manifest_raw:
        raise SystemExit(f"No <script type='__bundler/manifest'> found in {html_path}")
    manifest = json.loads(manifest_raw)

    ext_raw = grab_script(html, "ext_resources")
    ext_resources = json.loads(ext_raw) if ext_raw else []
    id_map = {e["uuid"]: e.get("id", e["uuid"]) for e in ext_resources}

    out_dir.mkdir(parents=True, exist_ok=True)

    summary = []
    used_names: set[str] = set()

    # First pass: decode all assets and collect jsx blobs (so we can name them by header hint)
    decoded: list[tuple[str, str, bytes]] = []  # (uuid, mime, bytes)
    for uuid, entry in manifest.items():
        raw = base64.b64decode(entry["data"])
        if entry.get("compressed"):
            raw = gzip.decompress(raw)
        decoded.append((uuid, entry.get("mime", "application/octet-stream"), raw))

    # Second pass: write files with heuristic names for known jsx
    for uuid, mime, raw in decoded:
        ext = MIME_TO_EXT.get(mime, "bin")
        filename = None

        # Heuristic for JSX / JS that represents the app
        if ext in ("jsx", "js"):
            try:
                text = raw.decode("utf-8", errors="replace")
                name = pick_name(text, used_names)
                if name:
                    filename = name
                    used_names.add(name)
            except Exception:
                pass

        if not filename:
            # Fall back to ext_resources id if present, or uuid
            base = id_map.get(uuid, uuid)
            safe = re.sub(r"[^A-Za-z0-9._-]", "_", base)[:80]
            filename = f"{safe}.{ext}"

        path = out_dir / filename
        # Avoid collisions
        i = 2
        while path.exists():
            stem = path.stem
            path = out_dir / f"{stem}_{i}.{ext}"
            i += 1
        path.write_bytes(raw)
        summary.append({
            "uuid": uuid,
            "mime": mime,
            "bytes": len(raw),
            "filename": path.name,
        })

    (out_dir / "manifest.json").write_text(
        json.dumps({"source": str(html_path), "assets": summary}, indent=2),
        encoding="utf-8",
    )
    return {"assets": summary, "out": str(out_dir)}


def main():
    if len(sys.argv) < 3:
        print(__doc__)
        sys.exit(2)
    html = pathlib.Path(sys.argv[1])
    out = pathlib.Path(sys.argv[2])
    if not html.exists():
        sys.exit(f"Input not found: {html}")
    result = extract(html, out)
    print(f"✓ Extracted {len(result['assets'])} assets to {result['out']}")
    print(f"  Manifest: {out}/manifest.json")
    print()
    # Short preview per asset
    for a in result["assets"]:
        print(f"  {a['mime']:28s} {a['bytes']:>8} B  {a['filename']}")


if __name__ == "__main__":
    main()
