"""Event store initialization."""

from pathlib import Path
from typing import Any, TYPE_CHECKING

from ...domain.contracts.event_store import NullEventStore
from ...domain.exceptions import ConfigurationError
from ...logging_config import get_logger
from ...observability.health import (
    EventStoreDurabilityStatus,
    set_event_store_durability_status,
)
from .components import create_persistent_event_store

if TYPE_CHECKING:
    from ...bootstrap.runtime import Runtime

logger = get_logger(__name__)


class EventStoreConfigurationError(ConfigurationError):
    """Raised when a durable event store cannot be initialized as configured.

    Surfaces (instead of silently degrading to a non-durable in-memory store)
    when the configured SQLite driver cannot be used -- e.g. the path/dir is
    not writable on a read-only deploy, or the SQLite backend is unavailable.

    Subclasses the domain :class:`ConfigurationError` so callers that guard the
    configuration boundary on that type also catch this fail-fast case.
    """


def init_event_store(runtime: "Runtime", config: dict[str, Any]) -> None:
    """Initialize event store for event sourcing.

    Configures the event store based on config.yaml settings.
    Defaults to SQLite if not specified.

    Durability policy: when a durable driver (``sqlite``) is configured but the
    store cannot be initialized (path not writable, backend unavailable), this
    fails fast with :class:`EventStoreConfigurationError` rather than silently
    swapping in a non-durable in-memory store. A non-durable store is only used
    when the operator opts in explicitly -- either ``driver: memory`` or
    ``allow_memory_fallback: true``. When the fallback is taken, the degraded
    durability posture is recorded so ``/health/ready`` reports it.

    Config example:
        event_store:
            enabled: true
            driver: sqlite  # or "memory"
            path: data/events.db
            allow_memory_fallback: false  # opt in to non-durable fallback

    Args:
        runtime: Runtime instance with event bus.
        config: Full configuration dictionary.

    Raises:
        EventStoreConfigurationError: when a durable driver is configured but
            cannot be initialized and no explicit memory fallback was requested,
            or when an unknown driver is configured.
    """
    backend = _selected_backend(config)
    if backend is not None:
        _install_from_backend(runtime, backend, str((config.get("persistence") or {}).get("backend")))
        return

    event_store_config = config.get("event_store", {})
    enabled = event_store_config.get("enabled", True)

    if not enabled:
        logger.info("event_store_disabled")
        runtime.event_bus.set_event_store(NullEventStore())
        set_event_store_durability_status(
            EventStoreDurabilityStatus(
                configured_driver="disabled",
                durable=False,
                degraded=False,
                detail="event store disabled",
            )
        )
        return

    driver = event_store_config.get("driver", "sqlite")
    allow_memory_fallback = bool(event_store_config.get("allow_memory_fallback", False))

    from ...domain.contracts.event_store import IEventStore

    event_store: IEventStore

    if driver == "memory":
        from ...infrastructure.persistence import InMemoryEventStore

        event_store = InMemoryEventStore()
        logger.info("event_store_initialized", driver="memory")
        set_event_store_durability_status(
            EventStoreDurabilityStatus(
                configured_driver="memory",
                durable=False,
                degraded=False,
                detail="in-memory store explicitly configured (non-durable)",
            )
        )
    elif driver == "sqlite":
        db_path = event_store_config.get("path", "data/events.db")
        try:
            Path(db_path).parent.mkdir(parents=True, exist_ok=True)
            _result = create_persistent_event_store(driver, event_store_config)
            if _result is None:
                raise EventStoreConfigurationError("SQLite event store is unavailable")
            event_store = _result
            logger.info("event_store_initialized", driver="sqlite", path=db_path)
            set_event_store_durability_status(
                EventStoreDurabilityStatus(
                    configured_driver="sqlite",
                    durable=True,
                    degraded=False,
                    detail=f"sqlite at {db_path}",
                )
            )
        except OSError as e:
            # Path/dir is not writable (e.g. a read-only deploy). Do NOT silently
            # drop durability -- fail fast unless an in-memory fallback was
            # explicitly opted into.
            if not allow_memory_fallback:
                raise EventStoreConfigurationError(
                    f"event store path {db_path!r} is not writable ({e}); "
                    "set event_store.driver: memory to explicitly opt into a "
                    "non-durable store, or event_store.allow_memory_fallback: true "
                    "to accept a non-durable in-memory fallback"
                ) from e
            logger.warning(
                "event_store_sqlite_fallback_to_memory",
                error=str(e),
                path=db_path,
                allow_memory_fallback=True,
            )
            from ...infrastructure.persistence import InMemoryEventStore

            event_store = InMemoryEventStore()
            logger.warning(
                "event_store_degraded_to_memory",
                driver="sqlite",
                reason="path_not_writable",
                path=db_path,
            )
            set_event_store_durability_status(
                EventStoreDurabilityStatus(
                    configured_driver="sqlite",
                    durable=False,
                    degraded=True,
                    detail=f"sqlite path {db_path} not writable; degraded to in-memory",
                )
            )
        except ImportError:
            # The SQLite backend could not be loaded. Same policy: fail fast unless
            # a non-durable fallback was explicitly requested.
            if not allow_memory_fallback:
                raise EventStoreConfigurationError(
                    "the SQLite event store backend could not be loaded; "
                    "install the persistence backend, set event_store.driver: memory "
                    "to explicitly opt into a non-durable store, or "
                    "event_store.allow_memory_fallback: true to accept a non-durable "
                    "in-memory fallback"
                )
            logger.warning(
                "event_store_sqlite_unavailable",
                fallback="memory",
                hint="SQLite event store could not be loaded.",
                allow_memory_fallback=True,
            )
            from ...infrastructure.persistence import InMemoryEventStore

            event_store = InMemoryEventStore()
            set_event_store_durability_status(
                EventStoreDurabilityStatus(
                    configured_driver="sqlite",
                    durable=False,
                    degraded=True,
                    detail="sqlite backend unavailable; degraded to in-memory",
                )
            )
    else:
        raise EventStoreConfigurationError(f"unknown event_store.driver {driver!r}; expected 'sqlite' or 'memory'")

    runtime.event_bus.set_event_store(event_store)
    _install_dispatch_checkpoint(runtime, event_store, event_store_config)


def _selected_backend(config: dict[str, Any]) -> Any:
    """The storage backend, if this deployment selected one.

    Read from the bootstrap holder rather than rebuilt here: the backend owns
    connection pools, and building a second one would give the event store a
    different database handle than everything else -- the very split this
    replaces. `config` is accepted for symmetry with the caller and is not read.
    """
    from .composition import get_persistence_backend

    return get_persistence_backend()


def _install_from_backend(runtime: Any, backend: Any, name: str) -> None:
    """Take the log and its delivery mark from the selected backend.

    No driver branch, no durability negotiation, no memory fallback. A backend
    was chosen as a whole and it is durable by definition -- the fallback logic
    below exists for the legacy per-subsystem configuration, where `sqlite`
    could fail to open a path and the operator had to be told rather than
    silently given a volatile store.
    """
    event_store = backend.event_store()
    runtime.event_bus.set_event_store(event_store)
    runtime.event_bus.set_dispatch_checkpoint(backend.dispatch_checkpoint())
    logger.info("event_store_initialized", driver=name, source="persistence_backend")
    set_event_store_durability_status(
        EventStoreDurabilityStatus(
            configured_driver=name,
            durable=True,
            degraded=False,
            detail=f"{name} backend selected by persistence.backend",
        )
    )


def _install_dispatch_checkpoint(runtime: Any, event_store: Any, event_store_config: dict[str, Any]) -> None:
    """Give the bus a delivery high-water mark. Does NOT deliver anything.

    Delivery is `recover_undelivered_events`, and it must run after the handlers
    are registered -- `init_event_handlers` comes later in bootstrap than this
    does. Sweeping here would deliver a crash's leftovers to an empty handler
    table and then advance the mark past them, destroying exactly the events the
    mechanism exists to save.

    A durable checkpoint over a volatile log would be worse than none: it would
    claim delivery of events that no longer exist. So the checkpoint's
    durability follows the store's.
    """
    from ...domain.contracts.dispatch_checkpoint import IDispatchCheckpoint
    from ...infrastructure.persistence import InMemoryDispatchCheckpoint, SqliteDispatchCheckpoint
    from ...infrastructure.persistence.sqlite_event_store import SQLiteEventStore

    # Keyed on the store that was actually built, not on the configured driver.
    # A configured `sqlite` that degraded to in-memory still reads as "sqlite"
    # here, and pairing that with a file-backed checkpoint would leave a durable
    # mark asserting delivery of events the volatile log no longer holds.
    checkpoint: IDispatchCheckpoint
    if isinstance(event_store, SQLiteEventStore):
        checkpoint = SqliteDispatchCheckpoint(event_store_config.get("path", "data/events.db"))
    else:
        checkpoint = InMemoryDispatchCheckpoint()

    runtime.event_bus.set_dispatch_checkpoint(checkpoint)


def recover_undelivered_events(runtime: Any) -> int:
    """Deliver events a previous run stored but never handed to handlers.

    MUST be called after the event handlers are registered. Called before them,
    it delivers to nobody and marks the events delivered anyway.

    Deliberately not fatal: an unreadable checkpoint costs re-delivery, which
    handlers must tolerate anyway, and refusing to boot over it would turn a
    recoverable state into an outage.

    **Standalone only.** The sweep reads the log from one shared mark and hands
    everything past it to local handlers, which is right when this process is
    the only one writing that log and wrong the moment it is not. With peers,
    it re-delivers *their* events to this instance's handlers -- a second export
    to the SIEM, a second cost record, for work another replica already
    accounted for. And the mark is worse than useless there in the other
    direction too: a peer that publishes advances it past events this instance
    never delivered, so the sweep skips exactly what it exists to recover.

    That is not a gap left open, it is where the recovery moved: effects follow
    the instance that produced the event (#790, phase 0.4), so a replica exports
    its own work and nobody else's. The residual exposure is an event appended
    by a pod that died before its handler ran -- microseconds, since delivery is
    inline right after the append -- and the event itself is still in the log.

    Keyed on the storage backend rather than on the lease keeper, which is not
    built yet at this point in bootstrap. They amount to the same question:
    selecting a backend is what makes peers possible.

    Returns:
        How many events were recovered.
    """
    from .composition import get_persistence_backend

    if get_persistence_backend() is not None:
        logger.info(
            "dispatch_recovery_skipped",
            detail=(
                "a storage backend is selected, so this log may have more than one writer; "
                "the startup sweep would re-deliver peers' events to this instance's handlers"
            ),
        )
        return 0

    try:
        # `runtime` is untyped here, so the count arrives as `Any`; narrow it at
        # the boundary rather than letting it leak out of a function that
        # promises an int.
        recovered = int(runtime.event_bus.dispatch_pending())
    except Exception as e:  # noqa: BLE001 -- fault-barrier: recovery must not block startup
        logger.warning("dispatch_recovery_failed", error=str(e))
        return 0
    if recovered:
        logger.warning("dispatch_recovery_completed", events_delivered=recovered)
    return recovered
