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

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


DEFAULT_BASE_URL = "http://docker.cin.su:8011"


def rpc(base_url: str, method: str, payload: dict[str, Any]) -> dict[str, Any]:
    body = json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8")
    request = urllib.request.Request(
        base_url.rstrip("/") + "/rpc",
        data=body,
        headers={"Content-Type": "application/json; charset=utf-8"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=int(payload.get("timeout_seconds") or 60)) as response:
        return json.loads(response.read().decode("utf-8"))


def should_auto_select(file_name: str) -> bool:
    value = str(file_name or "").strip().lower()
    return not value or value.startswith("placeholder")


def discover_module_target(base_url: str, base_id: str, table: str, timeout_seconds: int) -> dict[str, Any]:
    saved_state = rpc(
        base_url,
        "metadata.saved_state.modules.search",
        {
            "base_id": base_id,
            "tables": [table],
            "limit": 20,
            "scan_limit": 100,
            "include_storage": True,
            "timeout_seconds": timeout_seconds,
        },
    )
    if saved_state.get("status") != "ok":
        raise AssertionError(f"saved-state modules search failed: {saved_state.get('status')}")
    for module in saved_state.get("modules") or []:
        source = module.get("source") if isinstance(module.get("source"), dict) else {}
        file_name = str(source.get("file_name") or module.get("file_name") or "").strip()
        if not file_name:
            continue
        for stream in module.get("streams") or []:
            if not isinstance(stream, dict):
                continue
            preview = str(stream.get("preview") or "")
            if not stream.get("has_bsl_marker") and not any(
                marker in preview for marker in ("Процедура ", "Функция ", "&НаКлиенте", "&НаСервере")
            ):
                continue
            declaration = re.search(r"(?im)^\s*(?:Процедура|Функция)\s+[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*\s*\([^)]*\)", preview)
            old = declaration.group(0).strip() if declaration else "#Если " if "#Если " in preview else ""
            if not old:
                continue
            write_plan_target = stream.get("write_plan_target") if isinstance(stream.get("write_plan_target"), dict) else {}
            stream_index = int(stream.get("stream_index") or 0)
            return {
                "file_name": file_name,
                "stream_index": stream_index,
                "module_ref": stream.get("module_ref") or f"{table}:{file_name}#stream:{stream_index}",
                "old": old,
                "new": old + " ",
                "expected_contains": old.strip(),
                "expected_sha1": write_plan_target.get("expected_sha1") or "",
                "discovery": {"status": saved_state.get("status"), "counts": saved_state.get("counts")},
            }
    raise AssertionError("no saved-state BSL module stream candidates found")


def main() -> int:

    parser = argparse.ArgumentParser(description="Smoke saved-state BSL module stream write with apply_and_rollback.")
    parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
    parser.add_argument("--base-id", default="upo_test")
    parser.add_argument("--table", default="ConfigCASSave")
    parser.add_argument("--file-name", default="", help="Saved-state module file name. Empty or placeholder* auto-selects a BSL stream.")
    parser.add_argument(
        "--module-ref",
        default="",
        help="Exact module_ref returned by discovery. Useful for embedded form modules without a #stream suffix.",
    )
    parser.add_argument("--stream-index", type=int, default=4)
    parser.add_argument("--old", default="Перем Параметры;     ")
    parser.add_argument("--new", default="Перем Параметры;      ")
    parser.add_argument("--expected-contains", default="Перем Параметры;")
    parser.add_argument("--expected-sha1", default="")
    parser.add_argument("--report", type=Path, required=True)
    parser.add_argument("--timeout-seconds", type=int, default=30)
    parser.add_argument("--allow-empty-saved-state", action="store_true", help="Exit successfully with skipped status when the saved-state table has no modules.")
    args = parser.parse_args()

    auto_target: dict[str, Any] = {}
    if should_auto_select(args.file_name):
        try:
            auto_target = discover_module_target(args.base_url, args.base_id, args.table, args.timeout_seconds)
        except AssertionError as exc:
            if not args.allow_empty_saved_state:
                raise
            result = {
                "schema": "onec_module_stream_write_smoke.v1",
                "status": "skipped_no_saved_state_candidate",
                "skipped": True,
                "base_id": args.base_id,
                "table": args.table,
                "saved_state_table": args.table,
                "diagnostics": {"message": str(exc)},
            }
            args.report.parent.mkdir(parents=True, exist_ok=True)
            args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
            print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
            return 0
        args.file_name = auto_target["file_name"]
        args.stream_index = int(auto_target["stream_index"])
        args.old = auto_target["old"]
        args.new = auto_target["new"]
        args.expected_contains = auto_target["expected_contains"]
        if auto_target.get("expected_sha1") and not args.expected_sha1:
            args.expected_sha1 = str(auto_target["expected_sha1"])

    module_ref = str(
        args.module_ref
        or auto_target.get("module_ref")
        or f"{args.table}:{args.file_name}#stream:{args.stream_index}"
    )
    saved_state = rpc(
        args.base_url,
        "metadata.saved_state.modules.search",
        {
            "base_id": args.base_id,
            "tables": [args.table],
            "file_name": args.file_name,
            "limit": 1,
            "scan_limit": 10,
            "include_storage": True,
            "timeout_seconds": args.timeout_seconds,
        },
    )
    saved_state_preflight = {
        "status": saved_state.get("status"),
        "counts": saved_state.get("counts") or {},
    }
    if args.allow_empty_saved_state:
        if saved_state.get("status") == "ok" and ((saved_state.get("counts") or {}).get("modules") or 0) == 0:
            result = {
                "schema": "onec_module_stream_write_smoke.v1",
                "status": "skipped_no_saved_state",
                "skipped": True,
                "base_id": args.base_id,
                "table": args.table,
                "module_ref": module_ref,
                "saved_state_preflight": {"status": saved_state.get("status"), "counts": saved_state.get("counts")},
            }
            args.report.parent.mkdir(parents=True, exist_ok=True)
            args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
            print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
            return 0

    write_payload: dict[str, Any] = {
        "base_id": args.base_id,
        "target": {"kind": "module", "module_ref": module_ref},
        "mode": "apply_and_rollback",
        "allow_sql_saved_state_apply": True,
        "allow_sql_saved_state_rollback": True,
        "old": args.old,
        "new": args.new,
        "expected_contains": args.expected_contains,
        "timeout_seconds": args.timeout_seconds,
    }
    if args.expected_sha1:
        write_payload["expected_sha1"] = args.expected_sha1
    written = rpc(args.base_url, "metadata.write", write_payload)
    write_result = written.get("result") if isinstance(written.get("result"), dict) else {}
    write_plan = write_result.get("write_plan") if isinstance(write_result.get("write_plan"), dict) else {}
    if written.get("status") != "verified_and_rolled_back" or write_plan.get("allowed") is not True:
        result = {
            "schema": "onec_module_stream_write_smoke.v1",
            "status": "write_failed",
            "base_id": args.base_id,
            "table": args.table,
            "module_ref": module_ref,
            "saved_state_preflight": saved_state_preflight,
            "metadata_write": written,
            "write_plan": write_plan,
        }
        args.report.parent.mkdir(parents=True, exist_ok=True)
        args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        print(json.dumps({"status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
        return 1

    final = rpc(
        args.base_url,
        "query.run",
        {
            "base_id": args.base_id,
            "diagnostic": True,
            "query": (
                "SELECT FileName, DataSize, DATALENGTH(BinaryData) AS BinaryBytes, "
                "CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1 "
                f"FROM {args.table} WHERE FileName = '{args.file_name}'"
            ),
            "timeout_seconds": args.timeout_seconds,
        },
    )
    result = {
        "schema": "onec_module_stream_write_smoke.v1",
        "status": "verified_and_rolled_back",
        "base_id": args.base_id,
        "table": args.table,
        "module_ref": module_ref,
        "saved_state_preflight": saved_state_preflight,
        "write_plan": {
            "status": write_plan.get("status"),
            "allowed": write_plan.get("allowed"),
            "apply_method": (write_plan.get("route") or {}).get("apply_method"),
            "target_kind": (write_plan.get("target") or {}).get("target_kind"),
        },
        "metadata_write": {
            "status": written.get("status"),
            "routed_method": written.get("routed_method"),
            "result_status": write_result.get("status"),
            "applied": write_result.get("applied"),
            "rolled_back": write_result.get("rolled_back"),
        },
        "proposal": {key: (write_result.get("proposal") or {}).get(key) for key in ("status", "original", "encoded", "edits", "validation", "counts")},
        "apply_result": write_result.get("apply_result"),
        "rollback_result": write_result.get("rollback_result"),
        "verify_after_rollback": final,
    }
    args.report.parent.mkdir(parents=True, exist_ok=True)
    args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
    return 0


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