from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from typing import Any


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


for stream in (sys.stdout, sys.stderr):
    if hasattr(stream, "reconfigure"):
        stream.reconfigure(encoding="utf-8")


def rpc(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: int = 60) -> dict[str, Any]:
    body = json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8")
    service_token = os.environ.get("ONEC_ADAPTER_SERVICE_TOKEN", "").strip()
    headers = {"Content-Type": "application/json; charset=utf-8"}
    if service_token:
        headers["Authorization"] = f"Bearer {service_token}"
    request = urllib.request.Request(
        adapter_url.rstrip("/") + "/rpc",
        data=body,
        headers=headers,
        method="POST",
    )
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            return json.loads(response.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"{method} HTTP {exc.code}: {detail}") from exc


def post_json(url: str, payload: dict[str, Any], *, timeout: int, 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"),
        headers={"Content-Type": "application/json", "Accept": "application/json, text/event-stream", **(headers or {})},
        method="POST",
    )
    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 mcp_initialize(mcp_url: str, timeout: int) -> tuple[str | None, dict[str, Any]]:
    body = {
        "jsonrpc": "2.0",
        "id": "code-write-saved-state-initialize",
        "method": "initialize",
        "params": {
            "protocolVersion": "2025-06-18",
            "capabilities": {},
            "clientInfo": {"name": "smoke_1c_code_write_saved_state", "version": "1"},
        },
    }
    try:
        headers, data = post_json(mcp_url.rstrip("/") + "/mcp", body, timeout=timeout)
    except urllib.error.HTTPError as exc:
        raw = exc.read().decode("utf-8", errors="replace")
        return None, {"status": "error", "error": "mcp_initialize_http_error", "http_status": exc.code, "response": raw}
    except urllib.error.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: int, session_id: str | None) -> dict[str, Any]:
    headers: dict[str, str] = {}
    if session_id:
        headers["Mcp-Session-Id"] = session_id
    body = {
        "jsonrpc": "2.0",
        "id": f"code-write-saved-state-{method}",
        "method": "tools/call",
        "params": {"name": "onec_request", "arguments": {"method": method, "payload": payload}},
    }
    try:
        _headers, data = post_json(mcp_url.rstrip("/") + "/mcp", body, timeout=timeout, headers=headers)
    except urllib.error.HTTPError as exc:
        raw = exc.read().decode("utf-8", errors="replace")
        return {"status": "error", "error": "mcp_http_error", "http_status": exc.code, "response": raw}
    except urllib.error.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], *, transport: str, timeout: int, session_id: str | None) -> dict[str, Any]:
    if transport == "mcp":
        payload = {**payload, "mcp_sync": True}
        return mcp_rpc(endpoint_url, method, payload, timeout=timeout, session_id=session_id)
    return rpc(endpoint_url, method, payload, timeout=timeout)


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


def require_saved_state_write(result: dict[str, Any], label: str, failures: list[str]) -> None:
    write_mode = result.get("write_mode") if isinstance(result.get("write_mode"), dict) else {}
    require(result.get("status") == "applied", f"{label} must return status=applied", failures)
    require(result.get("applied") is True, f"{label} must set applied=true", failures)
    require(write_mode.get("target") == "saved_state", f"{label} must target saved_state", failures)
    require(write_mode.get("activation_state") == "not_activated", f"{label} must not activate runtime state", failures)


def first_unique_fragment(text: str) -> str:
    for line in text.splitlines():
        candidate = line.strip()
        if candidate and text.count(candidate) == 1:
            return candidate
    return text


def is_optional_write_gate_block(result: dict[str, Any]) -> bool:
    return result.get("status") == "blocked" and result.get("error") in {
        "needs_repository_lock",
        "support_gate_blocked",
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Smoke-test agent-facing 1C code.write saved-state workflow.")
    parser.add_argument("--transport", choices=("rest", "mcp"), default="rest")
    parser.add_argument("--adapter-url", default=DEFAULT_ADAPTER_URL)
    parser.add_argument("--mcp-url", default=DEFAULT_MCP_URL)
    parser.add_argument("--base-id", default="upo_test")
    parser.add_argument("--extension", default="test2")
    parser.add_argument("--object-type", default="CommonForm")
    parser.add_argument("--object-name", default="t_Форма")
    parser.add_argument("--routine-name", default="ЗаменаДомена")
    parser.add_argument("--allow-missing-target", action="store_true")
    parser.add_argument(
        "--allow-blocked-write-gate",
        action="store_true",
        help="Skip successfully when the target exists but repository/support policy blocks the first write.",
    )
    parser.add_argument("--timeout", type=int, default=60)
    parser.add_argument("--report", type=str)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()
    endpoint_url = args.mcp_url if args.transport == "mcp" else args.adapter_url

    selector = {
        "base_id": args.base_id,
        "object_type": args.object_type,
        "object_name": args.object_name,
        "routine_name": args.routine_name,
        "include_text": True,
        "max_chars": 4000,
    }
    failures: list[str] = []
    steps: list[dict[str, Any]] = []
    session_id: str | None = None
    initialize_result: dict[str, Any] | None = None

    if args.transport == "mcp":
        session_id, initialize_result = mcp_initialize(endpoint_url, args.timeout)
        contract_version = (((initialize_result.get("result") or {}).get("serverInfo") or {}).get("contract_version") if isinstance(initialize_result.get("result"), dict) else None)
        steps.append({"name": "mcp.initialize", "status": "ok" if session_id and contract_version == EXPECTED_CONTRACT_VERSION else "error", "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)

    before = rpc_call(endpoint_url, "code.read", {**selector, "state": "working"}, transport=args.transport, timeout=args.timeout, session_id=session_id)
    steps.append({"name": "code.read working before", "status": before.get("status")})
    if args.allow_missing_target and before.get("status") != "ok":
        report = {
            "schema": "onec_code_write_saved_state_smoke.v1",
            "status": "skipped_missing_target",
            "skipped": True,
            "endpoint_url": endpoint_url,
            "transport": args.transport,
            "base_id": args.base_id,
            "target": {"object_type": args.object_type, "object_name": args.object_name, "routine_name": args.routine_name},
            "steps": steps,
            "failures": [],
            **({"mcp_initialize": initialize_result} if initialize_result and args.transport == "mcp" else {}),
        }
        if args.report:
            os.makedirs(os.path.dirname(os.path.abspath(args.report)), exist_ok=True)
            with open(args.report, "w", encoding="utf-8") as handle:
                json.dump(report, handle, ensure_ascii=False, indent=2)
        if args.json:
            print(json.dumps(report, ensure_ascii=False, indent=2))
        else:
            print(f"1C code.write saved-state smoke: {report['status']}")
        return 0
    require(before.get("status") == "ok", "working code.read before write must be ok", failures)
    routine_text = str(before.get("text") or "")
    require(bool(routine_text.strip()), "working code.read must return routine text", failures)

    write = rpc_call(
        endpoint_url,
        "code.write",
        {
            "base_id": args.base_id,
            "object_type": args.object_type,
            "object_name": args.object_name,
            "routine_name": args.routine_name,
            "routine_text": routine_text,
            "mode": "apply",
        },
        transport=args.transport,
        timeout=args.timeout,
        session_id=session_id,
    )
    steps.append(
        {
            "name": "code.write apply",
            "status": write.get("status"),
            "applied": write.get("applied"),
            "write_mode": write.get("write_mode"),
        }
    )
    if args.allow_blocked_write_gate and is_optional_write_gate_block(write):
        report = {
            "schema": "onec_code_write_saved_state_smoke.v1",
            "status": "skipped_write_gate",
            "skipped": True,
            "endpoint_url": endpoint_url,
            "transport": args.transport,
            "base_id": args.base_id,
            "target": {"object_type": args.object_type, "object_name": args.object_name, "routine_name": args.routine_name},
            "steps": steps,
            "write_gate": {
                "status": write.get("status"),
                "error": write.get("error"),
                "diagnostics": write.get("diagnostics"),
            },
            "failures": [],
            **({"mcp_initialize": initialize_result} if initialize_result and args.transport == "mcp" else {}),
        }
        if args.report:
            os.makedirs(os.path.dirname(os.path.abspath(args.report)), exist_ok=True)
            with open(args.report, "w", encoding="utf-8") as handle:
                json.dump(report, handle, ensure_ascii=False, indent=2)
        if args.json:
            print(json.dumps(report, ensure_ascii=False, indent=2))
        else:
            print(f"1C code.write saved-state smoke: {report['status']}")
        return 0
    write_mode = write.get("write_mode") if isinstance(write.get("write_mode"), dict) else {}
    require_saved_state_write(write, "code.write routine apply", failures)

    after = rpc_call(endpoint_url, "code.read", {**selector, "state": "working"}, transport=args.transport, timeout=args.timeout, session_id=session_id)
    steps.append({"name": "code.read working after", "status": after.get("status"), "current_state": after.get("current_state")})
    require(after.get("status") == "ok", "working code.read after write must be ok", failures)
    require(after.get("text") == routine_text, "no-op code.write must preserve routine text", failures)
    current_state = after.get("current_state") if isinstance(after.get("current_state"), dict) else {}
    require(current_state.get("source") == "saved_state", "working code.read after write must read saved_state", failures)

    fragment = first_unique_fragment(routine_text)
    fragment_write = rpc_call(
        endpoint_url,
        "code.write",
        {
            "base_id": args.base_id,
            "object_type": args.object_type,
            "object_name": args.object_name,
            "routine_name": args.routine_name,
            "old": fragment,
            "new": fragment,
            "mode": "apply",
        },
        transport=args.transport,
        timeout=args.timeout,
        session_id=session_id,
    )
    steps.append(
        {
            "name": "code.write fragment apply",
            "status": fragment_write.get("status"),
            "applied": fragment_write.get("applied"),
            "write_mode": fragment_write.get("write_mode"),
        }
    )
    require_saved_state_write(fragment_write, "code.write fragment apply", failures)
    after_fragment = rpc_call(endpoint_url, "code.read", {**selector, "state": "working"}, transport=args.transport, timeout=args.timeout, session_id=session_id)
    steps.append({"name": "code.read working after fragment", "status": after_fragment.get("status"), "current_state": after_fragment.get("current_state")})
    require(after_fragment.get("status") == "ok", "working code.read after fragment write must be ok", failures)
    require(after_fragment.get("text") == routine_text, "no-op fragment code.write must preserve routine text", failures)

    module_selector = {key: value for key, value in selector.items() if key != "routine_name"}
    module_selector["max_chars"] = 200000
    module_before = rpc_call(endpoint_url, "code.read", {**module_selector, "state": "working"}, transport=args.transport, timeout=args.timeout, session_id=session_id)
    module_text = str(module_before.get("text") or "")
    steps.append({"name": "code.read full module before", "status": module_before.get("status"), "current_state": module_before.get("current_state"), "has_text": bool(module_text.strip())})
    require(module_before.get("status") == "ok", "working full module code.read before module write must be ok", failures)
    require(bool(module_text.strip()), "working full module code.read must return module text", failures)
    require("///----" not in module_text, "working full module code.read must hide the saved form module container marker", failures)
    module_write = rpc_call(
        endpoint_url,
        "code.write",
        {
            "base_id": args.base_id,
            "object_type": args.object_type,
            "object_name": args.object_name,
            "module_text": module_text,
            "mode": "apply",
        },
        transport=args.transport,
        timeout=args.timeout,
        session_id=session_id,
    )
    steps.append(
        {
            "name": "code.write full module apply",
            "status": module_write.get("status"),
            "applied": module_write.get("applied"),
            "write_mode": module_write.get("write_mode"),
        }
    )
    require_saved_state_write(module_write, "code.write full module apply", failures)
    module_after = rpc_call(endpoint_url, "code.read", {**module_selector, "state": "working"}, transport=args.transport, timeout=args.timeout, session_id=session_id)
    steps.append({"name": "code.read full module after", "status": module_after.get("status"), "current_state": module_after.get("current_state")})
    require(module_after.get("status") == "ok", "working full module code.read after module write must be ok", failures)
    require(module_after.get("text") == module_text, "no-op full module code.write must preserve module text", failures)
    require("///----" not in str(module_after.get("text") or ""), "working full module code.read after write must hide marker", failures)

    both = rpc_call(endpoint_url, "code.read", {**selector, "state": "both"}, transport=args.transport, timeout=args.timeout, session_id=session_id)
    steps.append({"name": "code.read both", "status": both.get("status"), "comparison": both.get("comparison"), "text_source": both.get("text_source")})
    require(both.get("status") == "ok", "code.read state=both must be ok", failures)
    require(both.get("text_source") == "saved_state", "state=both effective text must come from saved_state when present", failures)

    found = rpc_call(
        endpoint_url,
        "extension.objects.find",
        {
            "base_id": args.base_id,
            "extension": args.extension,
            "object_type": args.object_type,
            "query": args.object_name,
            "state": "working",
            "limit": 20,
        },
        transport=args.transport,
        timeout=args.timeout,
        session_id=session_id,
    )
    objects = [item for item in found.get("objects") or [] if isinstance(item, dict) and item.get("name") == args.object_name]
    steps.append({"name": "extension.objects.find working", "status": found.get("status"), "matches": len(objects)})
    require(bool(objects), "working extension.objects.find must see target form", failures)
    require(str((objects[0].get("activation_state") if objects else "") or "").startswith("saved_"), "target form must be marked as saved-state in working view", failures)

    saved_forms = rpc_call(
        endpoint_url,
        "metadata.saved_state.forms.search",
        {
            "base_id": args.base_id,
            "extension": args.extension,
            "form": args.object_name,
            "limit": 20,
        },
        transport=args.transport,
        timeout=args.timeout,
        session_id=session_id,
    )
    saved_form_matches = [
        item
        for item in saved_forms.get("forms") or []
        if isinstance(item, dict) and item.get("name") == args.object_name and item.get("table") in {"ConfigSave", "ConfigCASSave"}
    ]
    steps.append({"name": "metadata.saved_state.forms.search", "status": saved_forms.get("status"), "matches": len(saved_form_matches)})
    require(saved_forms.get("status") == "ok", "saved-state form search must be ok after code.write", failures)
    require(bool(saved_form_matches), "saved-state form search must find the target form after code.write", failures)
    if saved_form_matches:
        counts = saved_form_matches[0].get("counts") if isinstance(saved_form_matches[0].get("counts"), dict) else {}
        require(int(counts.get("items_total") or 0) > 0, "saved-state form structure must have decoded items after code.write", failures)

    report = {
        "schema": "onec_code_write_saved_state_smoke.v1",
        "status": "failed" if failures else "ok",
        "endpoint_url": endpoint_url,
        "transport": args.transport,
        "base_id": args.base_id,
        "target": {"object_type": args.object_type, "object_name": args.object_name, "routine_name": args.routine_name},
        "steps": steps,
        "failures": failures,
        **({"mcp_initialize": initialize_result} if initialize_result and args.transport == "mcp" and failures else {}),
    }
    if args.report:
        os.makedirs(os.path.dirname(os.path.abspath(args.report)), exist_ok=True)
        with open(args.report, "w", encoding="utf-8") as handle:
            json.dump(report, handle, ensure_ascii=False, indent=2)
    if args.json:
        print(json.dumps(report, ensure_ascii=False, indent=2))
    else:
        print(f"1C code.write saved-state smoke: {report['status']}")
        for step in steps:
            print(f"- {step['name']}: {step.get('status')}")
        for failure in failures:
            print(f"FAIL: {failure}", file=sys.stderr)
    return 1 if failures else 0


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