"""Shared ownership and lifecycle helpers for runtime Matrix event-cache services."""

from __future__ import annotations

import asyncio
from contextlib import asynccontextmanager
from dataclasses import dataclass
from importlib import import_module
from typing import TYPE_CHECKING, cast

from mindroom.config.matrix import CacheConfig
from mindroom.constants import RuntimePaths
from mindroom.matrix.cache.event_cache import EventCacheBackendUnavailableError
from mindroom.matrix.cache.postgres_redaction import redact_postgres_connection_info
from mindroom.matrix.cache.sqlite_event_cache import SqliteEventCache
from mindroom.matrix.cache.write_coordinator import EventCacheWriteCoordinator
from mindroom.tool_system.dependencies import ensure_optional_deps

_STARTUP_PREWARM_ROOM_CONCURRENCY = 1

if TYPE_CHECKING:
    from collections.abc import AsyncIterator
    from pathlib import Path

    import structlog

    from mindroom.matrix.cache import SharedConversationEventCache
    from mindroom.matrix.cache.postgres_event_cache import PostgresEventCache


class StartupThreadPrewarmRegistry:
    """Track one startup-wave claim set for room-level thread prewarm."""

    def __init__(self, *, room_concurrency: int = _STARTUP_PREWARM_ROOM_CONCURRENCY) -> None:
        self._lock = asyncio.Lock()
        self._claimed_rooms: set[tuple[str, str]] = set()
        self._room_slots = asyncio.Semaphore(max(1, room_concurrency))

    async def try_claim(self, principal_id: str, room_id: str) -> bool:
        """Claim one principal-owned room for this startup wave."""
        key = (principal_id, room_id)
        async with self._lock:
            if key in self._claimed_rooms:
                return False
            self._claimed_rooms.add(key)
            return True

    async def release(self, principal_id: str, room_id: str) -> None:
        """Release one principal-owned room claim for a retry."""
        async with self._lock:
            self._claimed_rooms.discard((principal_id, room_id))

    @asynccontextmanager
    async def room_slot(self) -> AsyncIterator[None]:
        """Limit concurrent room-level startup prewarm work across all bots."""
        async with self._room_slots:
            yield


@dataclass(frozen=True, slots=True)
class _EventCacheRuntimeIdentity:
    """Comparable runtime identity for one event-cache backend binding."""

    backend: str
    location: str
    namespace: str | None = None

    @property
    def redacted_location(self) -> str:
        """Return a log-safe description of the backing store location."""
        if self.backend != "postgres":
            return self.location
        return redact_postgres_connection_info(self.location)


@dataclass(slots=True)
class OwnedRuntimeSupport:
    """Concrete event-cache services owned by one runtime lifecycle."""

    event_cache: SharedConversationEventCache
    event_cache_write_coordinator: EventCacheWriteCoordinator
    startup_thread_prewarm_registry: StartupThreadPrewarmRegistry
    event_cache_identity: _EventCacheRuntimeIdentity


def _event_cache_runtime_identity(
    cache_config: CacheConfig,
    runtime_paths: RuntimePaths,
) -> _EventCacheRuntimeIdentity:
    """Return the concrete event-cache runtime identity implied by config."""
    if cache_config.backend != "postgres":
        return _EventCacheRuntimeIdentity(
            backend="sqlite",
            location=str(cache_config.resolve_db_path(runtime_paths)),
        )
    return _EventCacheRuntimeIdentity(
        backend="postgres",
        location=cache_config.resolve_postgres_database_url(runtime_paths),
        namespace=cache_config.resolve_namespace(runtime_paths),
    )


def _load_postgres_event_cache_class(runtime_paths: RuntimePaths) -> type[PostgresEventCache]:
    """Ensure Postgres dependencies are importable, then load the concrete backend class."""
    ensure_optional_deps(["psycopg"], "postgres", runtime_paths)
    postgres_module = import_module("mindroom.matrix.cache.postgres_event_cache")
    return cast("type[PostgresEventCache]", postgres_module.PostgresEventCache)


def _build_event_cache(
    cache_config: CacheConfig,
    runtime_paths: RuntimePaths,
) -> SharedConversationEventCache:
    """Build the configured event-cache backend without initializing it."""
    if cache_config.backend != "postgres":
        return SqliteEventCache(cache_config.resolve_db_path(runtime_paths))

    database_url = cache_config.resolve_postgres_database_url(runtime_paths)
    namespace = cache_config.resolve_namespace(runtime_paths)
    postgres_event_cache_class = _load_postgres_event_cache_class(runtime_paths)
    return postgres_event_cache_class(database_url=database_url, namespace=namespace)


def build_owned_runtime_support(
    *,
    db_path: Path | None = None,
    cache_config: CacheConfig | None = None,
    runtime_paths: RuntimePaths | None = None,
    logger: structlog.stdlib.BoundLogger,
    background_task_owner: object,
) -> OwnedRuntimeSupport:
    """Build one owned runtime-support bundle without initializing the cache."""
    if cache_config is None:
        if db_path is None:
            msg = "build_owned_runtime_support requires db_path or cache_config"
            raise ValueError(msg)
        cache_config = CacheConfig(db_path=str(db_path))
    if runtime_paths is None:
        if db_path is None:
            msg = "build_owned_runtime_support requires runtime_paths when db_path is omitted"
            raise ValueError(msg)
        runtime_paths = RuntimePaths(
            config_path=db_path.parent / "config.yaml",
            config_dir=db_path.parent,
            env_path=db_path.parent / ".env",
            storage_root=db_path.parent,
        )
    cache_identity = _event_cache_runtime_identity(cache_config, runtime_paths)
    return OwnedRuntimeSupport(
        event_cache=_build_event_cache(cache_config, runtime_paths),
        event_cache_write_coordinator=EventCacheWriteCoordinator(
            logger=logger,
            background_task_owner=background_task_owner,
        ),
        startup_thread_prewarm_registry=StartupThreadPrewarmRegistry(),
        event_cache_identity=cache_identity,
    )


async def _initialize_event_cache_best_effort(
    support: OwnedRuntimeSupport,
    *,
    logger: structlog.stdlib.BoundLogger,
    init_failure_reason_prefix: str,
) -> None:
    """Initialize event-cache storage without permanently disabling on transient outages."""
    if support.event_cache.is_initialized:
        return
    try:
        await support.event_cache.initialize()
    except EventCacheBackendUnavailableError as exc:
        logger.warning(
            "Event cache backend temporarily unavailable during init; will retry on demand",
            backend=support.event_cache_identity.backend,
            location=support.event_cache_identity.redacted_location,
            namespace=support.event_cache_identity.namespace,
            error=str(exc),
        )
    except Exception as exc:
        support.event_cache.disable(f"{init_failure_reason_prefix}:{exc}")
        logger.warning(
            "Event cache init failed; continuing without advisory cache",
            backend=support.event_cache_identity.backend,
            location=support.event_cache_identity.redacted_location,
            namespace=support.event_cache_identity.namespace,
            error=str(exc),
        )


async def sync_owned_runtime_support(
    support: OwnedRuntimeSupport | None,
    *,
    db_path: Path | None = None,
    cache_config: CacheConfig | None = None,
    runtime_paths: RuntimePaths | None = None,
    logger: structlog.stdlib.BoundLogger,
    background_task_owner: object,
    init_failure_reason_prefix: str,
    log_db_path_change: bool,
) -> OwnedRuntimeSupport:
    """Build, rebind, and initialize one owned runtime-support bundle."""
    if cache_config is None:
        if db_path is None:
            msg = "sync_owned_runtime_support requires db_path or cache_config"
            raise ValueError(msg)
        cache_config = CacheConfig(db_path=str(db_path))
    if runtime_paths is None:
        if db_path is None:
            msg = "sync_owned_runtime_support requires runtime_paths when db_path is omitted"
            raise ValueError(msg)
        runtime_paths = RuntimePaths(
            config_path=db_path.parent / "config.yaml",
            config_dir=db_path.parent,
            env_path=db_path.parent / ".env",
            storage_root=db_path.parent,
        )
    target_identity = _event_cache_runtime_identity(cache_config, runtime_paths)
    if support is None:
        support = build_owned_runtime_support(
            cache_config=cache_config,
            runtime_paths=runtime_paths,
            logger=logger,
            background_task_owner=background_task_owner,
        )
    else:
        support.event_cache_write_coordinator.background_task_owner = background_task_owner
        if not support.event_cache.is_initialized and support.event_cache_identity != target_identity:
            support = build_owned_runtime_support(
                cache_config=cache_config,
                runtime_paths=runtime_paths,
                logger=logger,
                background_task_owner=background_task_owner,
            )
        elif support.event_cache_identity != target_identity and log_db_path_change:
            logger.info(
                "Event cache backend change will apply after restart",
                active_backend=support.event_cache_identity.backend,
                active_location=support.event_cache_identity.redacted_location,
                active_namespace=support.event_cache_identity.namespace,
                configured_backend=target_identity.backend,
                configured_location=target_identity.redacted_location,
                configured_namespace=target_identity.namespace,
            )

    await _initialize_event_cache_best_effort(
        support,
        logger=logger,
        init_failure_reason_prefix=init_failure_reason_prefix,
    )
    return support


async def close_owned_runtime_support(
    support: OwnedRuntimeSupport,
    *,
    logger: structlog.stdlib.BoundLogger,
) -> None:
    """Close one owned runtime-support bundle in dependency order."""
    try:
        await support.event_cache_write_coordinator.close()
    except Exception as exc:
        logger.warning("Failed to close event cache write coordinator", error=str(exc))

    try:
        await support.event_cache.close()
    except Exception as exc:
        logger.warning("Failed to close event cache", error=str(exc))
