#!/usr/bin/env python3
"""Validate a 1C saved-state watch run list contract."""

from __future__ import annotations

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


EXPECTED_SCHEMA = "onec_saved_state_watch_run_list.v1"


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 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 linked_path(list_path: Path, value: Any) -> Path | None:
    if not value:
        return None
    path = Path(str(value))
    if path.is_absolute():
        return path
    return (list_path.parent / path).resolve()


def has_delta_changes(run: dict[str, Any]) -> bool:
    counts = run.get("counts") or {}
    return any(int(counts.get(key) or 0) > 0 for key in ("delta_objects_added", "delta_objects_removed", "delta_objects_changed"))


def check_run(list_path: Path, run: dict[str, Any], findings: list[dict[str, Any]], *, index: int) -> None:
    if not run.get("run"):
        findings.append(issue("error", "missing_run_name", "Run item is missing run name.", detail={"index": index}))
    run_dir = linked_path(list_path, run.get("run_dir"))
    if run_dir is None or not run_dir.exists():
        findings.append(issue("error", "missing_run_dir", "Run directory is missing.", path=run_dir or "<null>", detail={"index": index}))

    manifest_check = linked_path(list_path, run.get("manifest_check"))
    if manifest_check is not None:
        if not manifest_check.exists():
            findings.append(issue("error", "missing_manifest_check", "Run manifest_check file is missing.", path=manifest_check))
        elif run.get("manifest_check_passed") != load_json(manifest_check).get("passed"):
            findings.append(issue("error", "manifest_check_status_mismatch", "Run manifest_check_passed differs from linked check.", path=manifest_check))

    delta = linked_path(list_path, run.get("delta"))
    if run.get("delta") and (delta is None or not delta.exists()):
        findings.append(issue("error", "missing_delta", "Run delta file is missing.", path=delta or "<null>"))
    delta_check = linked_path(list_path, run.get("delta_check"))
    if delta_check is not None:
        if not delta_check.exists():
            findings.append(issue("error", "missing_delta_check", "Run delta_check file is missing.", path=delta_check))
        elif run.get("delta_check_passed") != load_json(delta_check).get("passed"):
            findings.append(issue("error", "delta_check_status_mismatch", "Run delta_check_passed differs from linked check.", path=delta_check))

    counts = run.get("counts")
    if not isinstance(counts, dict):
        findings.append(issue("error", "missing_run_counts", "Run item must include counts.", detail={"run": run.get("run")}))


def check_list(list_path: Path) -> dict[str, Any]:
    findings: list[dict[str, Any]] = []
    if not list_path.exists():
        findings.append(issue("error", "missing_list", "Watch run list file is missing.", path=list_path))
        return build_result(list_path, findings)
    data = load_json(list_path)
    if data.get("schema") != EXPECTED_SCHEMA:
        findings.append(issue("error", "invalid_schema", "Watch run list schema is invalid.", detail={"schema": data.get("schema")}))

    safety = data.get("safety") or {}
    if safety.get("read_only") is not True:
        findings.append(issue("error", "not_read_only", "Watch run list safety.read_only must be true."))
    if safety.get("sql_write_performed") is not False:
        findings.append(issue("error", "sql_write_flag", "Watch run list safety.sql_write_performed must be false."))

    markdown = linked_path(list_path, data.get("markdown"))
    if data.get("markdown") and (markdown is None or not markdown.exists()):
        findings.append(issue("error", "missing_markdown", "Linked Markdown run-list report is missing.", path=markdown or "<null>"))

    runs = data.get("runs")
    if not isinstance(runs, list):
        findings.append(issue("error", "invalid_runs", "runs must be an array."))
        runs = []
    for index, run in enumerate(runs):
        if isinstance(run, dict):
            check_run(list_path, run, findings, index=index)
        else:
            findings.append(issue("error", "invalid_run_item", "Run item must be an object.", detail={"index": index}))

    latest = data.get("latest")
    if runs:
        if not isinstance(latest, dict):
            findings.append(issue("error", "missing_latest", "latest must be present when runs are present."))
        elif latest.get("run") != runs[0].get("run"):
            findings.append(issue("error", "latest_mismatch", "latest must match the first run item.", detail={"latest": latest.get("run"), "first": runs[0].get("run")}))
    elif latest is not None:
        findings.append(issue("error", "unexpected_latest", "latest must be null when no runs are present."))

    counts = data.get("counts") or {}
    expected = {
        "runs": len(runs),
        "with_delta": sum(1 for run in runs if isinstance(run, dict) and run.get("delta")),
        "with_delta_changes": sum(1 for run in runs if isinstance(run, dict) and has_delta_changes(run)),
    }
    for key, value in expected.items():
        if counts.get(key) != value:
            findings.append(issue("error", "count_mismatch", "Watch run list count mismatch.", detail={"key": key, "reported": counts.get(key), "expected": value}))

    return build_result(list_path, findings)


def build_result(list_path: Path, findings: list[dict[str, Any]]) -> 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_saved_state_watch_run_list_check.v1",
        "list": str(list_path),
        "passed": not errors,
        "findings": findings,
        "counts": {"errors": len(errors), "warnings": len(warnings)},
        "safety": {"read_only": True, "sql_write_performed": False},
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate a 1C saved-state watch run list.")
    parser.add_argument("--list", type=Path, required=True)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    result = check_list(args.list)
    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())
