"""
Self-Improving Learning Loop — GUARDiA

4개 피드백 루프:
  1. RecurrenceTracker      — 재발 패턴 자동 감지 + Problem 티켓 격상
  2. SolutionEffectiveness  — KB 솔루션 효과 추적 + 검증된 룬북 승격
  3. AdaptiveThreshold      — 이상 탐지 임계값 자동 보정
  4. LessonMiner            — 패턴 마이닝 → 교훈 자동 생성
"""
from __future__ import annotations

import hashlib
import json
import logging
import re
import statistics
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple

from sqlalchemy import select, and_, desc, func
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

# ── 한국어/영어 불용어 ─────────────────────────────────────────────────────────

_STOPWORDS = {
    "이", "가", "을", "를", "의", "에", "는", "은", "그", "및", "또한",
    "the", "a", "an", "is", "are", "was", "were", "for", "with", "and",
    "sr", "요청", "작업", "처리", "확인", "수행", "필요", "발생",
}

_TECH_PATTERNS = [
    (r'\b(tomcat|jboss|weblogic|jeus|websphere)\b', "was"),
    (r'\b(nginx|apache|iis|webtob)\b', "web"),
    (r'\b(oracle|mysql|postgresql|mssql|tibero)\b', "db"),
    (r'\b(cpu|memory|메모리|heap|힙)\b', "resource"),
    (r'\b(disk|디스크|storage)\b', "disk"),
    (r'\b(deploy|배포|release|릴리즈)\b', "deploy"),
    (r'\b(restart|재기동|재시작)\b', "restart"),
    (r'\b(error|오류|에러|exception|예외)\b', "error"),
    (r'\b(timeout|타임아웃|연결|connection)\b', "network"),
    (r'\b(ssl|tls|인증서|certificate)\b', "ssl"),
    (r'\b(backup|백업)\b', "backup"),
    (r'\b(장애|incident|down|다운)\b', "incident"),
]


# ── 텍스트 지문 유틸 ───────────────────────────────────────────────────────────

def _tokenize(text: str) -> set:
    text = text.lower()
    tokens = re.split(r'[\s,;:.()[\]{}!?]+', text)
    return {t for t in tokens if len(t) >= 2 and t not in _STOPWORDS}


def _extract_tech_keywords(text: str) -> List[str]:
    text = text.lower()
    found = []
    for pattern, label in _TECH_PATTERNS:
        if re.search(pattern, text):
            found.append(label)
    return sorted(set(found))


def _jaccard(a: set, b: set) -> float:
    if not a or not b:
        return 0.0
    return len(a & b) / len(a | b)


def _make_pattern_key(inst_id: Optional[int], sr_type: str, keywords: List[str]) -> str:
    parts = [str(inst_id or "global"), sr_type] + sorted(keywords)
    return "|".join(parts)


def _hash_key(key: str) -> str:
    return hashlib.sha256(key.encode()).hexdigest()[:16]


# ═══════════════════════════════════════════════════════════════════════════════
# 1. RecurrenceTracker — 재발 패턴 감지
# ═══════════════════════════════════════════════════════════════════════════════

async def detect_recurrence(
    db: AsyncSession,
    sr_id: str,
    title: str,
    description: str,
    sr_type: str,
    inst_id: Optional[int] = None,
    escalate_threshold: int = 3,
    similarity_threshold: float = 0.35,
) -> Dict:
    """
    신규 SR과 유사한 재발 패턴이 있는지 감지한다.
    Returns: {
        "recurrence_found": bool,
        "pattern_id": int | None,
        "occurrence_count": int,
        "escalated": bool,
        "problem_id": str | None,
        "similar_sr_ids": list,
        "suggested_kb_ids": list,
    }
    """
    from models import RecurrencePattern, KBDocument

    text = f"{title} {description}"
    tech_kw = _extract_tech_keywords(text)
    tokens = _tokenize(text)

    pattern_key = _make_pattern_key(inst_id, sr_type, tech_kw)
    pattern_hash = _hash_key(pattern_key)

    # 동일 패턴 해시 먼저 검색
    existing = (await db.execute(
        select(RecurrencePattern).where(RecurrencePattern.pattern_hash == pattern_hash)
    )).scalars().first()

    # 해시 미일치 → 유사도 기반 폴백 검색 (최근 90일)
    if not existing:
        since = datetime.utcnow() - timedelta(days=90)
        candidates = (await db.execute(
            select(RecurrencePattern).where(
                and_(
                    RecurrencePattern.last_seen_at >= since,
                    RecurrencePattern.sr_type == sr_type,
                )
            ).order_by(desc(RecurrencePattern.occurrence_count)).limit(50)
        )).scalars().all()

        for cand in candidates:
            sig_tokens = _tokenize(cand.keyword_signature or "")
            sim = _jaccard(tokens, sig_tokens)
            if sim >= similarity_threshold:
                existing = cand
                break

    escalated = False
    problem_id = None

    if existing:
        # 재발 카운트 업데이트
        sr_ids = existing.sr_ids or []
        if sr_id not in sr_ids:
            sr_ids.append(sr_id)
        existing.sr_ids = sr_ids
        existing.occurrence_count = len(sr_ids)
        existing.last_seen_at = datetime.utcnow()
        await db.flush()

        # 임계값 초과 시 Problem 티켓 자동 생성
        if existing.occurrence_count >= escalate_threshold and not existing.escalated:
            pid = await _escalate_to_problem(db, existing, title, sr_type, inst_id)
            existing.escalated = True
            existing.problem_id = pid
            escalated = True
            problem_id = pid
            await db.flush()

        await db.commit()

        # 연관 KB 제안
        suggested_kbs = await _find_kb_for_pattern(db, existing.keyword_signature or text)

        return {
            "recurrence_found": True,
            "pattern_id": existing.id,
            "occurrence_count": existing.occurrence_count,
            "escalated": escalated,
            "problem_id": problem_id,
            "similar_sr_ids": (existing.sr_ids or [])[-5:],
            "suggested_kb_ids": suggested_kbs,
        }

    # 신규 패턴 등록
    new_pattern = RecurrencePattern(
        pattern_hash      = pattern_hash,
        pattern_key       = pattern_key[:200],
        sr_type           = sr_type,
        inst_id           = inst_id,
        keyword_signature = " ".join(sorted(tokens)[:30]),
        tech_keywords     = ",".join(tech_kw),
        occurrence_count  = 1,
        first_seen_at     = datetime.utcnow(),
        last_seen_at      = datetime.utcnow(),
        sr_ids            = [sr_id],
        escalated         = False,
    )
    db.add(new_pattern)
    await db.commit()
    await db.refresh(new_pattern)

    return {
        "recurrence_found": False,
        "pattern_id": new_pattern.id,
        "occurrence_count": 1,
        "escalated": False,
        "problem_id": None,
        "similar_sr_ids": [],
        "suggested_kb_ids": [],
    }


async def _escalate_to_problem(
    db: AsyncSession,
    pattern: "RecurrencePattern",
    title: str,
    sr_type: str,
    inst_id: Optional[int],
) -> Optional[str]:
    """재발 패턴을 Problem 티켓으로 자동 격상."""
    try:
        from models import ProblemRecord

        today = datetime.utcnow().strftime("%Y%m%d")
        prefix = f"PRB-{today}-"
        last = (await db.execute(
            select(ProblemRecord.prb_id)
            .where(ProblemRecord.prb_id.like(f"{prefix}%"))
            .order_by(desc(ProblemRecord.prb_id))
            .limit(1)
        )).scalar()
        seq = int(last.split("-")[-1]) + 1 if last else 1
        prb_id = f"{prefix}{seq:04d}"

        sr_count = pattern.occurrence_count
        prb = ProblemRecord(
            prb_id      = prb_id,
            title       = f"[자동감지] 반복 장애: {title[:80]}",
            description = (
                f"학습 엔진이 동일 유형 SR이 {sr_count}회 반복됨을 자동 감지했습니다.\n"
                f"SR 유형: {sr_type}\n"
                f"연관 SR: {', '.join((pattern.sr_ids or [])[-5:])}\n"
                f"기술 키워드: {pattern.tech_keywords or '-'}\n\n"
                f"근본 원인 분석(RCA) 후 영구 조치가 필요합니다."
            ),
            status      = "OPEN",
            priority    = "HIGH",
            inst_id     = inst_id,
            created_at  = datetime.utcnow(),
            source      = "learning-engine",
        )
        db.add(prb)
        await db.flush()
        logger.info("재발 패턴 → Problem 티켓 생성: %s (SR %d회)", prb_id, sr_count)
        return prb_id
    except Exception as e:
        logger.warning("Problem 티켓 자동 생성 실패: %s", e)
        return None


async def _find_kb_for_pattern(
    db: AsyncSession,
    keyword_text: str,
    limit: int = 3,
) -> List[int]:
    """패턴 키워드와 유사한 KB 문서 ID 목록 반환."""
    try:
        from models import KBDocument
        tokens = _tokenize(keyword_text)
        rows = (await db.execute(
            select(KBDocument)
            .order_by(desc(KBDocument.created_at))
            .limit(100)
        )).scalars().all()

        scored = []
        for doc in rows:
            doc_tokens = _tokenize(f"{doc.title} {doc.symptoms or ''} {doc.tags or ''}")
            sim = _jaccard(tokens, doc_tokens)
            if sim > 0.15:
                scored.append((sim, doc.id))

        scored.sort(reverse=True)
        return [doc_id for _, doc_id in scored[:limit]]
    except Exception:
        return []


# ═══════════════════════════════════════════════════════════════════════════════
# 2. SolutionEffectivenessTracker — KB 솔루션 효과 추적
# ═══════════════════════════════════════════════════════════════════════════════

async def record_kb_usage(
    db: AsyncSession,
    sr_id: str,
    kb_doc_id: str,
    kb_id: Optional[int] = None,
) -> Dict:
    """SR 해결에 사용된 KB를 기록한다."""
    from models import SolutionFeedback

    # 이미 기록된 경우 중복 방지
    existing = (await db.execute(
        select(SolutionFeedback).where(
            and_(
                SolutionFeedback.sr_id == sr_id,
                SolutionFeedback.kb_doc_id == kb_doc_id,
            )
        )
    )).scalars().first()

    if existing:
        return {"recorded": False, "reason": "이미 기록됨", "feedback_id": existing.id}

    fb = SolutionFeedback(
        sr_id       = sr_id,
        kb_id       = kb_id,
        kb_doc_id   = kb_doc_id,
        applied_at  = datetime.utcnow(),
        resolved    = True,
    )
    db.add(fb)
    await db.commit()
    await db.refresh(fb)
    logger.info("KB 사용 기록: SR=%s KB=%s", sr_id, kb_doc_id)
    return {"recorded": True, "feedback_id": fb.id}


async def check_solution_effectiveness(
    db: AsyncSession,
    feedback_id: int,
    recurrence_window_days: int = 7,
) -> Dict:
    """
    피드백 기록 이후 같은 패턴이 재발했는지 확인하고 효과 점수를 업데이트한다.
    자동 프로모션: KBDocument.effectiveness_score >= 10 → LessonLearned 생성
    """
    from models import SolutionFeedback, SRRequest, KBDocument

    fb = (await db.execute(
        select(SolutionFeedback).where(SolutionFeedback.id == feedback_id)
    )).scalars().first()

    if not fb:
        return {"error": "feedback not found"}

    sr = (await db.execute(
        select(SRRequest).where(SRRequest.sr_id == fb.sr_id)
    )).scalars().first()

    if not sr:
        return {"error": "SR not found"}

    # 해결 후 recurrence_window_days 내 같은 기관 동일 유형 SR 재발 확인
    since = fb.applied_at
    until = since + timedelta(days=recurrence_window_days)
    recurred = (await db.execute(
        select(SRRequest).where(
            and_(
                SRRequest.sr_id != fb.sr_id,
                SRRequest.inst_id == sr.inst_id,
                SRRequest.sr_type == sr.sr_type,
                SRRequest.created_at >= since,
                SRRequest.created_at <= until,
            )
        )
    )).scalars().first()

    if recurred:
        fb.recurred_within_days = (recurred.created_at - since).days
        fb.effectiveness_score = -1
    else:
        fb.recurred_within_days = None
        fb.effectiveness_score = 1

    fb.checked_at = datetime.utcnow()
    await db.flush()

    # KB 문서 효과 점수 누적
    score_delta = fb.effectiveness_score
    if fb.kb_doc_id:
        kb_doc = (await db.execute(
            select(KBDocument).where(KBDocument.doc_id == fb.kb_doc_id)
        )).scalars().first()
        if kb_doc:
            current = getattr(kb_doc, "effectiveness_score", 0) or 0
            kb_doc.effectiveness_score = current + score_delta
            await db.flush()

            # 자동 프로모션 체크
            if kb_doc.effectiveness_score >= 10:
                await _promote_kb_to_lesson(db, kb_doc)

    await db.commit()

    return {
        "feedback_id": feedback_id,
        "sr_id": fb.sr_id,
        "kb_doc_id": fb.kb_doc_id,
        "effectiveness_score": fb.effectiveness_score,
        "recurred": recurred is not None,
        "recurred_within_days": fb.recurred_within_days,
    }


async def _promote_kb_to_lesson(db: AsyncSession, kb_doc) -> Optional[Dict]:
    """KB 효과 점수 10 이상 → 검증된 룬북(LessonLearned)으로 자동 승격."""
    from models import LessonLearned

    # 이미 승격됐으면 스킵
    existing = (await db.execute(
        select(LessonLearned).where(LessonLearned.promoted_from_kb_id == kb_doc.id)
    )).scalars().first()
    if existing:
        return None

    today = datetime.utcnow().strftime("%Y%m%d")
    prefix = f"LESSON-{today}-"
    last = (await db.execute(
        select(LessonLearned.lesson_id)
        .where(LessonLearned.lesson_id.like(f"{prefix}%"))
        .order_by(desc(LessonLearned.lesson_id))
        .limit(1)
    )).scalar()
    seq = int(last.split("-")[-1]) + 1 if last else 1
    lesson_id = f"{prefix}{seq:04d}"

    lesson = LessonLearned(
        lesson_id            = lesson_id,
        title                = f"[검증됨] {kb_doc.title}",
        category             = kb_doc.category or "일반",
        problem_pattern      = kb_doc.symptoms or "",
        root_cause           = kb_doc.cause or "",
        effective_solution   = kb_doc.solution or "",
        prevention           = "",
        confidence_score     = kb_doc.effectiveness_score,
        source_kb_ids        = [kb_doc.id],
        source_sr_ids        = ([kb_doc.source_sr_id] if kb_doc.source_sr_id else []),
        promoted_from_kb_id  = kb_doc.id,
        is_verified          = True,
        created_at           = datetime.utcnow(),
    )
    db.add(lesson)
    await db.flush()
    logger.info("KB %d → LessonLearned %s 자동 승격 (효과점수=%d)",
                kb_doc.id, lesson_id, kb_doc.effectiveness_score)
    return {"lesson_id": lesson_id, "kb_id": kb_doc.id}


# ═══════════════════════════════════════════════════════════════════════════════
# 3. AdaptiveThresholdCalibrator — 이상 탐지 임계값 자동 보정
# ═══════════════════════════════════════════════════════════════════════════════

# 임계값 보정 경계 (기본값의 50%~200% 범위)
_THRESHOLD_MIN_RATIO = 0.5
_THRESHOLD_MAX_RATIO = 2.0
_FALSE_POSITIVE_TRIGGER = 0.25   # 오탐률 25% 초과 시 임계값 상향
_MISSED_TRIGGER = 3              # 누락 탐지 3회 초과 시 임계값 하향
_ADJUSTMENT_STEP = 0.08          # 1회 조정 폭 8%


async def record_anomaly_outcome(
    db: AsyncSession,
    source: str,
    metric_type: str,
    was_actual_incident: bool,
    base_threshold: Optional[float] = None,
) -> Dict:
    """
    이상 탐지 이벤트가 실제 장애로 이어졌는지 기록한다.
    was_actual_incident=True → True Positive
    was_actual_incident=False → False Positive (오탐)
    """
    from models import AdaptiveThreshold
    from core.anomaly import DEFAULT_THRESHOLDS

    rec = (await db.execute(
        select(AdaptiveThreshold).where(
            and_(
                AdaptiveThreshold.source == source,
                AdaptiveThreshold.metric_type == metric_type,
            )
        )
    )).scalars().first()

    if not rec:
        # 기본 임계값 참조
        if base_threshold is None:
            info = DEFAULT_THRESHOLDS.get(metric_type)
            base_threshold = info[0] if info else 90.0

        rec = AdaptiveThreshold(
            source             = source,
            metric_type        = metric_type,
            base_threshold     = base_threshold,
            adapted_threshold  = base_threshold,
            true_positive      = 0,
            false_positive     = 0,
            missed_count       = 0,
            adaptation_count   = 0,
            created_at         = datetime.utcnow(),
        )
        db.add(rec)
        await db.flush()

    if was_actual_incident:
        rec.true_positive += 1
    else:
        rec.false_positive += 1

    await db.commit()
    return {"source": source, "metric_type": metric_type,
            "true_positive": rec.true_positive, "false_positive": rec.false_positive}


async def record_missed_detection(
    db: AsyncSession,
    source: str,
    metric_type: str,
) -> Dict:
    """실제 장애였는데 탐지 못한 경우(누락 탐지)를 기록한다."""
    from models import AdaptiveThreshold
    from core.anomaly import DEFAULT_THRESHOLDS

    rec = (await db.execute(
        select(AdaptiveThreshold).where(
            and_(
                AdaptiveThreshold.source == source,
                AdaptiveThreshold.metric_type == metric_type,
            )
        )
    )).scalars().first()

    if not rec:
        info = DEFAULT_THRESHOLDS.get(metric_type)
        base = info[0] if info else 90.0
        rec = AdaptiveThreshold(
            source=source, metric_type=metric_type,
            base_threshold=base, adapted_threshold=base,
            true_positive=0, false_positive=0, missed_count=0,
            adaptation_count=0, created_at=datetime.utcnow(),
        )
        db.add(rec)
        await db.flush()

    rec.missed_count += 1
    await db.commit()
    return {"source": source, "metric_type": metric_type, "missed_count": rec.missed_count}


async def calibrate_threshold(
    db: AsyncSession,
    source: str,
    metric_type: str,
) -> Dict:
    """
    누적된 오탐/누락 데이터를 기반으로 임계값을 자동 보정한다.
    Returns: {"adjusted": bool, "old_threshold": float, "new_threshold": float, "reason": str}
    """
    from models import AdaptiveThreshold

    rec = (await db.execute(
        select(AdaptiveThreshold).where(
            and_(
                AdaptiveThreshold.source == source,
                AdaptiveThreshold.metric_type == metric_type,
            )
        )
    )).scalars().first()

    if not rec:
        return {"adjusted": False, "reason": "기록 없음"}

    total = rec.true_positive + rec.false_positive
    old_thr = rec.adapted_threshold
    new_thr = old_thr
    reason = "변경 없음"

    if total >= 5:
        fp_rate = rec.false_positive / total
        if fp_rate > _FALSE_POSITIVE_TRIGGER:
            # 오탐 많음 → 임계값 상향 (더 높은 값에서만 알림)
            new_thr = old_thr * (1 + _ADJUSTMENT_STEP)
            reason = f"오탐률 {fp_rate:.0%} > {_FALSE_POSITIVE_TRIGGER:.0%} → 임계값 상향"

    if rec.missed_count > _MISSED_TRIGGER:
        # 누락 탐지 많음 → 임계값 하향 (더 민감하게)
        new_thr = old_thr * (1 - _ADJUSTMENT_STEP)
        reason = f"누락 탐지 {rec.missed_count}회 > {_MISSED_TRIGGER}회 → 임계값 하향"

    # 범위 제한
    base = rec.base_threshold
    new_thr = max(base * _THRESHOLD_MIN_RATIO, min(base * _THRESHOLD_MAX_RATIO, new_thr))
    new_thr = round(new_thr, 2)

    if abs(new_thr - old_thr) < 0.01:
        return {"adjusted": False, "old_threshold": old_thr,
                "new_threshold": new_thr, "reason": reason}

    rec.adapted_threshold = new_thr
    rec.last_adapted_at   = datetime.utcnow()
    rec.adaptation_count  += 1
    # 보정 후 카운터 부분 리셋 (누적 오염 방지)
    rec.false_positive = rec.false_positive // 2
    rec.true_positive  = rec.true_positive // 2
    rec.missed_count   = 0
    await db.commit()

    logger.info("임계값 보정: %s/%s %.2f → %.2f (%s)", source, metric_type, old_thr, new_thr, reason)
    return {
        "adjusted": True,
        "source": source,
        "metric_type": metric_type,
        "old_threshold": old_thr,
        "new_threshold": new_thr,
        "reason": reason,
    }


async def get_adapted_threshold(
    db: AsyncSession,
    source: str,
    metric_type: str,
) -> Optional[float]:
    """현재 적응형 임계값 조회 (없으면 None — 기본값 사용)."""
    from models import AdaptiveThreshold
    rec = (await db.execute(
        select(AdaptiveThreshold.adapted_threshold).where(
            and_(
                AdaptiveThreshold.source == source,
                AdaptiveThreshold.metric_type == metric_type,
            )
        )
    )).scalar()
    return rec


# ═══════════════════════════════════════════════════════════════════════════════
# 4. LessonMiner — 패턴 마이닝 & 교훈 자동 생성
# ═══════════════════════════════════════════════════════════════════════════════

async def run_lesson_mining(
    db: AsyncSession,
    days_back: int = 30,
    min_occurrences: int = 3,
) -> Dict:
    """
    최근 N일 재발 패턴을 분석하여 교훈(LessonLearned)을 자동 생성한다.
    Returns: {"patterns_analyzed": int, "lessons_created": int, "lessons_updated": int}
    """
    from models import RecurrencePattern, SolutionFeedback, KBDocument, LessonLearned

    since = datetime.utcnow() - timedelta(days=days_back)
    patterns = (await db.execute(
        select(RecurrencePattern).where(
            and_(
                RecurrencePattern.last_seen_at >= since,
                RecurrencePattern.occurrence_count >= min_occurrences,
            )
        ).order_by(desc(RecurrencePattern.occurrence_count))
    )).scalars().all()

    created = 0
    updated = 0

    for pat in patterns:
        sr_ids = pat.sr_ids or []
        if not sr_ids:
            continue

        # 이 패턴의 SR들에서 사용된 KB 중 가장 효과적인 것 선택
        best_kb = await _find_best_kb_for_pattern_srs(db, sr_ids)

        # 기존 Lesson 확인
        existing_lesson = (await db.execute(
            select(LessonLearned).where(
                LessonLearned.lesson_id.like(f"%-PAT{pat.id}")
            )
        )).scalars().first()

        if existing_lesson:
            # 업데이트
            existing_lesson.confidence_score = pat.occurrence_count
            existing_lesson.source_sr_ids    = sr_ids[-10:]
            if best_kb:
                existing_lesson.effective_solution = best_kb.get("solution", existing_lesson.effective_solution)
            existing_lesson.updated_at = datetime.utcnow()
            updated += 1
        else:
            # 신규 생성
            today = datetime.utcnow().strftime("%Y%m%d")
            lesson_id = f"LESSON-{today}-PAT{pat.id}"

            solution_text = ""
            root_cause_text = ""
            if best_kb:
                solution_text   = best_kb.get("solution", "")
                root_cause_text = best_kb.get("cause", "")

            lesson = LessonLearned(
                lesson_id          = lesson_id,
                title              = f"[반복패턴] {pat.sr_type} 재발 {pat.occurrence_count}회 — {(pat.tech_keywords or '').replace(',', '/')}",
                category           = _infer_category(pat.tech_keywords or ""),
                problem_pattern    = pat.keyword_signature or "",
                root_cause         = root_cause_text or "반복 패턴 기반 분석 중",
                effective_solution = solution_text or "KB 문서 참조 또는 전문가 조치 필요",
                prevention         = f"동일 유형({pat.sr_type}) SR이 {pat.occurrence_count}회 반복됨. 근본 원인 제거 필요.",
                confidence_score   = pat.occurrence_count,
                source_kb_ids      = ([best_kb["id"]] if best_kb else []),
                source_sr_ids      = sr_ids[-10:],
                is_verified        = False,
                created_at         = datetime.utcnow(),
            )
            db.add(lesson)
            created += 1

    # pending 피드백 효과 일괄 검증
    await _batch_check_pending_feedbacks(db)

    # 임계값 일괄 보정
    calibration_results = await _batch_calibrate_thresholds(db)

    await db.commit()

    return {
        "patterns_analyzed": len(patterns),
        "lessons_created":   created,
        "lessons_updated":   updated,
        "thresholds_calibrated": calibration_results,
        "run_at": datetime.utcnow().isoformat(),
    }


async def _find_best_kb_for_pattern_srs(
    db: AsyncSession,
    sr_ids: List[str],
) -> Optional[Dict]:
    """패턴의 SR들에서 가장 효과적으로 사용된 KB 반환."""
    from models import SolutionFeedback, KBDocument

    feedbacks = (await db.execute(
        select(SolutionFeedback).where(
            and_(
                SolutionFeedback.sr_id.in_(sr_ids),
                SolutionFeedback.effectiveness_score > 0,
                SolutionFeedback.kb_doc_id.isnot(None),
            )
        ).order_by(desc(SolutionFeedback.effectiveness_score))
        .limit(1)
    )).scalars().first()

    if not feedbacks or not feedbacks.kb_doc_id:
        return None

    kb = (await db.execute(
        select(KBDocument).where(KBDocument.doc_id == feedbacks.kb_doc_id)
    )).scalars().first()

    if not kb:
        return None

    return {
        "id": kb.id,
        "doc_id": kb.doc_id,
        "title": kb.title,
        "solution": kb.solution,
        "cause": kb.cause,
    }


async def _batch_check_pending_feedbacks(db: AsyncSession) -> int:
    """효과 미검증 피드백을 일괄 처리한다."""
    from models import SolutionFeedback

    cutoff = datetime.utcnow() - timedelta(days=7)
    pending = (await db.execute(
        select(SolutionFeedback).where(
            and_(
                SolutionFeedback.checked_at.is_(None),
                SolutionFeedback.applied_at <= cutoff,
            )
        ).limit(50)
    )).scalars().all()

    checked = 0
    for fb in pending:
        try:
            await check_solution_effectiveness(db, fb.id)
            checked += 1
        except Exception as e:
            logger.debug("피드백 %d 검증 실패: %s", fb.id, e)

    return checked


async def _batch_calibrate_thresholds(db: AsyncSession) -> int:
    """적응형 임계값 일괄 보정."""
    from models import AdaptiveThreshold

    recs = (await db.execute(
        select(AdaptiveThreshold)
        .where(
            (AdaptiveThreshold.true_positive + AdaptiveThreshold.false_positive) >= 5
        )
        .limit(100)
    )).scalars().all()

    calibrated = 0
    for rec in recs:
        result = await calibrate_threshold(db, rec.source, rec.metric_type)
        if result.get("adjusted"):
            calibrated += 1

    return calibrated


def _infer_category(tech_keywords: str) -> str:
    kw = tech_keywords.lower()
    if "deploy" in kw:     return "배포"
    if "was" in kw:        return "WAS"
    if "db" in kw:         return "DB"
    if "web" in kw:        return "웹서버"
    if "resource" in kw:   return "서버 운영"
    if "network" in kw:    return "네트워크"
    if "ssl" in kw:        return "보안"
    if "incident" in kw:   return "장애 대응"
    return "일반"


# ═══════════════════════════════════════════════════════════════════════════════
# 통계 요약
# ═══════════════════════════════════════════════════════════════════════════════

async def get_learning_stats(db: AsyncSession) -> Dict:
    """학습 엔진 현황 통계 요약."""
    from models import (
        RecurrencePattern, SolutionFeedback, AdaptiveThreshold, LessonLearned, KBDocument
    )

    pattern_count = (await db.execute(
        select(func.count()).select_from(RecurrencePattern)
    )).scalar() or 0

    escalated_count = (await db.execute(
        select(func.count()).select_from(RecurrencePattern)
        .where(RecurrencePattern.escalated == True)
    )).scalar() or 0

    feedback_count = (await db.execute(
        select(func.count()).select_from(SolutionFeedback)
    )).scalar() or 0

    effective_count = (await db.execute(
        select(func.count()).select_from(SolutionFeedback)
        .where(SolutionFeedback.effectiveness_score > 0)
    )).scalar() or 0

    lesson_count = (await db.execute(
        select(func.count()).select_from(LessonLearned)
    )).scalar() or 0

    verified_lesson_count = (await db.execute(
        select(func.count()).select_from(LessonLearned)
        .where(LessonLearned.is_verified == True)
    )).scalar() or 0

    threshold_count = (await db.execute(
        select(func.count()).select_from(AdaptiveThreshold)
    )).scalar() or 0

    adapted_count = (await db.execute(
        select(func.count()).select_from(AdaptiveThreshold)
        .where(AdaptiveThreshold.adaptation_count > 0)
    )).scalar() or 0

    # 가장 자주 재발하는 패턴 Top 5
    top_patterns = (await db.execute(
        select(RecurrencePattern)
        .order_by(desc(RecurrencePattern.occurrence_count))
        .limit(5)
    )).scalars().all()

    return {
        "recurrence": {
            "total_patterns":    pattern_count,
            "escalated":         escalated_count,
            "top_recurring": [
                {
                    "id":           p.id,
                    "sr_type":      p.sr_type,
                    "count":        p.occurrence_count,
                    "tech_keywords": p.tech_keywords,
                    "last_seen":    p.last_seen_at.isoformat() if p.last_seen_at else None,
                }
                for p in top_patterns
            ],
        },
        "solution_effectiveness": {
            "total_feedbacks":   feedback_count,
            "effective":         effective_count,
            "effectiveness_rate": round(effective_count / feedback_count, 3) if feedback_count else 0,
        },
        "lessons": {
            "total":    lesson_count,
            "verified": verified_lesson_count,
        },
        "adaptive_thresholds": {
            "total_tracked":  threshold_count,
            "adapted":        adapted_count,
        },
        "generated_at": datetime.utcnow().isoformat(),
    }
