#!/usr/bin/env python3
"""Apply an approved kb_stage proposal into canonical public.* rows.

Stage 2 of the KB apply pipeline:  approve -> APPLY -> render -> surface.

Governance boundary
-------------------
This tool connects to Postgres as the narrow ``kb_apply`` login role, never as
superuser. ``kb_apply`` can insert reviewed claim/source/reasoning-tool rows and
write the strategy/evidence/edge tables required by supported contracts. It has
SELECT-only access to ``kb_stage.kb_proposals``; payload-bound SECURITY DEFINER
functions are the only way it can lock and finish an approved ledger row. It
holds no DELETE or DDL and no write access to unrelated tables (agents, budgets,
personas, ...). This enforces
"agents propose, but do not self-apply" at the database boundary rather than by
convention. The tool is an operator, invoked by a human/reviewer after approval;
it is deliberately not part of the agent runtime.

Flow
----
1. Read the proposal (must be ``status='approved'``).
2. Dispatch by ``proposal_type`` to build the canonical write from a strict
   ``apply_payload`` contract carried on the proposal payload.
3. Emit a single transaction: canonical writes, then a DO block that flips the
   ledger to ``applied`` asserting rowcount=1 (the concurrent double-apply
   guard), stamps ``applied_by_agent_id`` as a real FK, and verifies invariants
   (RAISE on violation -> rollback), COMMIT.

Prerequisites (one-time, superuser): scripts/kb_apply_prereqs.sql bootstraps the
``kb-apply`` service-agent row, grants ``kb_apply`` SELECT on public.agents, and
ensures the one-active-strategy unique index. Run it before the first apply.

``--dry-run`` prints the exact SQL and does not connect for writes.

Contract (v2)
-------------
Freeform Leo eval packets are NOT applied directly. Each proposal must carry a
strict ``apply_payload`` object; a separate normalizer produces it. Shapes:

  revise_strategy:
    {"apply_payload": {
        "agent_id": "<uuid>",
        "strategy": {"diagnosis": str, "guiding_policy": str,
                     "proximate_objectives": [ ... ]},
        "strategy_nodes": [{"node_type": "diagnosis|policy|objective",
                            "title": str, "body": str, "rank": int}, ...]}}

  add_edge:
    {"apply_payload": {"from_claim": "<uuid>", "to_claim": "<uuid>",
                       "edge_type": "<edge_type>", "weight": <0..1|null>}}

  attach_evidence:
    {"apply_payload": {"evidence": [{"claim_id": "<uuid>",
                                     "source_id": "<uuid>",
                                     "role": "<evidence_role>",
                                     "weight": <0..1|null>}, ...]}}

  approve_claim:
    {"apply_payload": {
        "agent_id": "<uuid|null>",
        "claims": [{"id": "<uuid>", "type": "<claim type>",
                    "text": str, "status": "open", "confidence": <0..1|null>,
                    "tags": [str, ...], "created_by": "<uuid|null>"}, ...],
        "sources": [{"id": "<uuid>", "source_type": "<source type>",
                     "url": str|null, "storage_path": str|null,
                     "excerpt": str|null, "hash": str,
                     "created_by": "<uuid|null>"}, ...],
        "evidence": [{"claim_id": "<uuid>", "source_id": "<uuid>",
                      "role": "<evidence role>", "weight": <0..1|null>,
                      "created_by": "<uuid|null>"}, ...],
        "edges": [{"from_claim": "<uuid>", "to_claim": "<uuid>",
                   "edge_type": "<edge type>", "weight": <0..1|null>,
                   "created_by": "<uuid|null>"}, ...],
        "reasoning_tools": [{"id": "<uuid>", "agent_id": "<uuid|null>",
                             "name": str, "description": str,
                             "category": str|null}, ...]}}

``approve_claim`` is the only source-creation contract. It is an atomic,
insert-only canonical bundle: existing IDs must match the reviewed payload
exactly, source hashes cannot alias another ID, and every row is verified before
the approved proposal ledger can flip to ``applied``.
"""

from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import sys
import uuid
from pathlib import Path
from typing import Any

import kb_apply_replay_receipt as replay_receipt

DEFAULT_SECRETS_FILE = "/home/teleo/.hermes/profiles/leoclean/secrets/kb-apply.env"
DEFAULT_CONTAINER = "teleo-pg"
DEFAULT_DB = "teleo"
DEFAULT_HOST = "127.0.0.1"
DEFAULT_ROLE = "kb_apply"

# Handle of the service-agent row that performs canonical writes. Bootstrapped
# once by superuser (scripts/kb_apply_prereqs.sql); kb_apply holds SELECT-only on
# public.agents so it can resolve this into applied_by_agent_id, never INSERT.
SERVICE_AGENT_HANDLE = "kb-apply"

APPLYABLE_TYPES = ("revise_strategy", "add_edge", "attach_evidence", "approve_claim")

CLAIM_TYPES = {"empirical", "structural", "normative", "meta", "concept"}
SOURCE_TYPES = {"paper", "article", "transcript", "observation", "dataset", "post", "dm", "other"}
EVIDENCE_ROLES = {"grounds", "illustrates", "contradicts"}
EDGE_TYPES = {
    "supports", "challenges", "requires", "relates", "contradicts",
    "supersedes", "derives_from", "cites", "causes", "constrains", "accelerates",
}

MAX_BUNDLE_ROWS = {
    "claims": 100,
    "sources": 500,
    "evidence": 1000,
    "edges": 1000,
    "reasoning_tools": 50,
}


# --------------------------------------------------------------------------- #
# SQL helpers                                                                  #
# --------------------------------------------------------------------------- #
def sql_literal(value: Any) -> str:
    """Render a Python value as an explicit PostgreSQL escape-string literal."""
    if value is None:
        return "null"
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, (int, float)):
        return repr(value)
    escaped = str(value).replace("\\", "\\\\").replace("'", "''")
    return "E'" + escaped + "'"


def _jsonb(value: Any) -> str:
    return sql_literal(json.dumps(value, sort_keys=True)) + "::jsonb"


def _text_array(values: list[str]) -> str:
    if not values:
        return "'{}'::text[]"
    return "array[" + ", ".join(sql_literal(value) for value in values) + "]::text[]"


def _uuid(value: Any, path: str, *, nullable: bool = False) -> str | None:
    if value is None and nullable:
        return None
    try:
        return str(uuid.UUID(str(value)))
    except (TypeError, ValueError, AttributeError) as exc:
        raise ValueError(f"{path} must be a UUID{' or null' if nullable else ''}") from exc


def _weight(value: Any, path: str) -> Any:
    if value is None:
        return None
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError(f"{path} must be a number from 0 to 1 or null")
    if not 0 <= value <= 1:
        raise ValueError(f"{path} must be between 0 and 1")
    return value


def _rows(payload: dict[str, Any], key: str, *, required: bool = False) -> list[dict[str, Any]]:
    value = payload.get(key)
    if value is None:
        value = []
    if not isinstance(value, list) or any(not isinstance(row, dict) for row in value):
        raise ValueError(f"approve_claim apply_payload.{key} must be a list of objects")
    if required and not value:
        raise ValueError(f"approve_claim apply_payload requires non-empty '{key}'")
    if len(value) > MAX_BUNDLE_ROWS[key]:
        raise ValueError(f"approve_claim apply_payload.{key} exceeds {MAX_BUNDLE_ROWS[key]} rows")
    return value


def _dedupe(values: list[Any], path: str) -> None:
    seen = set()
    for value in values:
        marker = json.dumps(value, sort_keys=True)
        if marker in seen:
            raise ValueError(f"{path} contains a duplicate: {value!r}")
        seen.add(marker)


def _reject_unknown(mapping: dict[str, Any], allowed: set[str], path: str) -> None:
    unknown = sorted(set(mapping) - allowed)
    if unknown:
        raise ValueError(f"{path} contains unknown fields: {unknown}")


def _validate_approval_meta(
    approval: dict[str, Any], proposal_type: str, apply_payload: dict[str, Any]
) -> None:
    if not isinstance(approval, dict):
        raise ValueError("approved proposal metadata is required")
    if approval.get("proposal_type") != proposal_type:
        raise ValueError("approved proposal_type does not match the apply builder")
    payload = approval.get("payload")
    if not isinstance(payload, dict) or payload.get("apply_payload") != apply_payload:
        raise ValueError("approved proposal payload does not match the strict apply payload")
    if not str(approval.get("reviewed_by_handle") or "").strip():
        raise ValueError("approved proposal requires reviewed_by_handle")
    if not str(approval.get("reviewed_at") or "").strip():
        raise ValueError("approved proposal requires reviewed_at")
    if not str(approval.get("review_note") or "").strip():
        raise ValueError("approved proposal requires review_note")
    _uuid(
        approval.get("reviewed_by_agent_id"),
        "approved proposal reviewed_by_agent_id",
        nullable=True,
    )


def _approval_guard_sql(proposal_id: str, approval: dict[str, Any]) -> str:
    return f"""select kb_stage.assert_approved_proposal(
  {sql_literal(proposal_id)}::uuid,
  {sql_literal(approval['proposal_type'])},
  {_jsonb(approval['payload'])},
  {sql_literal(approval['reviewed_by_handle'])},
  {sql_literal(approval.get('reviewed_by_agent_id'))}::uuid,
  {sql_literal(approval['reviewed_at'])}::timestamptz,
  {sql_literal(approval['review_note'])}
);"""


def _ledger_and_verify(
    proposal_id: str,
    applied_by: str,
    extra_checks: str,
    approval: dict[str, Any],
) -> str:
    """Verify canonical rows, then finish the locked, payload-bound apply."""
    checks_sql = f"""do $verify$
begin
{extra_checks}
end
$verify$;"""
    finish_sql = f"""select kb_stage.finish_approved_proposal(
  {sql_literal(proposal_id)}::uuid,
  {sql_literal(approval['proposal_type'])},
  {_jsonb(approval['payload'])},
  {sql_literal(approval['reviewed_by_handle'])},
  {sql_literal(approval.get('reviewed_by_agent_id'))}::uuid,
  {sql_literal(approval['reviewed_at'])}::timestamptz,
  {sql_literal(approval['review_note'])},
  {sql_literal(applied_by)}
);"""
    return checks_sql + "\n" + finish_sql


# --------------------------------------------------------------------------- #
# Per-type SQL builders (pure; unit-tested without a DB)                       #
# --------------------------------------------------------------------------- #
def build_revise_strategy_sql(
    apply_payload: dict[str, Any],
    proposal_id: str,
    applied_by: str | None,
    approval: dict[str, Any],
) -> str:
    _validate_approval_meta(approval, "revise_strategy", apply_payload)
    agent_id = apply_payload.get("agent_id")
    strategy = apply_payload.get("strategy") or {}
    nodes: list[dict[str, Any]] = apply_payload.get("strategy_nodes") or []
    if not agent_id:
        raise ValueError("revise_strategy apply_payload requires 'agent_id'")
    for key in ("diagnosis", "guiding_policy", "proximate_objectives"):
        if strategy.get(key) is None:
            raise ValueError(f"revise_strategy apply_payload.strategy requires '{key}'")
    if not nodes:
        raise ValueError("revise_strategy apply_payload requires non-empty 'strategy_nodes'")

    node_values = []
    for n in nodes:
        nt = n.get("node_type")
        if nt not in ("diagnosis", "policy", "objective"):
            raise ValueError(f"invalid strategy node_type: {nt!r}")
        if not n.get("title") or n.get("body") is None:
            raise ValueError("each strategy_node requires 'title' and 'body'")
        node_values.append(
            f"({sql_literal(agent_id)}::uuid, {sql_literal(nt)}, "
            f"{sql_literal(n['title'])}, {sql_literal(n['body'])}, "
            f"{int(n.get('rank', 1))})"
        )

    node_values_sql = ",\n  ".join(node_values)
    canonical = f"""
-- deactivate the current active strategy for this agent (one_active invariant)
update public.strategies
   set active = false
 where agent_id = {sql_literal(agent_id)}::uuid and active;

-- insert the new strategy as the next version, active
insert into public.strategies
       (agent_id, diagnosis, guiding_policy, proximate_objectives, version, active)
select {sql_literal(agent_id)}::uuid,
       {sql_literal(strategy['diagnosis'])},
       {sql_literal(strategy['guiding_policy'])},
       {_jsonb(strategy['proximate_objectives'])},
       coalesce(max(version), 0) + 1,
       true
  from public.strategies
 where agent_id = {sql_literal(agent_id)}::uuid;

-- retire the agent's current strategy nodes (shared NULL-agent nodes untouched)
update public.strategy_nodes
   set status = 'retired', updated_at = now()
 where agent_id = {sql_literal(agent_id)}::uuid and status <> 'retired';

-- insert the new node set
insert into public.strategy_nodes (agent_id, node_type, title, body, rank)
values
  {node_values_sql};
""".rstrip()

    checks = f"""  if (select count(*) from public.strategies
       where agent_id = {sql_literal(agent_id)}::uuid and active) <> 1 then
    raise exception 'apply_proposal: expected exactly one active strategy for agent %', {sql_literal(agent_id)};
  end if;"""

    ledger = _ledger_and_verify(
        proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval
    )
    return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval))


def build_add_edge_sql(
    apply_payload: dict[str, Any],
    proposal_id: str,
    applied_by: str | None,
    approval: dict[str, Any],
) -> str:
    _validate_approval_meta(approval, "add_edge", apply_payload)
    f = apply_payload.get("from_claim")
    t = apply_payload.get("to_claim")
    et = apply_payload.get("edge_type")
    w = apply_payload.get("weight")
    if not (f and t and et):
        raise ValueError("add_edge apply_payload requires 'from_claim', 'to_claim', 'edge_type'")
    if f == t:
        raise ValueError("add_edge from_claim and to_claim must differ")

    edge_lock_key = f"claim_edge:{f}:{t}:{et}"
    canonical = f"""
-- serialize semantic edge creation because the historical table has no
-- (from_claim,to_claim,edge_type) uniqueness constraint.
select pg_advisory_xact_lock(hashtextextended({sql_literal(edge_lock_key)}, 0));

-- insert the edge unless an identical (from,to,type) edge already exists
insert into public.claim_edges (from_claim, to_claim, edge_type, weight)
select {sql_literal(f)}::uuid, {sql_literal(t)}::uuid,
       {sql_literal(et)}::edge_type, {sql_literal(w)}
 where not exists (
   select 1 from public.claim_edges
    where from_claim = {sql_literal(f)}::uuid
      and to_claim = {sql_literal(t)}::uuid
      and edge_type = {sql_literal(et)}::edge_type);
""".rstrip()

    checks = f"""  if exists (select 1 from public.claim_edges
       where from_claim = {sql_literal(f)}::uuid
         and to_claim = {sql_literal(t)}::uuid
         and edge_type = {sql_literal(et)}::edge_type
         and weight is distinct from {sql_literal(w)}) then
    raise exception 'apply_proposal: claim_edge row does not match strict apply payload';
  end if;
  if not exists (select 1 from public.claim_edges
       where from_claim = {sql_literal(f)}::uuid
         and to_claim = {sql_literal(t)}::uuid
         and edge_type = {sql_literal(et)}::edge_type
         and weight is not distinct from {sql_literal(w)}) then
    raise exception 'apply_proposal: claim_edge row does not match strict apply payload';
  end if;"""

    ledger = _ledger_and_verify(
        proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval
    )
    return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval))


def build_attach_evidence_sql(
    apply_payload: dict[str, Any],
    proposal_id: str,
    applied_by: str | None,
    approval: dict[str, Any],
) -> str:
    _validate_approval_meta(approval, "attach_evidence", apply_payload)
    evidence: list[dict[str, Any]] = apply_payload.get("evidence") or []
    if not evidence:
        raise ValueError("attach_evidence apply_payload requires non-empty 'evidence'")

    statements = []
    checks = []
    for i, ev in enumerate(evidence):
        claim_id = ev.get("claim_id")
        source_id = ev.get("source_id")
        role = ev.get("role") or "grounds"
        weight = ev.get("weight")
        if not claim_id:
            raise ValueError(f"evidence[{i}] requires 'claim_id'")
        if not source_id:
            raise ValueError(
                f"evidence[{i}] requires an existing 'source_id'. "
                "kb_apply cannot create public.sources; mint the source upstream first."
            )
        statements.append(
            f"""insert into public.claim_evidence (claim_id, source_id, role, weight)
select {sql_literal(claim_id)}::uuid, {sql_literal(source_id)}::uuid,
       {sql_literal(role)}::evidence_role, {sql_literal(weight)}
on conflict (claim_id, source_id, role) do nothing;"""
        )
        checks.append(
            f"""  if exists (select 1 from public.claim_evidence
       where claim_id = {sql_literal(claim_id)}::uuid
         and source_id = {sql_literal(source_id)}::uuid
         and role = {sql_literal(role)}::evidence_role
         and weight is distinct from {sql_literal(weight)}) then
    raise exception 'apply_proposal: evidence row % does not match strict apply payload', {i};
  end if;
  if not exists (select 1 from public.claim_evidence
       where claim_id = {sql_literal(claim_id)}::uuid
         and source_id = {sql_literal(source_id)}::uuid
         and role = {sql_literal(role)}::evidence_role
         and weight is not distinct from {sql_literal(weight)}) then
    raise exception 'apply_proposal: evidence row % does not match strict apply payload', {i};
  end if;"""
        )

    canonical = "\n".join(statements)
    ledger = _ledger_and_verify(
        proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval
    )
    return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval))


def build_approve_claim_sql(
    apply_payload: dict[str, Any],
    proposal_id: str,
    applied_by: str | None,
    approval: dict[str, Any],
) -> str:
    """Build one conflict-safe transaction for a reviewed canonical graph bundle."""
    _validate_approval_meta(approval, "approve_claim", apply_payload)
    _reject_unknown(
        apply_payload,
        {
            "contract_version",
            "source_proposal_id",
            "agent_id",
            "claims",
            "sources",
            "evidence",
            "edges",
            "reasoning_tools",
            "normalization_manifest",
        },
        "approve_claim apply_payload",
    )
    if apply_payload.get("contract_version") != 2:
        raise ValueError("approve_claim apply_payload.contract_version must equal 2")
    for key in ("claims", "sources", "evidence", "edges", "reasoning_tools"):
        if key not in apply_payload:
            raise ValueError(f"approve_claim apply_payload requires explicit '{key}' collection")
    if apply_payload.get("source_proposal_id") is not None:
        _uuid(apply_payload["source_proposal_id"], "approve_claim.source_proposal_id")
    if apply_payload.get("normalization_manifest") is not None and not isinstance(
        apply_payload["normalization_manifest"], dict
    ):
        raise ValueError("approve_claim.normalization_manifest must be an object")
    agent_id = _uuid(apply_payload.get("agent_id"), "approve_claim.agent_id", nullable=True)
    raw_claims = _rows(apply_payload, "claims", required=True)
    raw_sources = _rows(apply_payload, "sources")
    raw_evidence = _rows(apply_payload, "evidence")
    raw_edges = _rows(apply_payload, "edges")
    raw_tools = _rows(apply_payload, "reasoning_tools")

    claims: list[dict[str, Any]] = []
    for index, row in enumerate(raw_claims):
        path = f"approve_claim.claims[{index}]"
        _reject_unknown(
            row,
            {"id", "type", "text", "status", "confidence", "tags", "created_by", "superseded_by"},
            path,
        )
        claim_type = row.get("type")
        if claim_type not in CLAIM_TYPES:
            raise ValueError(f"{path}.type must be one of {sorted(CLAIM_TYPES)}")
        text = row.get("text")
        if not isinstance(text, str) or not text.strip():
            raise ValueError(f"{path}.text must be a non-empty string")
        status = row.get("status", "open")
        if status != "open":
            raise ValueError(f"{path}.status must be 'open' for an insert-only approval")
        tags = row.get("tags") or []
        if not isinstance(tags, list) or any(not isinstance(tag, str) for tag in tags):
            raise ValueError(f"{path}.tags must be a list of strings")
        claims.append(
            {
                "id": _uuid(row.get("id"), f"{path}.id"),
                "type": claim_type,
                "text": text,
                "status": status,
                "confidence": _weight(row.get("confidence"), f"{path}.confidence"),
                "tags": tags,
                "created_by": _uuid(
                    row.get("created_by", agent_id), f"{path}.created_by", nullable=True
                ),
                "superseded_by": _uuid(
                    row.get("superseded_by"), f"{path}.superseded_by", nullable=True
                ),
            }
        )

    sources: list[dict[str, Any]] = []
    for index, row in enumerate(raw_sources):
        path = f"approve_claim.sources[{index}]"
        _reject_unknown(
            row,
            {"id", "source_type", "url", "storage_path", "excerpt", "hash", "created_by"},
            path,
        )
        source_type = row.get("source_type")
        if source_type not in SOURCE_TYPES:
            raise ValueError(f"{path}.source_type must be one of {sorted(SOURCE_TYPES)}")
        source_hash = row.get("hash")
        if not isinstance(source_hash, str) or not source_hash.strip():
            raise ValueError(f"{path}.hash must be a non-empty string")
        for key in ("url", "storage_path", "excerpt"):
            if row.get(key) is not None and not isinstance(row.get(key), str):
                raise ValueError(f"{path}.{key} must be a string or null")
        sources.append(
            {
                "id": _uuid(row.get("id"), f"{path}.id"),
                "source_type": source_type,
                "url": row.get("url"),
                "storage_path": row.get("storage_path"),
                "excerpt": row.get("excerpt"),
                "hash": source_hash,
                "created_by": _uuid(
                    row.get("created_by", agent_id), f"{path}.created_by", nullable=True
                ),
            }
        )

    evidence: list[dict[str, Any]] = []
    for index, row in enumerate(raw_evidence):
        path = f"approve_claim.evidence[{index}]"
        _reject_unknown(
            row,
            {"claim_id", "source_id", "role", "weight", "created_by"},
            path,
        )
        role = row.get("role", "grounds")
        if role not in EVIDENCE_ROLES:
            raise ValueError(f"{path}.role must be one of {sorted(EVIDENCE_ROLES)}")
        evidence.append(
            {
                "claim_id": _uuid(row.get("claim_id"), f"{path}.claim_id"),
                "source_id": _uuid(row.get("source_id"), f"{path}.source_id"),
                "role": role,
                "weight": _weight(row.get("weight"), f"{path}.weight"),
                "created_by": _uuid(
                    row.get("created_by", agent_id), f"{path}.created_by", nullable=True
                ),
            }
        )

    edges: list[dict[str, Any]] = []
    for index, row in enumerate(raw_edges):
        path = f"approve_claim.edges[{index}]"
        _reject_unknown(
            row,
            {"from_claim", "to_claim", "edge_type", "weight", "created_by"},
            path,
        )
        edge_type = row.get("edge_type")
        if edge_type not in EDGE_TYPES:
            raise ValueError(f"{path}.edge_type must be one of {sorted(EDGE_TYPES)}")
        from_claim = _uuid(row.get("from_claim"), f"{path}.from_claim")
        to_claim = _uuid(row.get("to_claim"), f"{path}.to_claim")
        if from_claim == to_claim:
            raise ValueError(f"{path} cannot be a self-edge")
        edges.append(
            {
                "from_claim": from_claim,
                "to_claim": to_claim,
                "edge_type": edge_type,
                "weight": _weight(row.get("weight"), f"{path}.weight"),
                "created_by": _uuid(
                    row.get("created_by", agent_id), f"{path}.created_by", nullable=True
                ),
            }
        )

    reasoning_tools: list[dict[str, Any]] = []
    for index, row in enumerate(raw_tools):
        path = f"approve_claim.reasoning_tools[{index}]"
        _reject_unknown(
            row,
            {"id", "agent_id", "name", "description", "category"},
            path,
        )
        name = row.get("name")
        description = row.get("description")
        category = row.get("category")
        if not isinstance(name, str) or not name.strip():
            raise ValueError(f"{path}.name must be a non-empty string")
        if not isinstance(description, str) or not description.strip():
            raise ValueError(f"{path}.description must be a non-empty string")
        if category is not None and not isinstance(category, str):
            raise ValueError(f"{path}.category must be a string or null")
        reasoning_tools.append(
            {
                "id": _uuid(row.get("id"), f"{path}.id"),
                "agent_id": _uuid(row.get("agent_id", agent_id), f"{path}.agent_id", nullable=True),
                "name": name,
                "description": description,
                "category": category,
            }
        )

    _dedupe([row["id"] for row in claims], "approve_claim.claims ids")
    _dedupe([row["id"] for row in sources], "approve_claim.sources ids")
    _dedupe([row["hash"] for row in sources], "approve_claim.sources hashes")
    _dedupe(
        [[row["claim_id"], row["source_id"], row["role"]] for row in evidence],
        "approve_claim.evidence keys",
    )
    _dedupe(
        [[row["from_claim"], row["to_claim"], row["edge_type"]] for row in edges],
        "approve_claim.edges keys",
    )
    _dedupe([row["id"] for row in reasoning_tools], "approve_claim.reasoning_tools ids")

    statements: list[str] = []
    checks: list[str] = []

    if sources:
        collision_checks = []
        for row in sources:
            collision_checks.append(
                f"""  if exists (select 1 from public.sources
       where hash = {sql_literal(row['hash'])}
         and id <> {sql_literal(row['id'])}::uuid) then
    raise exception 'approve_claim: source hash collision for source %', {sql_literal(row['id'])};
  end if;"""
            )
        statements.append(
            "do $source_conflicts$\nbegin\n"
            + "\n".join(collision_checks)
            + "\nend\n$source_conflicts$;"
        )

    for row in claims:
        tags = _text_array(row["tags"])
        statements.append(
            f"""insert into public.claims (id, type, text, status, confidence, tags, created_by, superseded_by)
select {sql_literal(row['id'])}::uuid, {sql_literal(row['type'])}, {sql_literal(row['text'])},
       {sql_literal(row['status'])}, {sql_literal(row['confidence'])}, {tags},
       {sql_literal(row['created_by'])}::uuid, {sql_literal(row['superseded_by'])}::uuid
on conflict (id) do nothing;"""
        )
        checks.append(
            f"""  if not exists (select 1 from public.claims
       where id = {sql_literal(row['id'])}::uuid
         and type = {sql_literal(row['type'])}
         and text = {sql_literal(row['text'])}
         and status = {sql_literal(row['status'])}
         and confidence is not distinct from {sql_literal(row['confidence'])}
         and tags is not distinct from {tags}
         and created_by is not distinct from {sql_literal(row['created_by'])}::uuid
         and superseded_by is not distinct from {sql_literal(row['superseded_by'])}::uuid) then
    raise exception 'approve_claim: claim row does not match strict apply payload: %', {sql_literal(row['id'])};
  end if;"""
        )

    for row in sources:
        statements.append(
            f"""insert into public.sources (id, source_type, url, storage_path, excerpt, hash, created_by)
select {sql_literal(row['id'])}::uuid, {sql_literal(row['source_type'])}, {sql_literal(row['url'])},
       {sql_literal(row['storage_path'])}, {sql_literal(row['excerpt'])}, {sql_literal(row['hash'])},
       {sql_literal(row['created_by'])}::uuid
on conflict do nothing;"""
        )
        checks.append(
            f"""  if exists (select 1 from public.sources
       where hash = {sql_literal(row['hash'])}
         and id <> {sql_literal(row['id'])}::uuid) then
    raise exception 'approve_claim: source hash collision for source %', {sql_literal(row['id'])};
  end if;
  if not exists (select 1 from public.sources
       where id = {sql_literal(row['id'])}::uuid
         and source_type = {sql_literal(row['source_type'])}
         and url is not distinct from {sql_literal(row['url'])}
         and storage_path is not distinct from {sql_literal(row['storage_path'])}
         and excerpt is not distinct from {sql_literal(row['excerpt'])}
         and hash = {sql_literal(row['hash'])}
         and created_by is not distinct from {sql_literal(row['created_by'])}::uuid) then
    raise exception 'approve_claim: source row does not match strict apply payload: %', {sql_literal(row['id'])};
  end if;"""
        )

    for row in reasoning_tools:
        statements.append(
            f"""insert into public.reasoning_tools (id, agent_id, name, description, category)
select {sql_literal(row['id'])}::uuid, {sql_literal(row['agent_id'])}::uuid,
       {sql_literal(row['name'])}, {sql_literal(row['description'])}, {sql_literal(row['category'])}
on conflict (id) do nothing;"""
        )
        checks.append(
            f"""  if not exists (select 1 from public.reasoning_tools
       where id = {sql_literal(row['id'])}::uuid
         and agent_id is not distinct from {sql_literal(row['agent_id'])}::uuid
         and name = {sql_literal(row['name'])}
         and description = {sql_literal(row['description'])}
         and category is not distinct from {sql_literal(row['category'])}) then
    raise exception 'approve_claim: reasoning tool row does not match strict apply payload: %', {sql_literal(row['id'])};
  end if;"""
        )

    for row in evidence:
        statements.append(
            f"""insert into public.claim_evidence (claim_id, source_id, role, weight, created_by)
select {sql_literal(row['claim_id'])}::uuid, {sql_literal(row['source_id'])}::uuid,
       {sql_literal(row['role'])}::evidence_role, {sql_literal(row['weight'])},
       {sql_literal(row['created_by'])}::uuid
on conflict (claim_id, source_id, role) do nothing;"""
        )
        checks.append(
            f"""  if exists (select 1 from public.claim_evidence
       where claim_id = {sql_literal(row['claim_id'])}::uuid
         and source_id = {sql_literal(row['source_id'])}::uuid
         and role = {sql_literal(row['role'])}::evidence_role
         and (weight is distinct from {sql_literal(row['weight'])}
           or created_by is distinct from {sql_literal(row['created_by'])}::uuid)) then
    raise exception 'approve_claim: evidence row does not match strict apply payload';
  end if;
  if not exists (select 1 from public.claim_evidence
       where claim_id = {sql_literal(row['claim_id'])}::uuid
         and source_id = {sql_literal(row['source_id'])}::uuid
         and role = {sql_literal(row['role'])}::evidence_role
         and weight is not distinct from {sql_literal(row['weight'])}
         and created_by is not distinct from {sql_literal(row['created_by'])}::uuid) then
    raise exception 'approve_claim: evidence row does not match strict apply payload';
  end if;"""
        )

    for row in edges:
        edge_lock_key = (
            f"claim_edge:{row['from_claim']}:{row['to_claim']}:{row['edge_type']}"
        )
        statements.append(
            f"""select pg_advisory_xact_lock(hashtextextended({sql_literal(edge_lock_key)}, 0));
insert into public.claim_edges (from_claim, to_claim, edge_type, weight, created_by)
select {sql_literal(row['from_claim'])}::uuid, {sql_literal(row['to_claim'])}::uuid,
       {sql_literal(row['edge_type'])}::edge_type, {sql_literal(row['weight'])},
       {sql_literal(row['created_by'])}::uuid
 where not exists (
   select 1 from public.claim_edges
    where from_claim = {sql_literal(row['from_claim'])}::uuid
      and to_claim = {sql_literal(row['to_claim'])}::uuid
      and edge_type = {sql_literal(row['edge_type'])}::edge_type);"""
        )
        checks.append(
            f"""  if exists (select 1 from public.claim_edges
       where from_claim = {sql_literal(row['from_claim'])}::uuid
         and to_claim = {sql_literal(row['to_claim'])}::uuid
         and edge_type = {sql_literal(row['edge_type'])}::edge_type
         and (weight is distinct from {sql_literal(row['weight'])}
           or created_by is distinct from {sql_literal(row['created_by'])}::uuid)) then
    raise exception 'approve_claim: edge row does not match strict apply payload';
  end if;
  if not exists (select 1 from public.claim_edges
       where from_claim = {sql_literal(row['from_claim'])}::uuid
         and to_claim = {sql_literal(row['to_claim'])}::uuid
         and edge_type = {sql_literal(row['edge_type'])}::edge_type
         and weight is not distinct from {sql_literal(row['weight'])}
         and created_by is not distinct from {sql_literal(row['created_by'])}::uuid) then
    raise exception 'approve_claim: edge row does not match strict apply payload';
  end if;"""
        )

    canonical = "\n\n".join(statements)
    ledger = _ledger_and_verify(
        proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval
    )
    return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval))


def _wrap_txn(canonical_sql: str, ledger_sql: str, approval_guard_sql: str) -> str:
    return (
        "begin;\n"
        "set local standard_conforming_strings = on;\n"
        f"{approval_guard_sql}\n"
        f"{canonical_sql}\n"
        f"{ledger_sql}\n"
        "commit;\n"
    )


BUILDERS = {
    "revise_strategy": build_revise_strategy_sql,
    "add_edge": build_add_edge_sql,
    "attach_evidence": build_attach_evidence_sql,
    "approve_claim": build_approve_claim_sql,
}


def build_apply_sql(proposal: dict[str, Any], applied_by: str | None) -> str:
    ptype = proposal["proposal_type"]
    if ptype not in BUILDERS:
        raise ValueError(f"apply_proposal cannot apply proposal_type {ptype!r}")
    payload = proposal.get("payload") or {}
    apply_payload = payload.get("apply_payload")
    if apply_payload is None:
        raise ValueError(
            "proposal payload has no 'apply_payload' strict contract. "
            "Run the normalizer to produce apply_payload before applying."
        )
    return BUILDERS[ptype](
        apply_payload,
        proposal["id"],
        applied_by or SERVICE_AGENT_HANDLE,
        proposal,
    )


def assert_applyable(proposal: dict[str, Any]) -> None:
    status = proposal.get("status")
    if status == "applied":
        raise SystemExit(f"proposal {proposal['id']} is already applied (idempotent no-op)")
    if status != "approved":
        raise SystemExit(
            f"proposal {proposal['id']} has status {status!r}; only 'approved' proposals apply"
        )


def assert_receiptable(proposal: dict[str, Any]) -> None:
    if proposal.get("status") != "applied":
        raise SystemExit(
            f"proposal {proposal['id']} has status {proposal.get('status')!r}; "
            "--receipt-only requires an applied proposal"
        )
    replay_receipt.expected_row_counts(proposal)


# --------------------------------------------------------------------------- #
# Execution (docker exec as the kb_apply role)                                 #
# --------------------------------------------------------------------------- #
def load_password(secrets_file: str) -> str:
    path = Path(secrets_file)
    if not path.is_file():
        raise SystemExit(f"secrets file not found: {secrets_file}")
    for line in path.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, _, val = line.partition("=")
        if key.strip() in ("KB_APPLY_PASSWORD", "KB_APPLY_DB_PASSWORD", "PGPASSWORD"):
            return val.strip().strip('"').strip("'")
    raise SystemExit(f"no KB_APPLY_PASSWORD/KB_APPLY_DB_PASSWORD/PGPASSWORD entry in {secrets_file}")


def run_psql(
    args: argparse.Namespace,
    sql: str,
    password: str,
    *,
    redact_output_on_error: bool = False,
) -> str:
    docker_binary = shutil.which("docker") or "docker"
    command = [
        docker_binary, "exec", "-e", "PGPASSWORD", "-i", args.container,
        "psql", "-U", args.role, "-h", args.host, "-d", args.db,
        "-v", "ON_ERROR_STOP=1", "-At", "-q",
    ]
    result = subprocess.run(
        command, input=sql, text=True, capture_output=True,
        env={"PGPASSWORD": password, "PATH": "/usr/bin:/bin:/usr/local/bin"},
        check=False,
    )
    if result.returncode != 0:
        if redact_output_on_error:
            raise SystemExit(
                f"psql failed ({result.returncode}); private postflight output redacted"
            )
        raise SystemExit(
            f"psql failed ({result.returncode})\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
        )
    return result.stdout


def load_proposal(args: argparse.Namespace, password: str) -> dict[str, Any]:
    sql = f"""select jsonb_build_object(
        'id', id::text,
        'proposal_type', proposal_type,
        'status', status,
        'payload', payload,
        'reviewed_by_handle', reviewed_by_handle,
        'reviewed_by_agent_id', reviewed_by_agent_id::text,
        'reviewed_at', reviewed_at::text,
        'review_note', review_note,
        'applied_by_handle', applied_by_handle,
        'applied_by_agent_id', applied_by_agent_id::text,
        'applied_at', applied_at::text)::text
      from kb_stage.kb_proposals
     where id = {sql_literal(args.proposal_id)}::uuid;"""
    out = run_psql(args, sql, password).strip()
    if not out:
        raise SystemExit(f"proposal not found: {args.proposal_id}")
    return json.loads(out)


def capture_replay_receipt(
    args: argparse.Namespace,
    proposal: dict[str, Any],
    password: str,
    *,
    apply_sql: str | None = None,
) -> tuple[Path, dict[str, Any]]:
    """Read exact postflight rows and atomically write a private replay receipt."""
    postflight_sql = replay_receipt.build_postflight_sql(proposal)
    raw = run_psql(
        args,
        postflight_sql,
        password,
        redact_output_on_error=True,
    ).strip()
    if not raw:
        raise RuntimeError("postflight query returned no row")
    canonical_rows = json.loads(raw)
    if not isinstance(canonical_rows, dict):
        raise RuntimeError("postflight query did not return a JSON object")
    apply_sql_source = "exact_executed_sql"
    if apply_sql is None:
        apply_sql = build_apply_sql(proposal, proposal.get("applied_by_handle"))
        apply_sql_source = "reconstructed_current_engine"
    receipt = replay_receipt.build_replay_receipt(
        proposal,
        canonical_rows,
        apply_sql_sha256=replay_receipt.sha256_text(apply_sql),
        apply_sql_source=apply_sql_source,
    )
    path = replay_receipt.resolve_receipt_path(
        str(proposal["id"]),
        receipt_out=args.receipt_out,
        receipt_dir=args.receipt_dir,
    )
    replay_receipt.write_private_receipt(path, receipt)
    return path, receipt


def parse_args(argv: list[str]) -> argparse.Namespace:
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("proposal_id", help="UUID of the approved proposal to apply")
    p.add_argument(
        "--applied-by",
        default=SERVICE_AGENT_HANDLE,
        help="agent handle recorded as applied_by_handle and resolved to the "
        "applied_by_agent_id FK (default: the kb-apply service agent)",
    )
    p.add_argument("--dry-run", action="store_true", help="print the SQL, do not execute")
    p.add_argument("--secrets-file", default=DEFAULT_SECRETS_FILE)
    p.add_argument("--container", default=DEFAULT_CONTAINER)
    p.add_argument("--db", default=DEFAULT_DB)
    p.add_argument("--host", default=DEFAULT_HOST)
    p.add_argument("--role", default=DEFAULT_ROLE)
    p.add_argument(
        "--receipt-only",
        action="store_true",
        help="recover the private row-level replay receipt for an already applied proposal; no writes",
    )
    p.add_argument(
        "--receipt-dir",
        default=None,
        help="private receipt directory (default: KB_APPLY_RECEIPT_DIR or "
        f"{replay_receipt.DEFAULT_RECEIPT_DIR})",
    )
    p.add_argument(
        "--receipt-out",
        default=None,
        help="exact private receipt path; overrides --receipt-dir",
    )
    args = p.parse_args(argv)
    if args.receipt_only and args.dry_run:
        p.error("--receipt-only and --dry-run are mutually exclusive")
    return args


def main(argv: list[str] | None = None) -> int:
    args = parse_args(sys.argv[1:] if argv is None else argv)

    if args.receipt_only:
        password = load_password(args.secrets_file)
        proposal = load_proposal(args, password)
        assert_receiptable(proposal)
        path, receipt = capture_replay_receipt(args, proposal, password)
        print(
            json.dumps(
                {
                    "proposal_id": proposal["id"],
                    "receipt_path": str(path),
                    "replay_material_sha256": receipt["hashes"]["replay_material_sha256"],
                    "replay_ready": receipt["replay_ready"],
                },
                sort_keys=True,
            )
        )
        return 0

    if args.dry_run:
        # Dry-run still needs the proposal to build SQL; read it as kb_apply.
        password = load_password(args.secrets_file)
        proposal = load_proposal(args, password)
        assert_applyable(proposal)
        print(build_apply_sql(proposal, args.applied_by))
        return 0

    password = load_password(args.secrets_file)
    proposal = load_proposal(args, password)
    assert_applyable(proposal)
    sql = build_apply_sql(proposal, args.applied_by)
    run_psql(args, sql, password)
    applied_proposal = load_proposal(args, password)
    try:
        path, receipt = capture_replay_receipt(
            args,
            applied_proposal,
            password,
            apply_sql=sql,
        )
    except Exception as exc:
        raise SystemExit(
            f"proposal {proposal['id']} committed, but private replay receipt capture failed: {exc}. "
            f"Recover without reapplying via: {Path(__file__).name} {proposal['id']} --receipt-only"
        ) from exc
    print(
        json.dumps(
            {
                "applied": True,
                "proposal_id": proposal["id"],
                "proposal_type": proposal["proposal_type"],
                "receipt_path": str(path),
                "replay_material_sha256": receipt["hashes"]["replay_material_sha256"],
                "replay_ready": receipt["replay_ready"],
            },
            sort_keys=True,
        )
    )
    return 0


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