#!/usr/bin/env python3
"""Validate a disposable 1C extension XML staging copy."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path
from typing import Any

from check_1c_patch_bundle import check_bundle


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 sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def issue(severity: str, code: str, message: str, *, path: Path | str | None = None, detail: dict[str, Any] | None = None) -> dict[str, Any]:
    result: dict[str, Any] = {"severity": severity, "code": code, "message": message}
    if path is not None:
        result["path"] = str(path)
    if detail:
        result["detail"] = detail
    return result


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 ValueError(relative_path)
    return path


def is_within(path: Path, root: Path) -> bool:
    try:
        path.resolve().relative_to(root.resolve())
        return True
    except ValueError:
        return False


def check_staging(staging_dir: Path) -> dict[str, Any]:
    findings: list[dict[str, Any]] = []
    file_checks: list[dict[str, Any]] = []
    manifest: dict[str, Any] | None = None

    if not staging_dir.exists() or not staging_dir.is_dir():
        findings.append(issue("error", "missing_staging_dir", "Staging directory is missing.", path=staging_dir))
        return build_result(staging_dir, manifest, findings, file_checks, None)

    manifest_path = staging_dir / "_codex_staging_manifest.json"
    if not manifest_path.exists():
        findings.append(issue("error", "missing_staging_manifest", "Staging manifest is missing.", path=manifest_path))
        return build_result(staging_dir, manifest, findings, file_checks, None)

    manifest = load_json(manifest_path)
    if manifest.get("schema") != "onec_extension_staging.v1":
        findings.append(issue("error", "invalid_staging_schema", "Staging manifest schema is not onec_extension_staging.v1.", path=manifest_path, detail={"schema": manifest.get("schema")}))

    declared_staging_dir = Path(str(manifest.get("staging_dir") or ""))
    if declared_staging_dir and declared_staging_dir.resolve() != staging_dir.resolve():
        findings.append(issue("error", "staging_dir_mismatch", "Manifest staging_dir does not match checked directory.", path=manifest_path, detail={"manifest_staging_dir": str(declared_staging_dir), "checked_staging_dir": str(staging_dir)}))

    safety = manifest.get("safety") if isinstance(manifest.get("safety"), dict) else {}
    expected_safety = {
        "source_extension_modified": False,
        "sql_modified": False,
        "requires_disposable_1c_validation": True,
    }
    for key, expected in expected_safety.items():
        actual = safety.get(key)
        if actual is not expected:
            findings.append(issue("error", "invalid_staging_safety_flag", "Staging safety flag has an unexpected value.", path=manifest_path, detail={"flag": key, "expected": expected, "actual": actual}))

    extension_root = Path(str(manifest.get("extension_root") or ""))
    if not extension_root.exists() or not extension_root.is_dir():
        findings.append(issue("error", "missing_extension_root", "Source extension root is missing.", path=extension_root))

    bundle_dir = Path(str(manifest.get("bundle_dir") or ""))
    bundle_check: dict[str, Any] | None = None
    if not bundle_dir.exists() or not bundle_dir.is_dir():
        findings.append(issue("error", "missing_bundle_dir", "Bundle directory recorded in staging manifest is missing.", path=bundle_dir))
    else:
        bundle_check = check_bundle(bundle_dir)
        if not bundle_check.get("passed"):
            findings.append(issue("error", "bundle_check_failed", "Bundle recorded in staging manifest does not pass validation.", path=bundle_dir, detail={"counts": bundle_check.get("counts")}))

    for record in manifest.get("files") or []:
        relative_raw = str(record.get("relative_path") or "")
        staged_path_raw = str(record.get("staged_path") or "")
        check: dict[str, Any] = {
            "relative_path": relative_raw,
            "staged_path": staged_path_raw,
            "exists": False,
            "expected_staged_sha256": record.get("staged_sha256"),
            "expected_working_sha256": record.get("expected_working_sha256"),
            "expected_source_original_sha256": record.get("source_original_sha256"),
        }
        try:
            relative = safe_relative(relative_raw)
        except ValueError:
            findings.append(issue("error", "unsafe_relative_path", "Unsafe relative_path in staging manifest.", path=manifest_path, detail={"relative_path": relative_raw}))
            file_checks.append(check)
            continue

        staged_path = Path(staged_path_raw) if staged_path_raw else staging_dir / relative
        expected_staged_path = staging_dir / relative
        if staged_path.resolve() != expected_staged_path.resolve():
            findings.append(issue("error", "staged_path_mismatch", "Manifest staged_path does not match staging_dir/relative_path.", path=manifest_path, detail={"staged_path": str(staged_path), "expected": str(expected_staged_path)}))
        if not is_within(staged_path, staging_dir):
            findings.append(issue("error", "staged_path_escape", "Manifest staged_path escapes staging directory.", path=staged_path))
            file_checks.append(check)
            continue

        check["exists"] = staged_path.exists()
        if not staged_path.exists():
            findings.append(issue("error", "missing_staged_file", "Staged file is missing.", path=staged_path))
        else:
            staged_hash = sha256_file(staged_path)
            check["staged_sha256"] = staged_hash
            expected_hashes = [record.get("staged_sha256"), record.get("expected_working_sha256")]
            for expected in [value for value in expected_hashes if value]:
                if staged_hash != expected:
                    findings.append(issue("error", "staged_file_hash_mismatch", "Staged file hash does not match manifest.", path=staged_path, detail={"expected": expected, "actual": staged_hash}))

        source_path = extension_root / relative
        check["source_path"] = str(source_path)
        check["source_exists"] = source_path.exists()
        expected_source_hash = record.get("source_original_sha256")
        if expected_source_hash is not None:
            if not source_path.exists():
                findings.append(issue("error", "missing_source_file", "Source extension file recorded during staging is now missing.", path=source_path))
            else:
                source_hash = sha256_file(source_path)
                check["source_sha256"] = source_hash
                if source_hash != expected_source_hash:
                    findings.append(issue("error", "source_file_changed", "Source extension file changed after staging was created.", path=source_path, detail={"expected": expected_source_hash, "actual": source_hash}))
        file_checks.append(check)

    return build_result(staging_dir, manifest, findings, file_checks, bundle_check)


def build_result(staging_dir: Path, manifest: dict[str, Any] | None, findings: list[dict[str, Any]], file_checks: list[dict[str, Any]], bundle_check: dict[str, Any] | None) -> dict[str, Any]:
    errors = [row for row in findings if row.get("severity") == "error"]
    warnings = [row for row in findings if row.get("severity") == "warning"]
    return {
        "schema": "onec_extension_staging_check.v1",
        "staging_dir": str(staging_dir),
        "staging_schema": (manifest or {}).get("schema"),
        "bundle_dir": (manifest or {}).get("bundle_dir"),
        "bundle_check": {
            "schema": (bundle_check or {}).get("schema"),
            "passed": (bundle_check or {}).get("passed"),
            "counts": (bundle_check or {}).get("counts"),
        } if bundle_check else None,
        "passed": not errors,
        "findings": findings,
        "file_checks": file_checks,
        "counts": {
            "files": len(file_checks),
            "errors": len(errors),
            "warnings": len(warnings),
        },
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate a disposable 1C extension XML staging copy.")
    parser.add_argument("--staging-dir", type=Path, required=True)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    result = check_staging(args.staging_dir)
    if args.output:
        write_json(args.output, result)
    print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "counts": result["counts"]}, ensure_ascii=False))
    return 0 if result["passed"] else 2


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