"""
GUARDiA ITSM — AI 에이전트 엔진 (Paperclip 스타일)

하트비트 기반 자율 운영 에이전트:
  INCIDENT_TRIAGE  : 미배정 인시던트 자동 분류·배정
  KB_CURATOR       : 해결된 SR → 지식베이스 자동 생성
  SSL_WATCHER      : SSL 만료 임박 → SR 자동 생성
  WBS_MONITOR      : WBS 지연 위험 LLM 분석
  PM_SUGGESTER     : PM 스케줄 미등록 서버 탐지

보안:
  - 외부 API 호출 없음 (Ollama localhost:11434만 사용)
  - CRITICAL 액션은 AgentApproval(PENDING) 생성 → 사람 승인 필요
  - 저위험 액션은 AUTO_APPROVED로 즉시 적용
"""
from __future__ import annotations

import json
import logging
from datetime import date, datetime
from typing import Optional
from uuid import uuid4

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

logger = logging.getLogger(__name__)


# ── 에이전트 엔진 ────────────────────────────────────────────────────────────

class AgentEngine:
    """
    Paperclip 스타일 에이전트 실행 엔진.
    scheduler.py의 하트비트 잡에서 호출된다.
    """

    def __init__(self) -> None:
        from core.llm_client import get_llm_client
        self.llm = get_llm_client()

    # ── 공통 실행 진입점 ─────────────────────────────────────────────────────

    async def run_heartbeat(self, agent_id: int) -> None:
        """에이전트 하트비트 — scheduler.py에서 호출."""
        from database import SessionLocal
        from models import AgentConfig, AgentStatus, AgentRole
        from sqlalchemy import select

        # 에이전트 상태 ACTIVE로 변경
        async with SessionLocal() as db:
            agent = (await db.execute(
                select(AgentConfig).where(AgentConfig.id == agent_id)
            )).scalars().first()

            if not agent or not agent.is_active:
                return

            if not await self.llm.health_check():
                logger.warning("[Agent %d] Ollama 응답 없음 — 하트비트 스킵", agent_id)
                return

            agent.status = AgentStatus.ACTIVE
            agent.last_heartbeat = datetime.now()
            await db.commit()

        # 역할별 핸들러 매핑
        handlers = {
            AgentRole.INCIDENT_TRIAGE: self._incident_triage,
            AgentRole.KB_CURATOR:      self._kb_curator,
            AgentRole.SSL_WATCHER:     self._ssl_watcher,
            AgentRole.WBS_MONITOR:     self._wbs_monitor,
            AgentRole.PM_SUGGESTER:    self._pm_suggester,
            AgentRole.DEVELOPER:       self._developer_agent,
        }

        try:
            async with SessionLocal() as db:
                agent = (await db.execute(
                    select(AgentConfig).where(AgentConfig.id == agent_id)
                )).scalars().first()
                if not agent:
                    return

                agent.status = AgentStatus.WORKING
                await db.flush()

                handler = handlers.get(agent.role)
                if handler:
                    await handler(db, agent)

                agent.status = AgentStatus.IDLE
                await db.commit()

        except Exception as exc:
            logger.error("[Agent %d] 하트비트 오류: %s", agent_id, exc, exc_info=True)
            async with SessionLocal() as db:
                a = (await db.execute(
                    select(AgentConfig).where(AgentConfig.id == agent_id)
                )).scalars().first()
                if a:
                    a.status = AgentStatus.ERROR
                    a.last_error = str(exc)[:500]
                    await db.commit()

    # ── 헬퍼 ─────────────────────────────────────────────────────────────────

    def _new_task(self, agent_id: int, title: str, input_data: dict | None = None):
        from models import AgentTask, AgentTaskStatus
        return AgentTask(
            agent_id=agent_id,
            title=title,
            status=AgentTaskStatus.IN_PROGRESS,
            input_data=input_data or {},
            started_at=datetime.now(),
        )

    def _approval(
        self,
        agent_id: int,
        task_id: int | None,
        action_type: str,
        action_data: dict,
        auto: bool = False,
    ):
        from models import AgentApproval, AgentApprovalStatus
        return AgentApproval(
            agent_id=agent_id,
            task_id=task_id,
            action_type=action_type,
            action_data=action_data,
            status=(
                AgentApprovalStatus.AUTO_APPROVED
                if auto
                else AgentApprovalStatus.PENDING
            ),
        )

    # ══════════════════════════════════════════════════════════════════════════
    # ── 1. 인시던트 트리아지 ────────────────────────────────────────────────
    # ══════════════════════════════════════════════════════════════════════════

    async def _incident_triage(self, db: AsyncSession, agent) -> None:
        """
        미배정 인시던트 → LLM 심각도·카테고리 분류 → 자동 배정.
        CRITICAL은 사람 승인 대기, 나머지는 자동 적용.
        """
        from models import Incident, IncidentStatus, AgentTask, AgentTaskStatus

        incidents = (await db.execute(
            select(Incident).where(
                Incident.assigned_to.is_(None),
                Incident.status.in_(["OPEN", "RECEIVED"]),
            ).limit(10)
        )).scalars().all()

        if not incidents:
            logger.debug("[INCIDENT_TRIAGE] 처리 대상 없음")
            return

        system_prompt = (
            agent.system_prompt
            or "당신은 ITSM 인시던트 트리아지 전문가입니다. 인시던트를 정확히 분류하세요."
        )

        for inc in incidents:
            task = self._new_task(
                agent.id,
                f"인시던트 트리아지 #{inc.incident_id}",
                {"incident_id": inc.id, "title": inc.title},
            )
            db.add(task)
            await db.flush()

            result = await self.llm.json_generate(
                prompt=(
                    f"인시던트를 분류하세요.\n\n"
                    f"제목: {inc.title}\n"
                    f"설명: {inc.description or '없음'}\n"
                    f"서비스: {inc.affected_service or '미지정'}\n\n"
                    f"응답 JSON:\n"
                    f'{{"severity":"CRITICAL|HIGH|MEDIUM|LOW",'
                    f'"category":"NETWORK|SERVER|APPLICATION|DATABASE|SECURITY|OTHER",'
                    f'"reason":"분류 근거 (50자 이내)"}}'
                ),
                model=agent.llm_model,
                system=system_prompt,
            )

            severity = result.get("severity", "MEDIUM")
            is_critical = severity == "CRITICAL"

            task.status = AgentTaskStatus.COMPLETED
            task.output_data = result
            task.tokens_used = 0
            task.completed_at = datetime.now()

            approval = self._approval(
                agent_id=agent.id,
                task_id=task.id,
                action_type="TRIAGE_INCIDENT",
                action_data={
                    "incident_id": inc.id,
                    "incident_ref": inc.incident_id,
                    "severity": severity,
                    "category": result.get("category", "OTHER"),
                    "reason": result.get("reason", ""),
                },
                auto=not is_critical,   # CRITICAL만 사람 승인
            )
            db.add(approval)

            # 자동 승인 액션 즉시 적용
            if not is_critical:
                from models import IncidentGrade
                grade_map = {
                    "CRITICAL": IncidentGrade.CRITICAL,
                    "HIGH":     IncidentGrade.HIGH,
                    "MEDIUM":   IncidentGrade.MEDIUM,
                    "LOW":      IncidentGrade.LOW,
                }
                inc.grade = grade_map.get(severity, IncidentGrade.MEDIUM)

            # 에이전트 통계 업데이트
            agent.total_tasks += 1

        await db.commit()
        logger.info("[INCIDENT_TRIAGE] %d건 처리 완료", len(incidents))

        # ── RCA 자동 초안 생성 (RESOLVED 상태, rca_draft 없는 인시던트) ──────
        await self._generate_rca_drafts(db, agent)

    # ── RCA 자동 초안 생성 헬퍼 ─────────────────────────────────────────────

    async def _generate_rca_drafts(self, db: AsyncSession, agent) -> None:
        """
        RESOLVED 장애 중 rca_draft 가 없는 항목에 대해 Ollama로 RCA 문서 초안을 자동 생성.
        생성된 초안은 AgentApproval(PENDING) 을 통해 담당자 검토 후 확정한다.
        외부 API 없음 — 모든 LLM 추론은 Ollama localhost:11434.
        """
        from models import Incident, IncidentStatus, AgentTask, AgentTaskStatus, AgentApproval

        resolved = (await db.execute(
            select(Incident).where(
                Incident.status == IncidentStatus.RESOLVED,
                Incident.rca_draft.is_(None),
            ).limit(5)
        )).scalars().all()

        if not resolved:
            return

        for inc in resolved:
            try:
                rca = await self.llm.json_generate(
                    prompt=(
                        f"장애 인시던트에 대한 RCA(근본 원인 분석) 초안을 작성하세요.\n\n"
                        f"제목: {inc.title}\n"
                        f"설명: {inc.description or '없음'}\n"
                        f"등급: {inc.grade}\n"
                        f"발생 시각: {inc.occurred_at}\n"
                        f"해소 시각: {inc.resolved_at}\n\n"
                        f"응답 JSON (한국어로 작성):\n"
                        f'{{"root_cause":"근본 원인 (2~3문장)",'
                        f'"timeline":"사건 타임라인",'
                        f'"impact":"영향 범위 (서비스/사용자 수)",'
                        f'"resolution":"해결 과정",'
                        f'"preventive_measures":"재발 방지 조치 (3가지 이상)",'
                        f'"lessons_learned":"교훈"}}'
                    ),
                    model=agent.llm_model,
                    system=(
                        agent.system_prompt
                        or "당신은 IT 인프라 장애 분석 전문가입니다. 정확하고 실용적인 RCA를 작성하세요."
                    ),
                )

                if not rca or not rca.get("root_cause"):
                    continue

                inc.rca_draft = json.dumps(rca, ensure_ascii=False)
                await db.flush()

                # 담당자 검토 필요 — PENDING 승인 생성
                task = self._new_task(
                    agent.id,
                    f"RCA 초안 생성: {inc.incident_id}",
                    {"incident_id": inc.id, "incident_ref": inc.incident_id},
                )
                db.add(task)
                await db.flush()
                task.status = "COMPLETED"
                task.output_data = rca
                task.completed_at = datetime.now()

                db.add(self._approval(
                    agent_id=agent.id,
                    task_id=task.id,
                    action_type="RCA_DRAFT",
                    action_data={"incident_id": inc.id, "incident_ref": inc.incident_id},
                    auto=False,   # 항상 사람 검토 필요
                ))
                agent.total_tasks += 1

            except Exception as exc:
                logger.warning("[RCA_DRAFT] 인시던트 %s 처리 실패: %s", inc.incident_id, exc)

        await db.commit()
        logger.info("[RCA_DRAFT] %d건 초안 생성 완료", len(resolved))

    # ══════════════════════════════════════════════════════════════════════════
    # ── 2. KB 큐레이터 ──────────────────────────────────────────────────────
    # ══════════════════════════════════════════════════════════════════════════

    async def _kb_curator(self, db: AsyncSession, agent) -> None:
        """
        해결된 SR 중 KB 미생성 항목 → LLM으로 KB 아티클 자동 생성.
        생성된 KB는 is_published=False(검토 대기)로 저장.
        """
        from models import (
            SRRequest, SRStatus, KBDocument,
            AgentTask, AgentTaskStatus, AgentApproval,
        )

        # 해결된 SR 중 이미 KB로 변환된 것 제외
        # (AgentApproval에서 같은 SR에 대한 CREATE_KB 이력 확인)
        existing_kb_sr_ids = [
            row[0]
            for row in (await db.execute(
                select(AgentApproval.action_data["sr_id"].as_string())
                .where(AgentApproval.action_type == "CREATE_KB")
            )).all()
            if row[0] is not None
        ]

        srs = (await db.execute(
            select(SRRequest).where(
                SRRequest.status == SRStatus.COMPLETED,
                SRRequest.id.notin_([int(x) for x in existing_kb_sr_ids if x.isdigit()]),
            ).limit(5)
        )).scalars().all()

        if not srs:
            logger.debug("[KB_CURATOR] 처리 대상 없음")
            return

        system_prompt = (
            agent.system_prompt
            or "당신은 IT 지식베이스 전문 에디터입니다. 체계적인 KB 아티클을 작성하세요."
        )

        for sr in srs:
            task = self._new_task(
                agent.id,
                f"KB 자동 생성: SR #{sr.sr_id}",
                {"sr_id": sr.id, "title": sr.title},
            )
            db.add(task)
            await db.flush()

            result = await self.llm.json_generate(
                prompt=(
                    f"해결된 서비스 요청으로 지식베이스 아티클을 작성하세요.\n\n"
                    f"SR 제목: {sr.title}\n"
                    f"SR 유형: {sr.sr_type}\n"
                    f"설명: {sr.description or '없음'}\n\n"
                    f"응답 JSON:\n"
                    f'{{"kb_title":"검색 가능한 KB 제목",'
                    f'"symptom":"증상 설명 (2~3문장)",'
                    f'"cause":"원인 분석 (2~3문장)",'
                    f'"solution":"해결 방법 (단계별)",'
                    f'"tags":["태그1","태그2","태그3"]}}'
                ),
                model=agent.llm_model,
                system=system_prompt,
            )

            if not result or not result.get("kb_title"):
                task.status = AgentTaskStatus.FAILED
                task.output_data = {"error": "LLM 응답 파싱 실패"}
                task.completed_at = datetime.now()
                continue

            # KB 문서 생성 (검토 대기 상태)
            kb = KBDocument(
                doc_id=f"KB-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}",
                category=sr.sr_type or "OTHER",
                title=result.get("kb_title", sr.title),
                symptoms=result.get("symptom", ""),
                cause=result.get("cause", ""),
                solution=result.get("solution", ""),
                tags=",".join(result.get("tags", [])),
                sr_type=sr.sr_type,
            )
            db.add(kb)
            await db.flush()

            # ── KB 품질 자동 평가 ───────────────────────────────────────────
            quality = await self.llm.json_generate(
                prompt=(
                    f"아래 KB 아티클의 품질을 평가하세요.\n\n"
                    f"제목: {kb.title}\n"
                    f"증상: {kb.symptoms}\n"
                    f"원인: {kb.cause}\n"
                    f"해결책: {kb.solution}\n\n"
                    f"응답 JSON (0~100 점수):\n"
                    f'{{"completeness":점수,"clarity":점수,"actionability":점수,"overall":점수}}'
                ),
                model=agent.llm_model,
            )

            overall = int(quality.get("overall", 0)) if quality else 0
            kb.quality_score = min(max(overall, 0), 100)

            if kb.quality_score >= 80:
                kb.published = True   # 자동 발행
                auto_approved = True
                logger.info("[KB_CURATOR] KB %s 자동 발행 (품질 %d점)", kb.doc_id, kb.quality_score)
            else:
                kb.published = False
                auto_approved = False
                logger.info("[KB_CURATOR] KB %s 수동 검토 필요 (품질 %d점)", kb.doc_id, kb.quality_score)
            # ────────────────────────────────────────────────────────────────

            task.status = AgentTaskStatus.COMPLETED
            task.output_data = {**result, "kb_doc_id": kb.doc_id, "quality_score": kb.quality_score}
            task.completed_at = datetime.now()

            db.add(self._approval(
                agent_id=agent.id,
                task_id=task.id,
                action_type="CREATE_KB",
                action_data={"sr_id": sr.id, "kb_doc_id": kb.doc_id, "quality_score": kb.quality_score},
                auto=auto_approved,
            ))
            agent.total_tasks += 1

        await db.commit()
        logger.info("[KB_CURATOR] %d건 처리 완료", len(srs))

    # ══════════════════════════════════════════════════════════════════════════
    # ── 3. SSL 감시자 ────────────────────────────────────────────────────────
    # ══════════════════════════════════════════════════════════════════════════

    async def _ssl_watcher(self, db: AsyncSession, agent) -> None:
        """
        SSL 만료 D-30 이내 서버 탐지 → SR 자동 생성.
        D-7 이하: HIGH 우선순위, 사람 승인 없이 즉시 생성.
        """
        from models import Server, SRRequest, SRStatus, SRType, AgentTask, AgentTaskStatus

        today = date.today()
        servers = (await db.execute(
            select(Server).where(Server.ssl_expire_date.isnot(None))
        )).scalars().all()

        created = 0
        for srv in servers:
            days_left = (srv.ssl_expire_date - today).days
            if not (0 <= days_left <= 30):
                continue

            # 이미 미완료 SSL 갱신 SR 있는지 확인
            existing = (await db.execute(
                select(SRRequest).where(
                    SRRequest.title.contains(f"SSL 갱신 [{srv.server_name}]"),
                    SRRequest.status.notin_([SRStatus.COMPLETED, SRStatus.REJECTED]),
                )
            )).scalars().first()
            if existing:
                continue

            priority = "HIGH" if days_left <= 7 else "MEDIUM"
            sr = SRRequest(
                sr_id=f"SR-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:5].upper()}",
                title=f"SSL 갱신 [{srv.server_name}] D-{days_left}",
                description=(
                    f"🤖 AI 에이전트(SSL 감시자)가 자동 생성한 서비스 요청입니다.\n\n"
                    f"서버명: {srv.server_name}\n"
                    f"만료일: {srv.ssl_expire_date.isoformat()}\n"
                    f"남은 기간: D-{days_left}\n\n"
                    f"조치: SSL 인증서 갱신 또는 교체 진행 필요"
                ),
                sr_type=SRType.OTHER,
                status=SRStatus.RECEIVED,
                priority=priority,
                requested_by=f"agent:{agent.id}",
            )
            db.add(sr)
            await db.flush()

            task = self._new_task(
                agent.id,
                f"SSL 만료 감지: {srv.server_name} D-{days_left}",
                {"server_id": srv.id, "days_left": days_left},
            )
            task.status = AgentTaskStatus.COMPLETED
            task.output_data = {"sr_id": sr.id, "priority": priority}
            task.completed_at = datetime.now()
            db.add(task)
            await db.flush()

            db.add(self._approval(
                agent_id=agent.id, task_id=task.id,
                action_type="CREATE_SR",
                action_data={"sr_db_id": sr.id, "server_id": srv.id, "days_left": days_left},
                auto=True,
            ))
            agent.total_tasks += 1
            created += 1

        if created:
            await db.commit()
            logger.info("[SSL_WATCHER] SSL 만료 SR %d건 자동 생성", created)

    # ══════════════════════════════════════════════════════════════════════════
    # ── 4. WBS 모니터 ────────────────────────────────────────────────────────
    # ══════════════════════════════════════════════════════════════════════════

    async def _wbs_monitor(self, db: AsyncSession, agent) -> None:
        """
        지연 WBS가 있는 SI 프로젝트 → LLM 위험 분석 → ProjectRisk 자동 생성.
        WBS 지연이 3건 이상이거나 10일 이상 초과 시 위험 생성.
        """
        from models import (
            WbsItem, WbsStatus, SiProject, ProjectRisk,
            RiskStatus, AgentTask, AgentTaskStatus, ProjectPhase,
        )

        today = date.today()

        projects = (await db.execute(
            select(SiProject).where(
                SiProject.is_active.is_(True),
                SiProject.phase.notin_([ProjectPhase.CLOSED]),
            )
        )).scalars().all()

        for proj in projects:
            delayed = (await db.execute(
                select(WbsItem).where(
                    WbsItem.project_id == proj.id,
                    WbsItem.is_leaf.is_(True),
                    WbsItem.planned_end < today,
                    WbsItem.completion_pct < 100,
                    WbsItem.status != WbsStatus.CANCELLED,
                )
            )).scalars().all()

            if not delayed:
                continue

            max_delay = max((today - w.planned_end).days for w in delayed)
            if len(delayed) < 3 and max_delay < 10:
                continue  # 경미한 지연은 스킵

            # 이미 AI 생성 위험이 있으면 스킵 (중복 방지)
            existing_risk = (await db.execute(
                select(ProjectRisk).where(
                    ProjectRisk.project_id == proj.id,
                    ProjectRisk.raised_by.startswith("agent:"),
                    ProjectRisk.status.notin_(["CLOSED", "ACCEPTED"]),
                )
            )).scalars().first()
            if existing_risk:
                continue

            total_wbs = (await db.execute(
                select(WbsItem).where(
                    WbsItem.project_id == proj.id,
                    WbsItem.is_leaf.is_(True),
                )
            )).scalars().all()
            avg_pct = (
                sum(w.completion_pct for w in total_wbs) / len(total_wbs)
                if total_wbs else 0
            )

            task = self._new_task(
                agent.id,
                f"WBS 위험 분석: {proj.project_name}",
                {"project_id": proj.id, "delayed_count": len(delayed), "max_delay_days": max_delay},
            )
            db.add(task)
            await db.flush()

            result = await self.llm.json_generate(
                prompt=(
                    f"SI 프로젝트 WBS 현황을 분석하고 위험을 평가하세요.\n\n"
                    f"프로젝트: {proj.project_name}\n"
                    f"전체 WBS: {len(total_wbs)}건 | 지연: {len(delayed)}건 | 최대 지연: {max_delay}일\n"
                    f"평균 진척률: {avg_pct:.1f}%\n\n"
                    f"응답 JSON:\n"
                    f'{{"risk_level":"CRITICAL|HIGH|MEDIUM|LOW",'
                    f'"probability":1~3,"impact":1~3,'
                    f'"title":"위험 제목 (30자 이내)",'
                    f'"description":"위험 상세 (100자 이내)",'
                    f'"mitigation":"완화 방안 (100자 이내)"}}'
                ),
                model=agent.llm_model,
                system=agent.system_prompt or "당신은 SI 프로젝트 관리 전문가입니다.",
            )

            prob = min(max(int(result.get("probability", 2)), 1), 3)
            impact = min(max(int(result.get("impact", 2)), 1), 3)
            score = prob * impact

            from models import RiskLevel
            level_map = {9: RiskLevel.CRITICAL, 6: RiskLevel.HIGH, 4: RiskLevel.HIGH,
                         3: RiskLevel.MEDIUM, 2: RiskLevel.MEDIUM}
            risk_level = next(
                (v for k, v in sorted(level_map.items(), reverse=True) if score >= k),
                RiskLevel.LOW,
            )

            risk = ProjectRisk(
                risk_id=f"RSK-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:5].upper()}",
                project_id=proj.id,
                title=result.get("title", f"WBS 지연 위험 ({len(delayed)}건)"),
                description=result.get("description", ""),
                probability_level=prob,
                impact_level=impact,
                risk_score=score,
                risk_level=risk_level,
                mitigation_plan=result.get("mitigation", ""),
                status=RiskStatus.IDENTIFIED,
                raised_by=f"agent:{agent.id}",
            )
            db.add(risk)
            await db.flush()

            task.status = AgentTaskStatus.COMPLETED
            task.output_data = {**result, "risk_id": risk.risk_id}
            task.completed_at = datetime.now()

            db.add(self._approval(
                agent_id=agent.id, task_id=task.id,
                action_type="CREATE_RISK",
                action_data={"risk_id": risk.id, "project_id": proj.id, "score": score},
                auto=(risk_level not in ["CRITICAL"]),
            ))
            agent.total_tasks += 1

        await db.commit()
        logger.info("[WBS_MONITOR] 위험 분석 완료")

        # ── WBS 지연 완료 예측 ────────────────────────────────────────────
        await self._predict_wbs_completion(db, agent)

    # ── WBS 완료 예측 헬퍼 ───────────────────────────────────────────────────

    async def _predict_wbs_completion(self, db: AsyncSession, agent) -> None:
        """
        최근 7일간 완료율 추이(기울기)로 프로젝트 완료 예상일을 계산한다.
        계획 종료일 초과 시 리스크 자동 등록.
        외부 API 없음 — 순수 계산 로직.
        """
        from models import SiProject, AgentTask, ProjectPhase, ProjectRisk, RiskLevel, RiskStatus
        from sqlalchemy import select
        import math

        today = date.today()

        projects = (await db.execute(
            select(SiProject).where(
                SiProject.is_active.is_(True),
                SiProject.phase.notin_([ProjectPhase.CLOSED]),
                SiProject.planned_end_date.isnot(None),
            )
        )).scalars().all()

        for proj in projects:
            try:
                curr_rate = float(proj.completion_rate or 0)
                # 7일 전 스냅샷이 없으면 estimated_completion에서 역산
                # (간소화: 7일 전 비율 = curr_rate - 7*daily_delta 로 추정)
                prev_rate_estimate = max(curr_rate - 7.0, 0.0)  # 이전 데이터 없으면 7% 성장 가정
                daily_delta = (curr_rate - prev_rate_estimate) / 7 if curr_rate > prev_rate_estimate else 0.0

                if daily_delta <= 0:
                    continue  # 진척 없는 프로젝트는 스킵

                remaining = 100.0 - curr_rate
                est_days = math.ceil(remaining / daily_delta)
                from datetime import timedelta
                est_completion = today + timedelta(days=est_days)

                # estimated_completion 업데이트
                proj.estimated_completion = est_completion

                # 계획 종료일 초과 여부 확인
                planned_end = proj.planned_end_date
                if isinstance(planned_end, str):
                    from datetime import datetime as dt
                    planned_end = dt.strptime(planned_end[:10], "%Y-%m-%d").date()

                if est_completion > planned_end:
                    delay_days = (est_completion - planned_end).days

                    # 이미 DELAY_PREDICTION 리스크가 있으면 스킵
                    existing = (await db.execute(
                        select(ProjectRisk).where(
                            ProjectRisk.project_id == proj.id,
                            ProjectRisk.raised_by == "agent:wbs_prediction",
                            ProjectRisk.status.notin_(["CLOSED"]),
                        )
                    )).scalars().first()

                    if not existing:
                        risk = ProjectRisk(
                            risk_id=f"RSK-PRED-{datetime.now().strftime('%Y%m%d')}-{proj.id}",
                            project_id=proj.id,
                            title=f"완료 예측 지연 ({delay_days}일 초과)",
                            description=(
                                f"현재 진척률 {curr_rate:.1f}% (일 평균 +{daily_delta:.2f}%).\n"
                                f"예상 완료일: {est_completion.isoformat()} "
                                f"(계획 종료일 {planned_end} 대비 {delay_days}일 초과)"
                            ),
                            probability_level=2,
                            impact_level=3,
                            risk_score=6,
                            risk_level=RiskLevel.HIGH,
                            mitigation_plan="일일 진척률 향상 계획 수립 및 자원 재배분 검토",
                            status=RiskStatus.IDENTIFIED,
                            raised_by="agent:wbs_prediction",
                        )
                        db.add(risk)
                        logger.info(
                            "[WBS_PREDICTION] 지연 리스크 등록: project=%s delay=%dd",
                            proj.project_name, delay_days
                        )

            except Exception as exc:
                logger.warning("[WBS_PREDICTION] project_id=%d 예측 오류: %s", proj.id, exc)

        await db.commit()
        logger.info("[WBS_PREDICTION] 완료 예측 업데이트 완료")

    # ══════════════════════════════════════════════════════════════════════════
    # ── 5. PM 제안 에이전트 ──────────────────────────────────────────────────
    # ══════════════════════════════════════════════════════════════════════════

    async def _pm_suggester(self, db: AsyncSession, agent) -> None:
        """PM 스케줄 미등록 활성 서버 탐지 → 알림 태스크 기록."""
        from models import Server, PmSchedule, AgentTask, AgentTaskStatus

        scheduled_server_ids = [
            row[0]
            for row in (await db.execute(
                select(PmSchedule.server_id).where(PmSchedule.is_active.is_(True))
            )).all()
            if row[0] is not None
        ]

        unmanaged = (await db.execute(
            select(Server).where(
                Server.id.notin_(scheduled_server_ids),
                Server.is_active.is_(True),
            ).limit(10)
        )).scalars().all()

        if not unmanaged:
            logger.debug("[PM_SUGGESTER] 미등록 서버 없음")
            return

        for srv in unmanaged:
            task = self._new_task(
                agent.id,
                f"PM 미등록 서버 감지: {srv.server_name}",
                {"server_id": srv.id, "server_name": srv.server_name},
            )
            task.status = AgentTaskStatus.COMPLETED
            task.output_data = {"message": "PM 스케줄 등록 권고"}
            task.completed_at = datetime.now()
            db.add(task)
            await db.flush()

            db.add(self._approval(
                agent_id=agent.id, task_id=task.id,
                action_type="SUGGEST_PM",
                action_data={"server_id": srv.id, "server_name": srv.server_name},
                auto=True,
            ))
            agent.total_tasks += 1

        await db.commit()
        logger.info("[PM_SUGGESTER] PM 미등록 서버 %d건 감지", len(unmanaged))

    # ══════════════════════════════════════════════════════════════════════════
    # ── 6. 개발자 에이전트 (Phase 1 Paperclip 스타일) ────────────────────────
    # ══════════════════════════════════════════════════════════════════════════

    async def _developer_agent(self, db: AsyncSession, agent) -> None:
        """
        PENDING 태스크를 받아 LLM으로 코드 생성.
        코드 포함 시 사람 승인(PENDING) 생성.
        """
        from models import AgentTask, AgentTaskStatus

        pending = (await db.execute(
            select(AgentTask).where(
                AgentTask.agent_id == agent.id,
                AgentTask.status == AgentTaskStatus.PENDING,
            ).limit(3)
        )).scalars().all()

        if not pending:
            return

        for task in pending:
            task.status = AgentTaskStatus.IN_PROGRESS
            task.started_at = datetime.now()
            await db.flush()

            input_data = task.input_data or {}
            prompt = input_data.get("prompt") or task.description or task.title

            resp = await self.llm.generate(
                prompt=prompt,
                model=agent.llm_model,
                system=(
                    agent.system_prompt
                    or (
                        "당신은 GUARDiA ITSM 백엔드 개발자입니다.\n"
                        "FastAPI + SQLAlchemy async 코드를 작성합니다.\n"
                        "보안 규칙: ServerOut에 ip_addr/ssh_user/os_pw_enc 포함 금지."
                    )
                ),
            )

            contains_code = any(kw in resp.content for kw in ["```python", "def ", "class ", "async def"])
            task.status = AgentTaskStatus.COMPLETED
            task.output_data = {"response": resp.content[:5000], "tokens": resp.tokens_total}
            task.tokens_used = resp.tokens_total
            task.completed_at = datetime.now()

            db.add(self._approval(
                agent_id=agent.id, task_id=task.id,
                action_type="CODE_CHANGE",
                action_data={"preview": resp.content[:500]},
                auto=not contains_code,
            ))
            agent.total_tasks += 1
            agent.total_tokens += resp.tokens_total

        await db.commit()
        logger.info("[DEVELOPER] %d건 태스크 처리", len(pending))


# ── 싱글턴 ──────────────────────────────────────────────────────────────────

_engine: Optional[AgentEngine] = None


def get_agent_engine() -> AgentEngine:
    """에이전트 엔진 싱글턴 반환."""
    global _engine
    if _engine is None:
        _engine = AgentEngine()
    return _engine
