#!/usr/bin/env python3
"""Normalize one rich KB packet and stage its strict child in a disposable clone."""

from __future__ import annotations

import argparse
import copy
import hashlib
import json
import sys
import uuid
from pathlib import Path
from typing import Any

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))

import apply_proposal as ap  # noqa: E402
import kb_proposal_normalize as normalize  # noqa: E402
import run_leo_clone_bound_handler_checkpoint as bound  # noqa: E402

V3_BUNDLE_SCHEMA = "livingip.kbSourceProposalBundle.v3"
V3_MANIFEST_SCHEMA = "livingip.kbSourceExtractionManifest.v3"


def canonical_sha256(value: Any) -> str:
    encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
    return hashlib.sha256(encoded).hexdigest()


def _uuid(value: Any, label: str) -> str:
    try:
        return str(uuid.UUID(str(value)))
    except ValueError as exc:
        raise bound.CheckpointError(f"{label} must be a full UUID") from exc


def prepare_normalized_child(parent: dict[str, Any]) -> dict[str, Any]:
    parent_id = _uuid(parent.get("id"), "parent proposal id")
    preview = normalize.normalize_proposal(parent)
    if preview.get("normalization_state") != "strict_children_ready" or preview.get("strict_child_count") != 1:
        raise bound.CheckpointError(
            "rich proposal did not normalize to exactly one strict child: "
            + json.dumps(
                {
                    "normalization_state": preview.get("normalization_state"),
                    "strict_child_count": preview.get("strict_child_count"),
                    "blocked_fragments": preview.get("blocked_fragments"),
                },
                sort_keys=True,
            )
        )
    strict = preview["strict_child_proposals"][0]
    proposal_type = strict.get("proposal_type")
    if proposal_type not in ap.APPLYABLE_TYPES:
        raise bound.CheckpointError(f"normalized child type is not applyable: {proposal_type!r}")
    payload = json.loads(json.dumps(strict.get("payload"), sort_keys=True))
    apply_payload = payload.get("apply_payload") if isinstance(payload, dict) else None
    if not isinstance(apply_payload, dict):
        raise bound.CheckpointError("normalized child has no strict payload.apply_payload object")
    manifest = apply_payload.setdefault("normalization_manifest", {})
    if not isinstance(manifest, dict):
        raise bound.CheckpointError("normalized child normalization_manifest must be an object")
    parent_payload = parent.get("payload")
    ingestion_manifest = parent_payload.get("ingestion_manifest") if isinstance(parent_payload, dict) else None
    if ingestion_manifest is not None:
        if not isinstance(ingestion_manifest, dict):
            raise bound.CheckpointError("parent payload.ingestion_manifest must be an object")
        manifest["ingestion_manifest"] = json.loads(json.dumps(ingestion_manifest, sort_keys=True))
    parent_packet_sha256 = canonical_sha256(parent)
    manifest["parent_packet_sha256"] = parent_packet_sha256
    child_payload_sha256 = canonical_sha256(payload)
    child_id = str(
        uuid.uuid5(
            uuid.NAMESPACE_URL,
            f"livingip:normalized-strict-child:{parent_id}:{proposal_type}:{child_payload_sha256}",
        )
    )
    source_ref = f"normalized:{parent_id}:{child_payload_sha256[:16]}"
    child = {
        "id": child_id,
        "proposal_type": proposal_type,
        "status": "pending_review",
        "proposed_by_handle": parent.get("proposed_by_handle"),
        "proposed_by_agent_id": parent.get("proposed_by_agent_id"),
        "channel": parent.get("channel") or "clone_composition",
        "source_ref": source_ref,
        "rationale": f"Strict normalized child of rich source packet {parent_id}.",
        "payload": payload,
        "parent_packet_sha256": parent_packet_sha256,
        "payload_sha256": child_payload_sha256,
    }
    validation_snapshot = {
        **child,
        "status": "approved",
        "reviewed_by_handle": "normalization-preflight",
        "reviewed_by_agent_id": None,
        "reviewed_at": "2000-01-01T00:00:00+00:00",
        "review_note": "Build-only validation of the exact normalized payload.",
    }
    ap.build_apply_sql(validation_snapshot, ap.SERVICE_AGENT_HANDLE)
    return child


def build_stage_sql(child: dict[str, Any]) -> str:
    proposal_id = ap.sql_literal(_uuid(child.get("id"), "child proposal id"))
    proposal_type = ap.sql_literal(str(child["proposal_type"]))
    proposed_by_handle = ap.sql_literal(str(child.get("proposed_by_handle") or ""))
    proposed_by_agent_id = child.get("proposed_by_agent_id")
    agent_sql = (
        f"{ap.sql_literal(_uuid(proposed_by_agent_id, 'proposed_by_agent_id'))}::uuid"
        if proposed_by_agent_id
        else "null::uuid"
    )
    channel = ap.sql_literal(str(child["channel"]))
    source_ref = ap.sql_literal(str(child["source_ref"]))
    rationale = ap.sql_literal(str(child["rationale"]))
    payload = ap.sql_literal(json.dumps(child["payload"], sort_keys=True))
    return f"""\\set ON_ERROR_STOP on
begin;
create temporary table normalized_stage_result (created boolean not null) on commit drop;
with inserted as (
  insert into kb_stage.kb_proposals
    (id, proposal_type, status, proposed_by_handle, proposed_by_agent_id,
     channel, source_ref, rationale, payload)
  values
    ({proposal_id}::uuid, {proposal_type}, 'pending_review', nullif({proposed_by_handle}, ''),
     {agent_sql}, {channel}, {source_ref}, {rationale}, {payload}::jsonb)
  on conflict (id) do nothing
  returning id
)
insert into normalized_stage_result(created)
select exists(select 1 from inserted);

do $normalized_stage$
declare
  marker_count integer;
  exact_count integer;
begin
  select count(*) into marker_count
    from kb_stage.kb_proposals
   where source_ref = {source_ref};
  select count(*) into exact_count
    from kb_stage.kb_proposals
   where id = {proposal_id}::uuid
     and proposal_type = {proposal_type}
     and status = 'pending_review'
     and proposed_by_handle is not distinct from nullif({proposed_by_handle}, '')
     and proposed_by_agent_id is not distinct from {agent_sql}
     and channel = {channel}
     and source_ref = {source_ref}
     and rationale = {rationale}
     and payload = {payload}::jsonb
     and reviewed_by_handle is null
     and reviewed_by_agent_id is null
     and reviewed_at is null
     and review_note is null
     and applied_by_handle is null
     and applied_by_agent_id is null
     and applied_at is null;
  if marker_count <> 1 or exact_count <> 1 then
    raise exception 'normalized stage validation failed: marker_count=%, exact_count=%',
      marker_count, exact_count;
  end if;
end
$normalized_stage$;

select jsonb_build_object(
  'created', (select created from normalized_stage_result),
  'proposal_id', id::text,
  'proposal_type', proposal_type,
  'status', status,
  'source_ref', source_ref,
  'payload_sha256', {ap.sql_literal(str(child["payload_sha256"]))}
)::text
from kb_stage.kb_proposals
where id = {proposal_id}::uuid;
commit;
"""


def prepare_compiled_v3_child(bundle: dict[str, Any]) -> dict[str, Any]:
    """Revalidate one build-only V3 compiler bundle before disposable staging."""

    if bundle.get("schema") != V3_BUNDLE_SCHEMA:
        raise bound.CheckpointError(f"compiled bundle schema must equal {V3_BUNDLE_SCHEMA!r}")
    if (
        bundle.get("status") != "pending_review"
        or bundle.get("build_only") is not True
        or bundle.get("database_write_performed") is not False
        or bundle.get("canonical_apply_performed") is not False
    ):
        raise bound.CheckpointError("compiled V3 bundle is not build-only pending-review material")

    hashes = bundle.get("hashes")
    if not isinstance(hashes, dict):
        raise bound.CheckpointError("compiled V3 bundle hashes must be an object")
    expected_bundle_hash = hashes.get("bundle_content_sha256")
    preimage = copy.deepcopy(bundle)
    preimage_hashes = preimage.get("hashes")
    if not isinstance(expected_bundle_hash, str) or not isinstance(preimage_hashes, dict):
        raise bound.CheckpointError("compiled V3 bundle content hash is missing")
    preimage_hashes.pop("bundle_content_sha256", None)
    if canonical_sha256(preimage) != expected_bundle_hash:
        raise bound.CheckpointError("compiled V3 bundle content hash does not match")

    manifest = bundle.get("validated_manifest")
    child = bundle.get("strict_child_proposal")
    if not isinstance(manifest, dict) or manifest.get("schema") != V3_MANIFEST_SCHEMA:
        raise bound.CheckpointError("compiled V3 bundle has no validated V3 manifest")
    if not isinstance(child, dict):
        raise bound.CheckpointError("compiled V3 bundle has no strict child proposal")
    payload = child.get("payload")
    apply_payload = payload.get("apply_payload") if isinstance(payload, dict) else None
    if (
        child.get("status") != "pending_review"
        or child.get("proposal_type") != "approve_claim"
        or not isinstance(apply_payload, dict)
        or apply_payload.get("contract_version") != 3
    ):
        raise bound.CheckpointError("compiled V3 child is not one pending-review approve_claim v3 proposal")

    payload_hash = canonical_sha256(payload)
    if child.get("payload_sha256") != payload_hash or hashes.get("strict_child_payload_sha256") != payload_hash:
        raise bound.CheckpointError("compiled V3 child payload hash does not match")
    if child.get("parent_packet_sha256") != hashes.get("parent_packet_sha256"):
        raise bound.CheckpointError("compiled V3 parent packet hash does not match")

    stage_preview = bundle.get("stage_preview")
    stage_sql_hash = hashlib.sha256(build_stage_sql(child).encode("utf-8")).hexdigest()
    if (
        not isinstance(stage_preview, dict)
        or stage_preview.get("target_table") != "kb_stage.kb_proposals"
        or stage_preview.get("target_status") != "pending_review"
        or stage_preview.get("executed") is not False
        or stage_preview.get("stage_sql_sha256") != stage_sql_hash
    ):
        raise bound.CheckpointError("compiled V3 stage preview does not match the exact child")

    validation_snapshot = copy.deepcopy(child)
    validation_snapshot.update(
        {
            "status": "approved",
            "reviewed_by_handle": "v3-compiler-preflight",
            "reviewed_by_agent_id": None,
            "reviewed_at": "2000-01-01T00:00:00+00:00",
            "review_note": "Build-only validation of exact compiled V3 material.",
        }
    )
    try:
        ap.build_apply_sql(validation_snapshot, ap.SERVICE_AGENT_HANDLE)
    except (TypeError, ValueError) as exc:
        raise bound.CheckpointError(f"compiled V3 child failed strict writer preflight: {exc}") from exc
    return copy.deepcopy(child)


def _stage_child(container: str, database: str, child: dict[str, Any]) -> dict[str, Any]:
    target = bound.container_identity(container)
    try:
        production = bound.container_identity(bound.PRODUCTION_CONTAINER)
    except bound.CheckpointError as exc:
        detail = str(exc).lower()
        if "no such object" not in detail and "no such container" not in detail:
            raise
        production = {"id": None, "mount_sources": []}
    bound.validate_disposable_target_identity(target, production)
    if target["id"] == production["id"]:
        raise bound.CheckpointError("normalized staging target resolves to production")
    result = bound._psql_json(str(target["id"]), database, build_stage_sql(child))
    if result.get("proposal_id") != child["id"] or result.get("status") != "pending_review":
        raise bound.CheckpointError("normalized staging did not return the exact pending child")
    return {"child": child, "database_receipt": result, "bound_container_id": str(target["id"])}


def stage_normalized_child(container: str, database: str, parent: dict[str, Any]) -> dict[str, Any]:
    return _stage_child(container, database, prepare_normalized_child(parent))


def stage_compiled_v3_bundle(container: str, database: str, bundle: dict[str, Any]) -> dict[str, Any]:
    return _stage_child(container, database, prepare_compiled_v3_child(bundle))


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--container", required=True)
    parser.add_argument("--db", required=True)
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument("--proposal-file", type=Path, help="rich V1/V2 proposal to normalize then stage")
    source.add_argument(
        "--compiled-v3-bundle", type=Path, help="build-only V3 compiler bundle to revalidate then stage"
    )
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv)
    try:
        if args.container == bound.PRODUCTION_CONTAINER:
            raise bound.CheckpointError("--container must be a disposable non-production target")
        input_path = args.compiled_v3_bundle or args.proposal_file
        packet = json.loads(input_path.read_text(encoding="utf-8"))
        if not isinstance(packet, dict):
            raise bound.CheckpointError("staging input must contain one JSON object")
        result = (
            stage_compiled_v3_bundle(args.container, args.db, packet)
            if args.compiled_v3_bundle
            else stage_normalized_child(args.container, args.db, packet)
        )
    except (OSError, json.JSONDecodeError, bound.CheckpointError, SystemExit, ValueError) as exc:
        print(json.dumps({"status": "rejected", "error": str(exc)}, sort_keys=True))
        return 2
    print(
        json.dumps(
            {
                "status": "pending_review",
                "proposal_id": result["child"]["id"],
                "payload_sha256": result["child"]["payload_sha256"],
                "created": result["database_receipt"].get("created"),
                "bound_container_id": result["bound_container_id"],
                "contract_version": (result["child"].get("payload") or {})
                .get("apply_payload", {})
                .get("contract_version", 2),
            },
            sort_keys=True,
        )
    )
    return 0


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