"""
Tri-temporal CloudEvents-style receipt envelope with canonicalization helpers.

This module provides a minimal, interoperable receipt envelope that carries:
- CloudEvents routing fields (specversion/id/source/type/subject/datacontenttype)
- Tri-temporal timestamps (valid_time, transaction_time, observed_at)
- Temporal semantics discriminator for ordering validation
- Optional proof and lineage metadata

Receipts are meant to be canonicalized with RFC 8785 (JCS) prior to hashing
and signing. The helper `compute_receipt_hash` produces a deterministic hash
over the receipt payload, excluding proof/lineage by default (detached
signature pattern).
"""

from __future__ import annotations

import os
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, Optional, Tuple

from receipts.compat.pyd import BaseModel, ConfigDictLike, Field, field_validator, model_validator
from receipts.canonicalize import compute_payload_hash, to_jcs_bytes
from receipts.base import _normalise_utc_millis
from receipts.merkle import compute_merkle_root


class TemporalSemantics(str, Enum):
    """
    Temporal semantics discriminator for receipt ordering validation.

    This field indicates the intended verification model for ordering of time
    fields, enabling different validation rules based on receipt type.

    - OBSERVED_EVENT: For events detected by a module or sensor.
      Ordering: valid_time.start <= observed_at <= recorded_at

    - PLANNED_EVENT: For actions planned to take effect in the future.
      Ordering: observed_at <= valid_time.start (plan comes before execution)

    - ASSERTED_CLAIM: For statements asserted about a past or present state.
      Ordering: recorded_at >= observed_at (can assert about past observations)
    """
    OBSERVED_EVENT = "OBSERVED_EVENT"
    PLANNED_EVENT = "PLANNED_EVENT"
    ASSERTED_CLAIM = "ASSERTED_CLAIM"

# Optional dependencies for signing/timestamping
try:  # pragma: no cover - exercised in environments with signing enabled
    from nacl.signing import SigningKey
    from nacl.exceptions import BadSignatureError
except Exception:  # pragma: no cover - signing not available in slim envs
    SigningKey = None  # type: ignore
    BadSignatureError = Exception  # type: ignore

try:  # pragma: no cover - TSA client may be unavailable in some envs
    from tools.crypto.rfc3161_client import (
        get_timestamp,
        get_timestamp_stub,
        RFC3161Error,
    )
except Exception:  # pragma: no cover
    get_timestamp = None  # type: ignore
    get_timestamp_stub = None  # type: ignore
    RFC3161Error = Exception  # type: ignore


class ValidTime(BaseModel):
    """Interval describing when a fact was true in the world."""

    model_config = ConfigDictLike(extra="forbid")

    start: datetime = Field(description="When the fact became valid in the world.")
    end: Optional[datetime] = Field(
        default=None,
        description="Optional end of validity. Null/open-ended if the fact is still valid.",
    )

    @model_validator(mode="after")
    def _normalize_and_validate(self) -> "ValidTime":
        start = _normalise_utc_millis(self.start)
        end = _normalise_utc_millis(self.end) if self.end else None

        if end and end < start:
            raise ValueError(
                "valid_time.end must be greater than or equal to valid_time.start"
            )

        object.__setattr__(self, "start", start)
        object.__setattr__(self, "end", end)
        return self


class TransactionTime(BaseModel):
    """Ledger time describing when the system recorded the receipt."""

    model_config = ConfigDictLike(extra="forbid")

    recorded_at: datetime = Field(description="When the system committed the receipt.")

    @model_validator(mode="after")
    def _normalize(self) -> "TransactionTime":
        object.__setattr__(self, "recorded_at", _normalise_utc_millis(self.recorded_at))
        return self


class Proof(BaseModel):
    """Cryptographic proof bundle (hash + signature + TSA)."""

    model_config = ConfigDictLike(extra="allow", frozen=True)

    hash_alg: str = Field(default="SHA-256", description="Hash algorithm used.")
    hash: str = Field(description="Payload hash (prefixed, e.g., sha256:<hex>).")
    signature_alg: Optional[str] = Field(default=None, description="Signature algorithm.")
    producer_signature: Optional[str] = Field(default=None, description="Detached signature over the payload hash.")
    tsa: Optional[str] = Field(default=None, description="RFC 3161 TSA endpoint.")
    tst_der_base64: Optional[str] = Field(default=None, description="Base64-encoded Time-Stamp Token (DER).")
    rekor_log_index: Optional[int] = Field(default=None, description="Optional Rekor log index for transparency anchoring.")
    rekor_uuid: Optional[str] = Field(default=None, description="Optional Rekor entry UUID.")


class Lineage(BaseModel):
    """Optional lineage information for Merkle/DAG linkage."""

    model_config = ConfigDictLike(extra="forbid", frozen=True)

    parent_hash: Optional[str] = Field(default=None, description="Hash of the immediate parent receipt.")
    merkle_root: Optional[str] = Field(default=None, description="Merkle root anchoring this receipt in a chain.")


TRI_TEMPORAL_SCHEMA_ID = "https://loom.example/schema/receipt.tri_temporal.v1.json"


class TriTemporalReceipt(BaseModel):
    """
    CloudEvents-compatible receipt envelope with tri-temporal timestamps.

    Fields map closely to CloudEvents, with additional temporal and proof metadata.
    The temporal_semantics field enables different validation rules based on
    receipt type (OBSERVED_EVENT, PLANNED_EVENT, ASSERTED_CLAIM).
    """

    model_config = ConfigDictLike(extra="forbid")

    specversion: str = Field(default="1.0", description="CloudEvents spec version.")
    id: str = Field(description="Unique event/receipt identifier.")
    source: str = Field(description="URI/URN identifying the event source.")
    type: str = Field(description="Event type, e.g., loom.receipt.v1.")
    subject: str = Field(description="Domain-specific subject, e.g., AgentActionReceipt.")
    dataschema: Optional[str] = Field(
        default=TRI_TEMPORAL_SCHEMA_ID,
        description="Schema URI for this receipt envelope.",
    )
    datacontenttype: str = Field(default="application/json", description="Content type for the data payload.")
    time: Optional[datetime] = Field(default=None, description="Optional CloudEvents time field.")

    # Temporal semantics discriminator - determines validation rules
    temporal_semantics: TemporalSemantics = Field(
        default=TemporalSemantics.OBSERVED_EVENT,
        description="Temporal semantics identifier for ordering validation.",
    )

    valid_time: ValidTime
    transaction_time: TransactionTime
    observed_at: datetime = Field(description="When a witness/sensor observed the event.")

    data: Dict[str, Any] = Field(default_factory=dict, description="Domain-specific payload.")
    proof: Optional[Proof] = Field(default=None, description="Optional cryptographic proof bundle.")
    lineage: Optional[Lineage] = Field(default=None, description="Optional lineage/Merkle metadata.")

    @field_validator("observed_at")
    @classmethod
    def _normalize_observed(cls, value: datetime) -> datetime:
        return _normalise_utc_millis(value)

    @field_validator("time")
    @classmethod
    def _normalize_time(cls, value: Optional[datetime]) -> Optional[datetime]:
        return _normalise_utc_millis(value) if value else None

    @model_validator(mode="after")
    def _enforce_temporal_order(self) -> "TriTemporalReceipt":
        """
        Enforce temporal ordering based on temporal_semantics discriminator.

        OBSERVED_EVENT: valid_time.start <= observed_at <= recorded_at
        PLANNED_EVENT:  observed_at <= valid_time.start (plan before execution)
        ASSERTED_CLAIM: recorded_at >= observed_at (can assert about past)
        """
        observed_at = self.observed_at
        start = self.valid_time.start
        end = self.valid_time.end
        recorded_at = self.transaction_time.recorded_at

        # Common validation: end must be >= start if present
        if end and end < start:
            raise ValueError("valid_time.end must be greater than or equal to valid_time.start")

        # Branch on temporal_semantics
        if self.temporal_semantics == TemporalSemantics.OBSERVED_EVENT:
            # Standard observed event: valid_time.start <= observed_at <= recorded_at
            if observed_at < start:
                raise ValueError(
                    f"OBSERVED_EVENT: observed_at ({observed_at}) must be >= valid_time.start ({start})"
                )
            if recorded_at < observed_at:
                raise ValueError(
                    f"OBSERVED_EVENT: recorded_at ({recorded_at}) must be >= observed_at ({observed_at})"
                )
            if end and observed_at > end:
                raise ValueError(
                    f"OBSERVED_EVENT: observed_at ({observed_at}) must be <= valid_time.end ({end})"
                )

        elif self.temporal_semantics == TemporalSemantics.PLANNED_EVENT:
            # Planned event: observed_at <= valid_time.start (plan comes before execution)
            if observed_at > start:
                raise ValueError(
                    f"PLANNED_EVENT: observed_at ({observed_at}) must be <= valid_time.start ({start})"
                )
            if recorded_at < observed_at:
                raise ValueError(
                    f"PLANNED_EVENT: recorded_at ({recorded_at}) must be >= observed_at ({observed_at})"
                )

        elif self.temporal_semantics == TemporalSemantics.ASSERTED_CLAIM:
            # Asserted claim: recorded_at >= observed_at (can assert about past observations)
            if recorded_at < observed_at:
                raise ValueError(
                    f"ASSERTED_CLAIM: recorded_at ({recorded_at}) must be >= observed_at ({observed_at})"
                )
            # Note: For claims, valid_time can be in the past relative to observed_at
            # (e.g., "I observed yesterday that X was true last week")

        return self

    def canonical_bytes(self, include_proof: bool = True, include_lineage: bool = True) -> bytes:
        """
        Return RFC 8785 canonical bytes for this receipt.

        Args:
            include_proof: Whether to include the proof object (default True).
            include_lineage: Whether to include lineage (default True).
        """
        payload = self.model_dump(mode="json", exclude_none=True)
        if not include_proof:
            payload.pop("proof", None)
        if not include_lineage:
            payload.pop("lineage", None)
        return to_jcs_bytes(payload)


def compute_receipt_hash(
    receipt: TriTemporalReceipt,
    algorithm: str = "sha256",
    include_proof: bool = False,
    include_lineage: bool = False,
) -> str:
    """
    Compute deterministic payload hash for a tri-temporal receipt.

    By default, proof/lineage are excluded to follow a detached signature pattern.
    """
    payload = receipt.model_dump(mode="json", exclude_none=True)
    if not include_proof:
        payload.pop("proof", None)
    if not include_lineage:
        payload.pop("lineage", None)
    return compute_payload_hash(payload, algorithm=algorithm)


def sign_and_timestamp_receipt(
    receipt: TriTemporalReceipt,
    *,
    signing_key: Optional["SigningKey"],
    tsa_server: Optional[str] = None,
    use_stub_tsa: bool = True,
) -> TriTemporalReceipt:
    """
    Produce proof for a tri-temporal receipt: hash → sign → optional RFC 3161 TST.

    This does not mutate the input; it returns a copy with proof populated.
    """
    if signing_key is None:
        raise ValueError("signing_key is required for signing tri-temporal receipts")
    if SigningKey is None:
        raise RuntimeError("PyNaCl SigningKey not available in this environment")

    canonical_bytes = receipt.canonical_bytes(include_proof=False, include_lineage=False)

    import hashlib
    import base64

    digest = hashlib.sha256(canonical_bytes).hexdigest()
    prefixed_hash = f"sha256:{digest}"

    signature = signing_key.sign(canonical_bytes).signature
    signature_b64 = base64.b64encode(signature).decode("ascii")

    tsa_fields: Dict[str, Any] = {}
    # Resolve TSA server from env if not provided
    resolved_tsa = tsa_server or os.getenv("LOOM_TSA_SERVER")
    if get_timestamp or get_timestamp_stub:
        try:
            if use_stub_tsa and get_timestamp_stub and not resolved_tsa:
                # Stub path for tests/dev
                tst_data = get_timestamp_stub(
                    bytes.fromhex(digest),
                    hash_algorithm="sha256",
                )
            elif get_timestamp:
                tst_data = get_timestamp(
                    bytes.fromhex(digest),
                    hash_algorithm="sha256",
                    tsa_server=resolved_tsa,
                    timeout=10,
                    retry_count=2,
                )
            else:
                tst_data = None
            if tst_data:
                tsa_fields = {
                    "tsa": tst_data.get("tsa_server", resolved_tsa),
                    "tst_der_base64": tst_data.get("tst_der_b64"),
                }
        except RFC3161Error:
            # Non-fatal: leave TSA fields empty
            pass

    proof = Proof(
        hash_alg="SHA-256",
        hash=prefixed_hash,
        signature_alg="ed25519",
        producer_signature=signature_b64,
        **tsa_fields,
    )

    return receipt.model_copy(update={"proof": proof})


def verify_receipt_signature(
    receipt: TriTemporalReceipt,
    verify_key_b64: str,
) -> Tuple[bool, Optional[str]]:
    """
    Verify Ed25519 signature on a tri-temporal receipt.

    Returns:
        (is_valid, error_message)
    """
    if SigningKey is None:
        return False, "PyNaCl not available"

    try:
        from nacl.signing import VerifyKey
        import base64
        import hashlib

        proof = receipt.proof
        if not proof or not proof.producer_signature:
            return False, "missing proof or producer_signature"

        verify_key = VerifyKey(base64.b64decode(verify_key_b64))
        canonical_bytes = receipt.canonical_bytes(include_proof=False, include_lineage=False)

        digest = hashlib.sha256(canonical_bytes).hexdigest()
        expected = f"sha256:{digest}"
        if proof.hash != expected:
            return False, "hash mismatch"

        verify_key.verify(canonical_bytes, base64.b64decode(proof.producer_signature))
        return True, None
    except BadSignatureError as exc:
        return False, f"bad signature: {exc}"
    except Exception as exc:  # pragma: no cover - unexpected runtime issues
        return False, str(exc)


def _strip_hash_prefix(hash_value: str) -> str:
    """Strip algorithm prefix (e.g., sha256:xxxx) to raw hex."""
    if ":" in hash_value:
        return hash_value.split(":", 1)[1]
    return hash_value


def attach_lineage(
    receipt: TriTemporalReceipt,
    *,
    parent_hash: Optional[str] = None,
    parent_merkle_root: Optional[str] = None,
    parent_transaction_time: Optional[datetime] = None,
) -> TriTemporalReceipt:
    """
    Attach lineage (parent hash + Merkle root) enforcing temporal monotonicity.

    Temporal rule: parent_transaction_time must be <= receipt.observed_at.

    Merkle rule:
        - If parent_hash is provided, Merkle root is computed over
          [parent_hash, child_hash] (stripping algorithm prefix).
        - If parent_merkle_root is provided without parent_hash, the new root
          is hash(parent_merkle_root || child_hash) for a simple roll-forward.
        - If neither parent is provided, root is just the child's hash.
    """
    if parent_transaction_time and parent_transaction_time > receipt.observed_at:
        raise ValueError("parent transaction_time must be <= child observed_at for lineage")

    child_hash_prefixed = compute_receipt_hash(receipt, include_proof=False, include_lineage=False)
    child_leaf = _strip_hash_prefix(child_hash_prefixed)

    if parent_hash:
        parent_leaf = _strip_hash_prefix(parent_hash)
        merkle_root = compute_merkle_root([parent_leaf, child_leaf])
        lineage = Lineage(parent_hash=parent_hash, merkle_root=merkle_root)
    elif parent_merkle_root:
        import hashlib

        # Roll-forward root = H(parent_root || child_leaf_bytes)
        combined = bytes.fromhex(parent_merkle_root) + bytes.fromhex(child_leaf)
        merkle_root = hashlib.sha256(combined).hexdigest()
        lineage = Lineage(parent_hash=None, merkle_root=merkle_root)
    else:
        lineage = Lineage(parent_hash=None, merkle_root=child_leaf)

    return receipt.model_copy(update={"lineage": lineage})


__all__ = [
    "TemporalSemantics",
    "ValidTime",
    "TransactionTime",
    "Proof",
    "Lineage",
    "TriTemporalReceipt",
    "compute_receipt_hash",
    "sign_and_timestamp_receipt",
    "verify_receipt_signature",
    "attach_lineage",
    "TRI_TEMPORAL_SCHEMA_ID",
]
