"""Domain events — immutable records of what happened.

Every state change in the system emits an event.  Events are persisted in
the EventStore for audit trail, replay, and debugging.

Usage:
    store = EventStore()
    store.append(ProjectCreated(aggregate_id="proj-123", name="my-app", source_type="local"))
    events = store.events_for("proj-123")
"""

from __future__ import annotations

import json
import time
import uuid
from dataclasses import dataclass, field, asdict
from enum import Enum
from pathlib import Path
from typing import Any, Callable

import structlog

logger = structlog.get_logger()


# ---------------------------------------------------------------------------
# Base event
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class DomainEvent:
    """Base class for all domain events.

    Attributes:
        event_id: Unique event identifier.
        aggregate_id: ID of the aggregate (project, ticket, etc.) this event belongs to.
        event_type: Event type name (auto-derived from class name).
        timestamp: Unix timestamp when the event occurred.
        data: Arbitrary event payload.
        actor: Who/what caused this event (user_id, "cli", "api", "mcp", "system").
    """

    event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    aggregate_id: str = ""
    event_type: str = ""
    timestamp: float = field(default_factory=time.time)
    data: dict[str, Any] = field(default_factory=dict)
    actor: str = "system"

    def __post_init__(self) -> None:
        if not self.event_type:
            # Use class name as event_type (frozen=True, so use object.__setattr__)
            object.__setattr__(self, "event_type", type(self).__name__)

    def to_dict(self) -> dict[str, Any]:
        """Serialize event to dict."""
        return asdict(self)

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> DomainEvent:
        """Deserialize event from dict."""
        return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})


# ---------------------------------------------------------------------------
# Project events
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class ProjectCreated(DomainEvent):
    """Emitted when a new project is added."""
    pass


@dataclass(frozen=True)
class ProjectUpdated(DomainEvent):
    """Emitted when a project is modified."""
    pass


@dataclass(frozen=True)
class ProjectRemoved(DomainEvent):
    """Emitted when a project is deleted."""
    pass


@dataclass(frozen=True)
class ProjectScanned(DomainEvent):
    """Emitted when a directory is scanned for projects."""
    pass


# ---------------------------------------------------------------------------
# Setup checklist events
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class SetupStepToggled(DomainEvent):
    """Emitted when a setup checklist step changes state."""
    pass


@dataclass(frozen=True)
class SetupChecklistReset(DomainEvent):
    """Emitted when the setup checklist is cleared."""
    pass


# ---------------------------------------------------------------------------
# User events
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class UserCreated(DomainEvent):
    """Emitted when a new user is added (manually or from git)."""
    pass


@dataclass(frozen=True)
class UserUpdated(DomainEvent):
    """Emitted when a user is modified."""
    pass


@dataclass(frozen=True)
class UserDeleted(DomainEvent):
    """Emitted when a user is removed."""
    pass


@dataclass(frozen=True)
class UsersSyncedFromGit(DomainEvent):
    """Emitted after a git-sync discovers new users."""
    pass


# ---------------------------------------------------------------------------
# Task/Ticket events
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class TaskCreated(DomainEvent):
    """Emitted when a task/ticket is created."""
    pass


@dataclass(frozen=True)
class TaskAssigned(DomainEvent):
    """Emitted when a task is assigned to a tool or human."""
    pass


@dataclass(frozen=True)
class TaskCompleted(DomainEvent):
    """Emitted when a task execution finishes."""
    pass


@dataclass(frozen=True)
class TaskFailed(DomainEvent):
    """Emitted when a task execution fails."""
    pass


@dataclass(frozen=True)
class TaskUpdated(DomainEvent):
    """Emitted when a task/ticket is updated."""
    pass


@dataclass(frozen=True)
class TaskDeleted(DomainEvent):
    """Emitted when a task/ticket is deleted."""
    pass


# ---------------------------------------------------------------------------
# Job events
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class JobDispatched(DomainEvent):
    """Emitted when a job is dispatched to a tool."""
    pass


@dataclass(frozen=True)
class JobCompleted(DomainEvent):
    """Emitted when a job finishes successfully."""
    pass


@dataclass(frozen=True)
class JobFailed(DomainEvent):
    """Emitted when a job fails."""
    pass


# ---------------------------------------------------------------------------
# Tool readiness events
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class ToolReadinessProbed(DomainEvent):
    """Emitted when a tool's readiness is checked (startup or periodic)."""
    pass


@dataclass(frozen=True)
class ToolStartupComplete(DomainEvent):
    """Emitted when the startup tool probe finishes for all tools."""
    pass


# ---------------------------------------------------------------------------
# MCP events
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class MCPToolInvoked(DomainEvent):
    """Emitted when an MCP tool is called."""
    pass


@dataclass(frozen=True)
class MCPServerRegistered(DomainEvent):
    """Emitted when an MCP server is registered."""
    pass


# ---------------------------------------------------------------------------
# Validation events (for vallm integration)
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class ValidationRequested(DomainEvent):
    """Emitted when code validation is requested."""
    pass


@dataclass(frozen=True)
class ValidationCompleted(DomainEvent):
    """Emitted when code validation finishes."""
    pass


# ---------------------------------------------------------------------------
# Event Store
# ---------------------------------------------------------------------------

class EventStore:
    """Append-only event store with optional SQLite persistence.

    The store keeps events in memory and optionally syncs to SQLite.
    Supports querying by aggregate_id and event_type.

    Usage:
        store = EventStore(persist_path=Path("events.sqlite"))
        store.append(ProjectCreated(aggregate_id="proj-123", data={"name": "my-app"}))
        events = store.events_for("proj-123")
        all_events = store.all_events(event_type="ProjectCreated")
    """

    _NAMESPACE = "domain_events"

    def __init__(self, persist_path: Path | None = None) -> None:
        self._events: list[DomainEvent] = []
        self._subscribers: list[Callable[[DomainEvent], None]] = []
        self._persist_path = persist_path
        self._store = None

        if persist_path:
            try:
                from proxym.storage import SQLiteNamespaceStore
                self._store = SQLiteNamespaceStore(persist_path)
                self._load()
            except Exception as e:
                logger.warning("event_store_init_failed", error=str(e))

    def append(self, event: DomainEvent) -> None:
        """Append an event and notify subscribers."""
        self._events.append(event)
        self._persist_event(event)
        logger.info(
            "domain_event",
            event_type=event.event_type,
            aggregate_id=event.aggregate_id,
            actor=event.actor,
        )
        for sub in self._subscribers:
            try:
                sub(event)
            except Exception as e:
                logger.warning("event_subscriber_error", error=str(e))

    def subscribe(self, handler: Callable[[DomainEvent], None]) -> None:
        """Register an event subscriber."""
        self._subscribers.append(handler)

    def events_for(self, aggregate_id: str) -> list[DomainEvent]:
        """Get all events for a given aggregate."""
        return [e for e in self._events if e.aggregate_id == aggregate_id]

    def all_events(
        self,
        event_type: str | None = None,
        since: float | None = None,
        limit: int = 100,
    ) -> list[DomainEvent]:
        """Query events with optional filters."""
        result = self._events
        if event_type:
            result = [e for e in result if e.event_type == event_type]
        if since:
            result = [e for e in result if e.timestamp >= since]
        return result[-limit:]

    def count(self) -> int:
        """Total number of stored events."""
        return len(self._events)

    # --- Persistence ---

    def _persist_event(self, event: DomainEvent) -> None:
        if self._store is None:
            return
        try:
            # Load existing, add new, save all (append-only via namespace store)
            existing = self._store.load_namespace(self._NAMESPACE)
            existing[event.event_id] = event.to_dict()
            self._store.save_namespace(self._NAMESPACE, existing)
        except Exception as e:
            logger.warning("event_persist_failed", error=str(e))

    def _load(self) -> None:
        if self._store is None:
            return
        try:
            raw = self._store.load_namespace(self._NAMESPACE)
            for event_id, payload in sorted(raw.items()):
                d = payload if isinstance(payload, dict) else json.loads(payload)
                evt = DomainEvent.from_dict(d)
                self._events.append(evt)
            if self._events:
                logger.info("event_store_loaded", count=len(self._events))
        except Exception as e:
            logger.warning("event_store_load_failed", error=str(e))
