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

import argparse
import json
import os
import re
import socket
import sys
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


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

ROOT = Path(__file__).resolve().parents[1]
DEFAULT_REPORT = ROOT / "reports" / "1c-mcp-selector-chain-smoke.json"
EXPECTED_CONTRACT_VERSION = "onec-selector-contract.v1"
FORBIDDEN_CONCRETE_SELECTOR_VALUES = (
    "ПечатьЭтикеток",
    "УОП_Печать",
    "ЦенаСоСкидкой",
    "fs_Отчеты",
)
DEFAULT_DISCOVERY_KINDS = ("DataProcessor", "Report", "Document", "Catalog", "InformationRegister")
LIVE_HELP_SELECTOR_METHODS = (
    "metadata.definition.find",
    "metadata.object.modules",
    "modules.read",
    "code.search",
    "code.read",
)
WORKING_STATE_METHODS = {
    "extension.objects.find",
    "metadata.resolve_overrides",
    "metadata.object.forms",
    "metadata.object.form.details",
    "metadata.form.decode",
    "modules.search",
    "code.search",
}
SEARCH_TOKEN_SKIP = {
    "procedure",
    "function",
    "endprocedure",
    "endfunction",
    "процедура",
    "функция",
    "конецпроцедуры",
    "конецфункции",
}
IDENTIFIER_LIKE_RE = re.compile(r"^[A-Za-zА-Яа-я_][A-Za-zА-Яа-я0-9_]*$")


def is_identifier_like(value: Any) -> bool:
    return bool(IDENTIFIER_LIKE_RE.fullmatch(str(value or "").strip()))


def text_from_module_read(result: dict[str, Any]) -> str:
    for key in ("text", "preview"):
        value = result.get(key)
        if isinstance(value, str) and value.strip():
            return value
    selection = result.get("selection")
    if isinstance(selection, dict):
        for key in ("text", "preview"):
            value = selection.get(key)
            if isinstance(value, str) and value.strip():
                return value
    return ""


def search_token_from_text(text: str) -> str | None:
    for match in re.finditer(r"[A-Za-zА-Яа-я_][A-Za-zА-Яа-я0-9_]{3,}", text):
        token = match.group(0)
        if token.casefold() not in SEARCH_TOKEN_SKIP:
            return token
    return None


def search_token_from_module_read(result: dict[str, Any]) -> tuple[str | None, str | None]:
    text_token = search_token_from_text(text_from_module_read(result))
    if text_token:
        return text_token, "modules.read.text"
    for routine in result.get("routines") or []:
        if not isinstance(routine, dict):
            continue
        name = routine.get("name")
        if is_identifier_like(name):
            return str(name), "modules.read.routines"
    return "Процедура", "generic_bsl_keyword"


def routine_name_from_module_read(result: dict[str, Any]) -> str | None:
    for routine in result.get("routines") or []:
        if not isinstance(routine, dict):
            continue
        name = routine.get("name")
        if is_identifier_like(name):
            return str(name)
    selection = result.get("selection")
    if isinstance(selection, dict):
        for key in ("routine_name", "name"):
            name = selection.get(key)
            if is_identifier_like(name):
                return str(name)
    return None


def first_write_plan_target_from_saved_state(result: dict[str, Any]) -> dict[str, Any] | None:
    for module in result.get("modules") or []:
        if not isinstance(module, dict):
            continue
        for stream in module.get("streams") or []:
            if not isinstance(stream, dict):
                continue
            target = stream.get("write_plan_target")
            if not isinstance(target, dict):
                continue
            has_internal_handle = bool(target.get("module_ref") and target.get("expected_sha1"))
            has_public_selector = bool(target.get("ref") and target.get("module"))
            if has_internal_handle or has_public_selector:
                return target
    return None


def first_public_form_write_plan_candidate(result: dict[str, Any]) -> dict[str, Any] | None:
    candidates: list[dict[str, Any]] = []
    for form in result.get("forms") or []:
        if not isinstance(form, dict):
            continue
        for match in form.get("matches") or []:
            if not isinstance(match, dict):
                continue
            selector = match.get("selector") if isinstance(match.get("selector"), dict) else {}
            if not selector.get("ref") or not selector.get("form"):
                continue
            if not any(selector.get(key) for key in ("command", "element", "attribute")):
                continue
            if any(key in selector for key in ("table", "file_name", "guid", "form_guid")):
                continue
            for prop in match.get("writable_properties") or []:
                if not isinstance(prop, dict) or prop.get("value_type") != "string":
                    continue
                property_name = str(prop.get("presentation") or prop.get("property") or "").strip()
                if not property_name:
                    continue
                old_value = str(prop.get("value") or "")
                normalized_property = str(prop.get("property") or "").strip().casefold()
                priority = 0 if normalized_property == "title" else (2 if normalized_property in {"id", "name"} else 1)
                candidates.append(
                    {
                        "priority": priority,
                        "target": {
                            "kind": "form",
                            **{
                                key: value
                                for key, value in selector.items()
                                if key in {"extension", "ref", "form", "command", "element", "attribute"} and value not in (None, "")
                            },
                        },
                        "edit": {
                            "property": property_name,
                            "value": f"{old_value} [NAME-FIRST PLAN]",
                        },
                    }
                )
    if not candidates:
        return None
    candidate = sorted(candidates, key=lambda item: int(item.get("priority") or 0))[0]
    candidate.pop("priority", None)
    return candidate


def live_coverage_from_steps(steps: list[dict[str, Any]]) -> dict[str, Any]:
    def last_step(name: str) -> dict[str, Any]:
        for step in reversed(steps):
            if isinstance(step, dict) and step.get("name") == name:
                return step
        return {}

    resolve = last_step("metadata.resolve_overrides")
    saved_state = last_step("metadata.saved_state.modules.search")
    write_plan = last_step("metadata.write.plan")
    form_search = last_step("metadata.saved_state.forms.search")
    form_write_plan = last_step("metadata.write.plan.form")
    skips = [
        {"step": str(step.get("name")), "status": step.get("status"), **({"reason": step.get("reason")} if step.get("reason") else {})}
        for step in steps
        if isinstance(step.get("status"), str) and str(step.get("status")).startswith("skipped")
    ]
    return {
        "resolve_overrides": {
            "attempted": bool(resolve),
            "status": resolve.get("status"),
            "write_plan_evidence": bool(resolve.get("write_plan_evidence")),
            "next_method": resolve.get("next_method"),
        },
        "saved_state_resolution": {
            "attempted": bool(saved_state),
            "status": saved_state.get("status"),
            "modules": int(saved_state.get("modules") or 0),
            "write_plan_target": bool(saved_state.get("write_plan_target")),
        },
        "write_plan_composition": {
            "attempted": bool(write_plan),
            "status": write_plan.get("status"),
            "composed": write_plan.get("allowed") is True,
            "from_write_plan_target": bool(write_plan.get("from_write_plan_target")),
        },
        "form_write_plan_composition": {
            "attempted": bool(form_write_plan),
            "search_attempted": bool(form_search),
            "search_status": form_search.get("status"),
            "candidate": bool(form_search.get("write_plan_candidate")),
            "status": form_write_plan.get("status"),
            "composed": form_write_plan.get("allowed") is True,
            "name_first": bool(form_write_plan.get("name_first")),
        },
        "skips": skips,
    }


def live_write_plan_composition_required_issue(coverage: dict[str, Any]) -> dict[str, Any] | None:
    composition = coverage.get("write_plan_composition") if isinstance(coverage.get("write_plan_composition"), dict) else {}
    if composition.get("composed") is True:
        return None
    return {
        "code": "live_write_plan_composition_required",
        "status": composition.get("status"),
        "saved_state_resolution": coverage.get("saved_state_resolution"),
    }


def live_form_write_plan_composition_required_issue(coverage: dict[str, Any]) -> dict[str, Any] | None:
    composition = coverage.get("form_write_plan_composition") if isinstance(coverage.get("form_write_plan_composition"), dict) else {}
    if composition.get("composed") is True and composition.get("name_first") is True:
        return None
    return {
        "code": "live_form_write_plan_composition_required",
        "status": composition.get("status"),
        "search_status": composition.get("search_status"),
        "candidate": composition.get("candidate"),
    }


def live_object_summary(*sources: dict[str, Any] | None) -> dict[str, Any]:
    summary: dict[str, Any] = {}
    for source in sources:
        if not isinstance(source, dict):
            continue
        for key in ("kind", "name", "guid", "ref"):
            value = source.get(key)
            if value is not None and value != "":
                summary[key] = value
    if "ref" not in summary and summary.get("kind") and summary.get("name"):
        summary["ref"] = f"{summary['kind']}.{summary['name']}"
    return summary


def selector_from_ref(ref: str | None) -> dict[str, str]:
    text = str(ref or "").strip()
    if "." not in text:
        return {}
    kind, name = text.split(".", 1)
    if not kind.strip() or not name.strip():
        return {}
    return {"kind": kind.strip(), "name": name.strip(), "ref": text}


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 = Request(
        url,
        data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
        method="POST",
        headers={"Content-Type": "application/json", **(headers or {})},
    )
    try:
        with 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 is not an object", "response": data}
    except (TimeoutError, socket.timeout) as exc:
        return {}, {"status": "error", "error": "timeout", "message": str(exc)}


def rest_rpc_call(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: float) -> 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"}
    if service_token:
        headers["Authorization"] = f"Bearer {service_token}"
    request = Request(
        adapter_url.rstrip("/") + "/rpc",
        data=body,
        method="POST",
        headers=headers,
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            raw = response.read().decode("utf-8")
            data = json.loads(raw) if raw else {}
            return data if isinstance(data, dict) else {"status": "error", "error": "rpc response is not an object", "response": data}
    except HTTPError as exc:
        raw = exc.read().decode("utf-8", errors="replace")
        return {"status": "error", "error": "http_error", "http_status": exc.code, "response": raw}
    except URLError as exc:
        return {"status": "error", "error": "url_error", "message": str(exc)}
    except (TimeoutError, socket.timeout) as exc:
        return {"status": "error", "error": "timeout", "message": str(exc)}


def mcp_rpc_call(mcp_url: str, method: str, payload: dict[str, Any], *, timeout: float, session_id: str | None = 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"smoke-{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:
        raw = exc.read().decode("utf-8", errors="replace")
        return {"status": "error", "error": "mcp_http_error", "http_status": exc.code, "response": raw}
    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, dict) else None
    if not isinstance(content, list) or not content:
        return {"status": "error", "error": "mcp_tool_content_missing", "result": result}
    first = content[0] if isinstance(content[0], dict) else {}
    text = first.get("text")
    if not isinstance(text, str):
        return {"status": "error", "error": "mcp_tool_text_missing", "content": content}
    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 mcp_initialize(mcp_url: str, *, timeout: float) -> tuple[str | None, dict[str, Any]]:
    body = {
        "jsonrpc": "2.0",
        "id": "smoke-initialize",
        "method": "initialize",
        "params": {
            "protocolVersion": "2025-06-18",
            "capabilities": {},
            "clientInfo": {"name": "smoke_1c_mcp_selector_chain", "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:
        raw = exc.read().decode("utf-8", errors="replace")
        return None, {"status": "error", "error": "mcp_initialize_http_error", "http_status": exc.code, "response": raw}
    except URLError as exc:
        return None, {"status": "error", "error": "mcp_initialize_url_error", "message": str(exc)}
    except (TimeoutError, socket.timeout) as exc:
        return None, {"status": "error", "error": "mcp_initialize_timeout", "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_onec_request_schema_check(mcp_url: str, *, timeout: float, session_id: str | None) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    headers = {"Accept": "application/json, text/event-stream"}
    if session_id:
        headers["Mcp-Session-Id"] = session_id
    body = {"jsonrpc": "2.0", "id": "smoke-tools-list", "method": "tools/list", "params": {}}
    try:
        _, data = post_json(mcp_url.rstrip("/") + "/mcp", body, timeout=timeout, headers=headers)
    except HTTPError as exc:
        raw = exc.read().decode("utf-8", errors="replace")
        return {"status": "error", "error": "mcp_tools_list_http_error", "http_status": exc.code}, [{"code": "live_mcp_tools_list_failed", "response": raw}]
    except URLError as exc:
        return {"status": "error", "error": "mcp_tools_list_url_error", "message": str(exc)}, [{"code": "live_mcp_tools_list_failed", "message": str(exc)}]
    if "error" in data:
        return {"status": "error", "error": "mcp_tools_list_jsonrpc_error", "details": data.get("error")}, [{"code": "live_mcp_tools_list_failed", "details": data.get("error")}]
    result = data.get("result") if isinstance(data.get("result"), dict) else {}
    tools = result.get("tools") or []
    onec_request = next((tool for tool in tools if isinstance(tool, dict) and tool.get("name") == "onec_request"), None)
    contract_version = result.get("contract_version")
    summary = {
        "status": "ok",
        "contract_version": contract_version,
        "tools": len(tools),
        "onec_request": bool(onec_request),
        "selector_guidance": False,
        "placeholder_examples": False,
    }
    issues: list[dict[str, Any]] = []
    if contract_version != EXPECTED_CONTRACT_VERSION:
        issues.append({"code": "live_mcp_contract_version_mismatch", "expected": EXPECTED_CONTRACT_VERSION, "actual": contract_version})
    if not isinstance(onec_request, dict):
        issues.append({"code": "live_mcp_onec_request_missing"})
        return summary, issues
    schema = onec_request.get("inputSchema") if isinstance(onec_request.get("inputSchema"), dict) else {}
    description = str(onec_request.get("description") or "")
    payload_schema = (schema.get("properties") or {}).get("payload") or {}
    payload_description = str(payload_schema.get("description") or "")
    selector_terms = ("ref", "kind/name/guid", "object_type/object_name/object_guid")
    summary["selector_guidance"] = all(term in description and term in payload_description for term in selector_terms)
    summary["placeholder_examples"] = "<metadata-kind>.<metadata-object-name>" in json.dumps(schema.get("examples") or [], ensure_ascii=False)
    if schema.get("required") != ["method"]:
        issues.append({"code": "live_mcp_onec_request_required_not_generic", "required": schema.get("required")})
    if "oneOf" in schema:
        issues.append({"code": "live_mcp_onec_request_enumerates_methods"})
    for term in selector_terms:
        if term not in description or term not in payload_description:
            issues.append({"code": "live_mcp_selector_guidance_missing", "term": term})
    if not summary["placeholder_examples"]:
        issues.append({"code": "live_mcp_placeholder_examples_missing"})
    return summary, issues


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


def with_working_state(payload: dict[str, Any], *, transport: str) -> dict[str, Any]:
    if payload.get("state") or payload.get("source_state"):
        return payload
    if transport == "mcp":
        return {**payload, "source_state": "working"}
    return {**payload, "state": "working"}


def adapter_help_methods_contract(endpoint_url: str, *, timeout: float, transport: str, session_id: str | None = None) -> tuple[dict[str, Any], list[dict[str, Any]]]:
    result = rpc_call(endpoint_url, "help.methods", {}, timeout=timeout, transport=transport, session_id=session_id)
    methods = {
        str(item.get("name") or ""): item
        for item in result.get("methods") or []
        if isinstance(item, dict) and item.get("name")
    }
    contract_version = result.get("contract_version")
    summary = {"status": result.get("status") or ("ok" if methods else "error"), "contract_version": contract_version, "methods": len(methods), "selector_capabilities": False}
    issues: list[dict[str, Any]] = []
    if contract_version != EXPECTED_CONTRACT_VERSION:
        issues.append({"code": "live_adapter_contract_version_mismatch", "expected": EXPECTED_CONTRACT_VERSION, "actual": contract_version})
    if not methods or result.get("status") == "error":
        return summary, [{"code": "live_adapter_help_methods_failed", "status": result.get("status"), "error": result.get("error"), "diagnostics": result.get("diagnostics")}]
    for method in LIVE_HELP_SELECTOR_METHODS:
        capabilities = (methods.get(method) or {}).get("selector_capabilities")
        if not isinstance(capabilities, dict):
            issues.append({"code": "live_adapter_selector_capabilities_missing", "method": method})
            continue
        if not capabilities.get("accepts_ref") or not capabilities.get("accepts_object_aliases"):
            issues.append({"code": "live_adapter_selector_capabilities_incomplete", "method": method, "selector_capabilities": capabilities})
    definition_capabilities = (methods.get("metadata.definition.find") or {}).get("selector_capabilities") or {}
    if not definition_capabilities.get("allows_global_areas") or not definition_capabilities.get("requires_query"):
        issues.append({"code": "live_adapter_definition_capabilities_incomplete", "selector_capabilities": definition_capabilities})
    if not ((methods.get("modules.read") or {}).get("selector_capabilities") or {}).get("accepts_module_ref"):
        issues.append({"code": "live_adapter_modules_read_module_ref_missing"})
    if not ((methods.get("code.read") or {}).get("selector_capabilities") or {}).get("accepts_module_ref"):
        issues.append({"code": "live_adapter_code_read_module_ref_missing"})
    if not ((methods.get("code.search") or {}).get("selector_capabilities") or {}).get("requires_query"):
        issues.append({"code": "live_adapter_code_search_requires_query_missing"})
    summary["selector_capabilities"] = not issues
    return summary, issues


def first_object_with_modules(endpoint_url: str, base_id: str, *, timeout: float, kinds: tuple[str, ...], transport: str = "rest", session_id: str | None = None) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
    attempts: list[dict[str, Any]] = []
    for kind in kinds:
        listed = rpc_call(
            endpoint_url,
            "metadata.objects.list",
            {"base_id": base_id, "kind": kind, "limit": 50, "include_storage": False},
            timeout=timeout,
            transport=transport,
            session_id=session_id,
        )
        objects = [item for item in listed.get("objects") or [] if isinstance(item, dict)]
        objects = sorted(objects, key=lambda item: (not is_identifier_like(item.get("name")), str(item.get("name") or "")))
        attempts.append({"method": "metadata.objects.list", "kind": kind, "status": listed.get("status"), "objects": len(objects)})
        for obj in objects:
            if not obj.get("name") and not obj.get("guid"):
                continue
            selector = {
                "base_id": base_id,
                "kind": obj.get("kind") or kind,
                **({"name": obj.get("name")} if obj.get("name") else {}),
                **({"guid": obj.get("guid")} if obj.get("guid") else {}),
                "include_storage": False,
            }
            modules = rpc_call(endpoint_url, "metadata.object.modules", selector, timeout=timeout, transport=transport, session_id=session_id)
            module_items = [item for item in modules.get("modules") or [] if isinstance(item, dict)]
            attempts.append(
                {
                    "method": "metadata.object.modules",
                    "kind": selector.get("kind"),
                    "status": modules.get("status"),
                    "modules": len(module_items),
                }
            )
            if modules.get("status") == "ok" and module_items:
                module_owner = modules.get("object") if isinstance(modules.get("object"), dict) else {}
                object_card: dict[str, Any] = {}
                if obj.get("guid") or module_owner.get("guid"):
                    card = rpc_call(
                        endpoint_url,
                        "metadata.object.get",
                        {
                            "base_id": base_id,
                            "kind": module_owner.get("kind") or obj.get("kind") or kind,
                            "guid": module_owner.get("guid") or obj.get("guid"),
                            "include_semantic": False,
                        },
                        timeout=timeout,
                        transport=transport,
                        session_id=session_id,
                    )
                    object_card = card.get("object") if isinstance(card.get("object"), dict) else {}
                    attempts.append({"method": "metadata.object.get", "kind": selector.get("kind"), "status": card.get("status"), "resolved": bool(object_card)})
                merged_object = {
                    **obj,
                    **{key: value for key, value in module_owner.items() if value is not None and value != ""},
                    **{key: value for key, value in object_card.items() if value is not None and value != ""},
                }
                return {"object": merged_object, "modules": modules, "module": module_items[0]}, attempts
    return None, attempts


def selected_object_with_modules(endpoint_url: str, base_id: str, selector: dict[str, Any], *, timeout: float, transport: str = "rest", session_id: str | None = None) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
    attempts: list[dict[str, Any]] = []
    payload = {"base_id": base_id, **{key: value for key, value in selector.items() if value is not None and value != ""}, "include_storage": False}
    modules = rpc_call(endpoint_url, "metadata.object.modules", payload, timeout=timeout, transport=transport, session_id=session_id)
    module_items = [item for item in modules.get("modules") or [] if isinstance(item, dict)]
    attempts.append({"method": "metadata.object.modules", "kind": payload.get("kind"), "status": modules.get("status"), "modules": len(module_items), "selected": True})
    if modules.get("status") != "ok" or not module_items:
        return None, attempts
    module_owner = modules.get("object") if isinstance(modules.get("object"), dict) else {}
    object_card: dict[str, Any] = {}
    get_payload = {
        "base_id": base_id,
        **({"kind": module_owner.get("kind") or payload.get("kind")} if module_owner.get("kind") or payload.get("kind") else {}),
        **({"name": module_owner.get("name") or payload.get("name")} if module_owner.get("name") or payload.get("name") else {}),
        **({"guid": module_owner.get("guid") or payload.get("guid")} if module_owner.get("guid") or payload.get("guid") else {}),
        **({"ref": module_owner.get("ref") or payload.get("ref")} if module_owner.get("ref") or payload.get("ref") else {}),
        "include_semantic": False,
    }
    card = rpc_call(endpoint_url, "metadata.object.get", get_payload, timeout=timeout, transport=transport, session_id=session_id)
    object_card = card.get("object") if isinstance(card.get("object"), dict) else {}
    attempts.append({"method": "metadata.object.get", "kind": get_payload.get("kind"), "status": card.get("status"), "resolved": bool(object_card), "selected": True})
    merged_object = {
        **selector,
        **{key: value for key, value in module_owner.items() if value is not None and value != ""},
        **{key: value for key, value in object_card.items() if value is not None and value != ""},
    }
    return {"object": merged_object, "modules": modules, "module": module_items[0]}, attempts


def build_live_report(
    endpoint_url: str,
    base_id: str,
    *,
    timeout: float,
    kinds: tuple[str, ...] = DEFAULT_DISCOVERY_KINDS,
    transport: str = "rest",
    object_selector: dict[str, Any] | None = None,
    require_write_plan_composition: bool = False,
    require_form_write_plan_composition: bool = False,
) -> dict[str, Any]:
    steps: list[dict[str, Any]] = []
    issues: list[dict[str, Any]] = []
    session_id: str | None = None
    if transport == "mcp":
        session_id, initialize_result = mcp_initialize(endpoint_url, timeout=timeout)
        initialize_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 initialize_contract_version == EXPECTED_CONTRACT_VERSION else "error",
                "session": bool(session_id),
                "contract_version": initialize_contract_version,
            }
        )
        if not session_id:
            issues.append({"code": "live_mcp_initialize_failed", "result": initialize_result})
            return {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "endpoint_url": endpoint_url, "transport": transport, "steps": steps, "issues": issues}
        if initialize_contract_version != EXPECTED_CONTRACT_VERSION:
            issues.append({"code": "live_mcp_initialize_contract_version_mismatch", "expected": EXPECTED_CONTRACT_VERSION, "actual": initialize_contract_version})
            return {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "endpoint_url": endpoint_url, "transport": transport, "steps": steps, "issues": issues}
        schema_summary, schema_issues = mcp_onec_request_schema_check(endpoint_url, timeout=timeout, session_id=session_id)
        steps.append(
            {
                "name": "mcp.tools.list",
                "status": "ok" if not schema_issues else "error",
                "contract_version": schema_summary.get("contract_version"),
                "onec_request": bool(schema_summary.get("onec_request")),
                "selector_guidance": bool(schema_summary.get("selector_guidance")),
                "placeholder_examples": bool(schema_summary.get("placeholder_examples")),
            }
        )
        if schema_issues:
            issues.extend(schema_issues)
            return {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "endpoint_url": endpoint_url, "transport": transport, "steps": steps, "issues": issues}
    help_summary, help_issues = adapter_help_methods_contract(endpoint_url, timeout=timeout, transport=transport, session_id=session_id)
    steps.append(
        {
            "name": "adapter.help.methods",
            "status": "ok" if not help_issues else "error",
            "contract_version": help_summary.get("contract_version"),
            "methods": int(help_summary.get("methods") or 0),
            "selector_capabilities": bool(help_summary.get("selector_capabilities")),
        }
    )
    if help_issues:
        issues.extend(help_issues)
        return {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "endpoint_url": endpoint_url, "transport": transport, "steps": steps, "issues": issues}
    if object_selector:
        discovered, discovery_attempts = selected_object_with_modules(endpoint_url, base_id, object_selector, timeout=timeout, transport=transport, session_id=session_id)
    else:
        discovered, discovery_attempts = first_object_with_modules(endpoint_url, base_id, timeout=timeout, kinds=kinds, transport=transport, session_id=session_id)
    steps.extend({"name": "discover", **attempt} for attempt in discovery_attempts)
    if not discovered:
        issues.append({"code": "live_object_with_modules_not_found", **({"selector": object_selector} if object_selector else {"kinds": list(kinds)})})
        return {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "steps": steps, "issues": issues}

    obj = discovered["object"]
    object_kind = obj.get("kind")
    object_name = obj.get("name")
    object_guid = obj.get("guid")
    object_ref = f"{object_kind}.{object_name}" if object_kind and object_name else None
    if not object_ref and not object_guid:
        issues.append({"code": "live_discovered_object_ref_missing", "object": obj})
        return {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "steps": steps, "issues": issues}
    definition_query = str(object_guid or object_ref)
    definition_payload = {
        "base_id": base_id,
        "query": definition_query,
        "areas": ["metadata"],
        "max_matches": 5,
        "exact_only": True,
        "timeout_seconds": int(timeout),
        **({"kind": object_kind} if object_kind else {}),
        **({"guid": object_guid} if object_guid else {}),
    }
    if object_ref and is_identifier_like(object_name):
        definition_payload["ref"] = object_ref

    definition = rpc_call(
        endpoint_url,
        "metadata.definition.find",
        definition_payload,
        timeout=timeout,
        transport=transport,
        session_id=session_id,
    )
    steps.append({"name": "metadata.definition.find", "status": definition.get("status"), "ref_present": bool(definition_payload.get("ref")), "guid_present": bool(definition_payload.get("guid"))})
    related = definition.get("related_selectors") if isinstance(definition.get("related_selectors"), dict) else {}
    modules_selector = related.get("modules") if isinstance(related.get("modules"), dict) else None
    canonical_object = definition.get("object") if isinstance(definition.get("object"), dict) else {}
    if definition.get("status") != "ok" or not modules_selector:
        issues.append(
            {
                "code": "live_definition_related_modules_missing",
                "status": definition.get("status"),
                "error": definition.get("error"),
                "diagnostics": definition.get("diagnostics"),
            }
        )
        return {
            "schema": "onec_mcp_selector_chain_live_smoke.v1",
            "passed": False,
            "endpoint_url": endpoint_url,
            "transport": transport,
            "base_id": base_id,
            "object": live_object_summary({"kind": object_kind, "name": object_name, "guid": object_guid, "ref": object_ref}, canonical_object, modules_selector),
            "steps": steps,
            "issues": issues,
        }
    canonical_summary = live_object_summary({"kind": object_kind, "name": object_name, "guid": object_guid, "ref": object_ref}, canonical_object, modules_selector)
    object_kind = canonical_summary.get("kind") or object_kind
    object_name = canonical_summary.get("name") or object_name
    object_guid = canonical_summary.get("guid") or object_guid
    object_ref = canonical_summary.get("ref") or object_ref
    if not modules_selector.get("ref"):
        issues.append({"code": "live_related_selector_ref_missing", "selector": modules_selector})

    modules = rpc_call(endpoint_url, str(modules_selector.get("method") or "metadata.object.modules"), modules_selector, timeout=timeout, transport=transport, session_id=session_id)
    module_items = [item for item in modules.get("modules") or [] if isinstance(item, dict)]
    steps.append({"name": "related_selectors.modules", "status": modules.get("status"), "modules": len(module_items)})
    if modules.get("status") != "ok" or not module_items:
        issues.append({"code": "live_related_modules_empty", "status": modules.get("status")})
        return {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "steps": steps, "issues": issues}

    module_ordinal = module_items[0].get("module_ordinal") or 1
    read_payload = {
        **{key: value for key, value in modules_selector.items() if key != "method" and value is not None and value != ""},
        "base_id": modules_selector.get("base_id") or base_id,
        "module_ordinal": module_ordinal,
        "preview": True,
        "max_chars": 1000,
    }
    if not read_payload.get("ref") and object_ref:
        read_payload["ref"] = object_ref
    read_result = rpc_call(endpoint_url, "modules.read", read_payload, timeout=timeout, transport=transport, session_id=session_id)
    steps.append({"name": "modules.read", "status": read_result.get("status"), "module_ordinal": module_ordinal, "used_ref": bool(read_payload.get("ref"))})
    if read_result.get("status") not in {"ok", "summary"}:
        issues.append({"code": "live_modules_read_failed", "status": read_result.get("status"), "diagnostics": read_result.get("diagnostics")})
        return {
            "schema": "onec_mcp_selector_chain_live_smoke.v1",
            "passed": False,
            "endpoint_url": endpoint_url,
            "transport": transport,
            "base_id": base_id,
            "object": live_object_summary({"kind": object_kind, "name": object_name, "guid": object_guid, "ref": object_ref}, canonical_object, modules_selector),
            "steps": steps,
            "issues": issues,
        }

    routine_name = routine_name_from_module_read(read_result)
    if routine_name:
        resolve_payload = {
            "base_id": base_id,
            "method_name": routine_name,
            "max_items": 10,
            "timeout_seconds": int(timeout),
            **({"ref": object_ref} if object_ref else {}),
            **({"object_type": object_kind} if object_kind else {}),
            **({"object_name": object_name} if object_name else {}),
            **({"object_guid": object_guid} if object_guid else {}),
        }
        resolve_payload = with_working_state(resolve_payload, transport=transport)
        resolve_result = rpc_call(
            endpoint_url,
            "metadata.resolve_overrides",
            resolve_payload,
            timeout=timeout,
            transport=transport,
            session_id=session_id,
        )
        evidence = resolve_result.get("write_plan_evidence") if isinstance(resolve_result.get("write_plan_evidence"), dict) else {}
        evidence_target = evidence.get("target") if isinstance(evidence.get("target"), dict) else {}
        next_resolution = evidence.get("next_resolution") if isinstance(evidence.get("next_resolution"), dict) else {}
        next_params = next_resolution.get("params") if isinstance(next_resolution.get("params"), dict) else {}
        steps.append(
            {
                "name": "metadata.resolve_overrides",
                "status": resolve_result.get("status"),
                "routine": routine_name,
                "chain": len([item for item in resolve_result.get("chain") or [] if isinstance(item, dict)]),
                "write_plan_evidence": bool(evidence),
                "next_method": next_resolution.get("method"),
                "working_state": resolve_payload.get("source_state") or resolve_payload.get("state"),
            }
        )
        if not evidence:
            issues.append(
                {
                    "code": "live_resolve_overrides_write_plan_evidence_missing",
                    "status": resolve_result.get("status"),
                    "error": resolve_result.get("error"),
                    "diagnostics": resolve_result.get("diagnostics"),
                }
            )
        if evidence and evidence.get("method") != "metadata.write.plan":
            issues.append({"code": "live_resolve_overrides_write_plan_method_mismatch", "actual": evidence.get("method")})
        if evidence and evidence_target.get("routine_name") != routine_name:
            issues.append(
                {
                    "code": "live_resolve_overrides_evidence_routine_mismatch",
                    "expected": routine_name,
                    "actual": evidence_target.get("routine_name"),
                }
            )
        if evidence and next_resolution.get("method") != "metadata.saved_state.modules.search":
            issues.append({"code": "live_resolve_overrides_next_method_mismatch", "actual": next_resolution.get("method")})
        if evidence and next_params.get("base_id") != base_id:
            issues.append({"code": "live_resolve_overrides_next_base_id_mismatch", "expected": base_id, "actual": next_params.get("base_id")})
        if evidence and next_params.get("query") != routine_name:
            issues.append({"code": "live_resolve_overrides_next_query_mismatch", "expected": routine_name, "actual": next_params.get("query")})
        if evidence and object_guid and evidence_target.get("object_guid") != object_guid:
            issues.append(
                {
                    "code": "live_resolve_overrides_evidence_owner_guid_mismatch",
                    "expected": object_guid,
                    "actual": evidence_target.get("object_guid"),
                }
            )
        if evidence and next_resolution.get("method") == "metadata.saved_state.modules.search" and next_params:
            saved_state = rpc_call(
                endpoint_url,
                "metadata.saved_state.modules.search",
                next_params,
                timeout=timeout,
                transport=transport,
                session_id=session_id,
            )
            saved_modules = [item for item in saved_state.get("modules") or [] if isinstance(item, dict)]
            write_plan_target = first_write_plan_target_from_saved_state(saved_state)
            steps.append(
                {
                    "name": "metadata.saved_state.modules.search",
                    "status": saved_state.get("status"),
                    "modules": len(saved_modules),
                    "write_plan_target": bool(write_plan_target),
                    "from_next_resolution": True,
                }
            )
            if saved_state.get("status") != "ok":
                issues.append(
                    {
                        "code": "live_saved_state_modules_search_failed",
                        "status": saved_state.get("status"),
                        "error": saved_state.get("error"),
                        "diagnostics": saved_state.get("diagnostics"),
                    }
                )
            elif write_plan_target:
                module_text = text_from_module_read(read_result)
                control_fragment = search_token_from_text(module_text) or routine_name
                plan_target = {**evidence_target, **write_plan_target}
                plan_intent = evidence.get("intent") if isinstance(evidence.get("intent"), dict) else {}
                plan_intent = {
                    "operation": plan_intent.get("operation") or "replace_with_control",
                    "control_fragment": plan_intent.get("control_fragment") or control_fragment,
                    "expected_old_contains": plan_intent.get("expected_old_contains") or control_fragment,
                    "new": plan_intent.get("new") or module_text or control_fragment,
                }
                write_plan = rpc_call(
                    endpoint_url,
                    "metadata.write.plan",
                    {
                        "base_id": base_id,
                        "target": plan_target,
                        "intent": plan_intent,
                        "resolve_origin": False,
                    },
                    timeout=timeout,
                    transport=transport,
                    session_id=session_id,
                )
                route = write_plan.get("route") if isinstance(write_plan.get("route"), dict) else {}
                hint = route.get("apply_payload_hint") if isinstance(route.get("apply_payload_hint"), dict) else {}
                hint_payload = hint.get("payload") if isinstance(hint.get("payload"), dict) else {}
                steps.append(
                    {
                        "name": "metadata.write.plan",
                        "status": write_plan.get("status"),
                        "allowed": write_plan.get("allowed"),
                        "apply_method": route.get("apply_method"),
                        "ready_for_apply_method": hint.get("ready_for_apply_method"),
                        "from_write_plan_target": True,
                    }
                )
                if write_plan.get("allowed") is not True:
                    issues.append({"code": "live_composed_write_plan_not_allowed", "status": write_plan.get("status"), "problems": write_plan.get("problems")})
                if route.get("apply_method") not in {"metadata.module.write_apply", "code.write"}:
                    issues.append({"code": "live_composed_write_plan_apply_method_mismatch", "actual": route.get("apply_method")})
                if hint.get("method") and hint.get("method") != route.get("apply_method"):
                    issues.append(
                        {
                            "code": "live_composed_write_plan_hint_method_mismatch",
                            "route_method": route.get("apply_method"),
                            "hint_method": hint.get("method"),
                        }
                    )
                if hint.get("ready_for_apply_method") is not True:
                    issues.append({"code": "live_composed_write_plan_hint_not_ready", "hint": hint})
                if write_plan_target.get("module_ref") and hint_payload.get("module_ref") != write_plan_target.get("module_ref"):
                    issues.append({"code": "live_composed_write_plan_module_ref_mismatch", "expected": write_plan_target.get("module_ref"), "actual": hint_payload.get("module_ref")})
                if write_plan_target.get("expected_sha1") and hint_payload.get("expected_sha1") != write_plan_target.get("expected_sha1"):
                    issues.append({"code": "live_composed_write_plan_expected_sha1_mismatch"})
                if write_plan_target.get("ref"):
                    name_resolution = route.get("name_resolution") if isinstance(route.get("name_resolution"), dict) else {}
                    resolution_selector = name_resolution.get("selector") if isinstance(name_resolution.get("selector"), dict) else {}
                    if resolution_selector.get("ref") != write_plan_target.get("ref"):
                        issues.append(
                            {
                                "code": "live_composed_write_plan_name_resolution_mismatch",
                                "expected": write_plan_target.get("ref"),
                                "actual": resolution_selector.get("ref"),
                            }
                        )
            else:
                steps.append({"name": "metadata.write.plan", "status": "skipped_no_saved_state_target", "from_write_plan_target": False})
    else:
        steps.append({"name": "metadata.resolve_overrides", "status": "skipped", "reason": "routine_name_not_found"})

    search_token, search_token_source = search_token_from_module_read(read_result)
    if not search_token:
        issues.append({"code": "live_modules_read_search_token_missing", "module_ordinal": module_ordinal})
    else:
        code_search_payload = {
            **{key: value for key, value in modules_selector.items() if key != "method" and value is not None and value != ""},
            "base_id": modules_selector.get("base_id") or base_id,
            "query": search_token,
            "module_ordinal": module_ordinal,
            "limit": 5,
        }
        if not code_search_payload.get("ref") and object_ref:
            code_search_payload["ref"] = object_ref
        code_search_payload = with_working_state(code_search_payload, transport=transport)
        code_search = rpc_call(endpoint_url, "code.search", code_search_payload, timeout=timeout, transport=transport, session_id=session_id)
        items = [item for item in code_search.get("items") or [] if isinstance(item, dict)]
        steps.append(
            {
                "name": "code.search",
                "status": code_search.get("status"),
                "items": len(items),
                "query_source": search_token_source,
                "working_state": code_search_payload.get("source_state") or code_search_payload.get("state"),
            }
        )
        if code_search.get("status") not in {"ok", "partial"} or not items:
            issues.append({"code": "live_code_search_empty", "status": code_search.get("status"), "query_source": search_token_source, "error": code_search.get("error"), "diagnostics": code_search.get("diagnostics")})
        else:
            item_selector = items[0].get("read_selector") if isinstance(items[0].get("read_selector"), dict) else None
            if not item_selector or item_selector.get("method") != "code.read":
                issues.append({"code": "live_code_search_read_selector_missing", "selector": item_selector})
            else:
                code_read_payload = {key: value for key, value in item_selector.items() if key != "method"}
                code_read_payload.setdefault("base_id", base_id)
                code_read_payload.setdefault("preview", True)
                code_read_payload.setdefault("max_chars", 1000)
                code_read = rpc_call(endpoint_url, "code.read", code_read_payload, timeout=timeout, transport=transport, session_id=session_id)
                steps.append({"name": "code.read", "status": code_read.get("status"), "used_read_selector": True})
                if code_read.get("status") not in {"ok", "summary"}:
                    issues.append({"code": "live_code_read_failed", "status": code_read.get("status"), "diagnostics": code_read.get("diagnostics")})

    if require_form_write_plan_composition:
        form_search = rpc_call(
            endpoint_url,
            "metadata.saved_state.forms.search",
            {
                "base_id": base_id,
                "limit": 20,
                "scan_limit": 1000,
                "timeout_seconds": int(timeout),
            },
            timeout=timeout,
            transport=transport,
            session_id=session_id,
        )
        form_candidate = first_public_form_write_plan_candidate(form_search)
        steps.append(
            {
                "name": "metadata.saved_state.forms.search",
                "status": form_search.get("status"),
                "forms": int((form_search.get("counts") or {}).get("forms") or 0),
                "write_plan_candidate": bool(form_candidate),
                "include_storage": False,
            }
        )
        if form_search.get("status") != "ok":
            issues.append({"code": "live_saved_state_forms_search_failed", "status": form_search.get("status")})
        elif not form_candidate:
            steps.append(
                {
                    "name": "metadata.write.plan.form",
                    "status": "skipped_no_public_form_target",
                    "allowed": False,
                    "name_first": False,
                }
            )
        else:
            form_plan_payload = {
                "base_id": base_id,
                "target": form_candidate["target"],
                "edits": [form_candidate["edit"]],
                "resolve_origin": False,
            }
            form_plan = rpc_call(
                endpoint_url,
                "metadata.write.plan",
                form_plan_payload,
                timeout=timeout,
                transport=transport,
                session_id=session_id,
            )
            form_route = form_plan.get("route") if isinstance(form_plan.get("route"), dict) else {}
            form_hint = form_route.get("apply_payload_hint") if isinstance(form_route.get("apply_payload_hint"), dict) else {}
            form_name_resolution = form_route.get("name_resolution") if isinstance(form_route.get("name_resolution"), dict) else {}
            steps.append(
                {
                    "name": "metadata.write.plan.form",
                    "status": form_plan.get("status"),
                    "allowed": form_plan.get("allowed"),
                    "apply_method": form_route.get("apply_method"),
                    "ready_for_apply_method": form_hint.get("ready_for_apply_method"),
                    "name_resolution": form_name_resolution.get("status"),
                    "name_first": not any(
                        key in form_plan_payload["target"]
                        for key in ("table", "file_name", "guid", "form_guid", "module_ref")
                    ),
                }
            )
            if form_plan.get("allowed") is not True:
                issues.append({"code": "live_form_write_plan_not_allowed", "status": form_plan.get("status"), "problems": form_plan.get("problems")})
            if form_route.get("apply_method") != "metadata.form.element.write_apply":
                issues.append({"code": "live_form_write_plan_apply_method_mismatch", "actual": form_route.get("apply_method")})
            if form_hint.get("ready_for_apply_method") is not True:
                issues.append({"code": "live_form_write_plan_hint_not_ready", "hint": form_hint})
            if form_name_resolution.get("status") != "resolved":
                issues.append({"code": "live_form_write_plan_name_resolution_missing", "name_resolution": form_name_resolution})

    coverage = live_coverage_from_steps(steps)
    if require_write_plan_composition:
        strict_issue = live_write_plan_composition_required_issue(coverage)
        if strict_issue:
            issues.append(strict_issue)
    if require_form_write_plan_composition:
        strict_form_issue = live_form_write_plan_composition_required_issue(coverage)
        if strict_form_issue:
            issues.append(strict_form_issue)

    return {
        "schema": "onec_mcp_selector_chain_live_smoke.v1",
        "passed": not issues,
        "endpoint_url": endpoint_url,
        "transport": transport,
        "base_id": base_id,
        "object": live_object_summary({"kind": object_kind, "name": object_name, "guid": object_guid, "ref": object_ref}, canonical_object, modules_selector),
        "coverage": coverage,
        "steps": steps,
        "issues": issues,
    }


def selector_chain_examples() -> list[dict[str, Any]]:
    return [
        {
            "name": "definition_to_related_modules",
            "steps": [
                {
                    "tool": "onec_request",
                    "method": "metadata.definition.find",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "query": "<metadata-kind>.<metadata-object-name>",
                        "areas": ["metadata"],
                    },
                    "next": "Use related_selectors.modules or related_selectors.modules_search from the result.",
                },
                {
                    "tool": "onec_request",
                    "method": "<related_selectors.modules.method>",
                    "payload": "<related_selectors.modules>",
                    "next": "Use a module read_selector returned by modules or switch to related_selectors.code_search.",
                },
            ],
        },
        {
            "name": "definition_to_code_read",
            "steps": [
                {
                    "tool": "onec_request",
                    "method": "metadata.definition.find",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "ref": "<metadata-kind>.<metadata-object-name>",
                        "areas": ["metadata", "object", "module"],
                    },
                    "next": "Use related_selectors.code_search and add query.",
                },
                {
                    "tool": "onec_request",
                    "method": "code.search",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "ref": "<metadata-kind>.<metadata-object-name>",
                        "query": "<bsl-fragment-or-routine-name>",
                        "source_state": "working",
                        "limit": 10,
                    },
                    "next": "Pass item.read_selector directly to code.read.",
                },
                {
                    "tool": "onec_request",
                    "method": "<item.read_selector.method>",
                    "payload": "<item.read_selector>",
                    "expected_method": "code.read",
                },
            ],
        },
        {
            "name": "global_code_search_to_direct_read",
            "steps": [
                {
                    "tool": "onec_request",
                    "method": "code.search",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "query": "<bsl-fragment-or-routine-name>",
                        "scope": "all",
                        "source_state": "working",
                        "resolve_owners": True,
                        "owner_scan_limit": 40,
                    },
                    "next": "If owner is unresolved, still pass item.read_selector to code.read before broadening search.",
                },
                {
                    "tool": "onec_request",
                    "method": "<item.read_selector.method>",
                    "payload": "<item.read_selector>",
                    "expected_method": "code.read",
                },
            ],
        },
        {
            "name": "report_to_separate_form_description",
            "steps": [
                {
                    "tool": "onec_request",
                    "method": "metadata.object.forms",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "ref": "Report.<metadata-object-name>",
                        "source_state": "working",
                    },
                    "next": "Forms are references of the report. Select a returned public form name; do not treat the report card as the form description.",
                },
                {
                    "tool": "onec_request",
                    "method": "metadata.object.form.details",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "ref": "Report.<metadata-object-name>",
                        "form": "<form-name-returned-by-metadata.object.forms>",
                        "source_state": "working",
                        "include_parameters": True,
                    },
                    "next": "Read the separate form description: attributes, parameters, commands, items, and form module summary. A command has no module; its handler is a routine in this form module when decoded evidence provides the link.",
                },
                {
                    "tool": "onec_request",
                    "method": "metadata.form.decode",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "ref": "Report.<metadata-object-name>",
                        "form": "<form-name-returned-by-metadata.object.forms>",
                        "source_state": "working",
                        "view": "structure",
                    },
                    "next": "Use the static structure projection only. Parent/child edges remain unresolved unless the adapter reports proven codec evidence.",
                },
            ],
        },
    ]


def collect_strings(value: Any) -> list[str]:
    if isinstance(value, str):
        return [value]
    if isinstance(value, dict):
        strings: list[str] = []
        for key, item in value.items():
            strings.extend(collect_strings(key))
            strings.extend(collect_strings(item))
        return strings
    if isinstance(value, list):
        strings = []
        for item in value:
            strings.extend(collect_strings(item))
        return strings
    return []


def validate_chain(chain: dict[str, Any]) -> list[dict[str, Any]]:
    issues: list[dict[str, Any]] = []
    steps = chain.get("steps")
    if not isinstance(steps, list) or not steps:
        return [{"code": "chain_steps_missing", "chain": chain.get("name")}]
    for index, step in enumerate(steps):
        if not isinstance(step, dict):
            issues.append({"code": "chain_step_not_object", "chain": chain.get("name"), "index": index})
            continue
        if step.get("tool") != "onec_request":
            issues.append({"code": "chain_step_tool_not_onec_request", "chain": chain.get("name"), "index": index})
        method = step.get("method")
        if not isinstance(method, str) or not method:
            issues.append({"code": "chain_step_method_missing", "chain": chain.get("name"), "index": index})
        payload = step.get("payload")
        if isinstance(payload, dict) and "base_id" not in payload:
            issues.append({"code": "chain_step_base_id_missing", "chain": chain.get("name"), "index": index})
        if method in {"code.search", "modules.search", "metadata.definition.find"} and isinstance(payload, dict):
            selector_present = any(key in payload for key in ("ref", "query", "kind", "name", "guid", "object_type", "object_name", "object_guid"))
            if not selector_present:
                issues.append({"code": "chain_step_selector_or_query_missing", "chain": chain.get("name"), "index": index})
        if method in WORKING_STATE_METHODS and isinstance(payload, dict) and not (payload.get("source_state") or payload.get("state")):
            issues.append({"code": "chain_step_working_state_missing", "chain": chain.get("name"), "index": index, "method": method})
        if step.get("expected_method") and "<item.read_selector.method>" not in str(method):
            issues.append({"code": "chain_step_does_not_use_read_selector_method", "chain": chain.get("name"), "index": index})
    for text in collect_strings(chain):
        for forbidden in FORBIDDEN_CONCRETE_SELECTOR_VALUES:
            if forbidden in text:
                issues.append({"code": "chain_contains_concrete_selector_value", "chain": chain.get("name"), "value": forbidden})
    return issues


def build_report() -> dict[str, Any]:
    chains = selector_chain_examples()
    issues: list[dict[str, Any]] = []
    for chain in chains:
        issues.extend(validate_chain(chain))
    return {
        "schema": "onec_mcp_selector_chain_smoke.v1",
        "passed": not issues,
        "chains": chains,
        "counts": {"chains": len(chains), "steps": sum(len(chain.get("steps") or []) for chain in chains)},
        "issues": issues,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Smoke check for generic 1C MCP selector chains.")
    parser.add_argument("--json", action="store_true", help="Print JSON report.")
    parser.add_argument("--report", type=Path, default=DEFAULT_REPORT, help="Write JSON report.")
    parser.add_argument("--no-report", action="store_true", help="Do not write a report file.")
    parser.add_argument("--live", action="store_true", help="Run optional live adapter smoke through /rpc.")
    parser.add_argument("--transport", choices=("rest", "mcp"), default="rest", help="Live smoke transport.")
    parser.add_argument("--adapter-url", default="http://docker.cin.su:8011", help="1C REST adapter base URL for --live.")
    parser.add_argument("--mcp-url", default="http://docker.cin.su:8021", help="1C MCP proxy base URL for --live --transport mcp.")
    parser.add_argument("--base-id", help="Concrete 1C base id for --live.")
    parser.add_argument("--ref", help="Optional concrete public object ref for deterministic --live checks, for example Kind.ObjectName.")
    parser.add_argument("--kind", help="Optional concrete metadata kind for deterministic --live checks.")
    parser.add_argument("--name", help="Optional concrete metadata object name for deterministic --live checks.")
    parser.add_argument("--guid", help="Optional concrete metadata object GUID for deterministic --live checks.")
    parser.add_argument("--timeout", type=float, default=15.0, help="HTTP timeout for --live.")
    parser.add_argument("--require-write-plan-composition", action="store_true", help="Fail live smoke when saved-state lookup does not produce a concrete write_plan_target and composed metadata.write.plan.")
    parser.add_argument("--require-form-write-plan-composition", action="store_true", help="Fail live smoke unless a public saved-state form selector composes metadata.write.plan without storage identifiers.")
    args = parser.parse_args()

    if args.live and not args.base_id:
        report = {"schema": "onec_mcp_selector_chain_live_smoke.v1", "passed": False, "issues": [{"code": "live_base_id_required"}]}
    elif args.live:
        endpoint_url = args.mcp_url if args.transport == "mcp" else args.adapter_url
        object_selector = {**selector_from_ref(args.ref), **{key: value for key, value in {"kind": args.kind, "name": args.name, "guid": args.guid}.items() if value}}
        report = build_live_report(
            endpoint_url,
            args.base_id,
            timeout=args.timeout,
            transport=args.transport,
            object_selector=object_selector or None,
            require_write_plan_composition=args.require_write_plan_composition,
            require_form_write_plan_composition=args.require_form_write_plan_composition,
        )
    else:
        report = build_report()
    if not args.no_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:
        print(json.dumps(report, ensure_ascii=False, indent=2))
    else:
        status = "passed" if report["passed"] else "failed"
        counts = report.get("counts") if isinstance(report.get("counts"), dict) else {}
        if counts:
            print(f"1C MCP selector chain smoke {status}: {counts.get('chains')} chains, {counts.get('steps')} steps.")
        else:
            print(
                json.dumps(
                    {
                        "status": status,
                        "schema": report.get("schema"),
                        "issues": report.get("issues") or [],
                        "steps": report.get("steps") or [],
                    },
                    ensure_ascii=False,
                    indent=2,
                )
            )
    return 0 if report["passed"] else 1


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