"""runner checkout <app> — materialize a scope subtree as local files.

The hosted mirror of `./r checkout`: your app is rows in hosted Runner, so the
serialization runs server-side and we just write the returned files under
./.tmp/<scope>/ — behavior bodies as .js, shaders .glsl, snippet workers .py, CSS
.css, trees/specs/rows .json, plus manifest.json + INDEX.md. Native file tooling
(grep with line numbers, read, surgical edit) then works on a real tree.

DISPOSABLE working copy — the DB stays the source of truth; write back with the
typed runner verbs. Secrets and per-user me.u… scopes are never serialized
server-side; runtime rows are never exported over the API.
"""
import json
import shutil
import sys
from pathlib import Path

from . import api, config


def checkout(scope=None, out=None, force=False):
    sc = scope or config.default_scope()
    if not sc:
        print("runner checkout: which app? e.g. `runner checkout acme.crm`", file=sys.stderr)
        return 2
    try:
        payload = api.get("/api/v1/checkout", {"scope": sc})
    except api.ApiError as e:
        print(f"checkout failed: {e}", file=sys.stderr)
        return 1

    root = payload.get("root", sc)
    dest = Path(out or ".tmp") / root
    if not _prepare_dest(dest, force):
        return 1

    files = dict(payload.get("files") or {})
    files["manifest.json"] = json.dumps(payload["manifest"], indent=2, ensure_ascii=False) + "\n"
    files["INDEX.md"] = payload["index"]

    written = 0
    for rel, content in files.items():
        full = _safe_join(dest, rel)
        if full is None:
            print(f"  ! skipped unsafe path {rel}", file=sys.stderr)
            continue
        full.parent.mkdir(parents=True, exist_ok=True)
        full.write_text(content, encoding="utf-8")
        written += 1

    print(f"checkout: wrote {written} files to {dest} ({payload.get('rows')} rows)", file=sys.stderr)
    print("  grep/read/edit the files; the DB stays source of truth (write back with the typed verbs).",
          file=sys.stderr)
    return 0


def _prepare_dest(dest: Path, force: bool) -> bool:
    """Clear a PRIOR checkout (recognised by its manifest.json) so a re-checkout is
    clean; refuse to touch any other non-empty dir unless --force (mirrors ./r)."""
    if dest.exists():
        if (dest / "manifest.json").exists() or force:
            shutil.rmtree(dest)
        elif any(dest.iterdir()):
            print(f"REJECTED: {dest} exists and isn't a prior checkout — pass --force", file=sys.stderr)
            return False
    dest.mkdir(parents=True, exist_ok=True)
    return True


def _safe_join(dest: Path, rel: str):
    """Resolve rel under dest, refusing any path that escapes it (defense in depth —
    the server only emits scope-relative paths)."""
    full = (dest / rel).resolve()
    base = dest.resolve()
    return full if full == base or base in full.parents else None
