#!/usr/bin/env python3
"""
Unified Event Envelope - Foundation for Large Event Model (LEM)

All Loom events (receipts, alerts, coordination, proofs) inherit from BaseEvent.
This provides:
- Consistent tri-temporal semantics
- Multi-tenant isolation
- Cryptographic provenance (DIP hash, signatures)
- DAG linkage (parent_ids)
- JSON serialization for event lake

Design:
- BaseEvent is the envelope; subclasses add domain payload
- All timestamps are UTC with microsecond precision
- event_id uses ULID (sortable, globally unique)
- Backwards compatible with existing receipt models
"""

import json
import hashlib
import time
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional, Sequence
from pathlib import Path

from alerts.types import AlertTier  # Import from types to avoid circular dependency

try:
    import blake3
    HAS_BLAKE3 = True
except ImportError:
    HAS_BLAKE3 = False


def generate_ulid() -> str:
    """
    Generate ULID (Universally Unique Lexicographically Sortable Identifier).

    Format: TTTTTTTTTTRRRRRRRRRRRRRRRR (26 chars, base32)
    - First 10 chars: timestamp (milliseconds since epoch)
    - Last 16 chars: randomness

    Properties:
    - Sortable by creation time
    - 128-bit uniqueness (like UUID)
    - Base32 encoded (Crockford alphabet)
    """
    import os
    import base64

    # Timestamp component (48 bits = 10 base32 chars)
    timestamp_ms = int(time.time() * 1000)

    # Randomness component (80 bits = 16 base32 chars)
    random_bytes = os.urandom(10)

    # Combine and encode
    ulid_bytes = timestamp_ms.to_bytes(6, 'big') + random_bytes

    # Base32 encode (Crockford alphabet)
    alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
    encoded = ""

    # Convert to base32
    num = int.from_bytes(ulid_bytes, 'big')
    while num > 0:
        encoded = alphabet[num % 32] + encoded
        num //= 32

    return encoded.zfill(26)


def compute_dip_hash(data: Dict[str, Any]) -> str:
    """
    Compute Deterministic Inference Pointer (DIP) hash.

    Uses Blake3 if available (faster), SHA-256 otherwise.
    Canonical JSON ensures determinism.
    """
    canonical = json.dumps(data, sort_keys=True, separators=(',', ':'))
    content = canonical.encode('utf-8')

    if HAS_BLAKE3:
        digest = blake3.blake3(content).hexdigest()
        return f"blake3:{digest}"
    else:
        digest = hashlib.sha256(content).hexdigest()
        return f"sha256:{digest}"


def _coerce_tier(value: Any, default: AlertTier = AlertTier.RECORDED) -> AlertTier:
    """
    Convert loose tier representations (str/int/AlertTier) into AlertTier.
    """
    if isinstance(value, AlertTier):
        return value
    if isinstance(value, str):
        candidate = value.strip().upper()
        try:
            return AlertTier[candidate]
        except KeyError:
            pass
    if isinstance(value, (int, float)):
        try:
            return AlertTier(int(value))
        except ValueError:
            pass
    return default


@dataclass
class BaseEvent:
    """
    Unified event envelope for all Loom events.

    All receipts, alerts, coordination acts, and proofs inherit from this.
    Provides tri-temporal semantics, multi-tenancy, and cryptographic provenance.

    Design Philosophy:
    - Event = immutable fact about what happened
    - Receipt = event with cryptographic proof (signature)
    - Alert = event with severity + routing
    - Everything flows through the same event lake

    Example:
        event = BaseEvent(
            event_type="loom.prophecy.clinical.v1",
            tenant_id="hospital_acme",
            namespaces=["icu", "sepsis"],
            data={"prediction": 0.85, "patient_id": "PT123"},
            parent_ids=["01JCAB123..."]  # Prior lab results
        )
    """

    # === Core Identity ===
    event_id: str = field(default_factory=generate_ulid)
    event_type: str = ""  # e.g., "loom.prophecy.clinical.v1"
    schema_version: str = "base_event.v1"

    # === Tri-Temporal Semantics ===
    # When the real-world event happened (e.g., patient vitals recorded)
    valid_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    # When we recorded this event in our system
    transaction_time: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    # When we learned/observed this event (for external data)
    observed_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    # === Multi-Tenancy & Routing ===
    tenant_id: str = "default"
    namespaces: List[str] = field(default_factory=list)  # ["mlb", "health", "icu"]

    # === Cryptographic Provenance ===
    dip_hash: str = ""  # Deterministic Inference Pointer
    gatebook_version: Optional[str] = None  # Policy version hash
    signature: Optional[str] = None  # Ed25519 signature (format: "ed25519:base64")

    # === DAG Linkage ===
    parent_ids: List[str] = field(default_factory=list)  # Upstream events

    # === Domain Payload ===
    data: Dict[str, Any] = field(default_factory=dict)

    # === Metadata ===
    source: Optional[str] = None  # Producing service/agent
    privacy_tags: List[str] = field(default_factory=list)  # ["PHI", "Financial", "Public"]
    traceparent: Optional[str] = None
    tracestate: Optional[str] = None

    # === Tiering + Quality Signals ===
    tier: AlertTier = AlertTier.RECORDED
    chi_score: float = 0.0
    dignity_score: Optional[float] = None

    def __post_init__(self):
        """Auto-compute DIP hash if not provided."""
        if not self.dip_hash:
            self.dip_hash = compute_dip_hash(self.data)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to JSON-serializable dict."""
        result = asdict(self)

        # Convert datetimes to ISO strings
        for field_name in ['valid_time', 'transaction_time', 'observed_at']:
            if field_name in result and isinstance(result[field_name], datetime):
                result[field_name] = result[field_name].isoformat()

        tier_value = result.get('tier')
        if isinstance(tier_value, AlertTier):
            result['tier'] = tier_value.name

        return result

    def to_json(self) -> str:
        """Canonical JSON representation."""
        return json.dumps(self.to_dict(), sort_keys=True, separators=(',', ':'))

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'BaseEvent':
        """Load from dict (inverse of to_dict)."""
        # Parse datetime strings
        for field_name in ['valid_time', 'transaction_time', 'observed_at']:
            if field_name in data and isinstance(data[field_name], str):
                data[field_name] = datetime.fromisoformat(data[field_name])

        tier_value = data.get('tier')
        if tier_value is not None:
            data['tier'] = _coerce_tier(tier_value)

        if 'chi_score' in data and data['chi_score'] is not None:
            data['chi_score'] = float(data['chi_score'])

        if 'dignity_score' in data and data['dignity_score'] is not None:
            data['dignity_score'] = float(data['dignity_score'])

        return cls(**data)

    def to_parquet_row(self) -> Dict[str, Any]:
        """
        Flatten for Parquet storage.

        Parquet partitioning:
        - /events/year=2025/month=11/day=07/tenant=acme/type=prophecy/
        """
        row = self.to_dict()

        # Add partition keys
        row['year'] = self.valid_time.year
        row['month'] = self.valid_time.month
        row['day'] = self.valid_time.day

        # Flatten nested data as JSON string (or use struct type in Parquet)
        if 'data' in row and isinstance(row['data'], dict):
            row['data_json'] = json.dumps(row['data'])
            del row['data']

        row['tier'] = self.tier.name
        row['chi_score'] = float(self.chi_score)
        row['dignity_score'] = float(self.dignity_score) if self.dignity_score is not None else None
        row['traceparent'] = self.traceparent
        row['tracestate'] = self.tracestate

        return row

    def up_tier(self, min_tier: AlertTier) -> None:
        """
        Upgrade this event to at least the provided tier (monotonic).
        """
        if min_tier.value > self.tier.value:
            self.tier = min_tier

    def inherit_from_parents(self, parents: Sequence['BaseEvent']) -> None:
        """
        Inherit strongest tier from parent events (non-reversible).
        """
        if not parents:
            return
        strongest = self.tier
        for parent in parents:
            if parent.tier.value > strongest.value:
                strongest = parent.tier
        self.up_tier(strongest)


@dataclass
class ReceiptEvent(BaseEvent):
    """
    Receipt = Event + cryptographic proof.

    Extends BaseEvent with receipt-specific fields:
    - verdict: pass/fail/error/refuse
    - cost: resource consumption
    - latency_ms: execution time
    """

    verdict: str = "pass"  # pass|fail|error|refuse
    cost: Dict[str, Any] = field(default_factory=dict)
    latency_ms: float = 0.0
    refusal_reason: Optional[str] = None


@dataclass
class AlertEvent(BaseEvent):
    """
    Alert = Event + severity + routing.

    Alerts are first-class events that flow through the same pipeline.
    """

    severity: str = "info"  # info|warning|critical
    alert_type: str = ""
    conditions: Dict[str, Any] = field(default_factory=dict)
    recommended_actions: List[str] = field(default_factory=list)
    inhibited_by: List[str] = field(default_factory=list)  # Other alert IDs

    # Routing
    notify: List[str] = field(default_factory=list)  # ["slack", "pagerduty", "email"]
    runbook_url: Optional[str] = None


# === Convenience Functions ===

def create_receipt_from_legacy(receipt_dict: Dict[str, Any]) -> ReceiptEvent:
    """
    Migrate legacy receipt → BaseEvent.

    Maps existing receipt fields to unified envelope.
    """
    default_tier = AlertTier.SIGNED if receipt_dict.get("signature") else AlertTier.RECORDED
    tier = _coerce_tier(receipt_dict.get("tier"), default=default_tier)

    chi_value = receipt_dict.get("chi_score")
    if chi_value is None:
        chi_value = receipt_dict.get("chi")
    chi_score = float(chi_value) if chi_value is not None else 0.0

    dignity_value = receipt_dict.get("dignity_score")
    if dignity_value is None:
        dignity_value = receipt_dict.get("dignity")
    dignity_score = float(dignity_value) if dignity_value is not None else None

    # Robust timestamp handling: accept `timestamp`, `observed_at`, `transaction_time`,
    # `valid_time` (string or numeric epoch), or fall back to now
    ts_candidate = (
        receipt_dict.get("timestamp")
        or receipt_dict.get("observed_at")
        or receipt_dict.get("transaction_time")
        or receipt_dict.get("valid_time")
    )

    def _parse_ts(val: Any) -> datetime:
        if val is None:
            return datetime.now(timezone.utc)
        # If numeric, assume seconds since epoch
        if isinstance(val, (int, float)):
            return datetime.fromtimestamp(float(val), tz=timezone.utc)
        # If already a datetime
        if isinstance(val, datetime):
            return val
        # Otherwise try ISO parsing, fallback to now
        try:
            return datetime.fromisoformat(str(val))
        except Exception:
            return datetime.now(timezone.utc)

    parsed_ts = _parse_ts(ts_candidate)

    return ReceiptEvent(
        event_id=receipt_dict.get("receipt_id", generate_ulid()),
        event_type=receipt_dict.get("schema", "loom.receipt.legacy.v1"),
        valid_time=parsed_ts,
        transaction_time=parsed_ts,
        observed_at=parsed_ts,
        tenant_id=receipt_dict.get("tenant_id", "default"),
        namespaces=receipt_dict.get("namespaces", []),
        dip_hash=receipt_dict.get("dip_hash", ""),
        signature=receipt_dict.get("signature"),
        tier=tier,
        chi_score=chi_score,
        dignity_score=dignity_score,
        traceparent=receipt_dict.get("traceparent"),
        tracestate=receipt_dict.get("tracestate"),
        parent_ids=receipt_dict.get("parent_ids", []),
        data=receipt_dict.get("data", receipt_dict),
        verdict=receipt_dict.get("verdict", "pass"),
        cost=receipt_dict.get("cost", {}),
        latency_ms=receipt_dict.get("latency_ms", 0.0),
        refusal_reason=receipt_dict.get("refusal_reason"),
    )


# === Example Usage ===

if __name__ == "__main__":
    # Example 1: Clinical prophecy event
    prophecy = BaseEvent(
        event_type="loom.prophecy.clinical.sepsis.v1",
        tenant_id="hospital_stanford",
        namespaces=["icu", "sepsis"],
        data={
            "patient_id": "PT12345",
            "prediction": 0.85,
            "model": "sepsis_ewm_v3",
            "features": ["hr", "temp", "wbc", "lactate"],
        },
        parent_ids=["01JCAB123PREVIOUS_LAB_RESULT"],
    )

    print("Prophecy Event:")
    print(json.dumps(prophecy.to_dict(), indent=2))
    print(f"\nDIP Hash: {prophecy.dip_hash}")
    print(f"Event ID (ULID): {prophecy.event_id}")

    # Example 2: Alert event
    alert = AlertEvent(
        event_type="loom.alert.cohort_drift.v1",
        tenant_id="hospital_stanford",
        namespaces=["icu"],
        severity="warning",
        alert_type="calibration_drift",
        conditions={
            "slice": "age>65",
            "ece_before": 0.05,
            "ece_after": 0.12,
            "p_value": 0.001,
        },
        recommended_actions=[
            "Review recent feature distributions",
            "Check for data pipeline changes",
            "Consider retraining with recent cohort",
        ],
        data={
            "drift_magnitude": 0.07,
            "affected_predictions": 1247,
        }
    )

    print("\n\nAlert Event:")
    print(json.dumps(alert.to_dict(), indent=2))

    # Example 3: Parquet row
    print("\n\nParquet Row (flattened):")
    print(json.dumps(prophecy.to_parquet_row(), indent=2))
