"""Download template starter snapshots and prepare reviewable Cloud contract changes.

python -m scripts.prepare_template_cloud_contract --output /private/template-review
Add --ref accounting-nuxt to select one template.

Reads the Core catalog and GitHub; writes only new local directories. Never writes Cloud
data, changes catalog SHAs, pushes branches, or deploys templates. Custom persistence
conflicts remain explicit migration work, not silently replaced by generic adapters.
"""

from __future__ import annotations

import argparse
import asyncio
import io
import json
import zipfile
from pathlib import Path
from urllib.parse import quote

import httpx
from sqlalchemy import text


def extract_snapshot(content: bytes, destination: Path) -> None:
    root = destination.resolve()
    with zipfile.ZipFile(io.BytesIO(content)) as archive:
        for entry in archive.infolist():
            parts = entry.filename.split("/")[1:]
            if not parts or entry.is_dir():
                continue
            if any(p in {"..", "."} or "\\" in p or ":" in p for p in parts):
                raise ValueError("Unsafe archive path")
            target = root.joinpath(*parts).resolve()
            if not target.is_relative_to(root):
                raise ValueError("Unsafe archive path")
            # Git symlinks must not turn this into an escape or a misleading source file.
            if (entry.external_attr >> 16) & 0o170000 == 0o120000:
                raise ValueError("Review source symlinks separately")
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_bytes(archive.read(entry))


def prepare_checkout(root: Path, framework: str) -> dict:
    from src.domain.codegen.backend_guard import validate_backend_repo
    from src.domain.codegen.record_adapter import platform_adapter_files
    from src.domain.templates.service import _with_cloud_access_files

    # Only platform documents and recognized adapters are candidates for automatic updates.
    names = [".cursor/instructions.md", ".env.example"]
    existing = {
        name: (root / name).read_text(encoding="utf-8") for name in names if (root / name).is_file()
    }
    proposed = _with_cloud_access_files(existing, framework)
    proposed.update(platform_adapter_files(framework))
    written, conflicts = [], []
    adapters = platform_adapter_files(framework)
    for name, content in proposed.items():
        target = root / name
        if target.is_symlink():
            raise ValueError("Cannot update a source symlink")
        if name in adapters and target.exists():
            source = target.read_text(encoding="utf-8")
            if "p2hs-records" not in source and "p2hs-auth-users" not in source and "p2hs-auth-password" not in source:
                conflicts.append(name)
                continue
        raw = content.encode() if isinstance(content, str) else content
        if target.is_file() and target.read_bytes() == raw:
            continue
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_bytes(raw)
        written.append(name)
    try:
        validate_backend_repo(str(root))
        guard = "passed"
    except ValueError as exc:
        guard = str(exc)
    return {
        "written": written,
        "custom_adapter_conflicts": conflicts,
        "backend_guard": guard,
        "status": "needs_build_and_runtime_review"
        if guard == "passed" and not conflicts
        else "needs_code_migration",
    }


async def run(args) -> int:
    from src.core.database import async_session_factory
    from src.integrations.github_client import GitHubAppClient

    root = Path(args.output).resolve()
    root.mkdir(parents=True, exist_ok=True)
    async with async_session_factory() as db:
        await db.execute(text("SET TRANSACTION READ ONLY"))
        sql = (
            "SELECT ref, framework, github_repo, starter_branch, github_installation_id "
            "FROM templates WHERE deleted_at IS NULL"
        )
        params = {}
        if args.ref:
            sql += " AND ref = :ref"
            params["ref"] = args.ref
        else:
            sql += " AND is_active = true"
        rows = (await db.execute(text(sql + " ORDER BY ref"), params)).mappings().all()
    reports = []
    for row in rows:
        report = dict(row)
        destination = (root / row["ref"]).resolve()
        if not destination.is_relative_to(root) or destination == root:
            raise ValueError("Invalid template reference")
        try:
            if destination.exists():
                raise ValueError("Output already exists; choose a fresh review directory")
            gh = GitHubAppClient(installation_id=row["github_installation_id"] or None)
            sha = await gh.get_ref_head_sha(row["github_repo"], row["starter_branch"])
            if not sha:
                raise ValueError("Starter branch is unavailable")
            report["source_sha"] = sha
            async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
                response = await client.get(
                    f"https://api.github.com/repos/{row['github_repo']}/zipball/"
                    f"{quote(sha, safe='')}",
                    headers=await gh._headers(),
                )
                response.raise_for_status()
            extract_snapshot(response.content, destination)
            report.update(prepare_checkout(destination, row["framework"]))
        except Exception as exc:
            # Do not print authentication exceptions or URLs containing credentials.
            report.update(status="failed", error_type=type(exc).__name__)
        reports.append(report)
        print(row["ref"], report["status"], flush=True)
    (root / "review.json").write_text(json.dumps(reports, indent=2), encoding="utf-8")
    return 1 if not rows or any(r["status"] == "failed" for r in reports) else 0


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--ref", help="One template reference; default: all active templates")
    parser.add_argument("--output", required=True, help="New local review directory")
    raise SystemExit(asyncio.run(run(parser.parse_args())))
