# #region TranslationEventLog [C:5] [TYPE Module] [SEMANTICS sqlalchemy, translate, event, log, audit]
# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [TranslationEvent]
# @RELATION DEPENDS_ON -> [MetricSnapshot]
# @PRE  Database session is open and valid.
# @POST  Events are persisted immutably; terminal events enforce exactly-one invariant per run.
# @SIDE_EFFECT  Writes TranslationEvent rows; prunes expired events with MetricSnapshot before deletion.
# @DATA_CONTRACT  Input[run_id:Optional[str], job_id:str, event_type:str, payload:dict] -> Output[TranslationEvent]
# @INVARIANT  Exactly one run_started + exactly one terminal event per non-null run_id.
# @RATIONALE  Immutable event log with nullable run_id allows job-level events (no run context) and run-level events.
# @REJECTED  stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.
# @REJECTED  Separate event table per entity — single TranslationEvent table with nullable run_id is simpler for audit.
from datetime import UTC, datetime, timedelta
from typing import Any
import uuid

from sqlalchemy.orm import Session

from ...core.logger import belief_scope, logger
from ...models.translate import MetricSnapshot, TranslationEvent

# Terminal events per run — exactly one of these must exist for a completed run.
TERMINAL_EVENT_TYPES = {"RUN_COMPLETED", "RUN_FAILED", "RUN_CANCELLED"}
# All valid event types for validation.
VALID_EVENT_TYPES = TERMINAL_EVENT_TYPES | {
    "JOB_CREATED", "JOB_UPDATED", "JOB_DELETED",
    "RUN_STARTED", "RUN_RETRYING", "RUN_RETRY_INSERT",
    "BATCH_STARTED", "BATCH_COMPLETED", "BATCH_FAILED", "BATCH_RETRYING",
    "TRANSLATION_PHASE_STARTED", "TRANSLATION_PHASE_COMPLETED",
    "INSERT_PHASE_STARTED", "INSERT_PHASE_COMPLETED",
    "PREVIEW_CREATED", "PREVIEW_ACCEPTED", "PREVIEW_DISCARDED",
    "SCHEDULE_CREATED", "SCHEDULE_UPDATED", "SCHEDULE_DELETED",
    "METRICS_SNAPSHOT_CREATED",
}

DEFAULT_RETENTION_DAYS = 90


# #region TranslationEventLog [C:5] [TYPE Class]
# @BRIEF Structured event logging for translation operations with terminal event invariant enforcement.
# @PRE  Database session is available.
# @POST  Events are written immutably; terminal events enforced per run.
# @SIDE_EFFECT  Writes TranslationEvent rows; prunes expired events.
# @INVARIANT  Exactly one run_started + exactly one terminal event per non-null run_id.
class TranslationEventLog:

    def __init__(self, db: Session):
        self.db = db

    # region log_event [TYPE Function]
    # @PURPOSE: Write an immutable event. Enforces terminal event invariant for non-null run_id.
    # @PRE  event_type must be a known type. If run_id is not None, enforce terminal invariant.
    # @POST  TranslationEvent row is created.
    # @SIDE_EFFECT  DB write.
    def log_event(
        self,
        job_id: str,
        event_type: str,
        payload: dict[str, Any] | None = None,
        run_id: str | None = None,
        created_by: str | None = None,
    ) -> TranslationEvent:
        with belief_scope("TranslationEventLog.log_event"):
            if event_type not in VALID_EVENT_TYPES:
                raise ValueError(
                    f"Invalid event_type '{event_type}'. "
                    f"Must be one of: {', '.join(sorted(VALID_EVENT_TYPES))}"
                )

            # Enforce terminal event invariant for non-null run_id
            if run_id is not None and event_type in TERMINAL_EVENT_TYPES:
                existing_terminal = (
                    self.db.query(TranslationEvent.id)
                    .filter(
                        TranslationEvent.run_id == run_id,
                        TranslationEvent.event_type.in_(TERMINAL_EVENT_TYPES),
                    )
                    .first()
                )
                if existing_terminal:
                    raise ValueError(
                        f"Run '{run_id}' already has a terminal event "
                        f"({existing_terminal[0]}). Cannot add another terminal event."
                    )

            # Enforce run_started invariant — must exist before other run events
            if run_id is not None and event_type not in {"RUN_STARTED"} | TERMINAL_EVENT_TYPES:
                has_started = (
                    self.db.query(TranslationEvent.id)
                    .filter(
                        TranslationEvent.run_id == run_id,
                        TranslationEvent.event_type == "RUN_STARTED",
                    )
                    .first()
                )
                if not has_started:
                    raise ValueError(
                        f"Cannot log '{event_type}' for run '{run_id}': "
                        f"RUN_STARTED event must precede other run events."
                    )

            event = TranslationEvent(
                id=str(uuid.uuid4()),
                job_id=job_id,
                run_id=run_id,
                event_type=event_type,
                event_data=payload or {},
                created_by=created_by,
                created_at=datetime.now(UTC),
            )
            self.db.add(event)
            self.db.flush()

            logger.reason(f"Event logged: {event_type}", {
                "event_id": event.id,
                "job_id": job_id,
                "run_id": run_id,
                "event_type": event_type,
            })
            return event
    # endregion log_event

    # region query_events [TYPE Function]
    # @PURPOSE: Query events with optional filters.
    # @PRE  None.
    # @POST  Returns list of TranslationEvent dicts matching filters.
    def query_events(
        self,
        job_id: str | None = None,
        run_id: str | None = None,
        event_type: str | None = None,
        limit: int = 100,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        with belief_scope("TranslationEventLog.query_events"):
            query = self.db.query(TranslationEvent)

            if job_id:
                query = query.filter(TranslationEvent.job_id == job_id)
            if run_id:
                query = query.filter(TranslationEvent.run_id == run_id)
            if event_type:
                query = query.filter(TranslationEvent.event_type == event_type)

            events = (
                query.order_by(TranslationEvent.created_at.desc())
                .offset(offset)
                .limit(limit)
                .all()
            )

            return [
                {
                    "id": e.id,
                    "job_id": e.job_id,
                    "run_id": e.run_id,
                    "event_type": e.event_type,
                    "event_data": e.event_data or {},
                    "created_by": e.created_by,
                    "created_at": e.created_at.isoformat() if e.created_at else None,
                }
                for e in events
            ]
    # endregion query_events

    # region prune_expired [TYPE Function]
    # @PURPOSE: Delete events older than retention_days. Persists MetricSnapshot before pruning.
    # @PRE  None.
    # @POST  Expired events are deleted; MetricSnapshot is created before deletion.
    # @SIDE_EFFECT  Creates MetricSnapshot row; deletes TranslationEvent rows.
    def prune_expired(
        self,
        retention_days: int = DEFAULT_RETENTION_DAYS,
        batch_size: int = 1000,
    ) -> dict[str, Any]:
        with belief_scope("TranslationEventLog.prune_expired"):
            cutoff = datetime.now(UTC) - timedelta(days=retention_days)
            logger.reason("Pruning expired events", {
                "cutoff": cutoff.isoformat(),
                "retention_days": retention_days,
            })

            # Find events to prune
            expired_query = self.db.query(TranslationEvent).filter(
                TranslationEvent.created_at < cutoff
            )
            total_expired = expired_query.count()

            if total_expired == 0:
                logger.reflect("No expired events to prune", {})
                return {"pruned": 0, "snapshot_id": None}

            # Compute per-language metrics from expired events before pruning
            per_language: dict[str, dict[str, int | float]] = {}
            expired_events = expired_query.all()
            for evt in expired_events:
                event_data = evt.event_data or {}
                lang = event_data.get("language_code") or event_data.get("target_language") or "_unknown_"
                if lang not in per_language:
                    per_language[lang] = {"cumulative_tokens": 0, "cumulative_cost": 0.0, "runs": 0}
                per_language[lang]["cumulative_tokens"] += event_data.get("token_count", 0) or 0
                per_language[lang]["cumulative_cost"] += event_data.get("cost", 0.0) or 0.0
                per_language[lang]["runs"] += 1 if event_data.get("run_id") else 0

            # Create MetricSnapshot before pruning
            # NOTE: job_id=None because this is an aggregate across ALL expired events,
            # not scoped to a single job. The FK is SET NULL (see MetricSnapshot model).
            snapshot = MetricSnapshot(
                id=str(uuid.uuid4()),
                job_id=None,
                key_hash=f"prune_{cutoff.timestamp():.0f}",
                covers_events_before=cutoff,
                total_records=total_expired,
                per_language_metrics=per_language or None,
                snapshot_date=datetime.now(UTC),
            )
            self.db.add(snapshot)
            self.db.flush()
            snapshot_id = snapshot.id

            # Delete in batches
            pruned = 0
            while True:
                batch_ids = (
                    self.db.query(TranslationEvent.id)
                    .filter(TranslationEvent.created_at < cutoff)
                    .limit(batch_size)
                    .all()
                )
                if not batch_ids:
                    break
                ids = [row[0] for row in batch_ids]
                self.db.query(TranslationEvent).filter(
                    TranslationEvent.id.in_(ids)
                ).delete(synchronize_session=False)
                self.db.flush()
                pruned += len(ids)
                logger.reason("Pruned batch", {"batch_size": len(ids), "total_pruned": pruned})

            self.db.commit()

            logger.reflect("Pruning complete", {
                "pruned": pruned,
                "snapshot_id": snapshot_id,
            })
            return {"pruned": pruned, "snapshot_id": snapshot_id}
    # endregion prune_expired

    # region get_run_event_summary [TYPE Function]
    # @PURPOSE: Get a summary of events for a run, including invariant check.
    # @PRE  run_id is not None.
    # @POST  Returns dict with event list and invariant validity.
    def get_run_event_summary(self, run_id: str) -> dict[str, Any]:
        with belief_scope("TranslationEventLog.get_run_event_summary"):
            events = self.query_events(run_id=run_id)

            has_started = any(e["event_type"] == "RUN_STARTED" for e in events)
            terminal_events = [e for e in events if e["event_type"] in TERMINAL_EVENT_TYPES]

            return {
                "run_id": run_id,
                "event_count": len(events),
                "has_run_started": has_started,
                "terminal_event_count": len(terminal_events),
                "terminal_events": terminal_events,
                "invariant_valid": has_started and len(terminal_events) <= 1,
                "events": events,
            }
    # endregion get_run_event_summary


# #endregion TranslationEventLog
# #endregion TranslationEventLog
