# SPDX-FileCopyrightText: 2026 Advisa EOOD (Sofia, Bulgaria)
# SPDX-License-Identifier: Apache-2.0
"""Profile-view exports for source-bound ActProof catalogue entries.

The catalogue entry remains the canonical profile object. A profile view is a
deterministic, renderable projection for websites, APIs, MCP servers, audit
packs and compliance interfaces. It is intentionally richer than a summary,
but it is not a receipt and it is not a legal-compliance determination.
"""
from __future__ import annotations

import argparse
import copy
import hashlib
import json
import sys
from datetime import datetime, timezone
from importlib import metadata
from pathlib import Path
from typing import Any

from actproof_events import get_profile_view_schema_path
from actproof_events.source_binding import (
    compute_field_source_coverage,
    explain_field_source,
    field_source_projection,
    list_field_derivations,
    list_source_atoms,
    validate_field_source_bindings,
)
from actproof_events.services import (
    BOUNDARY,
    BOUNDARY_ID,
    CLAIM_FLAGS,
    CATALOGUE_ENTRY_HASH_BASIS,
    get_field,
    get_profile,
    list_fields,
    source_basis,
)

PROFILE_VIEW_SCHEMA_ID = "actproof.profile_view.v1"
PROFILE_SEMANTIC_HASH_BASIS = (
    "sha256 over canonical JSON with sorted keys and compact separators, "
    "excluding profile hash fields, hash-basis fields and provenance"
)
PROFILE_ARTIFACT_HASH_BASIS = (
    "sha256 over canonical JSON with sorted keys and compact separators, "
    "excluding profile hash fields and provenance.generated_at; "
    "including provenance.package_version"
)
# Backward-compatible alias for consumers introduced to the initial profile-view export.
PROFILE_VIEW_HASH_BASIS = PROFILE_SEMANTIC_HASH_BASIS
DEFAULT_PROFILE_VIEW_TYPE = "public_projection"

_HASH_FIELDS = {
    "profile_view_hash",
    "profile_view_hash_basis",
    "profile_semantic_hash",
    "profile_semantic_hash_basis",
    "profile_artifact_hash",
    "profile_artifact_hash_basis",
}


def _package_version() -> str:
    """Return the installed package version, degrading cleanly in source checkouts."""
    try:
        return metadata.version("actproof-events")
    except metadata.PackageNotFoundError:
        pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml"
        if pyproject.exists():
            for line in pyproject.read_text(encoding="utf-8").splitlines():
                if line.strip().startswith("version ="):
                    return line.split("=", 1)[1].strip().strip("'\"")
        return "unknown"


def _utc_now() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def _pct(n: int, d: int) -> float:
    return round((n / d) * 100, 1) if d else 0.0


def _strip_hash_fields(view: dict[str, Any]) -> dict[str, Any]:
    clone = copy.deepcopy(view)
    for key in _HASH_FIELDS:
        clone.pop(key, None)
    return clone


def _semantic_hashable_payload(view: dict[str, Any]) -> dict[str, Any]:
    """Return the version-independent semantic projection hash basis.

    The semantic hash is intended for the public reproducibility claim: if the
    catalogue/profile projection says the same thing, consumers should receive
    the same hash even when the exporting package version or generation time
    changes. Provenance remains present in the JSON file but does not define
    the profile semantics.
    """
    clone = _strip_hash_fields(view)
    clone.pop("provenance", None)
    return clone


def _artifact_hashable_payload(view: dict[str, Any]) -> dict[str, Any]:
    """Return the release-specific artifact hash basis.

    The artifact hash is useful for release/package traceability. It retains
    package-version provenance but excludes the volatile generation timestamp
    and all self-referential hash fields.
    """
    clone = _strip_hash_fields(view)
    provenance = clone.get("provenance")
    if isinstance(provenance, dict):
        provenance.pop("generated_at", None)
    return clone


def _hash_payload(payload: dict[str, Any]) -> str:
    encoded = json.dumps(
        payload,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return "sha256:" + hashlib.sha256(encoded).hexdigest()


def compute_profile_semantic_hash(view: dict[str, Any]) -> str:
    """Compute the version-independent semantic hash of a profile view."""
    return _hash_payload(_semantic_hashable_payload(view))


def compute_profile_artifact_hash(view: dict[str, Any]) -> str:
    """Compute the release-specific artifact hash of a profile view."""
    return _hash_payload(_artifact_hashable_payload(view))


def compute_profile_view_hash(view: dict[str, Any]) -> str:
    """Compute the backward-compatible profile-view hash.

    This is an alias for the semantic hash. New consumers should prefer
    ``compute_profile_semantic_hash`` and read ``profile_semantic_hash``.
    """
    return compute_profile_semantic_hash(view)


def compute_profile_coverage(act_id: str, *, fields: list[dict[str, Any]] | None = None) -> dict[str, Any]:
    """Compute projection coverage metrics for one ActProof profile.

    Coverage metrics are package-native so website builders and other consumers
    do not have to reimplement the same counting logic.
    """
    fields = fields if fields is not None else [
        get_field(act_id, row["field_id"])
        for row in list_fields(act_id, required_only=False)
    ]
    total = len(fields)
    required = sum(1 for f in fields if f.get("required"))
    optional = total - required
    field_level = sum(1 for f in fields if f.get("source_basis_scope") == "field")
    act_level = sum(1 for f in fields if f.get("source_basis_scope") == "act")
    fallback = sum(1 for f in fields if f.get("fallback_used") is True)
    evidence_field = sum(1 for f in fields if f.get("evidence_scope") == "field")
    evidence_profile = sum(1 for f in fields if f.get("evidence_scope") == "profile")
    scored = sum(1 for f in fields if f.get("interpretive_status") != "unscored")
    unscored = total - scored
    tiered = sum(1 for f in fields if f.get("disclosure_tier") != "untiered")
    untiered = total - tiered
    by_status: dict[str, int] = {}
    by_mapping_type: dict[str, int] = {}
    by_disclosure_tier: dict[str, int] = {}
    by_source_scope: dict[str, int] = {}
    for f in fields:
        status = str(f.get("interpretive_status") or "unscored")
        mapping_type = str(f.get("mapping_type") or "unscored")
        disclosure = str(f.get("disclosure_tier") or "untiered")
        source_scope = str(f.get("source_basis_scope") or "unknown")
        by_status[status] = by_status.get(status, 0) + 1
        by_mapping_type[mapping_type] = by_mapping_type.get(mapping_type, 0) + 1
        by_disclosure_tier[disclosure] = by_disclosure_tier.get(disclosure, 0) + 1
        by_source_scope[source_scope] = by_source_scope.get(source_scope, 0) + 1

    required_field_level = sum(1 for f in fields if f.get("required") and f.get("source_basis_scope") == "field")
    required_fallback = sum(1 for f in fields if f.get("required") and f.get("fallback_used") is True)

    coverage = {
        "field_counts": {
            "total": total,
            "required": required,
            "optional": optional,
        },
        "field_source_basis": {
            "field_level": field_level,
            "act_level_fallback": act_level,
            "fallback_used": fallback,
            "coverage_ratio": _pct(field_level, total),
            "by_scope": dict(sorted(by_source_scope.items())),
        },
        "required_field_source_basis": {
            "required_total": required,
            "field_level": required_field_level,
            "fallback_used": required_fallback,
            "coverage_ratio": _pct(required_field_level, required),
        },
        "evidence_scope": {
            "field_level": evidence_field,
            "profile_level": evidence_profile,
        },
        "assessment": {
            "scored_fields": scored,
            "unscored_fields": unscored,
            "coverage_ratio": _pct(scored, total),
            "by_interpretive_status": dict(sorted(by_status.items())),
            "by_mapping_type": dict(sorted(by_mapping_type.items())),
        },
        "disclosure": {
            "tiered_fields": tiered,
            "untiered_fields": untiered,
            "coverage_ratio": _pct(tiered, total),
            "by_tier": dict(sorted(by_disclosure_tier.items())),
        },
    }

    # Enrich with the precision-tiered coverage derived from binding_granularity.
    # This is computed (never stored per-field), so it cannot drift from the
    # two stored facts. A template_field binding is not counted the same as a
    # section or obligation binding.
    try:
        precision_cov = compute_field_source_coverage(act_id)
        # Replace the older binary field/act coverage with the final 1.8.0
        # market-facing semantics: field_source_basis means release-gated
        # template-field coverage. Optional contextual derivations stay visible
        # in optional_field_source_basis and source_binding_precision, but do not
        # inflate the headline field-level coverage.
        coverage["field_source_basis"] = precision_cov["field_source_basis"]
        coverage["required_field_source_basis"] = precision_cov["required_field_source_basis"]
        coverage["optional_field_source_basis"] = precision_cov["optional_field_source_basis"]
        coverage["source_binding_precision"] = precision_cov["source_binding_precision"]
    except Exception:
        pass

    # Atom-side coverage: the missingness signal (which recorded atoms feed no
    # field). Computed, not stored. Lets a reviewer/agent see disclosed gaps.
    try:
        from actproof_events.source_binding import compute_source_atom_coverage
        coverage["source_atom_coverage"] = compute_source_atom_coverage(act_id)
    except Exception:
        pass

    return coverage


def build_profile_view(
    act_id: str,
    *,
    assessment_id: str | None = None,
    include_fields: bool = True,
    include_source_basis: bool = True,
    include_non_claims: bool = True,
    include_provenance: bool = True,
    include_governance: bool = True,
    generated_at: str | None = None,
) -> dict[str, Any]:
    """Build a rich public projection for one source-bound profile.

    Args:
        act_id: The ActProof act_type_id, for example
            ``op:eu.dora.ict_incident_notification_initial.v1``.
        assessment_id: Optional caller-defined assessment identifier to carry
            into provenance. It does not alter the canonical catalogue object.
        include_fields: Include the per-field projection rows.
        include_source_basis: Include act-level and field-level source basis.
        include_non_claims: Include machine-readable non-claims.
        include_provenance: Include package/build provenance.
        include_governance: Include bounded profile governance status when available.
        generated_at: Optional ISO timestamp override for reproducible builds.

    Returns:
        A JSON-serialisable profile-view dictionary. The canonical object remains
        the catalogue entry addressed by ``catalogue_entry_hash``.
    """
    profile = get_profile(act_id)
    field_rows = [
        get_field(act_id, row["field_id"])
        for row in list_fields(act_id, required_only=False)
    ]

    if include_source_basis:
        for f in field_rows:
            source_projection = field_source_projection(act_id, f["field_id"])
            if source_projection:
                f.update(source_projection)

    if not include_source_basis:
        for f in field_rows:
            f.pop("source_basis", None)
            f.pop("source_basis_scope", None)
            f.pop("fallback_used", None)

    coverage = compute_profile_coverage(act_id, fields=field_rows)
    required_labels = list(profile.get("required_evidence_labels") or [])
    source_instruments = source_basis(profile) if include_source_basis else []
    non_claims = list(profile.get("non_claims") or []) if include_non_claims else []

    projection: dict[str, Any] = {
        "profile_view_schema": PROFILE_VIEW_SCHEMA_ID,
        "profile_view_type": DEFAULT_PROFILE_VIEW_TYPE,
        "profile_view_semantics": "derived renderable view, not the canonical catalogue entry",
        "act_id": act_id,
        "assessment_id": assessment_id,
        "generated_from": "actproof-events public export layer",
        "canonical_object": {
            "kind": "actproof-events catalogue entry",
            "act_id": act_id,
            "catalogue_entry_hash": profile.get("catalogue_entry_hash"),
            "catalogue_entry_hash_basis": profile.get("catalogue_entry_hash_basis") or CATALOGUE_ENTRY_HASH_BASIS,
        },
        "profile": {
            "display_name": profile.get("display_name"),
            "claim_type": profile.get("claim_type"),
            "version": profile.get("version"),
            "maturity": (profile.get("profile_status") or {}).get("maturity"),
            "regulatory_citation": profile.get("regulatory_citation") or {},
            "catalogue_entry_hash": profile.get("catalogue_entry_hash"),
            "catalogue_entry_hash_basis": profile.get("catalogue_entry_hash_basis") or CATALOGUE_ENTRY_HASH_BASIS,
            "compatible_with_receipts": bool(profile.get("compatible_with_receipts")),
            "boundary": profile.get("boundary") or BOUNDARY,
            "boundary_id": profile.get("boundary_id") or BOUNDARY_ID,
            "claim_flags": profile.get("claim_flags") or CLAIM_FLAGS,
            "non_claims": non_claims,
            "required_evidence_labels": required_labels,
            "interpretive_summary": profile.get("interpretive_summary") or {},
        },
        "source_basis": {
            "scope": "mixed",
            "instruments": source_instruments,
            "source_atoms": list_source_atoms(act_id) if include_source_basis else [],
            "field_derivations": list_field_derivations(act_id) if include_source_basis else [],
        },
        "evidence": {
            "required_evidence_labels": required_labels,
            "evidence_scope": "profile",
        },
        "coverage": coverage,
        "non_claims": non_claims,
        "boundary": {
            "boundary_id": profile.get("boundary_id") or BOUNDARY_ID,
            "text": profile.get("boundary") or BOUNDARY,
        },
    }

    # Completeness / known-scope declaration: never imply "if it is not here it
    # does not matter". Computed from the derivations doc's completeness block.
    try:
        from actproof_events.source_binding import get_profile_completeness
        projection["completeness"] = get_profile_completeness(act_id)
    except Exception:
        pass

    # Profile governance status is a bounded review/challenge lifecycle object.
    # It does not claim legal compliance, bank approval or supervisory approval.
    if include_governance:
        try:
            from actproof_events.profile_governance import build_governance_status
            governance = build_governance_status(act_id)
            # Keep the profile-view semantic hash stable across exports: the
            # standalone governance-status command may include generated_at,
            # but the profile-view embeds the review state without volatile time.
            governance.pop("generated_at", None)
            projection["governance"] = governance
        except Exception:
            pass

    if include_fields:
        projection["fields"] = field_rows

    if include_provenance:
        projection["provenance"] = {
            "generated_from": "actproof-events",
            "package_name": "actproof-events",
            "package_version": _package_version(),
            "export_function": "actproof_events.exports.build_profile_view",
            "cli_command": "actproof-events export-profile-view",
            "act_id": act_id,
            "assessment_id": assessment_id,
            "catalogue_entry_hash": profile.get("catalogue_entry_hash"),
            "catalogue_entry_hash_basis": profile.get("catalogue_entry_hash_basis") or CATALOGUE_ENTRY_HASH_BASIS,
            "profile_view_schema": PROFILE_VIEW_SCHEMA_ID,
            "profile_view_type": DEFAULT_PROFILE_VIEW_TYPE,
            "profile_view_semantics": projection["profile_view_semantics"],
            "source_binding_release": "field_level_required_fields.v1",
            "generated_at": generated_at or _utc_now(),
        }

    projection["profile_semantic_hash_basis"] = PROFILE_SEMANTIC_HASH_BASIS
    projection["profile_artifact_hash_basis"] = PROFILE_ARTIFACT_HASH_BASIS
    projection["profile_view_hash_basis"] = PROFILE_VIEW_HASH_BASIS
    projection["profile_semantic_hash"] = compute_profile_semantic_hash(projection)
    projection["profile_view_hash"] = projection["profile_semantic_hash"]
    projection["profile_artifact_hash"] = compute_profile_artifact_hash(projection)
    return projection


def write_profile_view(
    act_id: str,
    out: str | Path,
    *,
    assessment_id: str | None = None,
    pretty: bool = True,
    include_fields: bool = True,
    include_source_basis: bool = True,
    include_non_claims: bool = True,
    include_provenance: bool = True,
    validate: bool = False,
) -> dict[str, Any]:
    """Build and write a profile view JSON file, returning the payload."""
    payload = build_profile_view(
        act_id,
        assessment_id=assessment_id,
        include_fields=include_fields,
        include_source_basis=include_source_basis,
        include_non_claims=include_non_claims,
        include_provenance=include_provenance,
    )
    if validate:
        errors = validate_profile_view(payload)
        if errors:
            raise ValueError(
                "generated projection failed profile_view.v1 schema validation: "
                + "; ".join(errors)
            )

    path = Path(out)
    path.parent.mkdir(parents=True, exist_ok=True)
    if pretty:
        text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
    else:
        text = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n"
    path.write_text(text, encoding="utf-8")
    return payload



def get_profile_view_schema() -> dict[str, Any]:
    """Load the packaged profile-view JSON Schema as a dictionary.

    In an installed wheel the schema lives under ``data/schemas/``. In a raw
    source checkout that built tree is not materialised, so this falls back to
    the repository source path ``spec/schemas/`` when available.

    Raises:
        FileNotFoundError: if the schema cannot be located in either place.
    """
    schema_path = get_profile_view_schema_path()
    if not schema_path.exists():
        repo_path = (
            Path(__file__).resolve().parents[1]
            / "spec"
            / "schemas"
            / "profile_view.v1.schema.json"
        )
        if repo_path.exists():
            schema_path = repo_path
        else:
            raise FileNotFoundError(
                "profile_view.v1 schema not found in the installed package "
                f"({schema_path}) or the repository source tree ({repo_path})."
            )
    return json.loads(schema_path.read_text(encoding="utf-8"))


def validate_profile_view(view: dict[str, Any]) -> list[str]:
    """Validate a profile-view projection against the packaged schema.

    Returns a list of human-readable validation error messages. An empty list
    means the projection conforms. This is intentionally non-raising so callers
    (including :func:`verify_profile_view`) can aggregate results.

    Raises:
        RuntimeError: if the optional ``jsonschema`` dependency is not
            installed, or if the schema file cannot be located.
    """
    try:
        from jsonschema.validators import Draft202012Validator  # type: ignore[import-not-found]
    except Exception as exc:  # pragma: no cover - depends on optional extra
        raise RuntimeError(
            "profile-view validation requires jsonschema; "
            "install actproof-events[schema-validation]"
        ) from exc

    try:
        schema = get_profile_view_schema()
    except FileNotFoundError as exc:
        raise RuntimeError(str(exc)) from exc

    validator = Draft202012Validator(schema)
    errors = sorted(validator.iter_errors(view), key=lambda e: list(e.path))
    messages: list[str] = []
    for err in errors:
        location = "/".join(str(p) for p in err.path) or "<root>"
        messages.append(f"{location}: {err.message}")
    return messages


def _count_provisional_atoms(view: dict[str, Any]) -> int:
    """Count distinct source atoms whose binding_status is provisional
    (official_text_sha256 not yet extracted/verified)."""
    seen: set[str] = set()
    for f in view.get("fields", []) or []:
        for e in f.get("source_basis", []) or []:
            if isinstance(e, dict) and e.get("binding_status") == "provisional":
                aid = e.get("source_atom_id") or e.get("eli") or str(e)
                seen.add(aid)
    return len(seen)


def _profile_review_status(view: dict[str, Any]) -> str | None:
    """Read the field-derivation review status as projected, if present.

    Falls back to inspecting per-field mapping_confidence: if every field
    derivation is 'draft', the profile review status is 'draft'."""
    rg = view.get("review_gate") or {}
    if rg.get("review_status"):
        return rg["review_status"]
    confidences = {
        (f.get("field_derivation") or {}).get("mapping_confidence")
        for f in view.get("fields", []) or []
        if f.get("field_derivation")
    }
    confidences.discard(None)
    if confidences == {"draft"}:
        return "draft"
    if confidences:
        return "mixed" if len(confidences) > 1 else next(iter(confidences))
    return None


def verify_profile_view(view: dict[str, Any] | str | Path) -> dict[str, Any]:
    """Verify a profile-view artifact against what this release guarantees.

    Accepts either a parsed projection dict or a path to a profile-view JSON
    file. Returns a structured report. This verifier checks only what
    actproof-events 1.8.0 actually ships:

      * ``valid_schema``          — conforms to the bundled profile_view.v1 schema.
      * ``semantic_hash_matches`` — recomputed profile_semantic_hash equals the
                                     stored one (version-independent identity).
      * ``artifact_hash_matches`` — recomputed profile_artifact_hash equals the
                                     stored one (release-specific identity).
      * ``catalogue_entry_hash_present`` — the canonical catalogue hash is carried.

    Field-level source binding is required for all required DORA fields in 1.8.0.
    Optional fields may still use act-level fallback and are reported separately.

    ``ok`` is True only when every hard check passes. Warnings describe known,
    disclosed limitations and do not set ``ok`` to False.
    """
    if isinstance(view, (str, Path)):
        view = json.loads(Path(view).read_text(encoding="utf-8"))

    report: dict[str, Any] = {
        "ok": False,
        "valid_schema": False,
        "semantic_hash_matches": False,
        "artifact_hash_matches": False,
        "catalogue_entry_hash_present": False,
        "errors": [],
        "warnings": [],
    }

    try:
        schema_errors = validate_profile_view(view)
        report["valid_schema"] = not schema_errors
        report["errors"].extend(schema_errors)
    except RuntimeError as exc:
        report["warnings"].append(str(exc))
        report["valid_schema"] = None  # could not be checked

    stored_semantic = view.get("profile_semantic_hash")
    stored_artifact = view.get("profile_artifact_hash") or view.get("profile_view_hash")

    recomputed_semantic = compute_profile_semantic_hash(view)
    recomputed_artifact = compute_profile_artifact_hash(view)

    report["semantic_hash_matches"] = bool(
        stored_semantic and stored_semantic == recomputed_semantic
    )
    report["artifact_hash_matches"] = bool(
        stored_artifact and stored_artifact == recomputed_artifact
    )
    report["profile_semantic_hash"] = recomputed_semantic
    report["profile_artifact_hash"] = recomputed_artifact

    if stored_semantic and not report["semantic_hash_matches"]:
        report["errors"].append(
            f"profile_semantic_hash mismatch: stored {stored_semantic}, "
            f"recomputed {recomputed_semantic}"
        )
    if stored_artifact and not report["artifact_hash_matches"]:
        report["errors"].append(
            f"profile_artifact_hash mismatch: stored {stored_artifact}, "
            f"recomputed {recomputed_artifact}"
        )

    provenance = view.get("provenance") or {}
    catalogue_hash = provenance.get("catalogue_entry_hash") or (
        view.get("canonical_object") or {}
    ).get("catalogue_entry_hash")
    report["catalogue_entry_hash_present"] = bool(catalogue_hash)
    if catalogue_hash:
        report["catalogue_entry_hash"] = catalogue_hash
    else:
        report["errors"].append("no catalogue_entry_hash carried in the artifact")

    coverage = view.get("coverage") or {}
    basis = coverage.get("field_source_basis") or {}
    required_basis = coverage.get("required_field_source_basis") or {}
    fallback_used = basis.get("fallback_used")
    required_fallback = required_basis.get("fallback_used")
    optional_fallback = fallback_used - required_fallback if isinstance(fallback_used, int) and isinstance(required_fallback, int) else None
    if required_fallback:
        report["errors"].append(
            f"{required_fallback} required fields use act-level source fallback; "
            "1.8.0 requires all required fields to be source-bound"
        )
    if optional_fallback:
        report["warnings"].append(
            f"{optional_fallback} optional fields still use act-level source fallback; "
            "optional field binding is reserved for a later release"
        )
    report["required_field_derivations_complete"] = required_basis.get("coverage_ratio") == 100.0 and not required_fallback
    report["field_derivations_complete"] = report["required_field_derivations_complete"]

    # Positive disclosure: how many required fields are clause-bound.
    req_field_level = required_basis.get("field_level")
    req_total = required_basis.get("required_total")
    if req_field_level and req_total:
        req_cell = required_basis.get("template_cell_bound", req_field_level)
        report["warnings"].append(
            f"{req_cell}/{req_total} required fields are template-cell bound to source atoms"
        )
    report["required_fields_clause_bound"] = req_field_level
    report["required_fields_template_cell_bound"] = required_basis.get("template_cell_bound")

    # Precision-tier disclosure: the honest binding-quality gradient.
    precision = coverage.get("source_binding_precision") or {}
    if precision:
        report["source_binding_precision"] = precision
        tiers = ", ".join(f"{k}:{v}" for k, v in precision.items() if v)
        report["warnings"].append(f"source-binding precision — {tiers}")

    # Honest disclosure: provisional text binding (official_text_sha256 pending).
    provisional = _count_provisional_atoms(view)
    if provisional:
        report["warnings"].append(
            f"{provisional} source atom(s) carry provisional text binding "
            f"(official_text_sha256 pending; ELI locator + pinned-PDF hash present)"
        )
    report["provisional_atoms"] = provisional

    # Honest disclosure: review status. Field derivations are authored, not yet
    # independently reviewed — surfaced so 'check us, don't trust us' is visible.
    review_status = _profile_review_status(view)
    if review_status and review_status == "draft":
        report["warnings"].append(
            "field derivations carry review_status=draft "
            "(authored, not yet maintainer/independently reviewed)"
        )
    report["review_status"] = review_status

    hard_checks = [
        report["semantic_hash_matches"],
        report["artifact_hash_matches"],
        report["catalogue_entry_hash_present"],
        report["required_field_derivations_complete"],
    ]
    if report["valid_schema"] is not None:
        hard_checks.append(report["valid_schema"])
    report["ok"] = all(hard_checks)
    return report


def _print_usage() -> None:
    print("actproof-events - public profile-view export utilities")
    print("")
    print("Usage:")
    print("  actproof-events export-profile-view ACT_ID --out profile-view.json [--pretty] [--validate]")
    print("  actproof-events validate-source-bindings ACT_ID")
    print("  actproof-events explain-field ACT_ID FIELD_ID")
    print("  actproof-events export-profile-lock ACT_ID --out profile-lock.json")
    print("  actproof-events export-prevalidation-report ACT_ID report.json --out prevalidation-report.json")
    print("  actproof-events export-review-checklist ACT_ID --out review-checklist.json")
    print("  actproof-events compare-schema ACT_ID external-schema.json --out mapping-report.json")
    print("  actproof-events diff-profile old.profile-view.json new.profile-view.json --out profile-diff.json")
    print("")
    print("Example:")
    print("  actproof-events export-profile-view op:eu.dora.ict_incident_notification_initial.v1 --out dora.profile-view.json")


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="actproof-events", description="ActProof Events export utilities")
    sub = parser.add_subparsers(dest="command")

    export = sub.add_parser("export-profile-view", help="Generate a public profile-view JSON projection")
    export.add_argument("act_id", help="ActProof act_type_id to export")
    export.add_argument("--out", required=True, help="Output JSON path")
    export.add_argument("--assessment-id", default=None, help="Optional caller-defined assessment id")
    export.add_argument("--pretty", action="store_true", help="Write indented JSON. This is the default.")
    export.add_argument("--compact", action="store_true", help="Write compact JSON instead of indented JSON")
    export.add_argument("--no-fields", action="store_true", help="Omit per-field rows")
    export.add_argument("--no-source-basis", action="store_true", help="Omit source-basis entries")
    export.add_argument("--no-non-claims", action="store_true", help="Omit non-claims from the projection")
    export.add_argument("--no-provenance", action="store_true", help="Omit provenance block")
    export.add_argument("--validate", action="store_true", help="Validate the generated projection against the packaged schema before writing")

    verify = sub.add_parser(
        "verify-profile-view",
        help="Verify an existing profile-view JSON (schema + both hashes + catalogue hash)",
    )
    verify.add_argument("path", help="Path to a profile-view JSON file to verify")

    validate_bindings = sub.add_parser(
        "validate-source-bindings",
        help="Validate required-field source bindings for a profile",
    )
    validate_bindings.add_argument("act_id", help="ActProof act_type_id to validate")

    explain = sub.add_parser(
        "explain-field",
        help="Explain the field-level source basis for one profile field",
    )
    explain.add_argument("act_id", help="ActProof act_type_id")
    explain.add_argument("field_id", help="Profile field id")
    explain.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    coverage_cmd = sub.add_parser(
        "source-coverage",
        help="Print precision-tiered source-binding coverage for a profile",
    )
    coverage_cmd.add_argument("act_id", help="ActProof act_type_id")
    coverage_cmd.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    atom_cov_cmd = sub.add_parser(
        "source-atom-coverage",
        help="Show which recorded source atoms are (un)used by fields \u2014 a missingness signal",
    )
    atom_cov_cmd.add_argument("act_id", help="ActProof act_type_id")
    atom_cov_cmd.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    completeness_cmd = sub.add_parser(
        "profile-completeness",
        help="Print the profile's explicit known-scope / not-exhaustive-of declaration",
    )
    completeness_cmd.add_argument("act_id", help="ActProof act_type_id")
    completeness_cmd.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    lock_cmd = sub.add_parser(
        "export-profile-lock",
        help="Export a bank-operable profile lockfile for local implementation",
    )
    lock_cmd.add_argument("act_id", help="ActProof act_type_id")
    lock_cmd.add_argument("--out", required=True, help="Output JSON path")
    lock_cmd.add_argument("--compact", action="store_true", help="Write compact JSON instead of indented JSON")

    verify_lock_cmd = sub.add_parser(
        "verify-profile-lock",
        help="Verify a stored profile lockfile against the installed package",
    )
    verify_lock_cmd.add_argument("path", help="Path to a profile lockfile JSON")

    review_cmd = sub.add_parser(
        "export-review-checklist",
        help="Export a bank-facing review checklist template for one profile",
    )
    review_cmd.add_argument("act_id", help="ActProof act_type_id")
    review_cmd.add_argument("--out", required=True, help="Output JSON path")
    review_cmd.add_argument("--compact", action="store_true", help="Write compact JSON instead of indented JSON")

    preval_report_cmd = sub.add_parser(
        "export-prevalidation-report",
        help="Export an audit-friendly pre-validation run report for a local report payload",
    )
    preval_report_cmd.add_argument("act_id", help="ActProof act_type_id")
    preval_report_cmd.add_argument("report_path", help="Path to a JSON report payload (field_id -> value)")
    preval_report_cmd.add_argument("--out", required=True, help="Output JSON path")
    preval_report_cmd.add_argument("--compact", action="store_true", help="Write compact JSON instead of indented JSON")


    compare_cmd = sub.add_parser(
        "compare-schema",
        help="Compare an external schema/field list to an ActProof profile; emits candidates for human review only",
    )
    compare_cmd.add_argument("act_id", help="ActProof act_type_id")
    compare_cmd.add_argument("schema_path", help="Path to external JSON Schema, field list, or sample report JSON")
    compare_cmd.add_argument("--out", help="Optional output JSON path for the mapping report")
    compare_cmd.add_argument("--external-system", default=None, help="Optional external system/schema label")
    compare_cmd.add_argument("--max-candidates", type=int, default=3, help="Maximum candidates per external field (default: 3)")
    compare_cmd.add_argument("--minimum-strength", choices=("weak", "medium", "strong"), default="medium", help="Minimum candidate strength to include (default: medium; use weak only for exploratory review)")
    compare_cmd.add_argument("--json", action="store_true", help="Emit full mapping report to stdout")
    compare_cmd.add_argument("--compact", action="store_true", help="Write compact JSON when --out is used")

    diff_cmd = sub.add_parser(
        "diff-profile",
        help="Compare two profile-view JSON exports and emit a change-control report",
    )
    diff_cmd.add_argument("old_profile_view", help="Old/baseline profile-view JSON path")
    diff_cmd.add_argument("new_profile_view", help="New/target profile-view JSON path")
    diff_cmd.add_argument("--out", help="Optional output JSON path for the diff report")
    diff_cmd.add_argument("--old-label", default=None, help="Optional label for the old profile-view")
    diff_cmd.add_argument("--new-label", default=None, help="Optional label for the new profile-view")
    diff_cmd.add_argument("--json", action="store_true", help="Emit full diff report to stdout")
    diff_cmd.add_argument("--compact", action="store_true", help="Write compact JSON when --out is used")

    gov_cmd = sub.add_parser(
        "governance-status",
        help="Show reviewed profile governance state and open challenge summary",
    )
    gov_cmd.add_argument("act_id", help="ActProof act_type_id")
    gov_cmd.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    review_records_cmd = sub.add_parser(
        "review-records",
        help="List bounded review records for a profile",
    )
    review_records_cmd.add_argument("act_id", help="ActProof act_type_id")
    review_records_cmd.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    challenge_records_cmd = sub.add_parser(
        "challenge-records",
        help="List public challenge/gap records for a profile",
    )
    challenge_records_cmd.add_argument("act_id", help="ActProof act_type_id")
    challenge_records_cmd.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    validate_gov_cmd = sub.add_parser(
        "validate-governance",
        help="Validate profile governance records and hash-bound review state",
    )
    validate_gov_cmd.add_argument("act_id", help="ActProof act_type_id")

    bank_poc_cmd = sub.add_parser(
        "export-bank-poc-pack",
        help="Export a local bank POC pack for one ActProof profile",
    )
    bank_poc_cmd.add_argument("act_id", help="ActProof act_type_id")
    bank_poc_cmd.add_argument("--out", required=True, help="Output directory for the POC pack")
    bank_poc_cmd.add_argument("--external-schema", default=None, help="Optional external schema/field-list JSON path")
    bank_poc_cmd.add_argument("--sample-report", default=None, help="Optional sample draft report JSON path")
    bank_poc_cmd.add_argument("--json", action="store_true", help="Emit machine-readable pack manifest")

    overlay_init_cmd = sub.add_parser(
        "init-overlay",
        help="Initialise a bank-owned profile overlay from a candidate mapping report",
    )
    overlay_init_cmd.add_argument("act_id", help="ActProof act_type_id")
    overlay_init_cmd.add_argument("--mapping-report", required=True, help="Candidate mapping report JSON from compare-schema")
    overlay_init_cmd.add_argument("--institution", default=None, help="Institution name to include in the overlay")
    overlay_init_cmd.add_argument("--out", required=True, help="Output bank overlay JSON path")
    overlay_init_cmd.add_argument("--compact", action="store_true", help="Write compact JSON instead of indented JSON")

    overlay_init_schema_cmd = sub.add_parser(
        "init-overlay-from-schema",
        help="Compare an external schema and initialise a draft bank overlay in one step",
    )
    overlay_init_schema_cmd.add_argument("act_id", help="ActProof act_type_id")
    overlay_init_schema_cmd.add_argument("schema_path", help="External JSON Schema, field list, or sample report JSON")
    overlay_init_schema_cmd.add_argument("--institution", default=None, help="Institution name to include in the overlay")
    overlay_init_schema_cmd.add_argument("--external-system", default=None, help="Optional external system/schema label")
    overlay_init_schema_cmd.add_argument("--out", required=True, help="Output bank overlay JSON path")
    overlay_init_schema_cmd.add_argument("--compact", action="store_true", help="Write compact JSON instead of indented JSON")

    overlay_validate_cmd = sub.add_parser(
        "validate-overlay",
        help="Validate a bank-owned overlay's profile hash, mapping decisions and review controls",
    )
    overlay_validate_cmd.add_argument("overlay_path", help="Bank overlay JSON path")

    overlay_status_cmd = sub.add_parser(
        "overlay-status",
        help="Show bank-owned overlay status and review readiness",
    )
    overlay_status_cmd.add_argument("overlay_path", help="Bank overlay JSON path")
    overlay_status_cmd.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    overlay_report_cmd = sub.add_parser(
        "export-overlay-report",
        help="Export an audit-friendly bank overlay report",
    )
    overlay_report_cmd.add_argument("overlay_path", help="Bank overlay JSON path")
    overlay_report_cmd.add_argument("--out", required=True, help="Output overlay report JSON path")
    overlay_report_cmd.add_argument("--compact", action="store_true", help="Write compact JSON instead of indented JSON")

    overlay_impact_cmd = sub.add_parser(
        "diff-overlay-impact",
        help="Compare an old/new profile-view pair against a bank overlay and emit an overlay impact report",
    )
    overlay_impact_cmd.add_argument("overlay_path", help="Bank overlay JSON path")
    overlay_impact_cmd.add_argument("old_profile_view", help="Old/baseline profile-view JSON path")
    overlay_impact_cmd.add_argument("new_profile_view", nargs="?", default=None, help="New/target profile-view JSON path (default: current profile)")
    overlay_impact_cmd.add_argument("--out", help="Optional output overlay impact report JSON path")
    overlay_impact_cmd.add_argument("--json", action="store_true", help="Emit full impact report to stdout")
    overlay_impact_cmd.add_argument("--compact", action="store_true", help="Write compact JSON when --out is used")

    lint = sub.add_parser(
        "lint-report",
        help="Lint an incident-report JSON payload against a profile",
    )
    lint.add_argument("act_id", help="ActProof act_type_id")
    lint.add_argument("report_path", help="Path to a JSON report payload (field_id -> value)")
    lint.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    preval = sub.add_parser(
        "prevalidate-report",
        help="Pre-validate an incident-report payload before cryptographic verification",
    )
    preval.add_argument("act_id", help="ActProof act_type_id")
    preval.add_argument("report_path", help="Path to a JSON report payload (field_id -> value)")
    preval.add_argument("--json", action="store_true", help="Emit machine-readable JSON")

    args = parser.parse_args(argv)
    if args.command is None:
        _print_usage()
        return 0

    if args.command == "verify-profile-view":
        try:
            report = verify_profile_view(args.path)
        except Exception as exc:
            print(f"actproof-events: verify failed: {exc}", file=sys.stderr)
            return 1
        print(f"verifying {args.path}")
        print(f"  schema valid           : {report['valid_schema']}")
        print(f"  semantic hash matches  : {report['semantic_hash_matches']}")
        print(f"  artifact hash matches  : {report['artifact_hash_matches']}")
        print(f"  catalogue hash present  : {report['catalogue_entry_hash_present']}")
        print(f"  profile_semantic_hash  : {report.get('profile_semantic_hash')}")
        print(f"  profile_artifact_hash  : {report.get('profile_artifact_hash')}")
        for w in report["warnings"]:
            print(f"  warning: {w}")
        for e in report["errors"]:
            print(f"  error  : {e}", file=sys.stderr)
        print(f"  OK                      : {report['ok']}")
        return 0 if report["ok"] else 1

    if args.command == "validate-source-bindings":
        errors = validate_field_source_bindings(args.act_id)
        coverage = compute_field_source_coverage(args.act_id)
        req = coverage["required_field_source_basis"]
        print(f"validating source bindings for {args.act_id}")
        field_basis = coverage["field_source_basis"]
        opt = coverage.get("optional_field_source_basis") or {}
        print(f"  required template-field coverage : {req.get('template_cell_bound')}/{req['required_total']} ({req['coverage_ratio']}%)")
        print(f"  release-gated field coverage     : {field_basis['field_level']}/{field_basis['field_level'] + field_basis.get('contextual_field_level', 0) + field_basis['act_level_fallback']} ({field_basis['coverage_ratio']}%)")
        if opt:
            print(f"  optional contextual derivations  : {opt.get('field_level')}/{opt.get('optional_total')} (not counted as template-field coverage)")
        if errors:
            for e in errors:
                print(f"  error: {e}", file=sys.stderr)
            return 1
        print("  OK                      : True")
        return 0

    if args.command == "explain-field":
        try:
            explanation = explain_field_source(args.act_id, args.field_id)
        except Exception as exc:
            print(f"actproof-events: explain-field failed: {exc}", file=sys.stderr)
            return 1
        if args.json:
            print(json.dumps(explanation, ensure_ascii=False, indent=2))
        else:
            print(f"field: {explanation['field_id']}")
            print(f"  source_basis_scope : {explanation['source_basis_scope']}")
            print(f"  fallback_used      : {explanation['fallback_used']}")
            print(f"  binding_granularity: {explanation.get('binding_granularity')}")
            print(f"  field_binding_status: {explanation.get('field_binding_status')}")
            print(f"  release_scope      : {explanation.get('release_scope')}")
            print(f"  counts toward gate : {explanation.get('counts_toward_required_release_gate')}")
            print(f"  source_atoms       : {len(explanation.get('source_atoms') or [])}")
            derivation = explanation.get("field_derivation") or {}
            if derivation:
                print(f"  derivation_type    : {derivation.get('derivation_type')}")
                print(f"  confidence         : {derivation.get('mapping_confidence')}")
                print(f"  interpretive_load  : {derivation.get('interpretive_load')}")
                print(f"  note               : {derivation.get('derivation_note')}")
        return 0

    if args.command == "source-coverage":
        from actproof_events.source_binding import compute_field_source_coverage as _cov
        cov = _cov(args.act_id)
        if args.json:
            print(json.dumps(cov, ensure_ascii=False, indent=2))
            return 0
        req = cov["required_field_source_basis"]
        opt = cov["optional_field_source_basis"]
        prec = cov["source_binding_precision"]
        print(f"source-binding coverage for {args.act_id}")
        print(f"  required (template-cell) : {req.get('template_cell_bound')}/{req['required_total']} ({req['coverage_ratio']}%)")
        print(f"  optional (experimental)  : {opt['field_level']}/{opt['optional_total']} contextual; {opt['template_cell_bound']} template-cell")
        print(f"  precision tiers          : " + ", ".join(f"{k}={v}" for k, v in prec.items() if v))
        return 0

    if args.command == "source-atom-coverage":
        from actproof_events.source_binding import compute_source_atom_coverage as _acov
        cov = _acov(args.act_id)
        if args.json:
            print(json.dumps(cov, ensure_ascii=False, indent=2))
            return 0
        print(f"source-atom coverage for {args.act_id}")
        print(f"  total source atoms          : {cov['total_source_atoms']}")
        print(f"  used by at least one field  : {cov['atoms_used_by_fields']}")
        print(f"  with required-field binding : {cov['atoms_with_required_field_bindings']}")
        print(f"  only in contextual bindings : {cov['atoms_only_in_contextual_bindings']}")
        print(f"  unused (gap signal)         : {cov['unused_source_atoms']}")
        for a in cov["unused_source_atom_ids"]:
            print(f"      - {a}")
        if cov["dangling_atom_references"]:
            print(f"  DANGLING refs (integrity!)  : {cov['dangling_atom_references']}")
            for a in cov["dangling_atom_reference_ids"]:
                print(f"      - {a}")
        return 0

    if args.command == "profile-completeness":
        from actproof_events.source_binding import get_profile_completeness as _comp
        comp = _comp(args.act_id)
        if args.json:
            print(json.dumps(comp, ensure_ascii=False, indent=2))
            return 0
        print(f"profile completeness for {args.act_id}")
        print(f"  status        : {comp.get('completeness_status')} (review: {comp.get('review_status')})")
        print(f"  known scope   : {comp.get('known_scope')}")
        print(f"  field IDs universal? : {comp.get('field_id_policy', {}).get('universal_claim')}")
        print(f"  NOT exhaustive of:")
        for x in comp.get("not_exhaustive_of", []):
            print(f"      - {x}")
        print(f"  challenges    : {', '.join(comp.get('challenge_types', []))}")
        if comp.get("challenge_channel"):
            print(f"  raise one at  : {comp['challenge_channel']}")
        return 0

    if args.command == "export-profile-lock":
        from actproof_events.bank_operability import build_profile_lock as _lock
        try:
            payload = _lock(args.act_id)
            Path(args.out).write_text(
                json.dumps(payload, ensure_ascii=False, sort_keys=False, indent=None if args.compact else 2) + "\n",
                encoding="utf-8",
            )
        except Exception as exc:
            print(f"actproof-events: export-profile-lock failed: {exc}", file=sys.stderr)
            return 1
        print(f"wrote profile lockfile: {args.out}")
        print(f"  profile_lock_hash: {payload.get('profile_lock_hash')}")
        print(f"  profile_semantic_hash: {payload.get('profile', {}).get('profile_semantic_hash')}")
        return 0

    if args.command == "verify-profile-lock":
        from actproof_events.bank_operability import verify_profile_lock as _verify
        try:
            result = _verify(args.path)
        except Exception as exc:
            print(f"actproof-events: verify-profile-lock failed: {exc}", file=sys.stderr)
            return 1
        for field, ok in result["checks"].items():
            print(f"  [{'ok' if ok else 'MISMATCH'}] {field}")
        if result["ok"]:
            print(f"profile lock OK for {result['act_id']}")
            return 0
        print(f"profile lock MISMATCH for {result['act_id']}:")
        for m in result["mismatches"]:
            print(f"  {m['field']}: expected {m['expected']!r}, got {m['actual']!r}")
        print(f"  note: {result['note']}")
        return 1

    if args.command == "export-review-checklist":
        from actproof_events.bank_operability import build_bank_review_checklist as _checklist
        try:
            payload = _checklist(args.act_id)
            Path(args.out).write_text(
                json.dumps(payload, ensure_ascii=False, sort_keys=False, indent=None if args.compact else 2) + "\n",
                encoding="utf-8",
            )
        except Exception as exc:
            print(f"actproof-events: export-review-checklist failed: {exc}", file=sys.stderr)
            return 1
        print(f"wrote review checklist: {args.out}")
        print(f"  review_checklist_hash: {payload.get('review_checklist_hash')}")
        print(f"  sections: {len(payload.get('checklist') or [])}")
        return 0

    if args.command == "export-prevalidation-report":
        from actproof_events.bank_operability import build_prevalidation_run_report as _run_report
        try:
            report_payload = json.loads(Path(args.report_path).read_text(encoding="utf-8"))
            payload = _run_report(args.act_id, report_payload)
            Path(args.out).write_text(
                json.dumps(payload, ensure_ascii=False, sort_keys=False, indent=None if args.compact else 2) + "\n",
                encoding="utf-8",
            )
        except Exception as exc:
            print(f"actproof-events: export-prevalidation-report failed: {exc}", file=sys.stderr)
            return 1
        print(f"wrote pre-validation report: {args.out}")
        print(f"  prevalidation_report_hash: {payload.get('prevalidation_report_hash')}")
        print(f"  status: {payload.get('run_summary', {}).get('prevalidation_status')}")
        return 0 if payload.get('run_summary', {}).get('prevalidation_status') != 'blocked' else 1


    if args.command == "compare-schema":
        from actproof_events.schema_mapping import compare_schema_file as _compare_schema_file
        try:
            report = _compare_schema_file(
                args.act_id,
                args.schema_path,
                external_system=args.external_system,
                max_candidates_per_field=args.max_candidates,
                minimum_strength=args.minimum_strength,
            )
        except Exception as exc:
            print(f"actproof-events: compare-schema failed: {exc}", file=sys.stderr)
            return 1
        if args.out:
            Path(args.out).write_text(
                json.dumps(report, ensure_ascii=False, sort_keys=False, indent=None if args.compact else 2) + "\n",
                encoding="utf-8",
            )
        if args.json or not args.out:
            if args.json:
                print(json.dumps(report, ensure_ascii=False, indent=2))
            else:
                summary = report.get("summary") or {}
                print(f"schema mapping candidates for {args.act_id}")
                print(f"  external system                  : {report.get('external_system')}")
                print(f"  external fields                  : {summary.get('external_field_count')}")
                print(f"  candidate-mapped external fields : {summary.get('candidate_mapped_external_fields')}")
                print(f"  unmapped external fields         : {summary.get('unmapped_external_field_count')}")
                print(f"  ambiguous mappings               : {summary.get('ambiguous_mapping_count')}")
                print(f"  ActProof required without candidate: {summary.get('actproof_required_without_candidate_count')}")
                print("  mapping_status                  : candidate_review_required")
                print("  review_required                 : True")
        if args.out:
            print(f"wrote candidate schema mapping report: {args.out}")
        return 0

    if args.command == "diff-profile":
        from actproof_events.profile_diff import diff_profile_view_files as _diff_profile_view_files
        try:
            report = _diff_profile_view_files(
                args.old_profile_view,
                args.new_profile_view,
                old_label=args.old_label,
                new_label=args.new_label,
            )
        except Exception as exc:
            print(f"actproof-events: diff-profile failed: {exc}", file=sys.stderr)
            return 1
        if report.get("error"):
            print(f"actproof-events: {report['error']}: {report.get('note','')}", file=sys.stderr)
            return 1
        if args.out:
            Path(args.out).write_text(
                json.dumps(report, ensure_ascii=False, sort_keys=False, indent=None if args.compact else 2) + "\n",
                encoding="utf-8",
            )
        if args.json or not args.out:
            if args.json:
                print(json.dumps(report, ensure_ascii=False, indent=2))
            else:
                summary = report.get("summary") or {}
                print("profile diff change-control report")
                print(f"  old: {report.get('comparison', {}).get('old_label')}")
                print(f"  new: {report.get('comparison', {}).get('new_label')}")
                print(f"  semantic hash changed : {summary.get('semantic_hash_changed')}")
                print(f"  fields added          : {summary.get('fields_added')}")
                print(f"  fields removed        : {summary.get('fields_removed')}")
                print(f"  fields changed        : {summary.get('fields_changed')}")
                print(f"  source atom changes   : {summary.get('source_atom_changes')}")
                print(f"  review status changes : {summary.get('review_status_changes')}")
                print(f"  coverage changes      : {summary.get('coverage_changes')}")
                print(f"  review required       : {summary.get('review_required')}")
        if args.out:
            print(f"wrote profile diff report: {args.out}")
        return 0

    if args.command == "governance-status":
        from actproof_events.profile_governance import build_governance_status as _gov_status
        try:
            status = _gov_status(args.act_id)
        except Exception as exc:
            print(f"actproof-events: governance-status failed: {exc}", file=sys.stderr)
            return 1
        if args.json:
            print(json.dumps(status, ensure_ascii=False, indent=2))
        else:
            print(f"governance status for {args.act_id}")
            print(f"  lifecycle_state          : {status.get('lifecycle_state')}")
            print(f"  latest_review_status     : {status.get('latest_review_status')}")
            print(f"  latest_review_record_hash: {status.get('latest_review_record_hash')}")
            print(f"  open challenges          : {status.get('open_challenges')}")
            print(f"  blocking challenges      : {status.get('blocking_challenges')}")
            print(f"  governance_status_hash   : {status.get('governance_status_hash')}")
            print(f"  boundary                 : {status.get('bank_use_boundary')}")
        return 0

    if args.command == "review-records":
        from actproof_events.profile_governance import list_review_records as _reviews
        records = _reviews(args.act_id)
        payload = {"schema": "actproof.review_records.v1", "act_id": args.act_id, "records": records}
        if args.json:
            print(json.dumps(payload, ensure_ascii=False, indent=2))
        else:
            print(f"review records for {args.act_id}")
            for r in records:
                print(f"  - {r.get('review_id')} :: {r.get('review_status')} :: {r.get('review_record_hash')}")
                for lim in r.get('review_limitations') or []:
                    print(f"      limitation: {lim}")
        return 0

    if args.command == "challenge-records":
        from actproof_events.profile_governance import list_challenge_records as _challenges
        records = _challenges(args.act_id)
        payload = {"schema": "actproof.challenge_records.v1", "act_id": args.act_id, "challenge_records": records}
        if args.json:
            print(json.dumps(payload, ensure_ascii=False, indent=2))
        else:
            print(f"challenge records for {args.act_id}")
            for r in records:
                print(f"  - {r.get('challenge_id')} :: {r.get('challenge_type')} :: {r.get('status')} :: {r.get('impact')}")
                print(f"      {r.get('summary')}")
        return 0

    if args.command == "validate-governance":
        from actproof_events.profile_governance import validate_profile_governance as _validate_gov
        errors = _validate_gov(args.act_id)
        print(f"validating profile governance for {args.act_id}")
        if errors:
            for e in errors:
                print(f"  error: {e}", file=sys.stderr)
            return 1
        print("  OK: True")
        return 0

    if args.command == "export-bank-poc-pack":
        from actproof_events.profile_governance import build_bank_poc_pack as _build_pack
        try:
            manifest = _build_pack(
                args.act_id,
                external_schema_path=args.external_schema,
                sample_report_path=args.sample_report,
                out_dir=args.out,
            )
        except Exception as exc:
            print(f"actproof-events: export-bank-poc-pack failed: {exc}", file=sys.stderr)
            return 1
        if args.json:
            print(json.dumps(manifest, ensure_ascii=False, indent=2))
        else:
            print(f"wrote bank POC pack: {args.out}")
            print(f"  files: {len(manifest.get('files') or [])}")
            print(f"  profile_semantic_hash: {manifest.get('profile_semantic_hash')}")
            print(f"  governance_status_hash: {manifest.get('governance_status_hash')}")
            print(f"  bank_poc_pack_hash: {manifest.get('bank_poc_pack_hash')}")
        return 0

    if args.command == "init-overlay":
        from actproof_events.bank_overlay import init_bank_overlay as _init_overlay, write_bank_overlay as _write_overlay
        try:
            mapping_report = json.loads(Path(args.mapping_report).read_text(encoding="utf-8"))
            overlay = _init_overlay(args.act_id, mapping_report, institution=args.institution)
            _write_overlay(overlay, args.out, compact=args.compact)
        except Exception as exc:
            print(f"actproof-events: init-overlay failed: {exc}", file=sys.stderr)
            return 1
        print(f"wrote bank overlay: {args.out}")
        print(f"  overlay_id: {overlay.get('overlay_id')}")
        print(f"  profile_semantic_hash: {overlay.get('profile_semantic_hash')}")
        print(f"  mappings: {len(overlay.get('mappings') or [])}")
        print(f"  missing required decisions: {len(overlay.get('missing_required_field_decisions') or [])}")
        return 0

    if args.command == "init-overlay-from-schema":
        from actproof_events.bank_overlay import init_bank_overlay_from_schema as _init_from_schema, write_bank_overlay as _write_overlay
        try:
            overlay = _init_from_schema(
                args.act_id,
                args.schema_path,
                institution=args.institution,
                external_system=args.external_system,
            )
            _write_overlay(overlay, args.out, compact=args.compact)
        except Exception as exc:
            print(f"actproof-events: init-overlay-from-schema failed: {exc}", file=sys.stderr)
            return 1
        print(f"wrote bank overlay: {args.out}")
        print(f"  overlay_id: {overlay.get('overlay_id')}")
        print(f"  profile_semantic_hash: {overlay.get('profile_semantic_hash')}")
        print(f"  mappings: {len(overlay.get('mappings') or [])}")
        return 0

    if args.command == "validate-overlay":
        from actproof_events.bank_overlay import load_bank_overlay as _load_overlay, validate_bank_overlay as _validate_overlay
        try:
            overlay = _load_overlay(args.overlay_path)
            errors = _validate_overlay(overlay)
        except Exception as exc:
            print(f"actproof-events: validate-overlay failed: {exc}", file=sys.stderr)
            return 1
        if errors:
            print("overlay validation: FAIL")
            for e in errors:
                print(f"  error: {e}")
            return 1
        print("overlay validation: PASS")
        return 0

    if args.command == "overlay-status":
        from actproof_events.bank_overlay import load_bank_overlay as _load_overlay, build_bank_overlay_status as _overlay_status
        try:
            status = _overlay_status(_load_overlay(args.overlay_path))
        except Exception as exc:
            print(f"actproof-events: overlay-status failed: {exc}", file=sys.stderr)
            return 1
        if args.json:
            print(json.dumps(status, ensure_ascii=False, indent=2))
        else:
            print(f"overlay_id: {status.get('overlay_id')}")
            print(f"  profile_hash_matches: {status.get('profile_semantic_hash_matches')}")
            print(f"  mappings accepted/rejected/needs_review: {status.get('mapping_counts', {}).get('accepted')}/{status.get('mapping_counts', {}).get('rejected')}/{status.get('mapping_counts', {}).get('needs_review')}")
            print(f"  missing required without decision: {status.get('missing_required_field_decisions', {}).get('without_decision')}")
            print(f"  external-only needing review: {status.get('external_only_fields', {}).get('needs_review')}")
            print(f"  ready_for_internal_poc: {status.get('ready_for_internal_poc')}")
        return 0

    if args.command == "export-overlay-report":
        from actproof_events.bank_overlay import load_bank_overlay as _load_overlay, build_bank_overlay_report as _overlay_report
        try:
            report = _overlay_report(_load_overlay(args.overlay_path))
            Path(args.out).write_text(json.dumps(report, ensure_ascii=False, indent=None if args.compact else 2) + "\n", encoding="utf-8")
        except Exception as exc:
            print(f"actproof-events: export-overlay-report failed: {exc}", file=sys.stderr)
            return 1
        print(f"wrote bank overlay report: {args.out}")
        print(f"  overlay_report_hash: {report.get('overlay_report_hash')}")
        print(f"  ready_for_internal_poc: {report.get('status', {}).get('ready_for_internal_poc')}")
        return 0

    if args.command == "diff-overlay-impact":
        from actproof_events.overlay_impact import diff_overlay_impact_files as _diff_impact
        try:
            report = _diff_impact(args.overlay_path, args.old_profile_view, args.new_profile_view)
        except Exception as exc:
            print(f"actproof-events: diff-overlay-impact failed: {exc}", file=sys.stderr)
            return 1
        if args.out:
            Path(args.out).write_text(json.dumps(report, ensure_ascii=False, indent=None if args.compact else 2) + "\n", encoding="utf-8")
        if args.json or not args.out:
            print(json.dumps(report, ensure_ascii=False, indent=2))
        else:
            print(f"wrote overlay impact report: {args.out}")
            print(f"  impact_status: {report.get('impact_status')}")
            print(f"  ready_to_carry_forward: {report.get('ready_to_carry_forward')}")
            print(f"  blocking_items: {report.get('summary', {}).get('blocking_items')}")
            print(f"  review_items: {report.get('summary', {}).get('review_items')}")
        return 0 if report.get("impact_status") in {"no_profile_change", "no_overlay_impact"} else 1

    if args.command == "lint-report":
        from actproof_events.services import lint_report as _lint
        try:
            report_payload = json.loads(Path(args.report_path).read_text(encoding="utf-8"))
        except Exception as exc:
            print(f"actproof-events: could not read report: {exc}", file=sys.stderr)
            return 1
        result = _lint(args.act_id, report_payload)
        if args.json:
            print(json.dumps(result, ensure_ascii=False, indent=2))
            return 0
        print(f"lint-report for {args.act_id}")
        print(f"  status                 : {result['status']}")
        print(f"  prevalidation status   : {result.get('prevalidation_status')}")
        print(f"  required present       : {result['required_present']}/{result['required_total']}")
        if result["missing_required"]:
            print(f"  missing required       : {', '.join(result['missing_required'])}")
        if result["unknown_fields"]:
            print(f"  unknown fields         : {', '.join(result['unknown_fields'])}")
        att = result["present_fields_needing_attention"]
        if att["high_interpretive_load"]:
            print(f"  high interpretive load : {', '.join(att['high_interpretive_load'])}")
        if att["needs_committee_evidence"]:
            print(f"  needs committee evidence: {', '.join(att['needs_committee_evidence'])}")
        return 0 if result["status"] != "incomplete" else 1

    if args.command == "prevalidate-report":
        from actproof_events.services import prevalidate_report as _prevalidate
        try:
            report_payload = json.loads(Path(args.report_path).read_text(encoding="utf-8"))
        except Exception as exc:
            print(f"actproof-events: could not read report: {exc}", file=sys.stderr)
            return 1
        result = _prevalidate(args.act_id, report_payload)
        if args.json:
            print(json.dumps(result, ensure_ascii=False, indent=2))
            return 0
        print(f"prevalidate-report for {args.act_id}")
        print(f"  prevalidation status   : {result['prevalidation_status']}")
        print(f"  ready for preverification: {result['ready_for_preverification']}")
        print(f"  required present       : {result['required_present']}/{result['required_total']}")
        if result["missing_required"]:
            print(f"  missing required       : {', '.join(result['missing_required'])}")
        if result["unknown_fields"]:
            print(f"  unknown fields         : {', '.join(result['unknown_fields'])}")
        return 0 if result["prevalidation_status"] != "blocked" else 1

    if args.command != "export-profile-view":
        parser.error(f"unknown command: {args.command}")

    try:
        payload = write_profile_view(
            args.act_id,
            args.out,
            assessment_id=args.assessment_id,
            pretty=not args.compact,
            include_fields=not args.no_fields,
            include_source_basis=not args.no_source_basis,
            include_non_claims=not args.no_non_claims,
            include_provenance=not args.no_provenance,
            validate=args.validate,
        )
    except Exception as exc:
        print(f"actproof-events: export failed: {exc}", file=sys.stderr)
        return 1

    field_count = (payload.get("coverage") or {}).get("field_counts", {}).get("total")
    print(f"wrote {args.out}")
    if field_count is not None:
        print(f"fields: {field_count}")
    print(f"profile_semantic_hash: {payload.get('profile_semantic_hash')}")
    print(f"profile_artifact_hash: {payload.get('profile_artifact_hash')}")
    return 0


__all__ = [
    "PROFILE_VIEW_SCHEMA_ID",
    "PROFILE_VIEW_HASH_BASIS",
    "PROFILE_SEMANTIC_HASH_BASIS",
    "PROFILE_ARTIFACT_HASH_BASIS",
    "build_profile_view",
    "write_profile_view",
    "compute_profile_coverage",
    "compute_profile_view_hash",
    "compute_profile_semantic_hash",
    "compute_profile_artifact_hash",
    "get_profile_view_schema",
    "validate_profile_view",
    "verify_profile_view",
    "validate_field_source_bindings",
    "explain_field_source",
    "main",
]


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