#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys
import urllib.request
from pathlib import Path
from typing import Any


DEFAULT_BASE_URL = "http://docker.cin.su:8011"
DEFAULT_TABLES = ("ConfigCASSave", "ConfigSave")
ALLOWED_TABLES = {"ConfigCASSave", "ConfigSave"}


def request_json(method: str, url: str, *, payload: dict[str, Any] | None, timeout: float) -> tuple[int, dict[str, Any] | str]:
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8") if payload is not None else None
    request = urllib.request.Request(
        url,
        data=data,
        method=method,
        headers={"Content-Type": "application/json; charset=utf-8"},
    )
    with urllib.request.urlopen(request, timeout=timeout) as response:
        raw = response.read().decode("utf-8")
        return response.status, json.loads(raw) if raw else {}


def rpc(base_url: str, method: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
    status, response = request_json(
        "POST",
        base_url.rstrip("/") + "/rpc",
        payload={"method": method, "payload": payload},
        timeout=timeout,
    )
    if status != 200 or not isinstance(response, dict):
        raise RuntimeError(f"{method} failed: status={status}, response={response!r}")
    return response


def health_summary(base_url: str, base_id: str, timeout: float) -> dict[str, Any]:
    status, response = request_json("GET", f"{base_url.rstrip('/')}/health?base_id={base_id}", payload=None, timeout=timeout)
    if status != 200 or not isinstance(response, dict):
        return {"status": "error", "http_status": status, "response": response}
    live_sql = response.get("live_sql") if isinstance(response.get("live_sql"), dict) else {}
    return {
        "status": response.get("status"),
        "contract_version": response.get("contract_version"),
        "live_sql": {
            "configured": live_sql.get("configured"),
            "server": live_sql.get("server"),
            "database": live_sql.get("database"),
        },
    }


def saved_state_row_counts(base_url: str, base_id: str, tables: list[str], timeout: float) -> dict[str, Any]:
    selects = [f"SELECT '{table}' AS TableName, COUNT(*) AS RowsCount FROM {table}" for table in tables]
    result = rpc(
        base_url,
        "query.run",
        {
            "base_id": base_id,
            "diagnostic": True,
            "query": "\nUNION ALL\n".join(selects),
            "timeout_seconds": int(timeout),
        },
        timeout,
    )
    counts: dict[str, int] = {table: 0 for table in tables}
    for row in result.get("rows") or []:
        if not isinstance(row, dict):
            continue
        table = str(row.get("TableName") or "")
        if table in counts and isinstance(row.get("RowsCount"), int):
            counts[table] = int(row["RowsCount"])
    return {
        "status": result.get("status"),
        "validation": result.get("validation"),
        "counts": counts,
        "raw_counts": result.get("counts"),
    }


def search_saved_state(base_url: str, base_id: str, tables: list[str], timeout: float) -> dict[str, Any]:
    common = {"base_id": base_id, "tables": tables, "limit": 3, "timeout_seconds": int(timeout)}
    forms = rpc(base_url, "metadata.saved_state.forms.search", common, timeout)
    modules = rpc(base_url, "metadata.saved_state.modules.search", {**common, "scan_limit": 100}, timeout)
    return {
        "forms": {
            "status": forms.get("status"),
            "counts": forms.get("counts"),
            "sample": forms.get("forms") or [],
        },
        "modules": {
            "status": modules.get("status"),
            "counts": modules.get("counts"),
            "sample": modules.get("modules") or [],
        },
    }


def build_report(base_url: str, base_id: str, tables: list[str], timeout: float, saved_state_table: str | None) -> dict[str, Any]:
    report: dict[str, Any] = {
        "schema": "onec_saved_state_strict_readiness.v1",
        "base_url": base_url,
        "base_id": base_id,
        "saved_state_table": saved_state_table,
        "tables": tables,
        "ready": False,
        "status": "error",
        "checks": {},
        "recommendations": [],
    }
    report["checks"]["health"] = health_summary(base_url, base_id, timeout)
    report["checks"]["row_counts"] = saved_state_row_counts(base_url, base_id, tables, timeout)
    report["checks"]["saved_state_search"] = search_saved_state(base_url, base_id, tables, timeout)

    row_counts = (report["checks"]["row_counts"] or {}).get("counts") or {}
    forms_counts = (((report["checks"]["saved_state_search"] or {}).get("forms") or {}).get("counts") or {})
    modules_counts = (((report["checks"]["saved_state_search"] or {}).get("modules") or {}).get("counts") or {})
    total_rows = sum(value for value in row_counts.values() if isinstance(value, int))
    forms = forms_counts.get("forms") if isinstance(forms_counts.get("forms"), int) else 0
    modules = modules_counts.get("modules") if isinstance(modules_counts.get("modules"), int) else 0

    report["summary"] = {
        "saved_state_rows": total_rows,
        "forms": forms,
        "modules": modules,
    }
    if forms > 0 and modules > 0:
        report["ready"] = True
        report["status"] = "ready"
        report["recommendations"].append("Strict saved-state smoke can be attempted: form and module saved-state candidates are present in the selected save layer.")
    elif total_rows == 0:
        report["status"] = "blocked_no_saved_state_rows"
        report["recommendations"].append(
            f"No unactivated Configurator changes are present in {', '.join(tables)}. To prepare a strict write test, copy the target object from Config/ConfigCAS into the selected save layer using the approved saved-state workflow, then rerun readiness."
        )
    else:
        report["status"] = "blocked_no_strict_candidates"
        report["recommendations"].append(
            "Saved-state rows exist, but the adapter did not find both form and module candidates needed by strict write-and-rollback smoke."
        )
    return report


def main() -> int:
    parser = argparse.ArgumentParser(description="Read-only readiness check for strict 1C saved-state smoke tests.")
    parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
    parser.add_argument("--base-id", default="upo_test")
    parser.add_argument("--table", action="append", choices=sorted(ALLOWED_TABLES), help="Saved-state table to inspect. Repeatable.")
    parser.add_argument("--saved-state-table", choices=sorted(ALLOWED_TABLES), help="Expected saved-state table for the strict smoke target.")
    parser.add_argument("--timeout", type=float, default=30.0)
    parser.add_argument("--report", type=Path)
    parser.add_argument("--require-ready", action="store_true", help="Exit non-zero when strict saved-state smoke is not ready.")
    parser.add_argument("--json", action="store_true", help="Print full JSON report.")
    args = parser.parse_args()

    if args.saved_state_table and args.table and any(table != args.saved_state_table for table in args.table):
        parser.error("--saved-state-table must match --table when both are provided")
    tables = args.table or ([args.saved_state_table] if args.saved_state_table else list(DEFAULT_TABLES))
    report = build_report(args.base_url, args.base_id, tables, args.timeout, args.saved_state_table)
    if args.report:
        args.report.parent.mkdir(parents=True, exist_ok=True)
        args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

    if args.json or args.require_ready or not report["ready"]:
        print(json.dumps(report, ensure_ascii=True, indent=2), file=sys.stderr if args.require_ready and not report["ready"] else sys.stdout)
    else:
        print(f"OK: strict saved-state smoke readiness passed for {args.base_id}.")
    return 0 if report["ready"] or not args.require_ready else 1


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

