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

import argparse
import json
import urllib.request
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError


DEFAULT_BASE_URL = "http://docker.cin.su:8011"
DEFAULT_MCP_URL = "http://docker.cin.su:8021"
EXPECTED_CONTRACT_VERSION = "onec-selector-contract.v1"


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


def rest_rpc(base_url: str, method: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
    _, data = post_json(
        base_url.rstrip("/") + "/rpc",
        {"method": method, "payload": payload},
        timeout=timeout,
        headers={"Content-Type": "application/json; charset=utf-8"},
    )
    return data


def mcp_initialize(mcp_url: str, timeout: float) -> tuple[str | None, dict[str, Any]]:
    body = {
        "jsonrpc": "2.0",
        "id": "saved-state-diff-initialize",
        "method": "initialize",
        "params": {
            "protocolVersion": "2025-06-18",
            "capabilities": {},
            "clientInfo": {"name": "smoke_1c_saved_state_diff", "version": "1"},
        },
    }
    try:
        headers, data = post_json(
            mcp_url.rstrip("/") + "/mcp",
            body,
            timeout=timeout,
            headers={"Accept": "application/json, text/event-stream"},
        )
    except HTTPError as exc:
        return None, {"status": "error", "error": "mcp_initialize_http_error", "http_status": exc.code, "response": exc.read().decode("utf-8", errors="replace")}
    except URLError as exc:
        return None, {"status": "error", "error": "mcp_initialize_url_error", "message": str(exc)}
    if "error" in data:
        return None, {"status": "error", "error": "mcp_initialize_jsonrpc_error", "details": data.get("error")}
    return headers.get("Mcp-Session-Id"), data


def mcp_rpc(mcp_url: str, method: str, payload: dict[str, Any], timeout: float, session_id: str | None) -> dict[str, Any]:
    headers = {"Accept": "application/json, text/event-stream"}
    if session_id:
        headers["Mcp-Session-Id"] = session_id
    body = {
        "jsonrpc": "2.0",
        "id": f"saved-state-diff-{method}",
        "method": "tools/call",
        "params": {"name": "onec_request", "arguments": {"method": method, "payload": payload}},
    }
    try:
        _, data = post_json(mcp_url.rstrip("/") + "/mcp", body, timeout=timeout, headers=headers)
    except HTTPError as exc:
        return {"status": "error", "error": "mcp_http_error", "http_status": exc.code, "response": exc.read().decode("utf-8", errors="replace")}
    except URLError as exc:
        return {"status": "error", "error": "mcp_url_error", "message": str(exc)}
    if "error" in data:
        return {"status": "error", "error": "mcp_jsonrpc_error", "details": data.get("error")}
    result = data.get("result") if isinstance(data.get("result"), dict) else {}
    content = result.get("content") if isinstance(result.get("content"), list) else []
    first = content[0] if content and isinstance(content[0], dict) else {}
    text = first.get("text")
    if not isinstance(text, str):
        return {"status": "error", "error": "mcp_tool_text_missing", "result": result}
    try:
        decoded = json.loads(text)
    except json.JSONDecodeError as exc:
        return {"status": "error", "error": "mcp_tool_text_not_json", "message": str(exc), "text": text[:500]}
    return decoded if isinstance(decoded, dict) else {"status": "error", "error": "mcp_tool_payload_not_object", "payload": decoded}


def rpc_call(endpoint_url: str, method: str, payload: dict[str, Any], timeout: float, *, transport: str, session_id: str | None) -> dict[str, Any]:
    if transport == "mcp":
        return mcp_rpc(endpoint_url, method, payload, timeout, session_id)
    return rest_rpc(endpoint_url, method, payload, timeout)


def require(condition: bool, message: str, failures: list[str]) -> None:
    if not condition:
        failures.append(message)


def method_names(help_result: dict[str, Any]) -> set[str]:
    names: set[str] = set()
    for item in help_result.get("methods") or []:
        if isinstance(item, dict) and isinstance(item.get("name"), str):
            names.add(item["name"])
        elif isinstance(item, str):
            names.add(item)
    return names


def first_saved_form(endpoint_url: str, base_id: str, timeout: float, *, transport: str, session_id: str | None, table: str) -> dict[str, Any] | None:
    result = rpc_call(
        endpoint_url,
        "metadata.saved_state.forms.search",
        {
            "base_id": base_id,
            "tables": [table],
            "limit": 1,
            "scan_limit": 200,
            "timeout_seconds": int(timeout),
            "include_storage": True,
        },
        timeout,
        transport=transport,
        session_id=session_id,
    )
    forms = result.get("forms") if isinstance(result.get("forms"), list) else []
    return forms[0] if forms and isinstance(forms[0], dict) else None


def run_smoke(endpoint_url: str, base_id: str, timeout: float, *, transport: str, saved_state_table: str) -> dict[str, Any]:
    failures: list[str] = []
    checks: dict[str, Any] = {}
    session_id: str | None = None
    initialize_result: dict[str, Any] | None = None
    if transport == "mcp":
        session_id, initialize_result = mcp_initialize(endpoint_url, timeout)
        contract_version = (((initialize_result.get("result") or {}).get("serverInfo") or {}).get("contract_version") if isinstance(initialize_result.get("result"), dict) else None)
        checks["mcp.initialize"] = {
            "status": "ok" if session_id and contract_version == EXPECTED_CONTRACT_VERSION else "error",
            "session": bool(session_id),
            "contract_version": contract_version,
        }
        require(bool(session_id), "MCP initialize must return Mcp-Session-Id", failures)
        require(contract_version == EXPECTED_CONTRACT_VERSION, "MCP initialize must report expected contract version", failures)

    help_result = rpc_call(endpoint_url, "help.methods", {}, timeout, transport=transport, session_id=session_id)
    names = method_names(help_result)
    checks["method_exposed"] = {
        "status": "ok" if "metadata.saved_state.diff" in names else "missing",
        "method_count": len(names),
        "method": "metadata.saved_state.diff",
    }
    require("metadata.saved_state.diff" in names, "metadata.saved_state.diff must be exposed by help.methods", failures)

    candidate = first_saved_form(endpoint_url, base_id, timeout, transport=transport, session_id=session_id, table=saved_state_table)
    checked_table = saved_state_table
    if candidate is None and saved_state_table == "ConfigSave":
        candidate = first_saved_form(endpoint_url, base_id, timeout, transport=transport, session_id=session_id, table="ConfigCASSave")
        checked_table = "ConfigCASSave"
    if candidate is None and saved_state_table == "ConfigCASSave":
        candidate = first_saved_form(endpoint_url, base_id, timeout, transport=transport, session_id=session_id, table="ConfigSave")
        checked_table = "ConfigSave"

    if candidate:
        diff_payload = {
            "base_id": base_id,
            "table": candidate.get("table") or checked_table,
            "file_name": candidate.get("file_name"),
            "max_changes": 20,
            "max_text_diff_lines": 20,
            "timeout_seconds": int(timeout),
            "include_storage": True,
        }
        diff = rpc_call(endpoint_url, "metadata.saved_state.diff", diff_payload, timeout, transport=transport, session_id=session_id)
        checks["diff_existing_saved_state"] = {
            "status": diff.get("status"),
            "schema": diff.get("schema"),
            "table": diff_payload["table"],
            "file_name": diff_payload["file_name"],
            "needs_prepare": diff.get("needs_prepare"),
            "freshness": (diff.get("freshness") or {}).get("status") if isinstance(diff.get("freshness"), dict) else None,
            "current_source": (diff.get("current_state") or {}).get("source") if isinstance(diff.get("current_state"), dict) else None,
        }
        require(diff.get("schema") == "onec_saved_state_diff.v1", "saved-state diff must return expected schema", failures)
        require(diff.get("status") in {"changed", "unchanged", "not_found"}, "saved-state diff must return changed/unchanged/not_found", failures)
        if diff.get("status") in {"changed", "unchanged"}:
            require(diff.get("needs_prepare") is False, "existing saved-state diff must not request prepare", failures)
            freshness = diff.get("freshness") if isinstance(diff.get("freshness"), dict) else {}
            require(freshness.get("status") == "live_sql_verified", "existing saved-state diff must be live SQL verified", failures)
    else:
        missing_file = "codex-smoke-missing-saved-state-target"
        diff = rpc_call(
            endpoint_url,
            "metadata.saved_state.diff",
            {
                "base_id": base_id,
                "table": saved_state_table,
                "file_name": missing_file,
                "timeout_seconds": int(timeout),
                "include_storage": True,
            },
            timeout,
            transport=transport,
            session_id=session_id,
        )
        checks["diff_missing_saved_state"] = {
            "status": diff.get("status"),
            "schema": diff.get("schema"),
            "table": saved_state_table,
            "file_name": missing_file,
            "needs_prepare": diff.get("needs_prepare"),
            "prepare_method": (diff.get("prepare_payload") or {}).get("method") if isinstance(diff.get("prepare_payload"), dict) else None,
        }
        require(diff.get("schema") == "onec_saved_state_diff.v1", "missing saved-state diff must return expected schema", failures)
        require(diff.get("status") == "not_found", "missing saved-state diff must return not_found", failures)
        require(diff.get("needs_prepare") is True, "missing saved-state diff must request prepare", failures)
        require((diff.get("prepare_payload") or {}).get("method") == "metadata.saved_state.prepare", "missing saved-state diff must include prepare payload", failures)

    return {
        "schema": "onec_saved_state_diff_smoke.v1",
        "status": "ok" if not failures else "failed",
        "endpoint_url": endpoint_url,
        "transport": transport,
        "base_id": base_id,
        "saved_state_table": saved_state_table,
        "checks": checks,
        "failures": failures,
        **({"mcp_initialize": initialize_result} if initialize_result and failures else {}),
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Smoke deployed metadata.saved_state.diff behavior without SQL writes.")
    parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
    parser.add_argument("--mcp-url", default=DEFAULT_MCP_URL)
    parser.add_argument("--transport", choices=("rest", "mcp"), default="rest")
    parser.add_argument("--base-id", default="upo_test")
    parser.add_argument("--saved-state-table", choices=("ConfigSave", "ConfigCASSave"), default="ConfigSave")
    parser.add_argument("--timeout", type=float, default=30.0)
    parser.add_argument("--report", type=Path)
    args = parser.parse_args()

    endpoint_url = args.mcp_url if args.transport == "mcp" else args.base_url
    report = run_smoke(endpoint_url, args.base_id, args.timeout, transport=args.transport, saved_state_table=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")
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 0 if report["status"] == "ok" else 1


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