"""Evidence bundle primitives and validation for schema v0.1."""

from __future__ import annotations

import hashlib
from collections.abc import Callable, Iterable, Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, TypeVar
from uuid import UUID

from .errors import EvidenceSerialisationError
from .serialise import canonical_json_bytes_for_signing, hash_canonical_json

EVIDENCE_SCHEMA_VERSION = "0.1"
_REQUIRED_FIELDS = (
    "schema_version",
    "transition_id",
    "entity_type",
    "entity_id",
    "actor",
    "authority",
    "timestamp",
    "before_hash",
    "after_hash",
)
_TRANSPARENCY_PROVENANCE_PREFIX = "transparency-log/"

__all__ = [
    "AuthoritySource",
    "EVIDENCE_SCHEMA_VERSION",
    "EvidenceBundle",
    "EvidenceProvenance",
    "EvidenceProvenanceHook",
    "EvidenceSignature",
    "EvidenceSignatureHook",
    "normalise_authority_sources",
    "normalise_evidence_provenance",
    "normalise_evidence_signatures",
    "validate_evidence_payload",
]

_T = TypeVar("_T")


@dataclass(frozen=True, slots=True)
class AuthoritySource:
    """Structured metadata describing one authority-anchored truth source."""

    source_type: str
    source_id: str
    reference: str
    details: Mapping[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        _validate_non_empty_string("source_type", self.source_type)
        _validate_non_empty_string("source_id", self.source_id)
        _validate_non_empty_string("reference", self.reference)
        if not isinstance(self.details, Mapping):
            msg = "details must be a mapping."
            raise ValueError(msg)
        object.__setattr__(self, "details", deepcopy(dict(self.details)))

    @classmethod
    def from_dict(cls, payload: Mapping[str, Any]) -> AuthoritySource:
        """Construct an authority source record from a mapping payload."""
        if not isinstance(payload, Mapping):
            msg = "authority_sources entries must be mappings."
            raise ValueError(msg)
        missing_fields = [
            name for name in ("source_type", "source_id", "reference") if name not in payload
        ]
        if missing_fields:
            msg = (
                "authority_sources entries are missing required fields: "
                f"{', '.join(missing_fields)}."
            )
            raise ValueError(msg)
        return cls(
            source_type=payload["source_type"],
            source_id=payload["source_id"],
            reference=payload["reference"],
            details=payload.get("details", {}),
        )

    def as_dict(self) -> dict[str, Any]:
        """Return a JSON-serialisable mapping in deterministic field order."""
        payload: dict[str, Any] = {
            "source_type": self.source_type,
            "source_id": self.source_id,
            "reference": self.reference,
        }
        if self.details:
            payload["details"] = deepcopy(dict(self.details))
        return payload


@dataclass(frozen=True, slots=True)
class EvidenceSignature:
    """Optional signature metadata for future cryptographic workflows."""

    key_id: str
    algorithm: str
    signature: str
    encoding: str = "base64"
    details: Mapping[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        _validate_non_empty_string("key_id", self.key_id)
        _validate_non_empty_string("algorithm", self.algorithm)
        _validate_non_empty_string("signature", self.signature)
        _validate_non_empty_string("encoding", self.encoding)
        if not isinstance(self.details, Mapping):
            msg = "signature details must be a mapping."
            raise ValueError(msg)
        object.__setattr__(self, "details", deepcopy(dict(self.details)))

    @classmethod
    def from_dict(cls, payload: Mapping[str, Any]) -> EvidenceSignature:
        """Construct a signature record from a mapping payload."""
        if not isinstance(payload, Mapping):
            msg = "signatures entries must be mappings."
            raise ValueError(msg)
        missing_fields = [
            name for name in ("key_id", "algorithm", "signature") if name not in payload
        ]
        if missing_fields:
            msg = f"signatures entries are missing required fields: {', '.join(missing_fields)}."
            raise ValueError(msg)
        return cls(
            key_id=payload["key_id"],
            algorithm=payload["algorithm"],
            signature=payload["signature"],
            encoding=payload.get("encoding", "base64"),
            details=payload.get("details", {}),
        )

    def as_dict(self) -> dict[str, Any]:
        """Return a JSON-serialisable mapping in deterministic field order."""
        payload: dict[str, Any] = {
            "key_id": self.key_id,
            "algorithm": self.algorithm,
            "signature": self.signature,
            "encoding": self.encoding,
        }
        if self.details:
            payload["details"] = deepcopy(dict(self.details))
        return payload


@dataclass(frozen=True, slots=True)
class EvidenceProvenance:
    """Optional provenance metadata attached to an evidence bundle."""

    source: str
    reference: str
    timestamp: str | None = None
    details: Mapping[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        _validate_non_empty_string("source", self.source)
        _validate_non_empty_string("reference", self.reference)
        if self.timestamp is not None:
            object.__setattr__(self, "timestamp", _normalise_timestamp(self.timestamp))
        if not isinstance(self.details, Mapping):
            msg = "provenance details must be a mapping."
            raise ValueError(msg)
        object.__setattr__(self, "details", deepcopy(dict(self.details)))

    @classmethod
    def from_dict(cls, payload: Mapping[str, Any]) -> EvidenceProvenance:
        """Construct a provenance record from a mapping payload."""
        if not isinstance(payload, Mapping):
            msg = "provenance entries must be mappings."
            raise ValueError(msg)
        missing_fields = [name for name in ("source", "reference") if name not in payload]
        if missing_fields:
            msg = f"provenance entries are missing required fields: {', '.join(missing_fields)}."
            raise ValueError(msg)
        return cls(
            source=payload["source"],
            reference=payload["reference"],
            timestamp=payload.get("timestamp"),
            details=payload.get("details", {}),
        )

    def as_dict(self) -> dict[str, Any]:
        """Return a JSON-serialisable mapping in deterministic field order."""
        payload: dict[str, Any] = {
            "source": self.source,
            "reference": self.reference,
        }
        if self.timestamp is not None:
            payload["timestamp"] = self.timestamp
        if self.details:
            payload["details"] = deepcopy(dict(self.details))
        return payload


def normalise_authority_sources(
    authority_sources: Iterable[AuthoritySource | Mapping[str, Any]] | None,
) -> tuple[AuthoritySource, ...]:
    """Normalise authority source inputs into validated AuthoritySource values."""
    return _normalise_items(
        values=authority_sources,
        label="authority_sources",
        mapping_parser=AuthoritySource.from_dict,
        instance_type=AuthoritySource,
    )


def normalise_evidence_signatures(
    signatures: Iterable[EvidenceSignature | Mapping[str, Any]] | None,
) -> tuple[EvidenceSignature, ...]:
    """Normalise evidence signature inputs into validated EvidenceSignature values."""
    return _normalise_items(
        values=signatures,
        label="signatures",
        mapping_parser=EvidenceSignature.from_dict,
        instance_type=EvidenceSignature,
    )


def normalise_evidence_provenance(
    provenance: Iterable[EvidenceProvenance | Mapping[str, Any]] | None,
) -> tuple[EvidenceProvenance, ...]:
    """Normalise evidence provenance inputs into validated EvidenceProvenance values."""
    return _normalise_items(
        values=provenance,
        label="provenance",
        mapping_parser=EvidenceProvenance.from_dict,
        instance_type=EvidenceProvenance,
    )


EvidenceSignatureHook = Callable[
    [dict[str, Any], bytes],
    Iterable[EvidenceSignature | Mapping[str, Any]] | None,
]
EvidenceProvenanceHook = Callable[
    [dict[str, Any]],
    Iterable[EvidenceProvenance | Mapping[str, Any]] | None,
]


@dataclass(frozen=True, slots=True)
class EvidenceBundle:
    """Canonical evidence payload for transition audit artefacts."""

    schema_version: str
    transition_id: str
    entity_type: str
    entity_id: str
    actor: str
    authority: str
    timestamp: str
    before_hash: str
    after_hash: str
    authority_sources: tuple[AuthoritySource, ...] = ()
    signatures: tuple[EvidenceSignature, ...] = ()
    provenance: tuple[EvidenceProvenance, ...] = ()

    def __post_init__(self) -> None:
        if self.schema_version != EVIDENCE_SCHEMA_VERSION:
            msg = f"Unsupported evidence schema version: {self.schema_version!r}."
            raise ValueError(msg)
        _validate_uuid(self.transition_id)
        _validate_non_empty_string("entity_type", self.entity_type)
        _validate_non_empty_string("entity_id", self.entity_id)
        _validate_non_empty_string("actor", self.actor)
        _validate_non_empty_string("authority", self.authority)
        object.__setattr__(
            self,
            "authority_sources",
            normalise_authority_sources(self.authority_sources),
        )
        object.__setattr__(self, "signatures", normalise_evidence_signatures(self.signatures))
        object.__setattr__(self, "provenance", normalise_evidence_provenance(self.provenance))
        object.__setattr__(self, "timestamp", _normalise_timestamp(self.timestamp))
        _validate_sha256("before_hash", self.before_hash)
        _validate_sha256("after_hash", self.after_hash)

    @classmethod
    def from_states(
        cls,
        *,
        transition_id: str | UUID,
        entity_type: str,
        entity_id: str,
        actor: str,
        authority: str,
        authority_sources: Iterable[AuthoritySource | Mapping[str, Any]] | None = None,
        before_state: Any,
        after_state: Any,
        timestamp: datetime | str | None = None,
        schema_version: str = EVIDENCE_SCHEMA_VERSION,
        signatures: Iterable[EvidenceSignature | Mapping[str, Any]] | None = None,
        provenance: Iterable[EvidenceProvenance | Mapping[str, Any]] | None = None,
        signature_hook: EvidenceSignatureHook | None = None,
        provenance_hook: EvidenceProvenanceHook | None = None,
    ) -> EvidenceBundle:
        """Build an evidence bundle with optional signature/provenance extensions."""
        if signature_hook is not None and not callable(signature_hook):
            msg = "signature_hook must be callable."
            raise TypeError(msg)
        if provenance_hook is not None and not callable(provenance_hook):
            msg = "provenance_hook must be callable."
            raise TypeError(msg)
        if isinstance(transition_id, UUID):
            transition_id_value = str(transition_id)
        else:
            transition_id_value = transition_id
        normalised_authority_sources = normalise_authority_sources(authority_sources)
        timestamp_value = _normalise_timestamp(timestamp)
        before_hash = _hash_state_for_evidence(before_state, field_name="before_state")
        after_hash = _hash_state_for_evidence(after_state, field_name="after_state")
        base_payload: dict[str, Any] = {
            "schema_version": schema_version,
            "transition_id": transition_id_value,
            "entity_type": entity_type,
            "entity_id": entity_id,
            "actor": actor,
            "authority": authority,
            "authority_sources": [source.as_dict() for source in normalised_authority_sources],
            "timestamp": timestamp_value,
            "before_hash": before_hash,
            "after_hash": after_hash,
        }
        signature_values = list(normalise_evidence_signatures(signatures))
        if signature_hook is not None:
            generated_signatures = signature_hook(
                dict(base_payload),
                canonical_json_bytes_for_signing(
                    base_payload,
                    exclude_fields=("signatures", "provenance"),
                ),
            )
            signature_values.extend(normalise_evidence_signatures(generated_signatures))
        provenance_values = list(normalise_evidence_provenance(provenance))
        if provenance_hook is not None:
            generated_provenance = provenance_hook(dict(base_payload))
            provenance_values.extend(normalise_evidence_provenance(generated_provenance))
        return cls(
            schema_version=schema_version,
            transition_id=transition_id_value,
            entity_type=entity_type,
            entity_id=entity_id,
            actor=actor,
            authority=authority,
            authority_sources=normalised_authority_sources,
            timestamp=timestamp_value,
            before_hash=before_hash,
            after_hash=after_hash,
            signatures=tuple(signature_values),
            provenance=tuple(provenance_values),
        )

    @classmethod
    def from_dict(cls, payload: Mapping[str, Any]) -> EvidenceBundle:
        """Construct and validate an evidence bundle from a plain mapping."""
        errors = validate_evidence_payload(payload)
        if errors:
            raise ValueError("; ".join(errors))
        authority_sources = payload.get("authority_sources")
        signatures = payload.get("signatures")
        provenance = payload.get("provenance")
        return cls(
            schema_version=str(payload["schema_version"]),
            transition_id=str(payload["transition_id"]),
            entity_type=str(payload["entity_type"]),
            entity_id=str(payload["entity_id"]),
            actor=str(payload["actor"]),
            authority=str(payload["authority"]),
            authority_sources=normalise_authority_sources(authority_sources),
            timestamp=str(payload["timestamp"]),
            before_hash=str(payload["before_hash"]),
            after_hash=str(payload["after_hash"]),
            signatures=normalise_evidence_signatures(signatures),
            provenance=normalise_evidence_provenance(provenance),
        )

    def as_dict(self) -> dict[str, Any]:
        """Return a JSON-serialisable mapping in schema field order."""
        payload: dict[str, Any] = {
            "schema_version": self.schema_version,
            "transition_id": self.transition_id,
            "entity_type": self.entity_type,
            "entity_id": self.entity_id,
            "actor": self.actor,
            "authority": self.authority,
            "authority_sources": [source.as_dict() for source in self.authority_sources],
            "timestamp": self.timestamp,
            "before_hash": self.before_hash,
            "after_hash": self.after_hash,
        }
        if self.signatures:
            payload["signatures"] = [signature.as_dict() for signature in self.signatures]
        if self.provenance:
            payload["provenance"] = [record.as_dict() for record in self.provenance]
        return payload

    def signable_bytes(self) -> bytes:
        """Return canonical bytes suitable for signature verification workflows."""
        return canonical_json_bytes_for_signing(
            self.as_dict(),
            exclude_fields=("signatures", "provenance"),
        )

    def transparency_log_payload(self) -> dict[str, Any]:
        """Return the canonical payload bound to transparency append/verify workflows."""
        return _transparency_log_payload_from_mapping(self.as_dict())

    def as_stream_event_payload(
        self,
        *,
        transition_name: str,
        emitted_at: datetime | str | None = None,
        stream: str = "icsd.evidence",
    ) -> dict[str, Any]:
        """Return a deterministic stream-event payload for auditor feed subscribers."""
        _validate_non_empty_string("transition_name", transition_name)
        _validate_non_empty_string("stream", stream)
        evidence_payload = self.as_dict()
        return {
            "stream_schema_version": self.schema_version,
            "event_type": f"icsd.evidence_bundle.v{self.schema_version}",
            "stream": stream.strip(),
            "emitted_at": _normalise_timestamp(emitted_at),
            "transition_name": transition_name.strip(),
            "transition_id": self.transition_id,
            "signature_count": len(self.signatures),
            "signable_hash": hashlib.sha256(self.signable_bytes()).hexdigest(),
            "evidence_hash": hash_canonical_json(evidence_payload),
            "evidence": evidence_payload,
        }

    def verify_integrity(self, *, before_state: Any, after_state: Any) -> bool:
        """Return True when supplied states match stored canonical hashes."""
        return self.before_hash == _hash_state_for_evidence(
            before_state,
            field_name="before_state",
        ) and self.after_hash == _hash_state_for_evidence(
            after_state,
            field_name="after_state",
        )


def validate_evidence_payload(payload: Mapping[str, Any]) -> tuple[str, ...]:
    """Return a tuple of schema/integrity validation errors for an evidence payload."""
    errors: list[str] = []
    if not isinstance(payload, Mapping):
        return ("Evidence payload must be a mapping.",)
    missing_fields = [name for name in _REQUIRED_FIELDS if name not in payload]
    if missing_fields:
        errors.append(f"Missing required evidence fields: {', '.join(missing_fields)}.")
        return tuple(errors)
    schema_version = payload["schema_version"]
    if schema_version != EVIDENCE_SCHEMA_VERSION:
        errors.append(f"Unsupported evidence schema version: {schema_version!r}.")
    try:
        _validate_uuid(payload["transition_id"])
    except ValueError as exc:
        errors.append(str(exc))
    for field_name in ("entity_type", "entity_id", "actor", "authority"):
        try:
            _validate_non_empty_string(field_name, payload[field_name])
        except ValueError as exc:
            errors.append(str(exc))
    try:
        _normalise_timestamp(payload["timestamp"])
    except ValueError as exc:
        errors.append(str(exc))
    if "authority_sources" in payload:
        try:
            normalise_authority_sources(payload["authority_sources"])
        except ValueError as exc:
            errors.append(str(exc))
    if "signatures" in payload:
        try:
            normalise_evidence_signatures(payload["signatures"])
        except ValueError as exc:
            errors.append(str(exc))
    if "provenance" in payload:
        try:
            normalise_evidence_provenance(payload["provenance"])
        except ValueError as exc:
            errors.append(str(exc))
    for field_name in ("before_hash", "after_hash"):
        try:
            _validate_sha256(field_name, payload[field_name])
        except ValueError as exc:
            errors.append(str(exc))
    return tuple(errors)


def _normalise_items(
    *,
    values: Iterable[_T | Mapping[str, Any]] | None,
    label: str,
    mapping_parser: Callable[[Mapping[str, Any]], _T],
    instance_type: type[_T],
) -> tuple[_T, ...]:
    if values is None:
        return ()
    if isinstance(values, (str, bytes, bytearray)):
        msg = f"{label} must be an iterable of {instance_type.__name__} or mapping values."
        raise ValueError(msg)
    normalised: list[_T] = []
    for index, value in enumerate(values):
        if isinstance(value, instance_type):
            normalised.append(value)
            continue
        if isinstance(value, Mapping):
            try:
                normalised.append(mapping_parser(value))
            except ValueError as exc:
                msg = f"{label}[{index}] {exc}"
                raise ValueError(msg) from exc
            continue
        msg = f"{label} must be an iterable of {instance_type.__name__} or mapping values."
        raise ValueError(msg)
    return tuple(normalised)


def _hash_state_for_evidence(state: Any, *, field_name: str) -> str:
    try:
        return hash_canonical_json(state)
    except EvidenceSerialisationError as exc:
        root_cause = exc.__cause__ if exc.__cause__ is not None else exc
        msg = (
            f"{field_name} must be canonical-JSON-serialisable before ICSD evidence "
            "hashes can be generated."
        )
        raise EvidenceSerialisationError(msg) from root_cause


def _transparency_log_payload_from_mapping(payload: Mapping[str, Any]) -> dict[str, Any]:
    """Return a deep-copied payload with transparency receipts removed from provenance."""
    if not isinstance(payload, Mapping):
        msg = "payload must be a mapping."
        raise TypeError(msg)
    cleaned_payload = deepcopy(dict(payload))
    provenance = cleaned_payload.get("provenance")
    if not isinstance(provenance, Sequence) or isinstance(provenance, (str, bytes, bytearray)):
        return cleaned_payload
    filtered_provenance = [
        deepcopy(item)
        for item in provenance
        if not (
            isinstance(item, Mapping)
            and isinstance(item.get("source"), str)
            and item["source"].startswith(_TRANSPARENCY_PROVENANCE_PREFIX)
        )
    ]
    if filtered_provenance:
        cleaned_payload["provenance"] = filtered_provenance
    else:
        cleaned_payload.pop("provenance", None)
    return cleaned_payload


def _validate_uuid(value: Any) -> None:
    if not isinstance(value, str):
        msg = f"transition_id must be a valid UUID string; got {value!r}."
        raise ValueError(msg)
    try:
        UUID(value)
    except (TypeError, ValueError) as exc:
        msg = f"transition_id must be a valid UUID string; got {value!r}."
        raise ValueError(msg) from exc


def _validate_non_empty_string(field_name: str, value: Any) -> None:
    if not isinstance(value, str) or not value.strip():
        msg = f"{field_name} must be a non-empty string."
        raise ValueError(msg)


def _normalise_timestamp(value: datetime | str | None) -> str:
    if value is None:
        timestamp = datetime.now(timezone.utc)
    elif isinstance(value, datetime):
        timestamp = value
    elif isinstance(value, str):
        parsed = _parse_timestamp(value)
        return parsed.isoformat().replace("+00:00", "Z")
    else:
        msg = "timestamp must be a datetime, ISO-8601 string, or None."
        raise ValueError(msg)
    if timestamp.tzinfo is None:
        timestamp = timestamp.replace(tzinfo=timezone.utc)
    else:
        timestamp = timestamp.astimezone(timezone.utc)
    return timestamp.isoformat().replace("+00:00", "Z")


def _parse_timestamp(raw_value: str) -> datetime:
    value = raw_value.strip()
    if value.endswith("Z"):
        value = f"{value[:-1]}+00:00"
    try:
        parsed = datetime.fromisoformat(value)
    except ValueError as exc:
        msg = f"timestamp must be a valid ISO-8601 UTC string; got {raw_value!r}."
        raise ValueError(msg) from exc
    if parsed.tzinfo is None or parsed.utcoffset() != timezone.utc.utcoffset(None):
        msg = f"timestamp must include a UTC offset; got {raw_value!r}."
        raise ValueError(msg)
    return parsed.astimezone(timezone.utc)


def _validate_sha256(field_name: str, value: Any) -> None:
    if not isinstance(value, str):
        msg = f"{field_name} must be a lowercase hexadecimal sha256 digest."
        raise ValueError(msg)
    if len(value) != 64 or any(character not in "0123456789abcdef" for character in value):
        msg = f"{field_name} must be a lowercase hexadecimal sha256 digest."
        raise ValueError(msg)
