#!/usr/bin/env python3
"""Create a review bundle from a ready 1C patch workspace without applying it."""

from __future__ import annotations

import argparse
import json
import shutil
import zipfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from check_1c_patch_preflight import build_preflight
from check_1c_patch_bundle import check_bundle
from diff_1c_patch_workspace import build_diff
from render_1c_patch_preflight_markdown import render as render_preflight_markdown


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8-sig"))


def write_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def safe_relative(relative_path: str) -> Path:
    path = Path(relative_path.replace("\\", "/"))
    if path.is_absolute() or ".." in path.parts or not str(path):
        raise SystemExit(f"Unsafe relative path: {relative_path}")
    return path


def slug_from_workspace(workspace: Path) -> str:
    return workspace.name or "onec-patch"


def modified_records(workspace: Path, diff: dict[str, Any]) -> list[dict[str, Any]]:
    manifest = load_json(workspace / "manifest.json")
    records_by_rel = {
        str(record.get("relative_path") or "").replace("\\", "/"): record
        for record in manifest.get("files") or []
    }
    result = []
    for item in diff.get("files") or []:
        if item.get("status") != "modified":
            continue
        relative = str(item.get("relative_path") or "").replace("\\", "/")
        record = dict(records_by_rel.get(relative) or {})
        record["relative_path"] = relative
        record["diff"] = {
            key: (item.get("diff") or {}).get(key)
            for key in ("added_lines", "removed_lines", "hunks", "patch_truncated", "patch_chars")
        }
        record["sha256"] = item.get("sha256")
        result.append(record)
    return result


def render_readme(bundle: dict[str, Any]) -> str:
    lines = [
        "# 1C Patch Review Bundle",
        "",
        f"Workspace: `{bundle.get('workspace')}`",
        f"Created UTC: `{bundle.get('created_at_utc')}`",
        f"Preflight status: `{(bundle.get('preflight') or {}).get('status')}`",
        f"Preferred extension: `{bundle.get('preferred_extension')}`",
        "",
        "## Rules",
        "",
        "- This bundle is for review and disposable-base validation.",
        "- It does not apply changes to SQL, Config, ConfigSave, ConfigCAS, or source extension files.",
        "- Validate loading/packaging in a disposable 1C base before any production action.",
        "",
        "## Modified Files",
        "",
    ]
    for item in bundle.get("files") or []:
        diff = item.get("diff") or {}
        lines.append(
            f"- `{item.get('relative_path')}` ({item.get('kind')}, +{diff.get('added_lines')}/-{diff.get('removed_lines')}, hunks={diff.get('hunks')})"
        )
    lines.extend(
        [
            "",
            "## Contents",
            "",
            "- `files/`: modified working files by extension-relative path.",
            "- `manifest.json`: machine-readable bundle manifest.",
            "- `preflight.json`: full preflight evidence.",
            "- `preflight.md`: human-readable preflight summary.",
            "- `diff.json`: workspace diff evidence.",
        ]
    )
    return "\n".join(lines) + "\n"


def create_bundle(workspace: Path, output_root: Path, slug: str | None, *, force: bool, max_patch_chars: int) -> dict[str, Any]:
    preflight = build_preflight(workspace, max_patch_chars=max_patch_chars)
    if preflight.get("status") != "ready_for_review":
        raise SystemExit(f"Workspace is not ready for review: {preflight.get('status')}")
    diff = build_diff(workspace, max_patch_chars=max_patch_chars)
    records = modified_records(workspace, diff)
    if not records:
        raise SystemExit("No modified files to bundle.")

    manifest = load_json(workspace / "manifest.json")
    bundle_slug = slug or slug_from_workspace(workspace)
    bundle_dir = output_root / bundle_slug
    if bundle_dir.exists() and not force:
        raise SystemExit(f"Bundle already exists: {bundle_dir}. Use --force to replace.")
    if bundle_dir.exists():
        shutil.rmtree(bundle_dir)
    files_root = bundle_dir / "files"
    files_root.mkdir(parents=True, exist_ok=True)

    copied = []
    for record in records:
        rel = safe_relative(str(record.get("relative_path") or ""))
        src = workspace / "working" / rel
        dst = files_root / rel
        dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src, dst)
        copied.append({**record, "bundle_path": str(Path("files") / rel)})

    bundle = {
        "schema": "onec_patch_bundle.v1",
        "created_at_utc": datetime.now(timezone.utc).isoformat(),
        "workspace": str(workspace),
        "source_manifest": str(workspace / "manifest.json"),
        "preferred_extension": manifest.get("preferred_extension"),
        "extension_root": manifest.get("extension_root"),
        "task": manifest.get("task"),
        "preflight": {
            "schema": preflight.get("schema"),
            "status": preflight.get("status"),
            "passed": preflight.get("passed"),
            "diff_summary": preflight.get("diff_summary"),
            "gates": preflight.get("gates"),
        },
        "files": copied,
        "counts": {
            "files": len(copied),
            "added_lines": sum(((item.get("diff") or {}).get("added_lines") or 0) for item in copied),
            "removed_lines": sum(((item.get("diff") or {}).get("removed_lines") or 0) for item in copied),
            "hunks": sum(((item.get("diff") or {}).get("hunks") or 0) for item in copied),
        },
    }
    write_json(bundle_dir / "manifest.json", bundle)
    write_json(bundle_dir / "preflight.json", preflight)
    (bundle_dir / "preflight.md").write_text(render_preflight_markdown(preflight), encoding="utf-8")
    write_json(bundle_dir / "diff.json", diff)
    (bundle_dir / "README.md").write_text(render_readme(bundle), encoding="utf-8")

    zip_path = bundle_dir.with_suffix(".zip")
    if zip_path.exists():
        zip_path.unlink()
    with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
        for path in sorted(bundle_dir.rglob("*")):
            if path.is_file():
                archive.write(path, path.relative_to(bundle_dir))
    bundle_check = check_bundle(bundle_dir, zip_path)
    if not bundle_check.get("passed"):
        raise SystemExit(f"Created bundle failed validation: {bundle_check.get('counts')}")

    return {
        "schema": "onec_patch_bundle_creation.v1",
        "bundle_dir": str(bundle_dir),
        "zip_path": str(zip_path),
        "manifest": str(bundle_dir / "manifest.json"),
        "preflight_status": preflight.get("status"),
        "bundle_check": {
            "schema": bundle_check.get("schema"),
            "passed": bundle_check.get("passed"),
            "counts": bundle_check.get("counts"),
        },
        "counts": bundle["counts"],
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Create a review bundle from a ready 1C patch workspace.")
    parser.add_argument("--workspace", type=Path, required=True)
    parser.add_argument("--output-root", type=Path, default=Path("reports/1c-sql/upo/patch-bundles"))
    parser.add_argument("--slug")
    parser.add_argument("--force", action="store_true")
    parser.add_argument("--max-patch-chars", type=int, default=200000)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    result = create_bundle(args.workspace, args.output_root, args.slug, force=args.force, max_patch_chars=args.max_patch_chars)
    if args.output:
        write_json(args.output, result)
    print(json.dumps(result, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
