"""
Playbook Repository - #7 Playbook 萃取
======================================
Playbook CRUD 操作 (Redis + PostgreSQL)

Phase 7.2: Repository 實作
建立時間: 2026-03-26 (台北時區)
建立者: Claude Code (#7 Playbook 萃取)

遵循 leWOOOgo 積木化原則:
- 實作 IPlaybookRepository Protocol
- Redis 為 Working Memory (7天 TTL)
- PostgreSQL 為 Episodic Memory

Phase 22 P2: 相似度計算邏輯移至 utils
2026-03-31 Claude Code (首席架構師技術債修復)
"""

import json

import structlog

from src.core.redis_client import get_redis
from src.models.playbook import (
    Playbook,
    PlaybookStatus,
    SymptomPattern,
)
from src.repositories.interfaces import IPlaybookRepository
from src.utils.similarity import calculate_symptom_similarity
from src.utils.timezone import now_taipei

logger = structlog.get_logger(__name__)

# Redis TTL: 7 天
PLAYBOOK_TTL_SECONDS = 7 * 24 * 60 * 60

# Redis Key 前綴
PLAYBOOK_KEY_PREFIX = "playbook:"
PLAYBOOK_INDEX_ALERT_PREFIX = "playbook:index:alert:"
PLAYBOOK_INDEX_SERVICE_PREFIX = "playbook:index:service:"


class PlaybookRepository:
    """
    Playbook Repository 實作

    儲存策略:
    - Redis: Working Memory (快速讀取，7天 TTL)
    - PostgreSQL: Episodic Memory (持久化，待實作)

    Phase 7.2 先實作 Redis 層，PostgreSQL 待 #7.5 整合
    """

    # === CRUD Operations ===

    async def create(self, playbook: Playbook) -> Playbook:
        """
        建立新的 Playbook

        1. 儲存到 Redis
        2. 建立索引 (alert_names, services)
        """
        try:
            redis_client = get_redis()

            # 確保有建立時間
            if not playbook.created_at:
                playbook.created_at = now_taipei()
            playbook.updated_at = now_taipei()

            # 儲存 Playbook
            key = f"{PLAYBOOK_KEY_PREFIX}{playbook.playbook_id}"
            await redis_client.set(
                key,
                json.dumps(playbook.to_redis_dict(), ensure_ascii=False),
                ex=PLAYBOOK_TTL_SECONDS,
            )

            # 建立索引
            await self._update_indexes(playbook)

            logger.info(
                "playbook_created",
                playbook_id=playbook.playbook_id,
                name=playbook.name,
            )
            return playbook

        except Exception as e:
            logger.error("playbook_create_failed", error=str(e))
            raise

    async def get_by_id(self, playbook_id: str) -> Playbook | None:
        """根據 ID 取得 Playbook"""
        try:
            redis_client = get_redis()
            key = f"{PLAYBOOK_KEY_PREFIX}{playbook_id}"
            data = await redis_client.get(key)

            if data:
                return Playbook.from_redis_dict(json.loads(data))
            return None

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

    async def update(self, playbook: Playbook) -> Playbook | None:
        """更新 Playbook"""
        try:
            existing = await self.get_by_id(playbook.playbook_id)
            if not existing:
                return None

            playbook.updated_at = now_taipei()

            redis_client = get_redis()
            key = f"{PLAYBOOK_KEY_PREFIX}{playbook.playbook_id}"
            await redis_client.set(
                key,
                json.dumps(playbook.to_redis_dict(), ensure_ascii=False),
                ex=PLAYBOOK_TTL_SECONDS,
            )

            # 更新索引
            await self._update_indexes(playbook)

            logger.info("playbook_updated", playbook_id=playbook.playbook_id)
            return playbook

        except Exception as e:
            logger.error(
                "playbook_update_failed",
                playbook_id=playbook.playbook_id,
                error=str(e),
            )
            return None

    async def delete(self, playbook_id: str) -> bool:
        """
        刪除 Playbook (軟刪除 → DEPRECATED)

        不真正刪除，而是將狀態改為 DEPRECATED
        """
        try:
            playbook = await self.get_by_id(playbook_id)
            if not playbook:
                return False

            playbook.status = PlaybookStatus.DEPRECATED
            playbook.updated_at = now_taipei()
            await self.update(playbook)

            logger.info("playbook_deprecated", playbook_id=playbook_id)
            return True

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

    # === Query Operations ===

    async def list_playbooks(
        self,
        status: PlaybookStatus | None = None,
        tags: list[str] | None = None,
        limit: int = 20,
        offset: int = 0,
    ) -> tuple[list[Playbook], int]:
        """
        列出 Playbooks

        注意: Redis 實作效率較低，後續需遷移到 PostgreSQL
        """
        try:
            redis_client = get_redis()

            # 掃描所有 Playbook keys
            pattern = f"{PLAYBOOK_KEY_PREFIX}PB-*"
            keys = []
            async for key in redis_client.scan_iter(match=pattern, count=100):
                keys.append(key)

            # 讀取並過濾
            all_playbooks: list[Playbook] = []
            for key in keys:
                data = await redis_client.get(key)
                if data:
                    playbook = Playbook.from_redis_dict(json.loads(data))

                    # 狀態過濾
                    if status and playbook.status != status:
                        continue

                    # 標籤過濾
                    if tags and not set(tags).intersection(set(playbook.tags)):
                        continue

                    all_playbooks.append(playbook)

            # 排序: 按 updated_at 降序
            all_playbooks.sort(key=lambda p: p.updated_at, reverse=True)

            total = len(all_playbooks)
            items = all_playbooks[offset : offset + limit]

            return items, total

        except Exception as e:
            logger.error("playbook_list_failed", error=str(e))
            return [], 0

    async def find_by_symptoms(
        self,
        symptoms: SymptomPattern,
        top_k: int = 5,
        min_similarity: float = 0.5,
    ) -> list[tuple[Playbook, float]]:
        """
        根據症狀模式找相似 Playbook

        策略:
        1. 從索引快速過濾候選
        2. 計算詳細相似度
        3. 返回 Top K
        """
        try:
            redis_client = get_redis()

            # 1. 使用索引找候選 Playbook IDs
            candidate_ids: set[str] = set()

            # 從 alert_names 索引查詢
            for alert_name in symptoms.alert_names:
                index_key = f"{PLAYBOOK_INDEX_ALERT_PREFIX}{alert_name}"
                members = await redis_client.smembers(index_key)
                candidate_ids.update(m.decode() if isinstance(m, bytes) else m for m in members)

            # 從 services 索引查詢
            for service in symptoms.affected_services:
                index_key = f"{PLAYBOOK_INDEX_SERVICE_PREFIX}{service}"
                members = await redis_client.smembers(index_key)
                candidate_ids.update(m.decode() if isinstance(m, bytes) else m for m in members)

            # 如果沒有索引命中，掃描所有 APPROVED Playbooks
            if not candidate_ids:
                playbooks, _ = await self.list_playbooks(
                    status=PlaybookStatus.APPROVED,
                    limit=100,
                )
                candidate_ids = {p.playbook_id for p in playbooks}

            # 2. 計算相似度
            results: list[tuple[Playbook, float]] = []

            for playbook_id in candidate_ids:
                playbook = await self.get_by_id(playbook_id)
                if not playbook:
                    continue

                # 只考慮 APPROVED 狀態
                if playbook.status != PlaybookStatus.APPROVED:
                    continue

                similarity = calculate_symptom_similarity(
                    symptoms,
                    playbook.symptom_pattern,
                )

                if similarity >= min_similarity:
                    results.append((playbook, similarity))

            # 3. 排序並返回 Top K
            results.sort(key=lambda x: x[1], reverse=True)
            return results[:top_k]

        except Exception as e:
            logger.error("playbook_find_symptoms_failed", error=str(e))
            return []

    async def update_stats(
        self,
        playbook_id: str,
        success: bool,
    ) -> bool:
        """更新執行統計"""
        try:
            playbook = await self.get_by_id(playbook_id)
            if not playbook:
                return False

            if success:
                playbook.success_count += 1
            else:
                playbook.failure_count += 1

            playbook.last_used_at = now_taipei()
            await self.update(playbook)

            logger.info(
                "playbook_stats_updated",
                playbook_id=playbook_id,
                success=success,
                success_rate=playbook.success_rate,
            )
            return True

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

    # === Index Management ===

    async def _update_indexes(self, playbook: Playbook) -> None:
        """更新索引"""
        try:
            redis_client = get_redis()

            # Alert names 索引
            for alert_name in playbook.symptom_pattern.alert_names:
                index_key = f"{PLAYBOOK_INDEX_ALERT_PREFIX}{alert_name}"
                await redis_client.sadd(index_key, playbook.playbook_id)
                await redis_client.expire(index_key, PLAYBOOK_TTL_SECONDS)

            # Services 索引
            for service in playbook.symptom_pattern.affected_services:
                index_key = f"{PLAYBOOK_INDEX_SERVICE_PREFIX}{service}"
                await redis_client.sadd(index_key, playbook.playbook_id)
                await redis_client.expire(index_key, PLAYBOOK_TTL_SECONDS)

        except Exception as e:
            logger.warning("playbook_index_update_failed", error=str(e))

    # === Learning Service 信心度調整 (2026-03-30 Claude Code) ===

    async def find_by_source_incident(
        self,
        incident_id: str,
    ) -> list[Playbook]:
        """
        根據來源 Incident ID 找 Playbook

        2026-03-30 Claude Code: Learning Service 信心度調整用
        尋找 source_incident_ids 包含此 incident_id 的 Playbooks
        """
        try:
            redis_client = get_redis()

            # 掃描所有 Playbook keys
            pattern = f"{PLAYBOOK_KEY_PREFIX}PB-*"
            results: list[Playbook] = []

            async for key in redis_client.scan_iter(match=pattern, count=100):
                data = await redis_client.get(key)
                if data:
                    playbook = Playbook.from_redis_dict(json.loads(data))
                    if incident_id in playbook.source_incident_ids:
                        results.append(playbook)

            return results

        except Exception as e:
            logger.error(
                "playbook_find_by_incident_failed",
                incident_id=incident_id,
                error=str(e),
            )
            return []

    async def adjust_confidence(
        self,
        playbook_id: str,
        delta: float,
        reason: str,
    ) -> Playbook | None:
        """
        調整 Playbook 信心度

        2026-03-30 Claude Code: Learning Service 信心度調整用

        邏輯:
        - ai_confidence += delta (clamp 到 0.0~1.0)
        - 若信心度 >= 0.9 且 status == DRAFT → 自動升級為 APPROVED
        - 若信心度 < 0.3 且 failure_rate > 50% → 自動降級為 DEPRECATED
        """
        try:
            playbook = await self.get_by_id(playbook_id)
            if not playbook:
                return None

            old_confidence = playbook.ai_confidence

            # 調整信心度 (clamp 到 0.0~1.0)
            playbook.ai_confidence = max(0.0, min(1.0, playbook.ai_confidence + delta))

            # 狀態自動轉換
            old_status = playbook.status

            # 高信心度自動升級
            if (
                playbook.ai_confidence >= 0.9
                and playbook.status == PlaybookStatus.DRAFT
            ):
                playbook.status = PlaybookStatus.APPROVED
                playbook.approved_by = "auto_learning"
                playbook.approved_at = now_taipei()
                note = f"\n[Auto-approved: confidence {playbook.ai_confidence:.2f}]"
                playbook.notes = (playbook.notes or "") + note

            # 低信心度 + 高失敗率 → 棄用
            elif (
                playbook.ai_confidence < 0.3
                and playbook.total_executions >= 5
                and playbook.failure_rate > 0.5
            ):
                playbook.status = PlaybookStatus.DEPRECATED
                conf = playbook.ai_confidence
                fail = playbook.failure_rate
                note = f"\n[Auto-deprecated: conf={conf:.2f}, fail={fail:.0%}]"
                playbook.notes = (playbook.notes or "") + note

            # 儲存
            updated = await self.update(playbook)

            logger.info(
                "playbook_confidence_adjusted",
                playbook_id=playbook_id,
                old_confidence=old_confidence,
                new_confidence=playbook.ai_confidence,
                old_status=old_status.value,
                new_status=playbook.status.value,
                reason=reason,
            )

            return updated

        except Exception as e:
            logger.error(
                "playbook_confidence_adjust_failed",
                playbook_id=playbook_id,
                delta=delta,
                error=str(e),
            )
            return None


# =============================================================================
# Singleton
# =============================================================================

_repository: PlaybookRepository | None = None


def get_playbook_repository() -> IPlaybookRepository:
    """取得 PlaybookRepository 單例"""
    global _repository
    if _repository is None:
        _repository = PlaybookRepository()
    return _repository
