#!/usr/bin/env python3
"""Audit how much of the canonical KB can be rebuilt from staged source data.

The auditor only issues aggregate SELECT queries inside a repeatable-read,
read-only transaction. It reports external snapshot recovery separately from
source-derived recompilation because those are materially different guarantees.
"""

from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime
from typing import Any

SAFE_NAME_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\Z")

REQUIRED_COLUMNS: dict[tuple[str, str], tuple[str, ...]] = {
    ("public", "agents"): ("id", "handle"),
    ("public", "claims"): (
        "id",
        "type",
        "text",
        "status",
        "confidence",
        "tags",
        "created_by",
        "superseded_by",
        "created_at",
        "updated_at",
    ),
    ("public", "sources"): (
        "id",
        "source_type",
        "url",
        "storage_path",
        "excerpt",
        "hash",
        "captured_at",
        "created_by",
        "created_at",
    ),
    ("public", "claim_evidence"): ("claim_id", "source_id", "role", "weight", "created_at"),
    ("public", "claim_edges"): ("from_claim", "to_claim", "edge_type", "created_at"),
    ("kb_stage", "canonical_mappings"): ("legacy_key", "target_table", "target_id", "first_run_id"),
    ("kb_stage", "import_runs"): (
        "id",
        "label",
        "repo_path",
        "inventory_path",
        "started_at",
        "completed_at",
    ),
    ("kb_stage", "inventory_items"): ("run_id",),
    ("kb_stage", "staged_claims"): (
        "run_id",
        "path",
        "title",
        "body",
        "claim_type",
        "status",
        "confidence_numeric",
        "tags",
        "created_by_handle",
        "created_at_legacy",
        "source_ref",
        "sourced_from",
        "warnings",
        "raw_frontmatter",
    ),
    ("kb_stage", "staged_sources"): (
        "run_id",
        "source_key",
        "path",
        "source_type",
        "url",
        "storage_path",
        "title",
        "excerpt",
        "hash",
        "captured_at",
        "created_by_handle",
        "warnings",
        "raw_frontmatter",
    ),
    ("kb_stage", "staged_claim_sources"): ("run_id", "claim_path", "source_key", "role", "weight"),
    ("kb_stage", "staged_claim_edges"): (
        "run_id",
        "from_path",
        "to_path",
        "edge_type",
        "resolution_state",
    ),
    ("kb_stage", "kb_proposals"): ("status", "payload", "applied_at"),
}


@dataclass(frozen=True)
class CommandResult:
    returncode: int
    stdout: str
    stderr: str


CommandRunner = Callable[[Sequence[str], str, int], CommandResult]


def _sql_values(rows: Sequence[Sequence[str]]) -> str:
    return ",\n        ".join(
        "(" + ", ".join("'" + value.replace("'", "''") + "'" for value in row) + ")" for row in rows
    )


def build_audit_sql() -> str:
    relation_rows = [(schema, table) for schema, table in REQUIRED_COLUMNS]
    column_rows = [
        (schema, table, column) for (schema, table), columns in REQUIRED_COLUMNS.items() for column in columns
    ]
    relation_values = _sql_values(relation_rows)
    column_values = _sql_values(column_rows)

    return f"""\
\\set ON_ERROR_STOP on
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;

WITH required_relations(schema_name, table_name) AS (
    VALUES
        {relation_values}
),
required_columns(schema_name, table_name, column_name) AS (
    VALUES
        {column_values}
)
SELECT (
    NOT EXISTS (
        SELECT 1
        FROM required_relations r
        WHERE to_regclass(r.schema_name || '.' || r.table_name) IS NULL
    )
    AND NOT EXISTS (
        SELECT 1
        FROM required_columns r
        LEFT JOIN information_schema.columns c
          ON c.table_schema = r.schema_name
         AND c.table_name = r.table_name
         AND c.column_name = r.column_name
        WHERE c.column_name IS NULL
    )
) AS schema_ok
\\gset

\\if :schema_ok
WITH claim_map AS (
    SELECT legacy_key, target_id, first_run_id
    FROM kb_stage.canonical_mappings
    WHERE target_table = 'claims'
),
source_map AS (
    SELECT legacy_key, target_id, first_run_id
    FROM kb_stage.canonical_mappings
    WHERE target_table = 'sources'
),
mapped_claim_stage AS (
    SELECT
        cm.target_id,
        sc.*,
        c.type AS canonical_type,
        c.text AS canonical_text,
        c.status AS canonical_status,
        c.confidence AS canonical_confidence,
        c.tags AS canonical_tags,
        c.created_by AS canonical_created_by,
        c.superseded_by AS canonical_superseded_by,
        c.created_at AS canonical_created_at,
        c.updated_at AS canonical_updated_at,
        actor.id AS staged_actor_id
    FROM claim_map cm
    JOIN kb_stage.staged_claims sc
      ON sc.run_id = cm.first_run_id
     AND sc.path = cm.legacy_key
    JOIN public.claims c ON c.id = cm.target_id
    LEFT JOIN public.agents actor ON actor.handle = sc.created_by_handle
),
mapped_source_stage AS (
    SELECT
        sm.target_id,
        ss.*,
        s.source_type AS canonical_source_type,
        s.url AS canonical_url,
        s.storage_path AS canonical_storage_path,
        s.excerpt AS canonical_excerpt,
        s.hash AS canonical_hash,
        s.captured_at AS canonical_captured_at,
        s.created_by AS canonical_created_by,
        s.created_at AS canonical_created_at,
        actor.id AS staged_actor_id
    FROM source_map sm
    JOIN kb_stage.staged_sources ss
      ON ss.run_id = sm.first_run_id
     AND ss.source_key = sm.legacy_key
    JOIN public.sources s ON s.id = sm.target_id
    LEFT JOIN public.agents actor ON actor.handle = ss.created_by_handle
),
evidence_projected AS (
    SELECT
        scs.claim_path,
        scs.source_key,
        cm.target_id AS claim_id,
        sm.target_id AS source_id,
        scs.role,
        scs.weight
    FROM kb_stage.staged_claim_sources scs
    LEFT JOIN claim_map cm ON cm.legacy_key = scs.claim_path
    LEFT JOIN source_map sm ON sm.legacy_key = scs.source_key
),
evidence_distinct AS (
    SELECT DISTINCT claim_id, source_id, role, weight
    FROM evidence_projected
    WHERE claim_id IS NOT NULL AND source_id IS NOT NULL
),
edge_projected AS (
    SELECT
        sce.resolution_state,
        from_map.target_id AS from_claim,
        to_map.target_id AS to_claim,
        sce.edge_type
    FROM kb_stage.staged_claim_edges sce
    LEFT JOIN claim_map from_map ON from_map.legacy_key = sce.from_path
    LEFT JOIN claim_map to_map ON to_map.legacy_key = sce.to_path
),
edge_distinct AS (
    SELECT DISTINCT from_claim, to_claim, edge_type
    FROM edge_projected
    WHERE resolution_state = 'resolved'
      AND from_claim IS NOT NULL
      AND to_claim IS NOT NULL
),
latest_import AS (
    SELECT max(completed_at) AS completed_at
    FROM kb_stage.import_runs
),
run_rows AS (
    SELECT
        r.id,
        r.label,
        r.repo_path,
        r.inventory_path,
        r.started_at,
        r.completed_at,
        (SELECT count(*) FROM kb_stage.inventory_items i WHERE i.run_id = r.id) AS inventory_items,
        (SELECT count(*) FROM kb_stage.staged_claims sc WHERE sc.run_id = r.id) AS staged_claims,
        (SELECT count(*) FROM kb_stage.staged_sources ss WHERE ss.run_id = r.id) AS staged_sources,
        (SELECT count(*) FROM kb_stage.staged_claim_sources scs WHERE scs.run_id = r.id) AS staged_claim_sources,
        (SELECT count(*) FROM kb_stage.staged_claim_edges sce WHERE sce.run_id = r.id) AS staged_claim_edges
    FROM kb_stage.import_runs r
),
proposal_status_counts AS (
    SELECT status, count(*) AS rows
    FROM kb_stage.kb_proposals
    GROUP BY status
)
SELECT jsonb_build_object(
    'identity', jsonb_build_object(
        'database', current_database(),
        'transaction_read_only', current_setting('transaction_read_only'),
        'server_version_num', current_setting('server_version_num')
    ),
    'canonical_counts', jsonb_build_object(
        'claims', (SELECT count(*) FROM public.claims),
        'sources', (SELECT count(*) FROM public.sources),
        'claim_evidence', (SELECT count(*) FROM public.claim_evidence),
        'claim_edges', (SELECT count(*) FROM public.claim_edges)
    ),
    'staging_counts', jsonb_build_object(
        'canonical_mappings', (SELECT count(*) FROM kb_stage.canonical_mappings),
        'import_runs', (SELECT count(*) FROM kb_stage.import_runs),
        'inventory_items', (SELECT count(*) FROM kb_stage.inventory_items),
        'staged_claims', (SELECT count(*) FROM kb_stage.staged_claims),
        'staged_sources', (SELECT count(*) FROM kb_stage.staged_sources),
        'staged_claim_sources', (SELECT count(*) FROM kb_stage.staged_claim_sources),
        'staged_claim_edges', (SELECT count(*) FROM kb_stage.staged_claim_edges),
        'kb_proposals', (SELECT count(*) FROM kb_stage.kb_proposals)
    ),
    'mappings', jsonb_build_object(
        'claims', jsonb_build_object(
            'rows', (SELECT count(*) FROM claim_map),
            'legacy_keys', (SELECT count(DISTINCT legacy_key) FROM claim_map),
            'target_ids', (SELECT count(DISTINCT target_id) FROM claim_map),
            'missing_canonical_targets', (
                SELECT count(*) FROM claim_map cm
                WHERE NOT EXISTS (SELECT 1 FROM public.claims c WHERE c.id = cm.target_id)
            ),
            'with_first_run_staged_row', (
                SELECT count(*) FROM claim_map cm
                WHERE EXISTS (
                    SELECT 1 FROM kb_stage.staged_claims sc
                    WHERE sc.run_id = cm.first_run_id AND sc.path = cm.legacy_key
                )
            ),
            'canonical_target_rows_covered', (
                SELECT count(DISTINCT cm.target_id)
                FROM claim_map cm JOIN public.claims c ON c.id = cm.target_id
            ),
            'canonical_target_rows_unmapped', (
                SELECT count(*) FROM public.claims c
                WHERE NOT EXISTS (SELECT 1 FROM claim_map cm WHERE cm.target_id = c.id)
            ),
            'staged_distinct_keys', (SELECT count(DISTINCT path) FROM kb_stage.staged_claims),
            'staged_distinct_keys_unmapped', (
                SELECT count(*)
                FROM (SELECT DISTINCT path FROM kb_stage.staged_claims) staged
                WHERE NOT EXISTS (SELECT 1 FROM claim_map cm WHERE cm.legacy_key = staged.path)
            )
        ),
        'sources', jsonb_build_object(
            'rows', (SELECT count(*) FROM source_map),
            'legacy_keys', (SELECT count(DISTINCT legacy_key) FROM source_map),
            'target_ids', (SELECT count(DISTINCT target_id) FROM source_map),
            'missing_canonical_targets', (
                SELECT count(*) FROM source_map sm
                WHERE NOT EXISTS (SELECT 1 FROM public.sources s WHERE s.id = sm.target_id)
            ),
            'with_first_run_staged_row', (
                SELECT count(*) FROM source_map sm
                WHERE EXISTS (
                    SELECT 1 FROM kb_stage.staged_sources ss
                    WHERE ss.run_id = sm.first_run_id AND ss.source_key = sm.legacy_key
                )
            ),
            'canonical_target_rows_covered', (
                SELECT count(DISTINCT sm.target_id)
                FROM source_map sm JOIN public.sources s ON s.id = sm.target_id
            ),
            'canonical_target_rows_unmapped', (
                SELECT count(*) FROM public.sources s
                WHERE NOT EXISTS (SELECT 1 FROM source_map sm WHERE sm.target_id = s.id)
            ),
            'staged_distinct_keys', (SELECT count(DISTINCT source_key) FROM kb_stage.staged_sources),
            'staged_distinct_keys_unmapped', (
                SELECT count(*)
                FROM (SELECT DISTINCT source_key FROM kb_stage.staged_sources) staged
                WHERE NOT EXISTS (SELECT 1 FROM source_map sm WHERE sm.legacy_key = staged.source_key)
            )
        ),
        'unsupported_target_rows', (
            SELECT count(*) FROM kb_stage.canonical_mappings
            WHERE target_table NOT IN ('claims', 'sources')
        )
    ),
    'claim_parity', jsonb_build_object(
        'compared_fields', jsonb_build_array(
            'type', 'text_from_title', 'status', 'confidence', 'tags', 'created_by',
            'created_at_when_legacy_timestamp_exists'
        ),
        'eligible_rows', (SELECT count(*) FROM mapped_claim_stage),
        'exact_projection_rows', (
            SELECT count(*) FROM mapped_claim_stage m
            WHERE m.canonical_type = m.claim_type
              AND m.canonical_text = m.title
              AND m.canonical_status = m.status
              AND m.canonical_confidence IS NOT DISTINCT FROM m.confidence_numeric
              AND m.canonical_tags IS NOT DISTINCT FROM m.tags
              AND m.canonical_created_by IS NOT DISTINCT FROM m.staged_actor_id
              AND (m.created_at_legacy IS NULL OR m.canonical_created_at = m.created_at_legacy)
        ),
        'field_matches', jsonb_build_object(
            'type', (SELECT count(*) FROM mapped_claim_stage WHERE canonical_type = claim_type),
            'text_from_title', (SELECT count(*) FROM mapped_claim_stage WHERE canonical_text = title),
            'status', (SELECT count(*) FROM mapped_claim_stage WHERE canonical_status = status),
            'confidence', (
                SELECT count(*) FROM mapped_claim_stage
                WHERE canonical_confidence IS NOT DISTINCT FROM confidence_numeric
            ),
            'tags', (
                SELECT count(*) FROM mapped_claim_stage
                WHERE canonical_tags IS NOT DISTINCT FROM tags
            ),
            'created_by', (
                SELECT count(*) FROM mapped_claim_stage
                WHERE canonical_created_by IS NOT DISTINCT FROM staged_actor_id
            ),
            'legacy_created_at_applicable', (
                SELECT count(*) FROM mapped_claim_stage WHERE created_at_legacy IS NOT NULL
            ),
            'legacy_created_at', (
                SELECT count(*) FROM mapped_claim_stage
                WHERE created_at_legacy IS NOT NULL AND canonical_created_at = created_at_legacy
            )
        ),
        'projection_limits', jsonb_build_object(
            'creator_handles_without_agent_row', (
                SELECT count(*) FROM mapped_claim_stage
                WHERE created_by_handle IS NOT NULL AND staged_actor_id IS NULL
            ),
            'staged_body_nonempty', (
                SELECT count(*) FROM mapped_claim_stage WHERE nullif(btrim(body), '') IS NOT NULL
            ),
            'staged_source_ref_nonempty', (
                SELECT count(*) FROM mapped_claim_stage WHERE nullif(btrim(source_ref), '') IS NOT NULL
            ),
            'staged_sourced_from_nonempty', (
                SELECT count(*) FROM mapped_claim_stage WHERE nullif(btrim(sourced_from), '') IS NOT NULL
            ),
            'staged_raw_frontmatter_nonempty', (
                SELECT count(*) FROM mapped_claim_stage WHERE raw_frontmatter <> '{{}}'::jsonb
            ),
            'staged_warnings_nonempty', (
                SELECT count(*) FROM mapped_claim_stage WHERE cardinality(warnings) > 0
            ),
            'canonical_superseded_by_nonnull', (
                SELECT count(*) FROM mapped_claim_stage WHERE canonical_superseded_by IS NOT NULL
            ),
            'canonical_updated_after_created', (
                SELECT count(*) FROM mapped_claim_stage WHERE canonical_updated_at > canonical_created_at
            )
        )
    ),
    'source_parity', jsonb_build_object(
        'compared_fields', jsonb_build_array(
            'source_type', 'url', 'storage_path', 'excerpt', 'hash', 'captured_at', 'created_by'
        ),
        'eligible_rows', (SELECT count(*) FROM mapped_source_stage),
        'exact_projection_rows', (
            SELECT count(*) FROM mapped_source_stage m
            WHERE m.canonical_source_type = m.source_type
              AND m.canonical_url IS NOT DISTINCT FROM m.url
              AND m.canonical_storage_path IS NOT DISTINCT FROM m.storage_path
              AND m.canonical_excerpt IS NOT DISTINCT FROM m.excerpt
              AND m.canonical_hash IS NOT DISTINCT FROM m.hash
              AND m.canonical_captured_at IS NOT DISTINCT FROM m.captured_at
              AND m.canonical_created_by IS NOT DISTINCT FROM m.staged_actor_id
        ),
        'field_matches', jsonb_build_object(
            'source_type', (
                SELECT count(*) FROM mapped_source_stage WHERE canonical_source_type = source_type
            ),
            'url', (
                SELECT count(*) FROM mapped_source_stage WHERE canonical_url IS NOT DISTINCT FROM url
            ),
            'storage_path', (
                SELECT count(*) FROM mapped_source_stage
                WHERE canonical_storage_path IS NOT DISTINCT FROM storage_path
            ),
            'excerpt', (
                SELECT count(*) FROM mapped_source_stage WHERE canonical_excerpt IS NOT DISTINCT FROM excerpt
            ),
            'hash', (
                SELECT count(*) FROM mapped_source_stage WHERE canonical_hash IS NOT DISTINCT FROM hash
            ),
            'captured_at', (
                SELECT count(*) FROM mapped_source_stage
                WHERE canonical_captured_at IS NOT DISTINCT FROM captured_at
            ),
            'created_by', (
                SELECT count(*) FROM mapped_source_stage
                WHERE canonical_created_by IS NOT DISTINCT FROM staged_actor_id
            )
        ),
        'projection_limits', jsonb_build_object(
            'creator_handles_without_agent_row', (
                SELECT count(*) FROM mapped_source_stage
                WHERE created_by_handle IS NOT NULL AND staged_actor_id IS NULL
            ),
            'staged_path_nonempty', (
                SELECT count(*) FROM mapped_source_stage WHERE nullif(btrim(path), '') IS NOT NULL
            ),
            'staged_title_nonempty', (
                SELECT count(*) FROM mapped_source_stage WHERE nullif(btrim(title), '') IS NOT NULL
            ),
            'staged_raw_frontmatter_nonempty', (
                SELECT count(*) FROM mapped_source_stage WHERE raw_frontmatter <> '{{}}'::jsonb
            ),
            'staged_warnings_nonempty', (
                SELECT count(*) FROM mapped_source_stage WHERE cardinality(warnings) > 0
            ),
            'canonical_created_at_without_source_projection', (SELECT count(*) FROM mapped_source_stage)
        )
    ),
    'claim_evidence_reconstruction', jsonb_build_object(
        'compared_fields', jsonb_build_array('claim_id', 'source_id', 'role', 'weight'),
        'not_replayed_fields', jsonb_build_array('created_by', 'created_at'),
        'staged_rows', (SELECT count(*) FROM evidence_projected),
        'rows_with_mapped_claim', (SELECT count(*) FROM evidence_projected WHERE claim_id IS NOT NULL),
        'rows_with_mapped_source', (SELECT count(*) FROM evidence_projected WHERE source_id IS NOT NULL),
        'rows_with_both_mapped_endpoints', (
            SELECT count(*) FROM evidence_projected WHERE claim_id IS NOT NULL AND source_id IS NOT NULL
        ),
        'distinct_projected_rows', (SELECT count(*) FROM evidence_distinct),
        'projected_rows_present_in_canonical', (
            SELECT count(*) FROM evidence_distinct e
            WHERE EXISTS (
                SELECT 1 FROM public.claim_evidence ce
                WHERE ce.claim_id = e.claim_id
                  AND ce.source_id = e.source_id
                  AND ce.role = e.role
                  AND ce.weight IS NOT DISTINCT FROM e.weight
            )
        ),
        'canonical_rows_covered', (
            SELECT count(*) FROM public.claim_evidence ce
            WHERE EXISTS (
                SELECT 1 FROM evidence_distinct e
                WHERE ce.claim_id = e.claim_id
                  AND ce.source_id = e.source_id
                  AND ce.role = e.role
                  AND ce.weight IS NOT DISTINCT FROM e.weight
            )
        ),
        'canonical_rows_total', (SELECT count(*) FROM public.claim_evidence)
    ),
    'claim_edge_reconstruction', jsonb_build_object(
        'compared_fields', jsonb_build_array('from_claim', 'to_claim', 'edge_type'),
        'not_replayed_fields', jsonb_build_array(
            'id', 'weight', 'created_by', 'created_at', 'duplicate_multiplicity'
        ),
        'staged_rows', (SELECT count(*) FROM edge_projected),
        'resolution_state_resolved_rows', (
            SELECT count(*) FROM edge_projected WHERE resolution_state = 'resolved'
        ),
        'resolved_rows_with_from_mapping', (
            SELECT count(*) FROM edge_projected
            WHERE resolution_state = 'resolved' AND from_claim IS NOT NULL
        ),
        'resolved_rows_with_to_mapping', (
            SELECT count(*) FROM edge_projected
            WHERE resolution_state = 'resolved' AND to_claim IS NOT NULL
        ),
        'resolved_rows_with_both_mapped_endpoints', (
            SELECT count(*) FROM edge_projected
            WHERE resolution_state = 'resolved' AND from_claim IS NOT NULL AND to_claim IS NOT NULL
        ),
        'distinct_projected_rows', (SELECT count(*) FROM edge_distinct),
        'projected_rows_present_in_canonical', (
            SELECT count(*) FROM edge_distinct e
            WHERE EXISTS (
                SELECT 1 FROM public.claim_edges ce
                WHERE ce.from_claim = e.from_claim
                  AND ce.to_claim = e.to_claim
                  AND ce.edge_type = e.edge_type
            )
        ),
        'canonical_rows_covered', (
            SELECT count(*) FROM public.claim_edges ce
            WHERE EXISTS (
                SELECT 1 FROM edge_distinct e
                WHERE ce.from_claim = e.from_claim
                  AND ce.to_claim = e.to_claim
                  AND ce.edge_type = e.edge_type
            )
        ),
        'canonical_rows_total', (SELECT count(*) FROM public.claim_edges)
    ),
    'later_and_unmapped', jsonb_build_object(
        'latest_import_completed_at', (SELECT completed_at FROM latest_import),
        'canonical_created_after_latest_import', jsonb_build_object(
            'claims', CASE WHEN (SELECT completed_at FROM latest_import) IS NULL THEN NULL ELSE (
                SELECT count(*) FROM public.claims c
                WHERE c.created_at > (SELECT completed_at FROM latest_import)
            ) END,
            'sources', CASE WHEN (SELECT completed_at FROM latest_import) IS NULL THEN NULL ELSE (
                SELECT count(*) FROM public.sources s
                WHERE s.created_at > (SELECT completed_at FROM latest_import)
            ) END,
            'claim_evidence', CASE WHEN (SELECT completed_at FROM latest_import) IS NULL THEN NULL ELSE (
                SELECT count(*) FROM public.claim_evidence ce
                WHERE ce.created_at > (SELECT completed_at FROM latest_import)
            ) END,
            'claim_edges', CASE WHEN (SELECT completed_at FROM latest_import) IS NULL THEN NULL ELSE (
                SELECT count(*) FROM public.claim_edges ce
                WHERE ce.created_at > (SELECT completed_at FROM latest_import)
            ) END
        ),
        'canonical_without_mapping', jsonb_build_object(
            'claims', (
                SELECT count(*) FROM public.claims c
                WHERE NOT EXISTS (SELECT 1 FROM claim_map cm WHERE cm.target_id = c.id)
            ),
            'sources', (
                SELECT count(*) FROM public.sources s
                WHERE NOT EXISTS (SELECT 1 FROM source_map sm WHERE sm.target_id = s.id)
            )
        ),
        'canonical_relations_not_reconstructible', jsonb_build_object(
            'claim_evidence', (
                SELECT count(*) FROM public.claim_evidence ce
                WHERE NOT EXISTS (
                    SELECT 1 FROM evidence_distinct e
                    WHERE ce.claim_id = e.claim_id
                      AND ce.source_id = e.source_id
                      AND ce.role = e.role
                      AND ce.weight IS NOT DISTINCT FROM e.weight
                )
            ),
            'claim_edges', (
                SELECT count(*) FROM public.claim_edges ce
                WHERE NOT EXISTS (
                    SELECT 1 FROM edge_distinct e
                    WHERE ce.from_claim = e.from_claim
                      AND ce.to_claim = e.to_claim
                      AND ce.edge_type = e.edge_type
                )
            )
        ),
        'staged_distinct_keys_without_mapping', jsonb_build_object(
            'claims', (
                SELECT count(*) FROM (SELECT DISTINCT path FROM kb_stage.staged_claims) staged
                WHERE NOT EXISTS (SELECT 1 FROM claim_map cm WHERE cm.legacy_key = staged.path)
            ),
            'sources', (
                SELECT count(*) FROM (SELECT DISTINCT source_key FROM kb_stage.staged_sources) staged
                WHERE NOT EXISTS (SELECT 1 FROM source_map sm WHERE sm.legacy_key = staged.source_key)
            )
        )
    ),
    'proposal_apply_payload', jsonb_build_object(
        'total_rows', (SELECT count(*) FROM kb_stage.kb_proposals),
        'status_counts', COALESCE(
            (SELECT jsonb_object_agg(status, rows ORDER BY status) FROM proposal_status_counts),
            '{{}}'::jsonb
        ),
        'with_apply_payload_key', (
            SELECT count(*) FROM kb_stage.kb_proposals WHERE payload ? 'apply_payload'
        ),
        'with_valid_apply_payload', (
            SELECT count(*) FROM kb_stage.kb_proposals
            WHERE jsonb_typeof(payload -> 'apply_payload') = 'object'
              AND payload -> 'apply_payload' <> '{{}}'::jsonb
        ),
        'applied_rows', (SELECT count(*) FROM kb_stage.kb_proposals WHERE applied_at IS NOT NULL),
        'applied_with_valid_apply_payload', (
            SELECT count(*) FROM kb_stage.kb_proposals
            WHERE applied_at IS NOT NULL
              AND jsonb_typeof(payload -> 'apply_payload') = 'object'
              AND payload -> 'apply_payload' <> '{{}}'::jsonb
        )
    ),
    'import_runs', COALESCE(
        (SELECT jsonb_agg(to_jsonb(run_rows) ORDER BY started_at) FROM run_rows),
        '[]'::jsonb
    )
)::text;
\\else
WITH required_relations(schema_name, table_name) AS (
    VALUES
        {relation_values}
),
required_columns(schema_name, table_name, column_name) AS (
    VALUES
        {column_values}
),
missing_relations AS (
    SELECT schema_name || '.' || table_name AS relation
    FROM required_relations
    WHERE to_regclass(schema_name || '.' || table_name) IS NULL
),
missing_columns AS (
    SELECT r.schema_name || '.' || r.table_name || '.' || r.column_name AS column_name
    FROM required_columns r
    LEFT JOIN information_schema.columns c
      ON c.table_schema = r.schema_name
     AND c.table_name = r.table_name
     AND c.column_name = r.column_name
    WHERE c.column_name IS NULL
)
SELECT jsonb_build_object(
    'status', 'error',
    'error', 'expected_schema_absent',
    'missing_relations', COALESCE(
        (SELECT jsonb_agg(relation ORDER BY relation) FROM missing_relations),
        '[]'::jsonb
    ),
    'missing_columns', COALESCE(
        (SELECT jsonb_agg(column_name ORDER BY column_name) FROM missing_columns),
        '[]'::jsonb
    )
)::text;
ROLLBACK;
\\quit 3
\\endif

ROLLBACK;
"""


def run_command(command: Sequence[str], stdin: str, timeout: int) -> CommandResult:
    completed = subprocess.run(
        list(command),
        input=stdin,
        text=True,
        capture_output=True,
        check=False,
        timeout=timeout,
    )
    return CommandResult(completed.returncode, completed.stdout, completed.stderr)


def _validate_name(label: str, value: str) -> None:
    if not SAFE_NAME_RE.fullmatch(value):
        raise ValueError(f"{label} must match {SAFE_NAME_RE.pattern}")


def _parse_json_output(stdout: str) -> dict[str, Any] | None:
    for line in reversed(stdout.splitlines()):
        candidate = line.strip()
        if not candidate:
            continue
        try:
            parsed = json.loads(candidate)
        except json.JSONDecodeError:
            continue
        if isinstance(parsed, dict):
            return parsed
    return None


def _coverage(covered: int, total: int) -> dict[str, int | float]:
    gap = max(total - covered, 0)
    percent = 100.0 if total == 0 and covered == 0 else round((covered / total) * 100, 2) if total else 0.0
    return {"covered": covered, "total": total, "gap": gap, "percent": percent}


def _error_report(
    *,
    container: str,
    database: str,
    error: str,
    details: dict[str, Any] | None = None,
) -> dict[str, Any]:
    return {
        "schema_version": "teleo.kb_rebuild_coverage.v1",
        "generated_at_utc": datetime.now(UTC).isoformat(),
        "audit_status": "error",
        "target": {"container": container, "database": database},
        "safety": {
            "transaction_mode": "repeatable_read_read_only",
            "database_mutation_attempted": False,
        },
        "error": error,
        "details": details or {},
    }


def _require_mapping(value: Any, label: str) -> dict[str, Any]:
    if not isinstance(value, dict):
        raise ValueError(f"missing or invalid aggregate section: {label}")
    return value


def build_report(raw: dict[str, Any], *, container: str, database: str) -> dict[str, Any]:
    identity = _require_mapping(raw.get("identity"), "identity")
    canonical = _require_mapping(raw.get("canonical_counts"), "canonical_counts")
    staging = _require_mapping(raw.get("staging_counts"), "staging_counts")
    mappings = _require_mapping(raw.get("mappings"), "mappings")
    claims_mapping = _require_mapping(mappings.get("claims"), "mappings.claims")
    sources_mapping = _require_mapping(mappings.get("sources"), "mappings.sources")
    claim_parity = _require_mapping(raw.get("claim_parity"), "claim_parity")
    source_parity = _require_mapping(raw.get("source_parity"), "source_parity")
    evidence = _require_mapping(raw.get("claim_evidence_reconstruction"), "claim_evidence_reconstruction")
    edges = _require_mapping(raw.get("claim_edge_reconstruction"), "claim_edge_reconstruction")
    later = _require_mapping(raw.get("later_and_unmapped"), "later_and_unmapped")
    proposals = _require_mapping(raw.get("proposal_apply_payload"), "proposal_apply_payload")
    import_runs = raw.get("import_runs")
    if not isinstance(import_runs, list):
        raise ValueError("missing or invalid aggregate section: import_runs")

    claim_mapping_rows = int(claims_mapping["rows"])
    source_mapping_rows = int(sources_mapping["rows"])
    claim_stage_origins = int(claims_mapping["with_first_run_staged_row"])
    source_stage_origins = int(sources_mapping["with_first_run_staged_row"])
    claim_exact = int(claim_parity["exact_projection_rows"])
    claim_eligible = int(claim_parity["eligible_rows"])
    source_exact = int(source_parity["exact_projection_rows"])
    source_eligible = int(source_parity["eligible_rows"])

    claims_mapping_report = {
        **claims_mapping,
        "canonical_coverage": _coverage(int(claims_mapping["canonical_target_rows_covered"]), int(canonical["claims"])),
        "first_run_stage_origin_coverage": _coverage(claim_stage_origins, claim_mapping_rows),
        "staged_key_mapping_coverage": _coverage(
            int(claims_mapping["staged_distinct_keys"]) - int(claims_mapping["staged_distinct_keys_unmapped"]),
            int(claims_mapping["staged_distinct_keys"]),
        ),
    }
    sources_mapping_report = {
        **sources_mapping,
        "canonical_coverage": _coverage(
            int(sources_mapping["canonical_target_rows_covered"]), int(canonical["sources"])
        ),
        "first_run_stage_origin_coverage": _coverage(source_stage_origins, source_mapping_rows),
        "staged_key_mapping_coverage": _coverage(
            int(sources_mapping["staged_distinct_keys"]) - int(sources_mapping["staged_distinct_keys_unmapped"]),
            int(sources_mapping["staged_distinct_keys"]),
        ),
    }
    claim_parity_report = {
        **claim_parity,
        "exact_projection_coverage": _coverage(claim_exact, claim_eligible),
    }
    source_parity_report = {
        **source_parity,
        "exact_projection_coverage": _coverage(source_exact, source_eligible),
    }
    evidence_report = {
        **evidence,
        "staged_projection_presence": _coverage(
            int(evidence["projected_rows_present_in_canonical"]), int(evidence["distinct_projected_rows"])
        ),
        "canonical_reconstruction_coverage": _coverage(
            int(evidence["canonical_rows_covered"]), int(evidence["canonical_rows_total"])
        ),
    }
    edge_report = {
        **edges,
        "staged_projection_presence": _coverage(
            int(edges["projected_rows_present_in_canonical"]), int(edges["distinct_projected_rows"])
        ),
        "canonical_reconstruction_coverage": _coverage(
            int(edges["canonical_rows_covered"]), int(edges["canonical_rows_total"])
        ),
    }
    proposal_report = {
        **proposals,
        "all_proposal_payload_coverage": _coverage(
            int(proposals["with_valid_apply_payload"]), int(proposals["total_rows"])
        ),
        "applied_proposal_payload_coverage": _coverage(
            int(proposals["applied_with_valid_apply_payload"]), int(proposals["applied_rows"])
        ),
    }

    gaps: list[dict[str, Any]] = []

    def add_gap(code: str, count: int, explanation: str) -> None:
        if count > 0:
            gaps.append({"code": code, "count": count, "explanation": explanation})

    add_gap(
        "canonical_claims_without_mapping",
        int(claims_mapping["canonical_target_rows_unmapped"]),
        "Canonical claim rows have no canonical_mappings origin.",
    )
    add_gap(
        "canonical_sources_without_mapping",
        int(sources_mapping["canonical_target_rows_unmapped"]),
        "Canonical source rows have no canonical_mappings origin.",
    )
    add_gap(
        "claim_mappings_without_first_run_stage",
        claim_mapping_rows - claim_stage_origins,
        "Claim mappings do not resolve to the staged claim row in their recorded first import run.",
    )
    add_gap(
        "source_mappings_without_first_run_stage",
        source_mapping_rows - source_stage_origins,
        "Source mappings do not resolve to the staged source row in their recorded first import run.",
    )
    add_gap(
        "mapped_claim_projection_mismatch",
        claim_eligible - claim_exact,
        "Mapped staged claim fields do not exactly match their canonical projection.",
    )
    add_gap(
        "mapped_source_projection_mismatch",
        source_eligible - source_exact,
        "Mapped staged source fields do not exactly match their canonical projection.",
    )
    add_gap(
        "staged_claim_keys_without_mapping",
        int(claims_mapping["staged_distinct_keys_unmapped"]),
        "Distinct staged claim keys have no canonical mapping.",
    )
    add_gap(
        "staged_source_keys_without_mapping",
        int(sources_mapping["staged_distinct_keys_unmapped"]),
        "Distinct staged source keys have no canonical mapping.",
    )
    add_gap(
        "mapping_rows_with_missing_canonical_target",
        int(claims_mapping["missing_canonical_targets"]) + int(sources_mapping["missing_canonical_targets"]),
        "Canonical mappings point to target rows that are absent.",
    )
    add_gap(
        "unsupported_mapping_target_rows",
        int(mappings["unsupported_target_rows"]),
        "Canonical mappings target tables outside the audited claims/sources rebuild contract.",
    )
    add_gap(
        "staged_claim_source_rows_with_unmapped_endpoint",
        int(evidence["staged_rows"]) - int(evidence["rows_with_both_mapped_endpoints"]),
        "Staged claim-source links cannot resolve both canonical endpoints.",
    )
    add_gap(
        "staged_evidence_projection_missing_in_canonical",
        int(evidence["distinct_projected_rows"]) - int(evidence["projected_rows_present_in_canonical"]),
        "A reconstructible staged evidence tuple is absent from canonical claim_evidence.",
    )
    add_gap(
        "canonical_claim_evidence_not_reconstructible",
        int(evidence["canonical_rows_total"]) - int(evidence["canonical_rows_covered"]),
        "Canonical claim_evidence rows have no exact staged_claim_sources projection.",
    )
    add_gap(
        "staged_edges_not_resolved",
        int(edges["staged_rows"]) - int(edges["resolution_state_resolved_rows"]),
        "Staged claim edges are not marked resolved.",
    )
    add_gap(
        "resolved_staged_edges_with_unmapped_endpoint",
        int(edges["resolution_state_resolved_rows"]) - int(edges["resolved_rows_with_both_mapped_endpoints"]),
        "Resolved staged edges cannot resolve both canonical claim endpoints.",
    )
    add_gap(
        "staged_edge_projection_missing_in_canonical",
        int(edges["distinct_projected_rows"]) - int(edges["projected_rows_present_in_canonical"]),
        "A reconstructible staged edge tuple is absent from canonical claim_edges.",
    )
    add_gap(
        "canonical_claim_edges_not_reconstructible",
        int(edges["canonical_rows_total"]) - int(edges["canonical_rows_covered"]),
        "Canonical claim_edges rows have no exact resolved staged edge projection.",
    )
    add_gap(
        "applied_proposals_without_valid_apply_payload",
        int(proposals["applied_rows"]) - int(proposals["applied_with_valid_apply_payload"]),
        "Applied proposal rows cannot be replayed from a non-empty object apply_payload.",
    )

    source_status = "complete" if not gaps else "partial"
    return {
        "schema_version": "teleo.kb_rebuild_coverage.v1",
        "generated_at_utc": datetime.now(UTC).isoformat(),
        "audit_status": "complete",
        "target": {"container": container, "database": database, "database_readback": identity.get("database")},
        "safety": {
            "transaction_mode": "repeatable_read_read_only",
            "transaction_read_only_readback": identity.get("transaction_read_only"),
            "database_mutation_attempted": False,
            "contains_private_bodies_or_source_excerpts": False,
        },
        "exact_snapshot_recovery": {
            "status": "external_snapshot_artifact_not_audited",
            "basis": "A complete database snapshot preserves canonical and staging state without semantic recompilation.",
            "verified_by_this_database_only_auditor": False,
            "current_state_counts": {"canonical": canonical, "staging": staging},
            "proof_still_required": [
                "snapshot checksum",
                "schema and constraint parity",
                "restored row-count and row-hash parity",
            ],
        },
        "source_derived_recompile_readiness": {
            "status": source_status,
            "canonical_counts": canonical,
            "canonical_mappings": {
                "claims": claims_mapping_report,
                "sources": sources_mapping_report,
                "unsupported_target_rows": mappings["unsupported_target_rows"],
            },
            "staged_to_canonical_field_parity": {
                "claims": claim_parity_report,
                "sources": source_parity_report,
            },
            "claim_evidence_from_staged_claim_sources": evidence_report,
            "claim_edges_from_resolved_staged_claim_edges": edge_report,
            "later_and_unmapped_rows": later,
            "proposal_apply_payload_coverage": proposal_report,
            "import_run_metadata": import_runs,
            "explicit_gaps": gaps,
        },
    }


def audit_database(
    *,
    container: str,
    database: str,
    db_user: str = "postgres",
    timeout: int = 120,
    runner: CommandRunner = run_command,
) -> dict[str, Any]:
    _validate_name("container", container)
    _validate_name("database", database)
    _validate_name("db_user", db_user)
    command = [
        "docker",
        "exec",
        "-i",
        container,
        "psql",
        "-X",
        "-U",
        db_user,
        "-d",
        database,
        "-Atq",
        "-v",
        "ON_ERROR_STOP=1",
    ]
    try:
        completed = runner(command, build_audit_sql(), timeout)
    except (OSError, subprocess.TimeoutExpired) as exc:
        return _error_report(
            container=container,
            database=database,
            error="docker_psql_execution_failed",
            details={"exception": type(exc).__name__, "message": str(exc)[:500]},
        )

    parsed = _parse_json_output(completed.stdout)
    if parsed and parsed.get("status") == "error":
        return _error_report(
            container=container,
            database=database,
            error=str(parsed.get("error") or "audit_query_failed"),
            details={
                "missing_relations": parsed.get("missing_relations", []),
                "missing_columns": parsed.get("missing_columns", []),
            },
        )
    if completed.returncode != 0 or parsed is None:
        return _error_report(
            container=container,
            database=database,
            error="audit_query_failed",
            details={
                "returncode": completed.returncode,
                "stderr_excerpt": completed.stderr.strip()[-1000:],
            },
        )
    try:
        return build_report(parsed, container=container, database=database)
    except (KeyError, TypeError, ValueError) as exc:
        return _error_report(
            container=container,
            database=database,
            error="malformed_audit_result",
            details={"message": str(exc)},
        )


def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--container", required=True, help="Local Docker PostgreSQL container name")
    parser.add_argument("--database", required=True, help="Database name inside the container")
    parser.add_argument("--db-user", default="postgres", help="Database role used for the read-only transaction")
    parser.add_argument("--timeout", type=int, default=120, help="Docker/psql timeout in seconds")
    parser.add_argument("--compact", action="store_true", help="Emit one-line JSON instead of indented JSON")
    return parser.parse_args(argv)


def main(argv: Sequence[str] | None = None) -> int:
    args = parse_args(argv)
    report = audit_database(
        container=args.container,
        database=args.database,
        db_user=args.db_user,
        timeout=args.timeout,
    )
    print(json.dumps(report, indent=None if args.compact else 2, sort_keys=True))
    return 0 if report.get("audit_status") == "complete" else 2


if __name__ == "__main__":
    sys.exit(main())
