"""
DualIncidentMemory - Incident 專用雙層記憶體適配器
=================================================
Phase 6.4e: 連接 IncidentEngine 與 DualMemoryProvider
Phase 16 R1.2: 完善 PostgreSQL 整合 (2026-03-26)

設計:
- 實作 IIncidentMemory 協定
- 內部使用 DualMemoryProvider
- 提供 Incident 專用的索引功能
- 透過 DI 注入 DB Adapter (解耦 SQLAlchemy Model)

統帥鐵律:
- Working Memory (Redis): 7 天 TTL
- Episodic Memory (PostgreSQL): 永久
- 反向索引: namespace:target → incident_id
"""

from datetime import datetime, timezone, timedelta
from typing import Any, Protocol, runtime_checkable

import structlog

from lewooogo_brain.interfaces.incident_processor import Incident

logger = structlog.get_logger(__name__)


# =============================================================================
# DB Adapter Protocol (依賴注入介面)
# =============================================================================

@runtime_checkable
class IIncidentDbAdapter(Protocol):
    """
    Incident DB Adapter 協定

    Phase 16: 解耦 SQLAlchemy Model，由調用者注入實現

    使用方式:
        class MyDbAdapter:
            async def load(self, incident_id: str) -> Incident | None: ...
            async def save(self, incident: Incident) -> bool: ...

        memory = DualIncidentMemory(redis, db_adapter=MyDbAdapter())
    """

    async def load(self, incident_id: str) -> Incident | None:
        """從 PostgreSQL 載入 Incident"""
        ...

    async def save(self, incident: Incident) -> bool:
        """儲存 Incident 到 PostgreSQL (upsert)"""
        ...


# 常量
WORKING_MEMORY_TTL = 604800  # 7 天
AGGREGATION_WINDOW_MINUTES = 30
INDEX_TTL = 1800  # 索引 30 分鐘 TTL


class DualIncidentMemory:
    """
    Incident 專用雙層記憶體適配器

    實作 IIncidentMemory 協定:
    - load_incident: 從 Working/Episodic 載入
    - save_incident: 儲存到 Working
    - persist_incident: 持久化到 Episodic
    - find_related_incident: 透過反向索引尋找相關 Incident
    - update_index: 更新反向索引

    反向索引結構:
        Key: awoooi:incident_index:{namespace}:{target}
        Value: incident_id
        TTL: 30 分鐘 (聚合窗口)

    Phase 16 DI 模式:
        透過 db_adapter 注入 PostgreSQL 操作，解耦 SQLAlchemy Model
    """

    def __init__(
        self,
        redis_client: Any,
        pg_session_factory: Any = None,
        db_adapter: IIncidentDbAdapter | None = None,
        key_prefix: str = "awoooi:incidents",
    ):
        """
        初始化適配器

        Args:
            redis_client: Redis 連線客戶端
            pg_session_factory: PostgreSQL Session 工廠 (已棄用，改用 db_adapter)
            db_adapter: PostgreSQL 操作適配器 (Phase 16 DI 模式)
            key_prefix: Redis Key 前綴
        """
        self._redis = redis_client
        self._pg_session_factory = pg_session_factory  # 向後兼容，但已棄用
        self._db_adapter = db_adapter
        self._key_prefix = key_prefix
        self._index_prefix = f"{key_prefix}:index"

    def _make_key(self, incident_id: str) -> str:
        """生成 Incident Key"""
        return f"{self._key_prefix}:{incident_id}"

    def _make_index_key(self, namespace: str, target: str) -> str:
        """生成索引 Key"""
        return f"{self._index_prefix}:{namespace}:{target}"

    async def load_incident(self, incident_id: str) -> Incident | None:
        """
        載入 Incident (Cache-Aside 模式)

        策略:
        1. 從 Redis (Working Memory) 讀取
        2. 若 miss，從 PostgreSQL (Episodic) 讀取
        3. 若 PG 有資料，回填到 Redis

        Args:
            incident_id: Incident ID

        Returns:
            Incident 或 None
        """
        try:
            key = self._make_key(incident_id)
            data = await self._redis.get(key)

            if data is not None:
                # Working Memory hit
                if isinstance(data, bytes):
                    data = data.decode()
                return Incident.model_validate_json(data)

            # Working Memory miss
            logger.debug("incident_not_found_in_working", incident_id=incident_id)

            # 嘗試從 Episodic Memory (PostgreSQL) 載入
            if self._db_adapter is not None:
                incident = await self._db_adapter.load(incident_id)
                if incident is not None:
                    # 回填到 Working Memory
                    await self.save_incident(incident)
                    logger.debug(
                        "incident_loaded_from_episodic_backfill",
                        incident_id=incident_id,
                    )
                    return incident

            return None

        except Exception as e:
            logger.error("load_incident_failed", incident_id=incident_id, error=str(e))
            return None

    async def save_incident(
        self,
        incident: Incident,
        ttl_seconds: int = WORKING_MEMORY_TTL,
    ) -> bool:
        """
        儲存 Incident 到 Working Memory (Redis)

        Args:
            incident: Incident 物件
            ttl_seconds: TTL (預設 7 天)

        Returns:
            是否成功
        """
        try:
            key = self._make_key(incident.incident_id)
            json_data = incident.model_dump_json()

            await self._redis.setex(key, ttl_seconds, json_data)

            logger.debug(
                "incident_saved_to_working",
                incident_id=incident.incident_id,
                ttl=ttl_seconds,
            )
            return True

        except Exception as e:
            logger.error(
                "save_incident_failed",
                incident_id=incident.incident_id,
                error=str(e),
            )
            return False

    async def persist_incident(self, incident: Incident) -> bool:
        """
        持久化到 Episodic Memory (PostgreSQL)

        Phase 16: 透過 DI 注入的 db_adapter 執行實際持久化

        Args:
            incident: Incident 物件

        Returns:
            是否成功
        """
        # Phase 16 DI 模式: 優先使用 db_adapter
        if self._db_adapter is not None:
            try:
                result = await self._db_adapter.save(incident)
                if result:
                    logger.debug(
                        "incident_persisted_to_episodic",
                        incident_id=incident.incident_id,
                        adapter="db_adapter",
                    )
                return result
            except Exception as e:
                logger.error(
                    "persist_incident_failed",
                    incident_id=incident.incident_id,
                    error=str(e),
                )
                return False

        # 向後兼容: pg_session_factory (已棄用)
        if self._pg_session_factory is not None:
            logger.warning(
                "pg_session_factory_deprecated_use_db_adapter",
                incident_id=incident.incident_id,
            )
            # 舊模式無法執行，因為沒有 Model 定義
            return False

        logger.warning(
            "no_db_adapter_configured_skip_persist",
            incident_id=incident.incident_id,
        )
        return False

    async def find_related_incident(
        self,
        namespace: str,
        target: str,
        window_minutes: int = AGGREGATION_WINDOW_MINUTES,
    ) -> Incident | None:
        """
        尋找相關的活躍 Incident (用於聚合)

        透過反向索引快速查找:
        1. 查詢索引 Key: namespace:target → incident_id
        2. 載入 Incident
        3. 檢查是否仍在聚合窗口內

        Args:
            namespace: 命名空間
            target: 目標服務
            window_minutes: 聚合窗口 (分鐘)

        Returns:
            相關 Incident 或 None
        """
        try:
            # Step 1: 查詢索引
            index_key = self._make_index_key(namespace, target)
            incident_id = await self._redis.get(index_key)

            if incident_id is None:
                return None

            # Step 2: 載入 Incident
            incident = await self.load_incident(incident_id)
            if incident is None:
                # 索引存在但 Incident 不存在，清除索引
                await self._redis.delete(index_key)
                return None

            # Step 3: 檢查聚合窗口
            window_start = datetime.now(timezone.utc) - timedelta(minutes=window_minutes)
            if incident.updated_at < window_start:
                # 超出聚合窗口，不聚合
                logger.debug(
                    "incident_outside_window",
                    incident_id=incident_id,
                    updated_at=incident.updated_at.isoformat(),
                )
                return None

            logger.debug(
                "found_related_incident",
                incident_id=incident_id,
                namespace=namespace,
                target=target,
            )
            return incident

        except Exception as e:
            logger.error(
                "find_related_incident_failed",
                namespace=namespace,
                target=target,
                error=str(e),
            )
            return None

    async def update_index(
        self,
        incident_id: str,
        namespace: str,
        target: str,
    ) -> bool:
        """
        更新反向索引

        索引結構:
            Key: awoooi:incident_index:{namespace}:{target}
            Value: incident_id
            TTL: 30 分鐘

        Args:
            incident_id: Incident ID
            namespace: 命名空間
            target: 目標服務

        Returns:
            是否成功
        """
        try:
            index_key = self._make_index_key(namespace, target)
            await self._redis.setex(index_key, INDEX_TTL, incident_id)

            logger.debug(
                "index_updated",
                incident_id=incident_id,
                namespace=namespace,
                target=target,
                ttl=INDEX_TTL,
            )
            return True

        except Exception as e:
            logger.error(
                "update_index_failed",
                incident_id=incident_id,
                namespace=namespace,
                target=target,
                error=str(e),
            )
            return False

    async def delete_incident(self, incident_id: str) -> bool:
        """
        刪除 Incident

        Args:
            incident_id: Incident ID

        Returns:
            是否成功
        """
        try:
            key = self._make_key(incident_id)
            result = await self._redis.delete(key)
            return result > 0

        except Exception as e:
            logger.error(
                "delete_incident_failed",
                incident_id=incident_id,
                error=str(e),
            )
            return False
