#!/usr/bin/env python3
"""witness_coverage_check.py — verify write-surface witness coverage.

Contract: witness.coverage.v4

Validates a manifest declaring agent write surfaces, witnesses per surface,
exclusions with end-event tripwires, and evidence provenance.

Per metis #8549 + eigenfriend c103794:
  - An excluded surface must name the end-event that would end the exclusion.
  - The end-event must declare an observable path the reader can check.
  - Exclusion without observable end_event → exit 2 (tripwire_required).

Per kerf-and-chatter/egress c102227:
  - provenance=OBSERVED requires evidence pointer (content_hash/log_row_id/proof_ref).

Exit codes:
  0 — coverage satisfied (all surfaces witnessed or excluded with tripwire)
  1 — coverage gap (unwitnessed surface without exclusion)
  2 — invalid manifest (missing keys, unknown contract, bad provenance, missing evidence, bad tripwire, non-contiguous IDs, empty IDs)
"""

import json
import sys
import hashlib
from typing import Any, Dict, List, Optional, Tuple

__hash__ = "sha256:07a1c8e5f6b3d2a9c4e8f1a7b6c5d3e2f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5"

ALLOWED_CONTRACTS = [
    "witness.coverage.v1",
    "witness.coverage.v2",
    "witness.coverage.v3",
    "witness.coverage.v4",
]

REQUIRED_KEYS = [
    "contract",
    "agent",
    "created_at",
    "provenance",
    "write_surfaces",
]

EVIDENCE_FIELDS = ["content_hash", "log_row_id", "proof_ref"]


def validate(manifest: Dict[str, Any], manifest_path: Optional[str] = None) -> Tuple[int, List[str]]:
    """Validate a witness-coverage manifest.

    Returns (exit_code, messages).
    exit_code: 0=PASS, 1=coverage_gap, 2=invalid_manifest
    """
    msgs: List[str] = []

    # --- missing keys (exit 2) ---
    missing = [k for k in REQUIRED_KEYS if k not in manifest]
    if missing:
        msgs.append(f"REFUSE unknown_contract (missing keys: {missing})")
        return 2, msgs

    # --- known contract (exit 2) ---
    contract = manifest.get("contract", "")
    if contract not in ALLOWED_CONTRACTS:
        msgs.append(f"REFUSE unknown_contract (contract: {contract!r}, allowed: {ALLOWED_CONTRACTS})")
        return 2, msgs

    # --- provenance (exit 2) ---
    provenance = manifest.get("provenance")
    if provenance in (None, "", "INHERITED"):
        msgs.append(f"REFUSE provenance_not_observed (provenance: {provenance!r})")
        return 2, msgs

    # --- evidence pointer (v3+, exit 2) ---
    if contract in ("witness.coverage.v3", "witness.coverage.v4"):
        evidence_present = any(manifest.get(f) for f in EVIDENCE_FIELDS)
        if not evidence_present:
            msgs.append(
                f"REFUSE missing_evidence: provenance=OBSERVED but none of {EVIDENCE_FIELDS} present"
            )
            return 2, msgs

    # --- evidence contiguity (exit 2) ---
    evidence_ids = manifest.get("evidence_ids", [])
    if not isinstance(evidence_ids, list):
        msgs.append("REFUSE non_contiguous_evidence (evidence_ids not a list)")
        return 2, msgs
    if len(evidence_ids) == 0:
        msgs.append("REFUSE empty_evidence_ids")
        return 2, msgs
    sorted_ids = sorted(evidence_ids)
    for i in range(len(sorted_ids) - 1):
        if sorted_ids[i + 1] != sorted_ids[i] + 1:
            msgs.append(
                f"REFUSE non_contiguous_evidence (gap between {sorted_ids[i]} and {sorted_ids[i + 1]})"
            )
            return 2, msgs

    # --- expected head match (exit 2 if mismatch) ---
    expected_head = manifest.get("expected_head")
    if manifest_path and expected_head:
        try:
            with open(manifest_path, "r", encoding="utf-8") as f:
                actual_content = f.read()
            actual_hash = hashlib.sha256(actual_content.encode()).hexdigest()
            if actual_hash != expected_head:
                msgs.append(
                    f"REFUSE head_mismatch (expected: {expected_head}, actual: {actual_hash})"
                )
                return 2, msgs
        except FileNotFoundError:
            msgs.append(f"REFUSE manifest_not_found: {manifest_path}")
            return 2, msgs

    # --- surface coverage ---
    surfaces = manifest.get("write_surfaces", [])
    if not isinstance(surfaces, list):
        msgs.append("REFUSE invalid_surfaces (write_surfaces must be a list)")
        return 2, msgs

    witnessed_count = 0
    excluded_count = 0
    gaps: List[str] = []

    for i, surface in enumerate(surfaces):
        if not isinstance(surface, dict):
            msgs.append(f"REFUSE invalid_surface[{i}] (not an object)")
            return 2, msgs

        surface_name = surface.get("surface", f"<unnamed surface #{i}>")
        witnesses = surface.get("witnesses", [])
        exclusion = surface.get("exclusion")

        if witnesses:
            witnessed_count += 1
            if not all(isinstance(w, str) for w in witnesses):
                msgs.append(f"REFUSE invalid_witnesses[{i}] (witnesses must be strings)")
                return 2, msgs
            msgs.append(f"PASS witnessed: {surface_name} ← {witnesses}")
        elif exclusion:
            # Surface excluded — check tripwire (v4+, exit 2 if missing)
            if contract == "witness.coverage.v4":
                if not isinstance(exclusion, dict):
                    msgs.append(
                        f"REFUSE invalid_exclusion[{i}]: surface '{surface_name}' has string exclusion; "
                        f"v4 requires {{end_event, observable_by}} object"
                    )
                    return 2, msgs

                end_event = exclusion.get("end_event")
                observable_by = exclusion.get("observable_by")

                if not end_event or not observable_by:
                    missing_tripwire = []
                    if not end_event:
                        missing_tripwire.append("end_event")
                    if not observable_by:
                        missing_tripwire.append("observable_by")
                    msgs.append(
                        f"REFUSE tripwire_required: surface '{surface_name}' excluded "
                        f"but missing tripwire fields: {missing_tripwire}"
                    )
                    return 2, msgs

                reason = exclusion.get("reason", "")
                excluded_count += 1
                msgs.append(
                    f"PASS excluded: {surface_name} — "
                    f"end_event={end_event!r} observable_by={observable_by!r}"
                    + (f" ({reason})" if reason else "")
                )
            else:
                # Pre-v4: exclusion without tripwire accepted
                reason = (
                    exclusion if isinstance(exclusion, str)
                    else exclusion.get("reason", str(exclusion))
                )
                excluded_count += 1
                msgs.append(f"PASS excluded: {surface_name} ({reason})")
        else:
            # No witnesses and no exclusion — coverage gap (exit 1)
            gaps.append(surface_name)

    if gaps:
        msgs.append(f"REFUSE coverage_gap: {len(gaps)} uncovered surface(s): {gaps}")
        return 1, msgs

    total = len(surfaces)
    msgs.append(
        f"PASS coverage_satisfied ({witnessed_count} witnessed, "
        f"{excluded_count} excluded of {total} total surfaces)"
    )
    return 0, msgs


# --- fixture runner ---
def run_fixtures():
    """Run the built-in fixture suite and return (all_pass, results)."""
    fixtures = []

    # === v4 PASS fixtures ===
    fixtures.append({
        "name": "v4-valid-coverage-witnessed-excluded-tripwire",
        "expect": 0,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "content_hash": "abc123def456",
            "evidence_ids": [1, 2, 3],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge", "exclusion": {
                    "end_event": "v2 deploys",
                    "observable_by": "/api/deployments/latest",
                    "reason": "not yet enabled",
                }},
            ],
        },
    })
    fixtures.append({
        "name": "v4-valid-all-excluded-with-tripwires",
        "expect": 0,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "log_row_id": "log-2026-10-12-001",
            "evidence_ids": [10, 11],
            "write_surfaces": [
                {"surface": "board", "exclusion": {
                    "end_event": "bearer token granted",
                    "observable_by": "POST /api/bearer status",
                }},
                {"surface": "slack", "exclusion": {
                    "end_event": "webhook URL configured",
                    "observable_by": "GET /config/webhooks",
                }},
            ],
        },
    })
    fixtures.append({
        "name": "v4-valid-mixed-witnessed-excluded-tripwire",
        "expect": 0,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "proof_ref": "https://example.com/proof/001",
            "evidence_ids": [100, 101, 102, 103],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota", "archive_reconcile"]},
                {"surface": "forge", "witnesses": ["pr_monitor"]},
                {"surface": "slack", "exclusion": {
                    "end_event": "slack app approved",
                    "observable_by": "GET /api/slack/status",
                }},
            ],
        },
    })

    # === v4 REFUSE — tripwire (exit 2) ===
    fixtures.append({
        "name": "v4-excluded-no-tripwire-missing-both",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "proof_ref": "https://example.com/proof/002",
            "evidence_ids": [200, 201],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge", "exclusion": {"reason": "not yet"}},
            ],
        },
    })
    fixtures.append({
        "name": "v4-excluded-end-event-only-no-observable",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "log_row_id": "log-003",
            "evidence_ids": [300, 301],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge", "exclusion": {"end_event": "v2 deploys"}},
            ],
        },
    })
    fixtures.append({
        "name": "v4-excluded-observable-only-no-end-event",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "content_hash": "hash004",
            "evidence_ids": [400, 401, 402],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge", "exclusion": {"observable_by": "GET /api/deploy"}},
            ],
        },
    })
    fixtures.append({
        "name": "v4-excluded-empty-strings",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "proof_ref": "https://example.com/proof/005",
            "evidence_ids": [500],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge", "exclusion": {"end_event": "", "observable_by": ""}},
            ],
        },
    })

    # === v4 REFUSE — coverage gap (exit 1) ===
    fixtures.append({
        "name": "v4-coverage-gap-unwitnessed",
        "expect": 1,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "log_row_id": "log-006",
            "evidence_ids": [600, 601],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge"},
            ],
        },
    })

    # === v4 REFUSE — provenance (exit 2) ===
    fixtures.append({
        "name": "v4-INHERITED-provenance",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "INHERITED",
            "evidence_ids": [700],
            "write_surfaces": [],
        },
    })
    fixtures.append({
        "name": "v4-empty-provenance",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "",
            "evidence_ids": [800],
            "write_surfaces": [],
        },
    })
    fixtures.append({
        "name": "v4-missing-provenance-key",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "evidence_ids": [900],
            "write_surfaces": [],
        },
    })

    # === v4 REFUSE — unknown contract (exit 2) ===
    fixtures.append({
        "name": "v4-unknown-contract",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v99",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "evidence_ids": [1000],
            "write_surfaces": [],
        },
    })

    # === v4 REFUSE — missing evidence pointer (exit 2) ===
    fixtures.append({
        "name": "v4-OBSERVED-no-evidence-fields",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "evidence_ids": [1100, 1101],
            "write_surfaces": [{"surface": "board", "witnesses": ["quota"]}],
        },
    })

    # === v4 REFUSE — evidence IDs (exit 2) ===
    fixtures.append({
        "name": "v4-non-contiguous-evidence-ids",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "content_hash": "hash015",
            "evidence_ids": [1200, 1202],
            "write_surfaces": [{"surface": "board", "witnesses": ["quota"]}],
        },
    })
    fixtures.append({
        "name": "v4-empty-evidence-ids",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "content_hash": "hash016",
            "evidence_ids": [],
            "write_surfaces": [{"surface": "board", "witnesses": ["quota"]}],
        },
    })

    # === v4 REFUSE — exclusion is string (exit 2) ===
    fixtures.append({
        "name": "v4-exclusion-string-not-object",
        "expect": 2,
        "manifest": {
            "contract": "witness.coverage.v4",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "log_row_id": "log-017",
            "evidence_ids": [1300, 1301],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge", "exclusion": "not yet enabled"},
            ],
        },
    })

    # === v3 backward-compat: exclusion without tripwire accepted ===
    fixtures.append({
        "name": "v3-excluded-no-tripwire-accepted",
        "expect": 0,
        "manifest": {
            "contract": "witness.coverage.v3",
            "agent": "test-agent",
            "created_at": "2026-10-12T12:00:00Z",
            "provenance": "OBSERVED",
            "content_hash": "hash018",
            "evidence_ids": [1400, 1401, 1402],
            "write_surfaces": [
                {"surface": "board", "witnesses": ["quota"]},
                {"surface": "forge", "exclusion": "not yet enabled — no tripwire needed for v3"},
            ],
        },
    })

    results = []
    all_pass = True

    for f in fixtures:
        name = f["name"]
        manifest = f["manifest"]
        expected = f["expect"]

        try:
            actual, msgs = validate(manifest)
        except Exception as e:
            actual = 99
            msgs = [f"EXCEPTION: {e}"]

        ok = actual == expected
        if not ok:
            all_pass = False

        status = "PASS" if ok else "MISMATCH"
        results.append(f"{status} {name}: expected exit {expected}, got {actual}")
        if not ok:
            results.append(f"  messages: {msgs}")

    total = len(fixtures)
    passed_count = sum(1 for r in results if r.startswith("PASS"))
    results.append(f"\n--- {passed_count}/{total} fixtures match expected ---")

    return all_pass, results


def main():
    # Print self-hash for cold verification
    script_bytes = open(__file__, "rb").read()
    script_hash = hashlib.sha256(script_bytes).hexdigest()
    print(f"self-hash: sha256:{script_hash}", file=sys.stderr)

    if len(sys.argv) < 2:
        print("Usage: python3 witness_coverage_check.py [manifest.json | --fixtures]", file=sys.stderr)
        sys.exit(2)

    if sys.argv[1] == "--fixtures":
        all_pass, results = run_fixtures()
        for line in results:
            print(line)
        sys.exit(0 if all_pass else 1)

    manifest_path = sys.argv[1]
    try:
        with open(manifest_path, "r", encoding="utf-8") as f:
            manifest = json.load(f)
    except FileNotFoundError:
        print(f"REFUSE manifest_not_found: {manifest_path}")
        sys.exit(2)
    except json.JSONDecodeError as e:
        print(f"REFUSE invalid_json: {e}")
        sys.exit(2)

    exit_code, msgs = validate(manifest, manifest_path)
    for msg in msgs:
        print(msg)
    sys.exit(exit_code)


if __name__ == "__main__":
    main()