"""
ORM models + Pydantic schemas for GUARDiA ITSM
"""
from __future__ import annotations

import hashlib
import json
from datetime import datetime, date
from enum import Enum
from typing import Any, Optional, List

from pydantic import BaseModel, ConfigDict
from sqlalchemy import (
    BigInteger, Boolean, Column, Date, DateTime, Float, ForeignKey, Integer, JSON, String, Text, func
)
from sqlalchemy.orm import relationship, backref

from database import Base


# ── Enums ──────────────────────────────────────────────────────────────────────

class SRStatus(str, Enum):
    RECEIVED            = "RECEIVED"
    PARSED              = "PARSED"
    PENDING_APPROVAL    = "PENDING_APPROVAL"
    APPROVED            = "APPROVED"
    IN_PROGRESS         = "IN_PROGRESS"
    PENDING_PM_VALIDATION = "PENDING_PM_VALIDATION"
    COMPLETED           = "COMPLETED"
    FAILED_ROLLBACK     = "FAILED_ROLLBACK"
    REJECTED            = "REJECTED"


class SRType(str, Enum):
    DEPLOY   = "DEPLOY"
    RESTART  = "RESTART"
    LOG      = "LOG"
    INQUIRY  = "INQUIRY"
    OTHER    = "OTHER"


class Priority(str, Enum):
    CRITICAL = "CRITICAL"
    HIGH     = "HIGH"
    MEDIUM   = "MEDIUM"
    LOW      = "LOW"


class ApprovalResult(str, Enum):
    PENDING  = "PENDING"
    APPROVED = "APPROVED"
    REJECTED = "REJECTED"


# ── ORM Models ─────────────────────────────────────────────────────────────────

class Institution(Base):
    __tablename__ = "tb_inst_meta"

    id              = Column(Integer, primary_key=True, index=True)
    inst_code       = Column(String(20), unique=True, nullable=False, index=True)
    inst_name       = Column(String(100), nullable=False)
    org_type        = Column(String(50))
    contact_pm      = Column(String(100))
    address         = Column(String(300))
    region          = Column(String(50))
    phone           = Column(String(20))
    contract_start  = Column(Date)
    contract_end    = Column(Date)
    sla_hours       = Column(Integer, default=4)
    note            = Column(Text)
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())

    servers         = relationship("Server", back_populates="institution")
    sr_requests     = relationship("SRRequest", back_populates="institution")
    contacts        = relationship("InstContact", back_populates="institution",
                                   cascade="all, delete-orphan")


class InstContact(Base):
    """기관 담당자 연락처."""
    __tablename__ = "tb_inst_contact"

    id           = Column(Integer, primary_key=True, index=True)
    inst_id      = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=False)
    contact_name = Column(String(50), nullable=False)
    dept         = Column(String(100))
    position     = Column(String(50))
    role         = Column(String(30), nullable=False, default="ENGINEER")
    # MANAGER | ENGINEER | PM | SECURITY | HELPDESK
    email        = Column(String(100))
    phone        = Column(String(20))
    mobile       = Column(String(20))
    is_primary   = Column(Boolean, default=False)
    is_active    = Column(Boolean, default=True)
    note         = Column(Text)
    created_at   = Column(DateTime, default=func.now())

    institution  = relationship("Institution", back_populates="contacts")


class Server(Base):
    __tablename__ = "tb_server_info"

    id              = Column(Integer, primary_key=True, index=True)
    inst_id         = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=False)
    server_name     = Column(String(100), nullable=False)
    server_role     = Column(String(20))      # WEB / WAS / DB / ESB / BATCH
    os_type         = Column(String(50))
    os_version      = Column(String(50))
    ip_addr         = Column(String(45))      # NOT exposed in API responses
    ssh_user        = Column(String(50))      # NOT exposed
    os_pw_enc       = Column(Text)            # AES-256 encrypted, NEVER exposed
    port            = Column(Integer, default=22)
    ssh_method      = Column(String(20), default="PASSWORD")   # PASSWORD | KEY | KEY_WITH_PASS
    ssh_key_path    = Column(String(255))     # SSH 키 경로 (관제 PC 상의 경로)
    # WEB 정보
    web_server      = Column(String(50))      # Apache | Nginx | IIS | WebtoB
    web_version     = Column(String(30))
    web_port        = Column(Integer)
    web_conf_path   = Column(String(255))
    # WAS 정보
    was_type        = Column(String(50))      # Tomcat | JBoss | WebLogic | JEUS | WebSphere
    was_version     = Column(String(30))
    was_port        = Column(Integer)
    was_home        = Column(String(255))
    was_deploy_path = Column(String(255))
    jdk_version     = Column(String(20))
    heap_min        = Column(String(20))      # e.g. 512m
    heap_max        = Column(String(20))      # e.g. 2048m
    # DB 정보
    db_type         = Column(String(30))      # PostgreSQL | Oracle | MySQL | MSSQL | Tibero
    db_version      = Column(String(30))
    db_port         = Column(Integer)
    db_name         = Column(String(100))
    db_schema_name  = Column(String(100))
    # 유지보수 정보
    cpu_cores       = Column(Integer)
    memory_gb       = Column(Integer)
    disk_total_gb   = Column(Integer)
    maintenance_contact = Column(String(50))  # 담당자 사번
    install_date    = Column(Date)
    eol_date        = Column(Date)
    ssl_cert_path   = Column(String(255))
    ssl_expire_date = Column(Date)
    backup_path     = Column(String(255))
    note            = Column(Text)
    is_active       = Column(Boolean, default=True)
    last_check      = Column(DateTime)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())

    institution     = relationship("Institution", back_populates="servers")


class SRRequest(Base):
    __tablename__ = "tb_sr_request"

    id           = Column(Integer, primary_key=True, index=True)
    sr_id        = Column(String(30), unique=True, nullable=False, index=True)
    inst_id      = Column(Integer, ForeignKey("tb_inst_meta.id"))
    sr_type      = Column(String(20), default=SRType.OTHER)
    title        = Column(String(200), nullable=False)
    description  = Column(Text)
    status       = Column(String(30), default=SRStatus.RECEIVED)
    priority     = Column(String(20), default=Priority.MEDIUM)
    requested_by = Column(String(100))
    assigned_to  = Column(String(100))
    target_server = Column(String(100))
    created_at   = Column(DateTime, default=func.now())
    updated_at   = Column(DateTime, default=func.now(), onupdate=func.now())
    # ── A-2: SLA 타이머 & 자동 에스컬레이션 ─────────────────────────────────
    sla_deadline    = Column(DateTime, nullable=True)     # SLA 마감 시각
    sla_breached    = Column(Boolean, default=False)      # 마감 초과 여부
    escalated_at    = Column(DateTime, nullable=True)     # 에스컬레이션 발생 시각
    escalated_to    = Column(String(100), nullable=True)  # 에스컬레이션 수신자
    # ── G-7: AI 자동 분류 제안 ───────────────────────────────────────────────
    ai_suggestion   = Column(Text, nullable=True)         # JSON: AI 분류 결과
    # ── G-9: Jira 연동 ──────────────────────────────────────────────────────
    jira_key        = Column(String(30), nullable=True)   # Jira 이슈 키 (GUARDIA-42)

    institution  = relationship("Institution", back_populates="sr_requests")
    approvals    = relationship("ApprovalFlow", back_populates="sr_request")
    audit_logs   = relationship("AuditLog", back_populates="sr_request")


class ApprovalFlow(Base):
    __tablename__ = "tb_approval_flow"

    id          = Column(Integer, primary_key=True, index=True)
    sr_id       = Column(String(30), ForeignKey("tb_sr_request.sr_id"), nullable=False)
    approver    = Column(String(100), nullable=False)
    result      = Column(String(20), default=ApprovalResult.PENDING)
    comment     = Column(Text)
    decided_at  = Column(DateTime)
    created_at  = Column(DateTime, default=func.now())
    # ── G-11: 다중 승인 체계 ─────────────────────────────────────────────────
    delegate_to    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    delegate_until = Column(DateTime, nullable=True)
    deadline_at    = Column(DateTime, nullable=True)
    signature      = Column(Text, nullable=True)  # 전자서명 해시

    sr_request  = relationship("SRRequest", back_populates="approvals")


class AuditLog(Base):
    __tablename__ = "tb_audit_log"

    id           = Column(Integer, primary_key=True, index=True)
    sr_id        = Column(String(30), ForeignKey("tb_sr_request.sr_id"), nullable=True)
    actor        = Column(String(100))
    action       = Column(String(100), nullable=False)
    detail       = Column(Text)
    prev_hash    = Column(String(64))
    log_hash     = Column(String(64))
    created_at   = Column(DateTime, default=func.now())
    # ── D-5: 불변 감사 로그 확장 필드 ─────────────────────────────────────────
    entity_type  = Column(String(50), nullable=True)   # SR|RFC|PAM|USER|LDAP|VULN 등
    entity_id    = Column(String(50), nullable=True)   # 대상 엔티티 ID
    ip_addr_hash = Column(String(64), nullable=True)   # 클라이언트 IP SHA-256 (원본 저장 금지)
    severity     = Column(String(20), default="INFO")  # INFO|WARN|ERROR|CRITICAL

    sr_request   = relationship("SRRequest", back_populates="audit_logs")


class SRAutoReview(Base):
    """SR 접수 자동 리뷰 결과 — tmux 스냅샷 + Ollama AI 분석."""
    __tablename__ = "tb_sr_auto_review"

    id                   = Column(Integer, primary_key=True, index=True)
    sr_id                = Column(String(30), ForeignKey("tb_sr_request.sr_id"),
                                  unique=True, nullable=False, index=True)
    harness_name         = Column(String(100))
    status               = Column(String(20), default="pending")
    # reviewing | completed | failed
    summary              = Column(Text)
    root_cause           = Column(Text)
    recommended_actions  = Column(Text)   # JSON array
    estimated_minutes    = Column(Integer)
    risk_level           = Column(String(20))  # LOW|MEDIUM|HIGH|CRITICAL
    similar_count        = Column(Integer, default=0)
    auto_resolvable      = Column(Boolean, default=False)
    server_snapshot      = Column(Text)   # JSON: 서버 상태 스냅샷
    tmux_session         = Column(String(100))  # 엔지니어가 attach 할 세션명
    started_at           = Column(DateTime)
    completed_at         = Column(DateTime)


class OpsTask(Base):
    __tablename__ = "tb_ops_task"

    id           = Column(Integer, primary_key=True, index=True)
    sr_id        = Column(String(30), ForeignKey("tb_sr_request.sr_id"))
    task_name    = Column(String(200), nullable=False)
    task_order   = Column(Integer, default=0)
    status       = Column(String(30), default="PENDING")
    result_msg   = Column(Text)
    started_at   = Column(DateTime)
    finished_at  = Column(DateTime)
    created_at   = Column(DateTime, default=func.now())


# ── Self-Improving Learning Loop ORM Models ────────────────────────────────────

class RecurrencePattern(Base):
    """재발 패턴 추적 — 같은 문제가 반복될 때 자동 감지."""
    __tablename__ = "tb_recurrence_pattern"

    id                = Column(Integer, primary_key=True, index=True)
    pattern_hash      = Column(String(16), unique=True, index=True)
    pattern_key       = Column(String(200))
    sr_type           = Column(String(20), index=True)
    inst_id           = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    keyword_signature = Column(Text)          # 핵심 토큰 집합 (공백 구분)
    tech_keywords     = Column(String(200))   # 기술 키워드 (콤마 구분)
    occurrence_count  = Column(Integer, default=1)
    first_seen_at     = Column(DateTime, default=func.now())
    last_seen_at      = Column(DateTime, default=func.now())
    sr_ids            = Column(JSON)          # 연관 SR ID 목록
    escalated         = Column(Boolean, default=False)
    problem_id        = Column(String(30), nullable=True)
    created_at        = Column(DateTime, default=func.now())
    updated_at        = Column(DateTime, default=func.now(), onupdate=func.now())


class SolutionFeedback(Base):
    """KB 솔루션 효과 추적 — 해결책이 실제로 효과 있었는지 피드백."""
    __tablename__ = "tb_solution_feedback"

    id                   = Column(Integer, primary_key=True, index=True)
    sr_id                = Column(String(30), ForeignKey("tb_sr_request.sr_id"), index=True)
    kb_id                = Column(Integer, nullable=True)     # KBDocument.id (FK 미설정 — 유연성)
    kb_doc_id            = Column(String(30), nullable=True, index=True)
    applied_at           = Column(DateTime, default=func.now())
    resolved             = Column(Boolean, default=True)
    recurred_within_days = Column(Integer, nullable=True)
    effectiveness_score  = Column(Integer, default=0)         # +1 성공, -1 재발
    feedback_note        = Column(Text, nullable=True)
    checked_at           = Column(DateTime, nullable=True)
    created_at           = Column(DateTime, default=func.now())


class AdaptiveThreshold(Base):
    """적응형 임계값 — 이상 탐지 오탐/누락 기반 자동 보정."""
    __tablename__ = "tb_adaptive_threshold"

    id                = Column(Integer, primary_key=True, index=True)
    source            = Column(String(200), index=True)
    metric_type       = Column(String(50), index=True)
    base_threshold    = Column(Float, nullable=False)
    adapted_threshold = Column(Float, nullable=False)
    true_positive     = Column(Integer, default=0)
    false_positive    = Column(Integer, default=0)
    missed_count      = Column(Integer, default=0)
    last_adapted_at   = Column(DateTime, nullable=True)
    adaptation_count  = Column(Integer, default=0)
    created_at        = Column(DateTime, default=func.now())
    updated_at        = Column(DateTime, default=func.now(), onupdate=func.now())


class LessonLearned(Base):
    """검증된 교훈 — 효과 점수 10 이상 KB 또는 패턴 마이닝으로 자동 승격."""
    __tablename__ = "tb_lesson_learned"

    id                   = Column(Integer, primary_key=True, index=True)
    lesson_id            = Column(String(40), unique=True, index=True)
    title                = Column(String(200), nullable=False)
    category             = Column(String(50))
    problem_pattern      = Column(Text)
    root_cause           = Column(Text)
    effective_solution   = Column(Text)
    prevention           = Column(Text)
    confidence_score     = Column(Integer, default=0)
    source_kb_ids        = Column(JSON)
    source_sr_ids        = Column(JSON)
    promoted_from_kb_id  = Column(Integer, nullable=True)
    is_verified          = Column(Boolean, default=False)
    created_at           = Column(DateTime, default=func.now())
    updated_at           = Column(DateTime, default=func.now(), onupdate=func.now())


# ── Pydantic Schemas ───────────────────────────────────────────────────────────

class InstitutionOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    inst_code: str
    inst_name: str
    org_type: Optional[str]
    contact_pm: Optional[str]
    address: Optional[str]
    region: Optional[str]
    phone: Optional[str]
    contract_start: Optional[date]
    contract_end: Optional[date]
    sla_hours: int
    note: Optional[str]
    is_active: bool
    created_at: datetime
    updated_at: Optional[datetime]


class InstitutionCreate(BaseModel):
    inst_code: str
    inst_name: str
    org_type: Optional[str] = None
    contact_pm: Optional[str] = None
    address: Optional[str] = None
    region: Optional[str] = None
    phone: Optional[str] = None
    contract_start: Optional[date] = None
    contract_end: Optional[date] = None
    sla_hours: int = 4
    note: Optional[str] = None


class InstitutionUpdate(BaseModel):
    inst_name: Optional[str] = None
    org_type: Optional[str] = None
    contact_pm: Optional[str] = None
    address: Optional[str] = None
    region: Optional[str] = None
    phone: Optional[str] = None
    contract_start: Optional[date] = None
    contract_end: Optional[date] = None
    sla_hours: Optional[int] = None
    note: Optional[str] = None
    is_active: Optional[bool] = None


class InstContactOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    inst_id: int
    contact_name: str
    dept: Optional[str]
    position: Optional[str]
    role: str
    email: Optional[str]
    phone: Optional[str]
    mobile: Optional[str]
    is_primary: bool
    is_active: bool
    note: Optional[str]
    created_at: datetime


class InstContactCreate(BaseModel):
    contact_name: str
    dept: Optional[str] = None
    position: Optional[str] = None
    role: str = "ENGINEER"
    email: Optional[str] = None
    phone: Optional[str] = None
    mobile: Optional[str] = None
    is_primary: bool = False
    note: Optional[str] = None


class InstContactUpdate(BaseModel):
    contact_name: Optional[str] = None
    dept: Optional[str] = None
    position: Optional[str] = None
    role: Optional[str] = None
    email: Optional[str] = None
    phone: Optional[str] = None
    mobile: Optional[str] = None
    is_primary: Optional[bool] = None
    is_active: Optional[bool] = None
    note: Optional[str] = None


class ServerOut(BaseModel):
    """Server info — sensitive fields (ip_addr, ssh_user, os_pw_enc) intentionally omitted."""
    model_config = ConfigDict(from_attributes=True)

    id: int
    server_name: str
    server_role: Optional[str]
    os_type: Optional[str]
    os_version: Optional[str]
    ssh_method: Optional[str]
    web_server: Optional[str]
    web_version: Optional[str]
    web_port: Optional[int]
    web_conf_path: Optional[str]
    was_type: Optional[str]
    was_version: Optional[str]
    was_port: Optional[int]
    was_home: Optional[str]
    was_deploy_path: Optional[str]
    jdk_version: Optional[str]
    heap_min: Optional[str]
    heap_max: Optional[str]
    db_type: Optional[str]
    db_version: Optional[str]
    db_port: Optional[int]
    db_name: Optional[str]
    db_schema_name: Optional[str]
    cpu_cores: Optional[int]
    memory_gb: Optional[int]
    disk_total_gb: Optional[int]
    maintenance_contact: Optional[str]
    install_date: Optional[date]
    eol_date: Optional[date]
    ssl_expire_date: Optional[date]
    ssl_cert_path: Optional[str]
    backup_path: Optional[str]
    note: Optional[str]
    is_active: bool
    last_check: Optional[datetime]
    created_at: datetime
    updated_at: Optional[datetime]


class ServerUpdate(BaseModel):
    server_name: Optional[str] = None
    server_role: Optional[str] = None
    os_type: Optional[str] = None
    os_version: Optional[str] = None
    ssh_method: Optional[str] = None
    web_server: Optional[str] = None
    web_version: Optional[str] = None
    web_port: Optional[int] = None
    web_conf_path: Optional[str] = None
    was_type: Optional[str] = None
    was_version: Optional[str] = None
    was_port: Optional[int] = None
    was_home: Optional[str] = None
    was_deploy_path: Optional[str] = None
    jdk_version: Optional[str] = None
    heap_min: Optional[str] = None
    heap_max: Optional[str] = None
    db_type: Optional[str] = None
    db_version: Optional[str] = None
    db_port: Optional[int] = None
    db_name: Optional[str] = None
    db_schema_name: Optional[str] = None
    cpu_cores: Optional[int] = None
    memory_gb: Optional[int] = None
    disk_total_gb: Optional[int] = None
    maintenance_contact: Optional[str] = None
    install_date: Optional[date] = None
    eol_date: Optional[date] = None
    ssl_expire_date: Optional[date] = None
    ssl_cert_path: Optional[str] = None
    backup_path: Optional[str] = None
    note: Optional[str] = None
    is_active: Optional[bool] = None


class SRCreate(BaseModel):
    title: str
    description: Optional[str] = None
    sr_type: SRType = SRType.OTHER
    priority: Priority = Priority.MEDIUM
    requested_by: str
    assigned_to: Optional[str] = None
    target_server: Optional[str] = None
    inst_code: Optional[str] = None


class SROut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: str
    sr_type: str
    title: str
    description: Optional[str]
    status: str
    priority: str
    requested_by: str
    assigned_to: Optional[str]
    target_server: Optional[str]
    created_at: datetime
    updated_at: datetime
    # A-2 SLA 필드
    sla_deadline: Optional[datetime] = None
    sla_breached: bool = False
    escalated_at: Optional[datetime] = None
    escalated_to: Optional[str] = None


class SRStatusUpdate(BaseModel):
    status: SRStatus
    actor: str
    comment: Optional[str] = None


class ApprovalCreate(BaseModel):
    approver: str
    result: ApprovalResult
    comment: Optional[str] = None


class ApprovalOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: str
    approver: str
    result: str
    comment: Optional[str]
    decided_at: Optional[datetime]
    created_at: datetime


class AuditLogOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: Optional[str]
    actor: Optional[str]
    action: str
    detail: Optional[str]
    log_hash: Optional[str]
    prev_hash: Optional[str]
    entity_type: Optional[str]
    entity_id: Optional[str]
    severity: Optional[str]
    created_at: datetime


# ── Audit hash helper ──────────────────────────────────────────────────────────

def compute_log_hash(prev_hash: Optional[str], actor: str, action: str,
                     detail: str, ts: str) -> str:
    payload = json.dumps(
        {"prev": prev_hash or "", "actor": actor, "action": action,
         "detail": detail, "ts": ts},
        ensure_ascii=False, sort_keys=True
    )
    return hashlib.sha256(payload.encode()).hexdigest()


# ── WorkLog ORM ────────────────────────────────────────────────────────────────

# ── User / Auth Models ─────────────────────────────────────────────────────────

class UserRole(str, Enum):
    ADMIN           = "ADMIN"
    ENGINEER        = "ENGINEER"
    PM              = "PM"
    DEPLOY_ENGINEER = "DEPLOY_ENGINEER"   # Priority 4 신규: 배포 전담 엔지니어
    CUSTOMER        = "CUSTOMER"


class User(Base):
    __tablename__ = "tb_user"

    id             = Column(Integer, primary_key=True, index=True)
    username       = Column(String(50), unique=True, nullable=False, index=True)
    display_name   = Column(String(100))
    role           = Column(String(20), nullable=False, default=UserRole.ENGINEER)
    hashed_pw      = Column(String(200), nullable=False)
    must_change_pw = Column(Boolean, default=True)
    inst_code      = Column(String(20), nullable=True)   # CUSTOMER 역할의 소속 기관
    email          = Column(String(100), nullable=True)
    phone          = Column(String(20), nullable=True)
    dept           = Column(String(100), nullable=True)
    position       = Column(String(50), nullable=True)
    is_active      = Column(Boolean, default=True)
    created_at     = Column(DateTime, default=func.now())
    last_login_at  = Column(DateTime, nullable=True)
    # ── A-5: MFA (TOTP) ──────────────────────────────────────────────────────
    mfa_enabled     = Column(Boolean, default=False)         # MFA 활성 여부
    totp_secret_enc = Column(String(200), nullable=True)     # AES-GCM 암호화된 TOTP 시크릿
    # ── D-1: LDAP/AD 연동 ─────────────────────────────────────────────────────
    auth_type       = Column(String(20), default="LOCAL")    # LOCAL | LDAP
    department      = Column(String(100), nullable=True)     # LDAP department
    # -- Login security: account lockout
    failed_login_count = Column(Integer, default=0)
    locked_until       = Column(DateTime, nullable=True)


class UserOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    username: str
    display_name: Optional[str]
    role: str
    must_change_pw: bool
    inst_code: Optional[str]
    is_active: bool
    mfa_enabled: bool = False


class WorkActionType(str, Enum):
    CMDB_CHECK  = "CMDB_CHECK"   # 자산 확인
    SSH_CONNECT = "SSH_CONNECT"  # SSH 접속
    SSH_EXEC    = "SSH_EXEC"     # 명령 실행
    SOURCE_MOD  = "SOURCE_MOD"   # 소스/파일 배포
    HEALTH_CHECK = "HEALTH_CHECK" # 헬스체크
    RESULT      = "RESULT"       # 결과 기록
    COMPLETE    = "COMPLETE"     # 완료 처리


class WorkLog(Base):
    __tablename__ = "tb_work_log"

    id          = Column(Integer, primary_key=True, index=True)
    sr_id       = Column(String(30), ForeignKey("tb_sr_request.sr_id"), nullable=False)
    engineer    = Column(String(100), default="GUARDiA-AI")
    action_type = Column(String(30), nullable=False)
    content     = Column(Text)      # 수행 내용
    result      = Column(Text)      # 결과 출력
    is_success  = Column(Boolean, default=True)
    created_at  = Column(DateTime, default=func.now())


class Rating(Base):
    __tablename__ = "tb_rating"

    id         = Column(Integer, primary_key=True, index=True)
    sr_id      = Column(String(30), ForeignKey("tb_sr_request.sr_id"), unique=True)
    customer   = Column(String(100))
    stars      = Column(Integer)   # 1~5
    comment    = Column(Text)
    created_at = Column(DateTime, default=func.now())




class LicenseRecord(Base):
    """GUARDiA 라이선스 등록 이력 테이블."""
    __tablename__ = "tb_license"

    id           = Column(Integer, primary_key=True, index=True)
    license_key  = Column(Text, nullable=False)
    license_id   = Column(String(50), unique=True, nullable=False, index=True)
    edition      = Column(String(20), nullable=False)
    customer     = Column(String(200), nullable=False)
    issued_at    = Column(DateTime, nullable=True)
    expires_at   = Column(DateTime, nullable=False)
    limits       = Column(JSON, nullable=True)
    is_active    = Column(Boolean, default=True)
    is_trial     = Column(Boolean, default=False)
    activated_by = Column(String(50), nullable=True)
    activated_at = Column(DateTime, default=func.now())


# ── G-8: 보안 패치 추적 ──────────────────────────────────────────────────────

class VulnPatchRecord(Base):
    """취약점 패치 이력 테이블."""
    __tablename__ = "tb_vuln_patch"

    id          = Column(Integer, primary_key=True, index=True)
    server_id   = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    scan_id     = Column(String(50), nullable=True)
    cve_id      = Column(String(30), nullable=False, index=True)
    cvss_score  = Column(Float, nullable=True)
    severity    = Column(String(20), nullable=True)   # CRITICAL|HIGH|MEDIUM|LOW
    status      = Column(String(20), default="OPEN")  # OPEN|PATCHED|WONTFIX|MITIGATED
    patch_note  = Column(Text, nullable=True)
    patched_at  = Column(DateTime, nullable=True)
    patched_by  = Column(String(50), nullable=True)
    created_at  = Column(DateTime, default=func.now())
    updated_at  = Column(DateTime, default=func.now(), onupdate=func.now())


# ── G-10: PWA Push 구독 ──────────────────────────────────────────────────────

class PushSubscription(Base):
    """PWA 푸시 구독 정보 테이블."""
    __tablename__ = "tb_push_subscription"

    id          = Column(Integer, primary_key=True, index=True)
    user_id     = Column(Integer, ForeignKey("tb_user.id"), nullable=False, index=True)
    endpoint    = Column(Text, nullable=False, unique=True)
    p256dh      = Column(Text, nullable=False)
    auth        = Column(Text, nullable=False)
    user_agent  = Column(String(200), nullable=True)
    created_at  = Column(DateTime, default=func.now())


# ── Pydantic Schemas (new) ─────────────────────────────────────────────────────

class WorkLogOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: str
    engineer: Optional[str]
    action_type: str
    content: Optional[str]
    result: Optional[str]
    is_success: bool
    created_at: datetime


class WorkStepIn(BaseModel):
    engineer: str = "GUARDiA-AI"
    action_type: WorkActionType
    content: str
    result: Optional[str] = None
    is_success: bool = True


class RatingCreate(BaseModel):
    customer: str
    stars: int         # 1~5
    comment: Optional[str] = None


class RatingOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: str
    customer: Optional[str]
    stars: int
    comment: Optional[str]
    created_at: datetime


# ── Engineer Profile ────────────────────────────────────────────────────────────

class EngineerProfile(Base):
    """엔지니어 스킬 프로필 — 자동 배정에 사용."""
    __tablename__ = "tb_engineer_profile"

    id            = Column(Integer, primary_key=True, index=True)
    username      = Column(String(50), unique=True, nullable=False, index=True)
    display_name  = Column(String(100))
    skill_types   = Column(String(200))   # comma-sep SRType, e.g. "DEPLOY,RESTART"
    inst_affinity = Column(String(200))   # comma-sep inst_code, e.g. "MOF,MOIS"
    max_workload  = Column(Integer, default=5)
    is_available  = Column(Boolean, default=True)
    created_at    = Column(DateTime, default=func.now())


class EngineerProfileOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    username: str
    display_name: Optional[str]
    skill_types: Optional[str]
    inst_affinity: Optional[str]
    max_workload: int
    is_available: bool


# ── Knowledge Base ─────────────────────────────────────────────────────────────

class KBDocument(Base):
    """기술 지식 문서 — 장애 증상·원인·해결 방법."""
    __tablename__ = "tb_kb_document"

    id            = Column(Integer, primary_key=True, index=True)
    doc_id        = Column(String(30), unique=True, nullable=False, index=True)
    category      = Column(String(50))
    title         = Column(String(200), nullable=False)
    symptoms      = Column(Text)
    cause         = Column(Text)
    solution      = Column(Text)
    commands      = Column(Text)
    tags          = Column(String(300))
    sr_type       = Column(String(20))
    quality_score = Column(Integer, default=0)
    published     = Column(Boolean, default=False)
    source_sr_id  = Column(String(20), nullable=True, index=True)
    author        = Column(String(100), nullable=True)
    updated_at    = Column(DateTime, default=func.now(), onupdate=func.now())
    created_at    = Column(DateTime, default=func.now())


class KBDocumentOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    doc_id: str
    category: Optional[str]
    title: str
    symptoms: Optional[str]
    cause: Optional[str]
    solution: Optional[str]
    commands: Optional[str]
    tags: Optional[str]
    sr_type: Optional[str]


class KBSearchResult(BaseModel):
    doc: KBDocumentOut
    score: float
    matched_keywords: List[str]


# ── Shell Script Library ────────────────────────────────────────────────────────

class ShellScript(Base):
    """쉘 스크립트 라이브러리 — SM·정기점검·수시점검 등 모든 운영 스크립트 관리."""
    __tablename__ = "tb_shell_script"

    id               = Column(Integer, primary_key=True, index=True)
    script_name      = Column(String(200), nullable=False)
    category         = Column(String(30), nullable=False)  # SM|REGULAR|ADHOC|DEPLOY|SECURITY|MONITORING
    sub_category     = Column(String(50))
    target_layer     = Column(String(20), default="ALL")   # WEB|WAS|DB|ESB|ALL
    os_type          = Column(String(20), default="LINUX") # LINUX|WINDOWS|ALL
    description      = Column(Text, nullable=False)
    script_body      = Column(Text, nullable=False)
    parameters       = Column(Text)  # JSON string: [{"name":"TARGET","desc":"...","required":true}]
    sample_output    = Column(Text)
    is_dangerous     = Column(Boolean, default=False)
    requires_approval = Column(Boolean, default=False)
    author           = Column(String(50))
    version          = Column(String(20), default="1.0.0")
    tags             = Column(String(300))   # comma-separated
    inst_id          = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    # NULL = 공용 스크립트
    is_active        = Column(Boolean, default=True)
    use_count        = Column(Integer, default=0)
    created_at       = Column(DateTime, default=func.now())
    updated_at       = Column(DateTime, default=func.now(), onupdate=func.now())


class ShellScriptOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    script_name: str
    category: str
    sub_category: Optional[str]
    target_layer: str
    os_type: str
    description: str
    script_body: str
    parameters: Optional[str]
    sample_output: Optional[str]
    is_dangerous: bool
    requires_approval: bool
    author: Optional[str]
    version: str
    tags: Optional[str]
    inst_id: Optional[int]
    is_active: bool
    use_count: int
    created_at: datetime
    updated_at: Optional[datetime]


class ShellScriptCreate(BaseModel):
    script_name: str
    category: str
    sub_category: Optional[str] = None
    target_layer: str = "ALL"
    os_type: str = "LINUX"
    description: str
    script_body: str
    parameters: Optional[str] = None   # JSON string
    sample_output: Optional[str] = None
    is_dangerous: bool = False
    requires_approval: bool = False
    author: Optional[str] = None
    version: str = "1.0.0"
    tags: Optional[str] = None
    inst_id: Optional[int] = None


class ShellScriptUpdate(BaseModel):
    script_name: Optional[str] = None
    category: Optional[str] = None
    sub_category: Optional[str] = None
    target_layer: Optional[str] = None
    os_type: Optional[str] = None
    description: Optional[str] = None
    script_body: Optional[str] = None
    parameters: Optional[str] = None
    sample_output: Optional[str] = None
    is_dangerous: Optional[bool] = None
    requires_approval: Optional[bool] = None
    author: Optional[str] = None
    version: Optional[str] = None
    tags: Optional[str] = None
    is_active: Optional[bool] = None


# ── Work Timetable ──────────────────────────────────────────────────────────────

class WorkTimetable(Base):
    """작업 타임테이블 — SM·정기점검·PM·SR 기반 모든 작업 이력."""
    __tablename__ = "tb_work_timetable"

    id               = Column(Integer, primary_key=True, index=True)
    work_type        = Column(String(30), nullable=False)
    # REGULAR_CHECK | PM | SR | ADHOC | DEPLOY | EMERGENCY
    title            = Column(String(200), nullable=False)
    inst_id          = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    server_id        = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    sr_id            = Column(String(30), nullable=True)
    script_id        = Column(Integer, ForeignKey("tb_shell_script.id"), nullable=True)
    scheduled_at     = Column(DateTime, nullable=False)
    started_at       = Column(DateTime, nullable=True)
    completed_at     = Column(DateTime, nullable=True)
    content          = Column(Text, nullable=False)
    command_or_shell = Column(Text, nullable=True)
    result           = Column(Text, nullable=True)
    result_status    = Column(String(20), default="PENDING")
    # PENDING | SUCCESS | FAILED | PARTIAL | CANCELLED
    assignee         = Column(String(50), nullable=True)
    reviewer         = Column(String(50), nullable=True)
    note             = Column(Text, nullable=True)
    created_by       = Column(String(50), nullable=True)
    created_at       = Column(DateTime, default=func.now())
    updated_at       = Column(DateTime, default=func.now(), onupdate=func.now())

    institution      = relationship("Institution", foreign_keys=[inst_id])
    server           = relationship("Server",      foreign_keys=[server_id])
    script           = relationship("ShellScript", foreign_keys=[script_id])


class WorkTimetableOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    work_type: str
    title: str
    inst_id: Optional[int]
    server_id: Optional[int]
    sr_id: Optional[str]
    script_id: Optional[int]
    scheduled_at: datetime
    started_at: Optional[datetime]
    completed_at: Optional[datetime]
    content: str
    command_or_shell: Optional[str]
    result: Optional[str]
    result_status: str
    assignee: Optional[str]
    reviewer: Optional[str]
    note: Optional[str]
    created_by: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]


class WorkTimetableCreate(BaseModel):
    work_type: str
    title: str
    inst_id: Optional[int] = None
    server_id: Optional[int] = None
    sr_id: Optional[str] = None
    script_id: Optional[int] = None
    scheduled_at: datetime
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    content: str
    command_or_shell: Optional[str] = None
    result: Optional[str] = None
    result_status: str = "PENDING"
    assignee: Optional[str] = None
    reviewer: Optional[str] = None
    note: Optional[str] = None


class WorkTimetableUpdate(BaseModel):
    work_type: Optional[str] = None
    title: Optional[str] = None
    inst_id: Optional[int] = None
    server_id: Optional[int] = None
    sr_id: Optional[str] = None
    script_id: Optional[int] = None
    scheduled_at: Optional[datetime] = None
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    content: Optional[str] = None
    command_or_shell: Optional[str] = None
    result: Optional[str] = None
    result_status: Optional[str] = None
    assignee: Optional[str] = None
    reviewer: Optional[str] = None
    note: Optional[str] = None


# ── SR Attachment ───────────────────────────────────────────────────────────────

class SRAttachment(Base):
    """SR 첨부 파일 — 요청 시 업로드된 파일 메타데이터."""
    __tablename__ = "tb_sr_attachment"

    id            = Column(Integer, primary_key=True, index=True)
    sr_id         = Column(String(30), ForeignKey("tb_sr_request.sr_id"), nullable=False, index=True)
    original_name = Column(String(255), nullable=False)   # 원본 파일명
    stored_name   = Column(String(255), nullable=False)   # 저장 파일명 (충돌 방지)
    file_path     = Column(String(512), nullable=False)   # 서버 내부 경로 — API 응답에 노출 금지
    file_size     = Column(Integer, nullable=False)        # bytes
    content_type  = Column(String(100))
    uploaded_by   = Column(String(100), nullable=False)
    created_at    = Column(DateTime, default=func.now())

    sr_request    = relationship("SRRequest", foreign_keys=[sr_id])


class SRAttachmentOut(BaseModel):
    """첨부파일 응답 스키마 — file_path 노출 금지."""
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: str
    original_name: str
    file_size: int
    content_type: Optional[str]
    uploaded_by: str
    created_at: datetime


# ── Notification Log ────────────────────────────────────────────────────────────

class NotificationLog(Base):
    """알림 발송 이력 — 이메일·메신저 전송 결과 추적."""
    __tablename__ = "tb_notification_log"

    id          = Column(Integer, primary_key=True, index=True)
    sr_id       = Column(String(30), nullable=True, index=True)
    channel     = Column(String(20), nullable=False)   # EMAIL | MESSENGER
    recipient   = Column(String(200))                  # 수신자 (이메일 주소 또는 채널명)
    subject     = Column(String(300))
    status      = Column(String(20), nullable=False)   # SENT | FAILED | SKIPPED
    error_msg   = Column(Text, nullable=True)
    sent_at     = Column(DateTime, default=func.now())


class NotificationLogOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: Optional[str]
    channel: str
    recipient: Optional[str]
    subject: Optional[str]
    status: str
    error_msg: Optional[str]
    sent_at: datetime


# ── Project Source (바이브 코딩 / 배포 파이프라인) ──────────────────────────────

class Project(Base):
    """소스 프로젝트 등록 — 바이브 코딩·배포 파이프라인 연결."""
    __tablename__ = "tb_project"

    id               = Column(Integer, primary_key=True, index=True)
    project_name     = Column(String(100), unique=True, nullable=False, index=True)
    description      = Column(Text, nullable=True)
    source_path      = Column(String(512), nullable=True)   # 로컬 개발 PC 소스 절대 경로
    repo_url         = Column(String(512), nullable=True)   # Git 원격 주소
    branch           = Column(String(100), default="main")
    build_cmd        = Column(Text, nullable=True)           # 예: mvn clean package -DskipTests
    test_cmd         = Column(Text, nullable=True)           # 예: mvn test
    deploy_server_id = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    deploy_path      = Column(String(512), nullable=True)   # 서버 내 배포 경로
    was_restart_cmd  = Column(Text, nullable=True)           # 예: systemctl restart tomcat9
    health_check_url = Column(String(512), nullable=True)   # 예: http://localhost:8080/health
    jenkins_job_name = Column(String(200), nullable=True)   # Jenkins Job 이름 (미설정 시 "{project_name}-deploy" 사용)
    sr_type_hint     = Column(String(20), nullable=True)    # 연관 SR 유형 힌트
    # ── B-3: 코드 리뷰 연동 필드 ──────────────────────────────────────────────
    project_dir      = Column(String(200), nullable=True)   # C:\GUARDiA\projects\ 하위 폴더명
    tech_stack       = Column(String(50), nullable=True)    # java/python/php/javascript/mixed
    last_review_at   = Column(DateTime, nullable=True)
    last_review_score= Column(Integer, nullable=True)       # 0-100 종합 점수
    created_by       = Column(String(100), nullable=True)
    created_at       = Column(DateTime, default=func.now())
    updated_at       = Column(DateTime, default=func.now(), onupdate=func.now())

    deploy_server    = relationship("Server", foreign_keys=[deploy_server_id])


class ProjectOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    project_name: str
    description: Optional[str]
    source_path: Optional[str]
    repo_url: Optional[str]
    branch: str
    build_cmd: Optional[str]
    test_cmd: Optional[str]
    deploy_server_id: Optional[int]
    deploy_path: Optional[str]
    was_restart_cmd: Optional[str]
    health_check_url: Optional[str]
    jenkins_job_name: Optional[str]
    sr_type_hint: Optional[str]
    project_dir: Optional[str]
    tech_stack: Optional[str]
    last_review_at: Optional[datetime]
    last_review_score: Optional[int]
    created_by: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]


class ProjectCreate(BaseModel):
    project_name: str
    description: Optional[str] = None
    source_path: Optional[str] = None
    repo_url: Optional[str] = None
    branch: str = "main"
    build_cmd: Optional[str] = None
    test_cmd: Optional[str] = None
    deploy_server_id: Optional[int] = None
    deploy_path: Optional[str] = None
    was_restart_cmd: Optional[str] = None
    health_check_url: Optional[str] = None
    jenkins_job_name: Optional[str] = None
    sr_type_hint: Optional[str] = None
    project_dir: Optional[str] = None
    tech_stack: Optional[str] = None


class ProjectUpdate(BaseModel):
    project_name: Optional[str] = None
    description: Optional[str] = None
    source_path: Optional[str] = None
    repo_url: Optional[str] = None
    branch: Optional[str] = None
    build_cmd: Optional[str] = None
    test_cmd: Optional[str] = None
    deploy_server_id: Optional[int] = None
    deploy_path: Optional[str] = None
    was_restart_cmd: Optional[str] = None
    health_check_url: Optional[str] = None
    jenkins_job_name: Optional[str] = None
    sr_type_hint: Optional[str] = None


# ── Vibe Session (Claude CLI 바이브 코딩 세션) ──────────────────────────────────

class VibeSessionStatus(str, Enum):
    PENDING   = "PENDING"
    CODING    = "CODING"
    BUILDING  = "BUILDING"
    TESTING   = "TESTING"
    DEPLOYING = "DEPLOYING"
    COMPLETED = "COMPLETED"
    FAILED    = "FAILED"
    CANCELLED = "CANCELLED"


class VibeSession(Base):
    """Claude CLI 바이브 코딩 세션 — SR → 코딩 → 빌드 → 배포 파이프라인 추적."""
    __tablename__ = "tb_vibe_session"

    id               = Column(Integer, primary_key=True, index=True)
    sr_id            = Column(String(30), ForeignKey("tb_sr_request.sr_id"),
                               nullable=True, index=True)
    project_id       = Column(Integer, ForeignKey("tb_project.id"), nullable=True)
    claude_session_id = Column(String(128), nullable=True, index=True)
    # Claude CLI 세션 ID (claude --session-id 또는 SDK 세션 ID)
    workspace_path   = Column(String(512), nullable=True)   # 실제 작업 디렉터리
    status           = Column(String(20), nullable=False, default=VibeSessionStatus.PENDING)
    started_by       = Column(String(100), nullable=True)
    started_at       = Column(DateTime, default=func.now())
    coded_at         = Column(DateTime, nullable=True)       # 코딩 완료 시각
    built_at         = Column(DateTime, nullable=True)       # 빌드 완료 시각
    tested_at        = Column(DateTime, nullable=True)       # 테스트 완료 시각
    deployed_at      = Column(DateTime, nullable=True)       # 배포 완료 시각
    build_log        = Column(Text, nullable=True)           # 빌드 로그 요약
    test_result      = Column(Text, nullable=True)           # 테스트 결과 요약
    deploy_log       = Column(Text, nullable=True)           # 배포 로그 요약
    error_msg        = Column(Text, nullable=True)           # 실패 시 오류 요약

    sr_request       = relationship("SRRequest", foreign_keys=[sr_id])
    project          = relationship("Project",    foreign_keys=[project_id])


class VibeSessionOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sr_id: Optional[str]
    project_id: Optional[int]
    claude_session_id: Optional[str]
    workspace_path: Optional[str]
    status: str
    started_by: Optional[str]
    started_at: datetime
    coded_at: Optional[datetime]
    built_at: Optional[datetime]
    tested_at: Optional[datetime]
    deployed_at: Optional[datetime]
    build_log: Optional[str]
    test_result: Optional[str]
    deploy_log: Optional[str]
    error_msg: Optional[str]


class VibeSessionCreate(BaseModel):
    sr_id: Optional[str] = None
    project_id: Optional[int] = None
    claude_session_id: Optional[str] = None
    workspace_path: Optional[str] = None
    started_by: Optional[str] = None


class VibeSessionUpdate(BaseModel):
    status: Optional[VibeSessionStatus] = None
    claude_session_id: Optional[str] = None
    workspace_path: Optional[str] = None
    build_log: Optional[str] = None
    test_result: Optional[str] = None
    deploy_log: Optional[str] = None
    error_msg: Optional[str] = None


# ═══════════════════════════════════════════════════════════════════════════════
# ── SSL 인증서 이력 (tb_ssl_history) ──────────────────────────────────────────
# ═══════════════════════════════════════════════════════════════════════════════

class SslAlertLevel(str, Enum):
    OK      = "OK"       # 30일 초과
    WARN    = "WARN"     # 7~30일
    URGENT  = "URGENT"   # 1~7일
    EXPIRED = "EXPIRED"  # 만료됨


class SslHistory(Base):
    """SSL 인증서 갱신 이력 — Server.ssl_expire_date 변경 추적."""
    __tablename__ = "tb_ssl_history"

    id            = Column(Integer, primary_key=True, index=True)
    server_id     = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False, index=True)
    cert_domain   = Column(String(255), nullable=True)   # CN / SAN 주도메인
    old_expire    = Column(Date, nullable=True)           # 이전 만료일
    new_expire    = Column(Date, nullable=True)           # 갱신 후 만료일
    issuer        = Column(String(255), nullable=True)    # 발급기관
    cert_path     = Column(String(512), nullable=True)    # 서버 내 인증서 경로
    renewed_by    = Column(String(100), nullable=True)    # 갱신 처리자
    sr_id         = Column(String(30), ForeignKey("tb_sr_request.sr_id"), nullable=True)
    note          = Column(Text, nullable=True)
    created_at    = Column(DateTime, default=func.now())

    server        = relationship("Server", foreign_keys=[server_id])


class SslHistoryOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    server_id: int
    cert_domain: Optional[str]
    old_expire: Optional[date]
    new_expire: Optional[date]
    issuer: Optional[str]
    cert_path: Optional[str]
    renewed_by: Optional[str]
    sr_id: Optional[str]
    note: Optional[str]
    created_at: datetime


class SslHistoryCreate(BaseModel):
    server_id: int
    cert_domain: Optional[str] = None
    old_expire: Optional[date] = None
    new_expire: Optional[date] = None
    issuer: Optional[str] = None
    cert_path: Optional[str] = None
    renewed_by: Optional[str] = None
    sr_id: Optional[str] = None
    note: Optional[str] = None


class SslExpiryInfo(BaseModel):
    """SSL 만료 현황 API 응답 — ServerOut + 만료 정보 조합."""
    model_config = ConfigDict(from_attributes=True)
    server_id: int
    server_name: str
    inst_name: Optional[str] = None
    ssl_cert_path: Optional[str]
    ssl_expire_date: Optional[date]
    days_left: Optional[int]
    alert_level: SslAlertLevel
    last_checked: Optional[datetime] = None


# ═══════════════════════════════════════════════════════════════════════════════
# ── 정기 PM 체크리스트 (tb_pm_template / tb_pm_schedule / tb_pm_result) ───────
# ═══════════════════════════════════════════════════════════════════════════════

class PmFrequency(str, Enum):
    WEEKLY    = "WEEKLY"
    BIWEEKLY  = "BIWEEKLY"    # 격주
    MONTHLY   = "MONTHLY"
    QUARTERLY = "QUARTERLY"   # 분기
    SEMIANNUAL= "SEMIANNUAL"  # 반기
    ANNUAL    = "ANNUAL"
    CUSTOM    = "CUSTOM"      # cron 표현식


class PmItemResult(str, Enum):
    PASS    = "PASS"
    FAIL    = "FAIL"
    NA      = "NA"            # 해당 없음
    WARNING = "WARNING"


class PmTemplate(Base):
    """PM 체크리스트 템플릿 — 서버 유형별 기본 점검 항목."""
    __tablename__ = "tb_pm_template"

    id            = Column(Integer, primary_key=True, index=True)
    template_name = Column(String(100), nullable=False)
    server_role   = Column(String(20), nullable=True)  # WEB/WAS/DB/ALL 등
    category      = Column(String(50), nullable=True)  # OS/WEB/WAS/DB/SECURITY/NETWORK
    item_order    = Column(Integer, default=0)          # 항목 순서
    item_title    = Column(String(200), nullable=False) # 점검 항목명
    item_desc     = Column(Text, nullable=True)         # 점검 방법 설명
    check_command = Column(Text, nullable=True)         # 확인 명령어 (참고용)
    expected_value= Column(String(200), nullable=True)  # 정상 기준값
    is_mandatory  = Column(Boolean, default=True)       # 필수 항목 여부
    is_active     = Column(Boolean, default=True)
    created_at    = Column(DateTime, default=func.now())


class PmSchedule(Base):
    """정기 PM 반복 스케줄 — 자동으로 WorkTimetable 생성."""
    __tablename__ = "tb_pm_schedule"

    id             = Column(Integer, primary_key=True, index=True)
    schedule_name  = Column(String(100), nullable=False)
    inst_id        = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    server_id      = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    # 서버 미지정 시 기관 전체 서버 대상
    frequency      = Column(String(20), nullable=False, default=PmFrequency.MONTHLY)
    cron_expr      = Column(String(50), nullable=True)  # CUSTOM 시 cron 표현식
    day_of_month   = Column(Integer, nullable=True)     # MONTHLY: 매월 N일
    month_of_year  = Column(Integer, nullable=True)     # ANNUAL: N월
    advance_days   = Column(Integer, default=7)         # 시작 N일 전 WorkTimetable 생성
    template_ids   = Column(String(200), nullable=True) # 사용할 PmTemplate ID 목록 (콤마)
    assignee       = Column(String(100), nullable=True)
    reviewer       = Column(String(100), nullable=True)
    notify_before  = Column(Boolean, default=True)      # D-7 사전 알림
    notify_after   = Column(Boolean, default=True)      # 완료 후 기관 알림
    is_active      = Column(Boolean, default=True)
    last_generated = Column(DateTime, nullable=True)    # 마지막 WorkTimetable 생성 시각
    next_scheduled = Column(DateTime, nullable=True)    # 다음 예정 일시
    created_by     = Column(String(100), nullable=True)
    created_at     = Column(DateTime, default=func.now())
    updated_at     = Column(DateTime, default=func.now(), onupdate=func.now())

    institution    = relationship("Institution", foreign_keys=[inst_id])
    server         = relationship("Server", foreign_keys=[server_id])


class PmResult(Base):
    """PM 점검 실행 결과 — 항목별 체크 기록."""
    __tablename__ = "tb_pm_result"

    id              = Column(Integer, primary_key=True, index=True)
    timetable_id    = Column(Integer, ForeignKey("tb_work_timetable.id"),
                              nullable=False, index=True)
    template_id     = Column(Integer, ForeignKey("tb_pm_template.id"), nullable=True)
    item_title      = Column(String(200), nullable=False)
    item_desc       = Column(Text, nullable=True)
    check_command   = Column(Text, nullable=True)
    actual_value    = Column(Text, nullable=True)        # 실제 확인값
    result          = Column(String(20), default=PmItemResult.NA)
    result_note     = Column(Text, nullable=True)        # 비고
    checked_by      = Column(String(100), nullable=True)
    checked_at      = Column(DateTime, nullable=True)
    created_at      = Column(DateTime, default=func.now())

    timetable       = relationship("WorkTimetable", foreign_keys=[timetable_id])
    template        = relationship("PmTemplate", foreign_keys=[template_id])


# ── Pydantic schemas ─────────────────────────────────────────────────────────

class PmTemplateOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    template_name: str
    server_role: Optional[str]
    category: Optional[str]
    item_order: int
    item_title: str
    item_desc: Optional[str]
    check_command: Optional[str]
    expected_value: Optional[str]
    is_mandatory: bool
    is_active: bool


class PmTemplateCreate(BaseModel):
    template_name: str
    server_role: Optional[str] = None
    category: Optional[str] = None
    item_order: int = 0
    item_title: str
    item_desc: Optional[str] = None
    check_command: Optional[str] = None
    expected_value: Optional[str] = None
    is_mandatory: bool = True


class PmScheduleOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    schedule_name: str
    inst_id: Optional[int]
    server_id: Optional[int]
    frequency: str
    cron_expr: Optional[str]
    day_of_month: Optional[int]
    advance_days: int
    template_ids: Optional[str]
    assignee: Optional[str]
    reviewer: Optional[str]
    notify_before: bool
    notify_after: bool
    is_active: bool
    next_scheduled: Optional[datetime]
    created_by: Optional[str]
    created_at: datetime


class PmScheduleCreate(BaseModel):
    schedule_name: str
    inst_id: Optional[int] = None
    server_id: Optional[int] = None
    frequency: PmFrequency = PmFrequency.MONTHLY
    cron_expr: Optional[str] = None
    day_of_month: Optional[int] = None
    month_of_year: Optional[int] = None
    advance_days: int = 7
    template_ids: Optional[str] = None
    assignee: Optional[str] = None
    reviewer: Optional[str] = None
    notify_before: bool = True
    notify_after: bool = True


class PmResultOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    timetable_id: int
    template_id: Optional[int]
    item_title: str
    item_desc: Optional[str]
    check_command: Optional[str]
    actual_value: Optional[str]
    result: str
    result_note: Optional[str]
    checked_by: Optional[str]
    checked_at: Optional[datetime]


class PmResultUpdate(BaseModel):
    actual_value: Optional[str] = None
    result: PmItemResult = PmItemResult.NA
    result_note: Optional[str] = None


# ── Tenant AI (테넌트별 개인화 모델 + KB) ────────────────────────────────────

class TenantAIModel(Base):
    __tablename__ = "tb_tenant_ai_model"

    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(String(100), nullable=False, index=True)
    model_name   = Column(String(100))
    base_model   = Column(String(100), default="llama3")
    dataset_size = Column(Integer, default=0)
    status       = Column(String(20), default="pending")   # pending|training|ready|failed
    accuracy     = Column(Float, nullable=True)
    created_at   = Column(DateTime, default=func.now())


class TenantKBDoc(Base):
    __tablename__ = "tb_tenant_kb_doc"

    id         = Column(Integer, primary_key=True, index=True)
    tenant_id  = Column(String(100), nullable=False, index=True)
    title      = Column(String(300))
    content    = Column(Text)
    created_at = Column(DateTime, default=func.now())


# ── Workflow Engine (정의 + 실행 이력) ───────────────────────────────────────

class WorkflowDefinition(Base):
    __tablename__ = "tb_workflow_definition"

    id         = Column(Integer, primary_key=True, index=True)
    name       = Column(String(300))
    trigger    = Column(Text, nullable=True)   # JSON
    steps      = Column(Text, nullable=True)   # JSON
    active     = Column(Boolean, default=False)
    created_at = Column(DateTime, default=func.now())

    runs = relationship("WorkflowRun", back_populates="definition",
                        cascade="all, delete-orphan")


class WorkflowRun(Base):
    __tablename__ = "tb_workflow_run"

    id            = Column(Integer, primary_key=True, index=True)
    definition_id = Column(Integer, ForeignKey("tb_workflow_definition.id"), nullable=True)
    trigger_data  = Column(Text, nullable=True)   # JSON
    step_results  = Column(Text, nullable=True)   # JSON
    status        = Column(String(20), default="running")   # running|success|failed
    started_at    = Column(DateTime, default=func.now())
    finished_at   = Column(DateTime, nullable=True)

    definition = relationship("WorkflowDefinition", back_populates="runs")


# ═══════════════════════════════════════════════════════════════════════════════
# ── 장애 관리 (tb_incident) ────────────────────────────────────────────────────
# ═══════════════════════════════════════════════════════════════════════════════

class IncidentGrade(str, Enum):
    P1 = "P1"   # 전체 서비스 중단 (Critical)
    P2 = "P2"   # 주요 기능 장애 (High)
    P3 = "P3"   # 일부 기능 장애 (Medium)
    P4 = "P4"   # 성능 저하 / 경미 (Low)


class IncidentStatus(str, Enum):
    OPEN        = "OPEN"
    INVESTIGATING = "INVESTIGATING"
    MITIGATED   = "MITIGATED"   # 임시 조치
    RESOLVED    = "RESOLVED"
    CLOSED      = "CLOSED"


class Incident(Base):
    """장애 마스터 — P1~P4 등급, 발생~복구~RCA 전체 추적."""
    __tablename__ = "tb_incident"

    id              = Column(Integer, primary_key=True, index=True)
    incident_id     = Column(String(30), unique=True, nullable=False, index=True)
    # 예: INC-20260525-001234
    inst_id         = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    title           = Column(String(200), nullable=False)
    description     = Column(Text, nullable=True)
    grade           = Column(String(5), nullable=False, default=IncidentGrade.P3)
    status          = Column(String(20), nullable=False, default=IncidentStatus.OPEN)
    affected_servers= Column(Text, nullable=True)   # JSON 배열 (server_id 목록)
    affected_service= Column(String(200), nullable=True)
    occurred_at     = Column(DateTime, nullable=False, default=func.now())
    detected_at     = Column(DateTime, nullable=True)
    mitigated_at    = Column(DateTime, nullable=True)
    resolved_at     = Column(DateTime, nullable=True)
    closed_at       = Column(DateTime, nullable=True)
    rca             = Column(Text, nullable=True)   # Root Cause Analysis
    prevention      = Column(Text, nullable=True)   # 재발 방지 조치
    kb_doc_id       = Column(String(30), nullable=True)  # 연관 KB 문서
    reported_by     = Column(String(100), nullable=True)
    assigned_to     = Column(String(100), nullable=True)
    escalated_to    = Column(String(100), nullable=True)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())

    institution     = relationship("Institution", foreign_keys=[inst_id])


class IncidentSR(Base):
    """장애 ↔ SR 연결 (1:N — 하나의 장애에 복수 SR 묶기)."""
    __tablename__ = "tb_incident_sr"

    id          = Column(Integer, primary_key=True, index=True)
    incident_id = Column(String(30), ForeignKey("tb_incident.incident_id"), nullable=False)
    sr_id       = Column(String(30), ForeignKey("tb_sr_request.sr_id"), nullable=False)
    created_at  = Column(DateTime, default=func.now())


class IncidentOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    incident_id: str
    inst_id: Optional[int]
    title: str
    description: Optional[str]
    grade: str
    status: str
    affected_service: Optional[str]
    occurred_at: datetime
    detected_at: Optional[datetime]
    mitigated_at: Optional[datetime]
    resolved_at: Optional[datetime]
    rca: Optional[str]
    prevention: Optional[str]
    reported_by: Optional[str]
    assigned_to: Optional[str]
    created_at: datetime


class IncidentCreate(BaseModel):
    inst_id: Optional[int] = None
    title: str
    description: Optional[str] = None
    grade: IncidentGrade = IncidentGrade.P3
    affected_servers: Optional[str] = None
    affected_service: Optional[str] = None
    occurred_at: Optional[datetime] = None
    reported_by: Optional[str] = None
    assigned_to: Optional[str] = None


class IncidentUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    grade: Optional[IncidentGrade] = None
    status: Optional[IncidentStatus] = None
    affected_service: Optional[str] = None
    mitigated_at: Optional[datetime] = None
    resolved_at: Optional[datetime] = None
    rca: Optional[str] = None
    prevention: Optional[str] = None
    kb_doc_id: Optional[str] = None
    assigned_to: Optional[str] = None
    escalated_to: Optional[str] = None


# ═══════════════════════════════════════════════════════════════════════════════
# ── 온콜/당직 관리 (tb_oncall_schedule) ──────────────────────────────────────
# ═══════════════════════════════════════════════════════════════════════════════

class OncallSchedule(Base):
    """온콜/당직 일정 — 일별 당직자 + 에스컬레이션 대상."""
    __tablename__ = "tb_oncall_schedule"

    id              = Column(Integer, primary_key=True, index=True)
    duty_date       = Column(Date, nullable=False, index=True)    # 당직 날짜
    shift           = Column(String(20), default="ALL_DAY")
    # ALL_DAY / DAYTIME(09-18) / NIGHTTIME(18-09)
    engineer        = Column(String(100), nullable=False, index=True)  # 당직 담당자
    backup_engineer = Column(String(100), nullable=True)               # 백업 담당자
    escalation_to   = Column(String(100), nullable=True)               # 에스컬레이션
    note            = Column(Text, nullable=True)
    created_by      = Column(String(100), nullable=True)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())


class OncallScheduleOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    duty_date: date
    shift: str
    engineer: str
    backup_engineer: Optional[str]
    escalation_to: Optional[str]
    note: Optional[str]


class OncallScheduleCreate(BaseModel):
    duty_date: date
    shift: str = "ALL_DAY"
    engineer: str
    backup_engineer: Optional[str] = None
    escalation_to: Optional[str] = None
    note: Optional[str] = None


# ═══════════════════════════════════════════════════════════════════════════════
# ── On-Call 자동 로테이션 설정 (tb_oncall_rotate_config)  A-5 ─────────────────
# ═══════════════════════════════════════════════════════════════════════════════

class OncallRotateConfig(Base):
    """
    On-Call 자동 로테이션 설정.
    엔지니어 목록을 순환하여 매일 당직자를 자동 배정한다.
    레코드는 1개(싱글톤)로 운영 — id=1 을 upsert 방식으로 사용.
    """
    __tablename__ = "tb_oncall_rotate_config"

    id              = Column(Integer, primary_key=True, index=True)
    is_active       = Column(Boolean, default=True)          # 자동 배정 활성 여부
    # 로테이션 대상 엔지니어 목록 (JSON 배열 문자열, 예: '["alice","bob","charlie"]')
    engineer_list   = Column(Text, nullable=False, default="[]")
    current_index   = Column(Integer, default=0)             # 현재 순번
    rotate_days     = Column(Integer, default=1)             # 순환 주기(일)
    default_shift   = Column(String(20), default="ALL_DAY")  # ALL_DAY/DAYTIME/NIGHTTIME
    # 에스컬레이션 체인: 당직 미응답 시 순서대로 알림
    escalation_chain = Column(Text, nullable=True)           # JSON 배열 문자열
    notify_on_assign = Column(Boolean, default=True)         # 배정 시 알림 발송
    advance_days     = Column(Integer, default=1)            # 며칠 전에 미리 배정
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())
    updated_by      = Column(String(100), nullable=True)


class OncallRotateConfigOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    is_active: bool
    engineer_list: str   # raw JSON string; 클라이언트에서 파싱
    current_index: int
    rotate_days: int
    default_shift: str
    escalation_chain: Optional[str]
    notify_on_assign: bool
    advance_days: int
    updated_at: Optional[datetime]
    updated_by: Optional[str]


class OncallRotateConfigUpdate(BaseModel):
    is_active: Optional[bool] = None
    engineer_list: Optional[List[str]] = None   # 파이썬 리스트로 입력
    current_index: Optional[int] = None
    rotate_days: Optional[int] = None
    default_shift: Optional[str] = None
    escalation_chain: Optional[List[str]] = None
    notify_on_assign: Optional[bool] = None
    advance_days: Optional[int] = None


# ═══════════════════════════════════════════════════════════════════════════════
# ── 배치 작업 관리 (tb_batch_job) ─────────────────────────────────────────────
# ═══════════════════════════════════════════════════════════════════════════════

class BatchJobStatus(str, Enum):
    ACTIVE   = "ACTIVE"
    DISABLED = "DISABLED"


class BatchRunResult(str, Enum):
    SUCCESS = "SUCCESS"
    FAILED  = "FAILED"
    TIMEOUT = "TIMEOUT"
    RUNNING = "RUNNING"
    SKIPPED = "SKIPPED"


class BatchJob(Base):
    """배치 작업 등록 — cron 주기, 실행 서버, 명령어."""
    __tablename__ = "tb_batch_job"

    id            = Column(Integer, primary_key=True, index=True)
    job_name      = Column(String(100), nullable=False, unique=True)
    inst_id       = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    server_id     = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    cron_expr     = Column(String(50), nullable=False)   # "0 2 * * *"
    command       = Column(Text, nullable=False)
    timeout_sec   = Column(Integer, default=3600)
    status        = Column(String(20), default=BatchJobStatus.ACTIVE)
    alert_on_fail = Column(Boolean, default=True)
    owner         = Column(String(100), nullable=True)
    last_run_at   = Column(DateTime, nullable=True)
    last_result   = Column(String(20), nullable=True)
    created_at    = Column(DateTime, default=func.now())
    updated_at    = Column(DateTime, default=func.now(), onupdate=func.now())

    server        = relationship("Server", foreign_keys=[server_id])


class BatchRun(Base):
    """배치 실행 이력."""
    __tablename__ = "tb_batch_run"

    id          = Column(Integer, primary_key=True, index=True)
    job_id      = Column(Integer, ForeignKey("tb_batch_job.id"), nullable=False, index=True)
    started_at  = Column(DateTime, nullable=False, default=func.now())
    ended_at    = Column(DateTime, nullable=True)
    result      = Column(String(20), default=BatchRunResult.RUNNING)
    exit_code   = Column(Integer, nullable=True)
    stdout_tail = Column(Text, nullable=True)   # 마지막 100줄
    error_msg   = Column(Text, nullable=True)
    sr_id       = Column(String(30), nullable=True)  # 실패 시 자동 생성 SR


class BatchJobOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    job_name: str
    inst_id: Optional[int]
    server_id: Optional[int]
    cron_expr: str
    command: str
    timeout_sec: int
    status: str
    alert_on_fail: bool
    owner: Optional[str]
    last_run_at: Optional[datetime]
    last_result: Optional[str]


class BatchJobCreate(BaseModel):
    job_name: str
    inst_id: Optional[int] = None
    server_id: Optional[int] = None
    cron_expr: str
    command: str
    timeout_sec: int = 3600
    alert_on_fail: bool = True
    owner: Optional[str] = None


class BatchRunOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    job_id: int
    started_at: datetime
    ended_at: Optional[datetime]
    result: str
    exit_code: Optional[int]
    stdout_tail: Optional[str]
    error_msg: Optional[str]
    sr_id: Optional[str]


# ═══════════════════════════════════════════════════════════════════════════════
# ── SI 프로젝트 관리 — 표준 방법론 (분석→설계→구현→인도)
# ═══════════════════════════════════════════════════════════════════════════════

class ProjectPhase(str, Enum):
    INITIATION     = "INITIATION"      # 착수
    ANALYSIS       = "ANALYSIS"        # 분석
    DESIGN         = "DESIGN"          # 설계
    IMPLEMENTATION = "IMPLEMENTATION"  # 구현
    DEPLOYMENT     = "DEPLOYMENT"      # 인도
    STABILIZATION  = "STABILIZATION"   # 안정화
    CLOSED         = "CLOSED"          # 종료


class PhaseActivity(str, Enum):
    # 분석 단계
    ASIS_ANALYSIS        = "ASIS_ANALYSIS"         # 현행시스템분석
    REQ_COLLECTION       = "REQ_COLLECTION"        # 요구사항수집및정의
    PROCESS_REDESIGN     = "PROCESS_REDESIGN"      # 업무프로세스재설계
    DATA_MODELING        = "DATA_MODELING"         # 데이터모델링
    REQ_CONFIRMATION     = "REQ_CONFIRMATION"      # 요구사항명세서확정
    # 설계 단계
    ARCH_DESIGN          = "ARCH_DESIGN"           # 시스템아키텍처설계
    UI_UX_DESIGN         = "UI_UX_DESIGN"          # UI/UX설계
    DB_DESIGN            = "DB_DESIGN"             # 상세데이터설계
    INTERFACE_DESIGN     = "INTERFACE_DESIGN"      # 인터페이스설계
    FRAMEWORK_DESIGN     = "FRAMEWORK_DESIGN"      # 공통모듈/프레임워크설계
    # 구현 단계
    ENV_SETUP            = "ENV_SETUP"             # 환경구축
    CODE_DEV             = "CODE_DEV"              # 소스코드개발
    UNIT_TEST            = "UNIT_TEST"             # 단위테스트
    INTEGRATION_TEST     = "INTEGRATION_TEST"      # 통합테스트
    DATA_MIGRATION       = "DATA_MIGRATION"        # 데이터이관
    # 인도 단계
    UAT                  = "UAT"                   # 사용자수용성테스트
    PROD_DEPLOY          = "PROD_DEPLOY"           # 운영환경배포
    USER_TRAINING        = "USER_TRAINING"         # 사용자교육
    STABILIZATION_SUPPORT = "STABILIZATION_SUPPORT" # 안정화지원
    CLOSEOUT             = "CLOSEOUT"              # 최종산출물검수및종료


# 단계별 기본 서브 활동 정의 (프로젝트 생성 시 체크리스트 자동 생성용)
PHASE_DEFAULT_ACTIVITIES: dict = {
    ProjectPhase.ANALYSIS: [
        (PhaseActivity.ASIS_ANALYSIS,    "현행 시스템 분석 (AS-IS)"),
        (PhaseActivity.REQ_COLLECTION,   "요구사항 수집 및 정의"),
        (PhaseActivity.PROCESS_REDESIGN, "업무 프로세스 재설계 (TO-BE)"),
        (PhaseActivity.DATA_MODELING,    "데이터 모델링 (개념/논리)"),
        (PhaseActivity.REQ_CONFIRMATION, "요구사항 명세서 확정 (고객 승인)"),
    ],
    ProjectPhase.DESIGN: [
        (PhaseActivity.ARCH_DESIGN,      "시스템 아키텍처 설계"),
        (PhaseActivity.UI_UX_DESIGN,     "UI/UX 설계 (스토리보드·와이어프레임)"),
        (PhaseActivity.DB_DESIGN,        "상세 데이터 설계 (물리 DB)"),
        (PhaseActivity.INTERFACE_DESIGN, "인터페이스 설계 (API 명세)"),
        (PhaseActivity.FRAMEWORK_DESIGN, "공통 모듈/프레임워크 설계"),
    ],
    ProjectPhase.IMPLEMENTATION: [
        (PhaseActivity.ENV_SETUP,        "개발/테스트 환경 구축"),
        (PhaseActivity.CODE_DEV,         "소스 코드 개발"),
        (PhaseActivity.UNIT_TEST,        "단위 테스트"),
        (PhaseActivity.INTEGRATION_TEST, "통합 테스트"),
        (PhaseActivity.DATA_MIGRATION,   "데이터 이관 개발"),
    ],
    ProjectPhase.DEPLOYMENT: [
        (PhaseActivity.UAT,                   "사용자 수용성 테스트 (UAT)"),
        (PhaseActivity.PROD_DEPLOY,           "운영 환경 배포"),
        (PhaseActivity.USER_TRAINING,         "사용자 교육 (매뉴얼·교육 실시)"),
        (PhaseActivity.STABILIZATION_SUPPORT, "안정화 지원"),
        (PhaseActivity.CLOSEOUT,              "최종 산출물 검수 및 프로젝트 종료"),
    ],
}


class ReqType(str, Enum):
    FUNCTIONAL     = "FUNCTIONAL"      # 기능 요구사항
    NON_FUNCTIONAL = "NON_FUNCTIONAL"  # 비기능 요구사항
    CONSTRAINT     = "CONSTRAINT"      # 제약 조건
    INTERFACE      = "INTERFACE"       # 인터페이스 요구사항
    MIGRATION      = "MIGRATION"       # 데이터이관 요구사항


class ReqStatus(str, Enum):
    DRAFT     = "DRAFT"
    CONFIRMED = "CONFIRMED"
    CHANGED   = "CHANGED"
    CANCELLED = "CANCELLED"


class WbsStatus(str, Enum):
    NOT_STARTED = "NOT_STARTED"
    IN_PROGRESS = "IN_PROGRESS"
    COMPLETED   = "COMPLETED"
    DELAYED     = "DELAYED"
    CANCELLED   = "CANCELLED"


class RiskLevel(str, Enum):
    LOW      = "LOW"
    MEDIUM   = "MEDIUM"
    HIGH     = "HIGH"
    CRITICAL = "CRITICAL"


class RiskStatus(str, Enum):
    OPEN      = "OPEN"
    MITIGATED = "MITIGATED"
    ACCEPTED  = "ACCEPTED"
    CLOSED    = "CLOSED"


class IssueType(str, Enum):
    BUG                = "BUG"
    REQUIREMENT_CHANGE = "REQUIREMENT_CHANGE"
    RESOURCE           = "RESOURCE"
    SCHEDULE           = "SCHEDULE"
    TECHNICAL          = "TECHNICAL"
    OTHER              = "OTHER"


class IssueStatus(str, Enum):
    OPEN        = "OPEN"
    IN_PROGRESS = "IN_PROGRESS"
    RESOLVED    = "RESOLVED"
    CLOSED      = "CLOSED"
    DEFERRED    = "DEFERRED"


class CrType(str, Enum):
    SCOPE    = "SCOPE"
    SCHEDULE = "SCHEDULE"
    BUDGET   = "BUDGET"
    SPEC     = "SPEC"


class CrStatus(str, Enum):
    DRAFT       = "DRAFT"
    REVIEW      = "REVIEW"
    APPROVED    = "APPROVED"
    REJECTED    = "REJECTED"
    IMPLEMENTED = "IMPLEMENTED"


class TestType(str, Enum):
    UNIT        = "UNIT"
    INTEGRATION = "INTEGRATION"
    UAT         = "UAT"
    PERFORMANCE = "PERFORMANCE"
    SECURITY    = "SECURITY"


class SiTestResult(str, Enum):
    PASS    = "PASS"
    FAIL    = "FAIL"
    BLOCKED = "BLOCKED"
    NOT_RUN = "NOT_RUN"


class DefectSeverity(str, Enum):
    CRITICAL = "CRITICAL"
    HIGH     = "HIGH"
    MEDIUM   = "MEDIUM"
    LOW      = "LOW"


class DefectStatus(str, Enum):
    OPEN     = "OPEN"
    ASSIGNED = "ASSIGNED"
    FIXED    = "FIXED"
    VERIFIED = "VERIFIED"
    CLOSED   = "CLOSED"
    DEFERRED = "DEFERRED"


# ── ORM: SI 프로젝트 마스터 ───────────────────────────────────────────────────

class SiProject(Base):
    """SI 프로젝트 마스터 — 착수부터 SM 전환까지."""
    __tablename__ = "tb_si_project"

    id               = Column(Integer, primary_key=True, index=True)
    project_code     = Column(String(30), unique=True, nullable=False, index=True)
    project_name     = Column(String(200), nullable=False)
    inst_id          = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    phase            = Column(String(30), nullable=False, default=ProjectPhase.INITIATION)
    description      = Column(Text, nullable=True)
    contract_amount  = Column(Integer, nullable=True)     # 계약금액 (만원)
    planned_start    = Column(Date, nullable=True)
    planned_end      = Column(Date, nullable=True)
    actual_start     = Column(Date, nullable=True)
    actual_end       = Column(Date, nullable=True)
    pm_user          = Column(String(100), nullable=True)  # 담당 PM
    dev_lead         = Column(String(100), nullable=True)  # 개발 리드
    customer_pm      = Column(String(100), nullable=True)  # 고객사 PM
    team_members     = Column(Text, nullable=True)         # JSON array of usernames
    budget_total     = Column(Integer, nullable=True)      # 예산 (만원)
    budget_used      = Column(Integer, default=0)
    overall_progress = Column(Integer, default=0)          # 전체 진척률 0-100
    health_status    = Column(String(10), default="GREEN") # GREEN / YELLOW / RED
    is_active        = Column(Boolean, default=True)
    converted_to_sm  = Column(Boolean, default=False)      # SM 전환 여부
    note             = Column(Text, nullable=True)
    created_by       = Column(String(100), nullable=True)
    created_at       = Column(DateTime, default=func.now())
    updated_at       = Column(DateTime, default=func.now(), onupdate=func.now())

    institution      = relationship("Institution", foreign_keys=[inst_id])
    requirements     = relationship("SiRequirement",    back_populates="project",
                                    cascade="all, delete-orphan")
    wbs_items        = relationship("WbsItem",          back_populates="project",
                                    cascade="all, delete-orphan")
    issues           = relationship("ProjectIssue",     back_populates="project",
                                    cascade="all, delete-orphan")
    risks            = relationship("ProjectRisk",      back_populates="project",
                                    cascade="all, delete-orphan")
    milestones       = relationship("ProjectMilestone", back_populates="project",
                                    cascade="all, delete-orphan")
    change_requests  = relationship("ChangeRequest",    back_populates="project",
                                    cascade="all, delete-orphan")
    test_plans       = relationship("SiTestPlan",       back_populates="project",
                                    cascade="all, delete-orphan")
    phase_checklists = relationship("SiPhaseChecklist", back_populates="project",
                                    cascade="all, delete-orphan")
    deliverables     = relationship("Deliverable",      back_populates="project",
                                    cascade="all, delete-orphan")


class SiPhaseChecklist(Base):
    """단계별 서브 활동 체크리스트 — 표준 방법론 4단계 자동 생성."""
    __tablename__ = "tb_si_phase_checklist"

    id            = Column(Integer, primary_key=True, index=True)
    project_id    = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    phase         = Column(String(30), nullable=False)
    activity      = Column(String(50), nullable=False)
    activity_name = Column(String(200), nullable=False)
    is_done       = Column(Boolean, default=False)
    done_by       = Column(String(100), nullable=True)
    done_at       = Column(DateTime, nullable=True)
    note          = Column(Text, nullable=True)
    created_at    = Column(DateTime, default=func.now())

    project       = relationship("SiProject", back_populates="phase_checklists")


# ── ORM: WBS ─────────────────────────────────────────────────────────────────

class WbsItem(Base):
    """WBS 항목 — 자기 참조 계층(최대 4레벨), 리프 노드만 진척 직접 입력."""
    __tablename__ = "tb_wbs_item"

    id             = Column(Integer, primary_key=True, index=True)
    project_id     = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    parent_id      = Column(Integer, ForeignKey("tb_wbs_item.id"), nullable=True)
    wbs_code       = Column(String(50), nullable=False)    # 예: 1.2.3
    title          = Column(String(200), nullable=False)
    phase          = Column(String(30), nullable=True)
    activity       = Column(String(50), nullable=True)
    assignee       = Column(String(100), nullable=True)
    planned_start  = Column(Date, nullable=True)
    planned_end    = Column(Date, nullable=True)
    actual_start   = Column(Date, nullable=True)
    actual_end     = Column(Date, nullable=True)
    completion_pct = Column(Integer, default=0)            # 0-100 (리프만 직접 입력)
    status         = Column(String(20), default=WbsStatus.NOT_STARTED)
    weight         = Column(Integer, default=1)            # 부모 진척 가중치
    level          = Column(Integer, default=1)            # 1~4
    is_leaf        = Column(Boolean, default=True)
    deliverable    = Column(String(200), nullable=True)
    note           = Column(Text, nullable=True)
    created_at     = Column(DateTime, default=func.now())
    updated_at     = Column(DateTime, default=func.now(), onupdate=func.now())

    project        = relationship("SiProject", back_populates="wbs_items")
    children       = relationship(
        "WbsItem",
        backref=backref("parent", remote_side="WbsItem.id"),
        foreign_keys="WbsItem.parent_id",
    )


# ── ORM: 요구사항 ─────────────────────────────────────────────────────────────

class SiRequirement(Base):
    """요구사항 관리 — RTM(요구사항→WBS→TC) 추적."""
    __tablename__ = "tb_si_requirement"

    id                  = Column(Integer, primary_key=True, index=True)
    req_id              = Column(String(30), unique=True, nullable=False, index=True)
    project_id          = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    req_type            = Column(String(30), nullable=False, default=ReqType.FUNCTIONAL)
    title               = Column(String(200), nullable=False)
    description         = Column(Text, nullable=True)
    source              = Column(String(200), nullable=True)   # 출처 (RFP p.XX, 인터뷰 등)
    priority            = Column(String(20), default="MEDIUM")
    status              = Column(String(20), default=ReqStatus.DRAFT)
    wbs_item_id         = Column(Integer, ForeignKey("tb_wbs_item.id"), nullable=True)
    acceptance_criteria = Column(Text, nullable=True)
    created_by          = Column(String(100), nullable=True)
    confirmed_by        = Column(String(100), nullable=True)
    confirmed_at        = Column(DateTime, nullable=True)
    created_at          = Column(DateTime, default=func.now())
    updated_at          = Column(DateTime, default=func.now(), onupdate=func.now())

    project             = relationship("SiProject", back_populates="requirements")
    wbs_item            = relationship("WbsItem", foreign_keys=[wbs_item_id])
    test_cases          = relationship("SiTestCase", back_populates="requirement")


# ── ORM: 이슈 ─────────────────────────────────────────────────────────────────

class ProjectIssue(Base):
    """프로젝트 이슈 관리 — 일정/자원/기술 등 장애 요인 추적."""
    __tablename__ = "tb_project_issue"

    id           = Column(Integer, primary_key=True, index=True)
    issue_id     = Column(String(30), unique=True, nullable=False, index=True)
    project_id   = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    wbs_item_id  = Column(Integer, ForeignKey("tb_wbs_item.id"), nullable=True)
    issue_type   = Column(String(30), nullable=False, default=IssueType.OTHER)
    title        = Column(String(200), nullable=False)
    description  = Column(Text, nullable=True)
    priority     = Column(String(20), default="MEDIUM")
    status       = Column(String(20), default=IssueStatus.OPEN)
    raised_by    = Column(String(100), nullable=True)
    assigned_to  = Column(String(100), nullable=True)
    due_date     = Column(Date, nullable=True)
    resolved_at  = Column(DateTime, nullable=True)
    resolution   = Column(Text, nullable=True)
    impact       = Column(Text, nullable=True)
    from_risk_id = Column(Integer, nullable=True)   # 위험 전환 원본 ID
    created_at   = Column(DateTime, default=func.now())
    updated_at   = Column(DateTime, default=func.now(), onupdate=func.now())

    project      = relationship("SiProject", back_populates="issues")


# ── ORM: 위험 ─────────────────────────────────────────────────────────────────

class ProjectRisk(Base):
    """위험 관리 — 확률×영향 매트릭스, HIGH×HIGH 시 자동 이슈 전환."""
    __tablename__ = "tb_project_risk"

    id                = Column(Integer, primary_key=True, index=True)
    risk_id           = Column(String(30), unique=True, nullable=False, index=True)
    project_id        = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    title             = Column(String(200), nullable=False)
    description       = Column(Text, nullable=True)
    category          = Column(String(50), nullable=True)   # 기술/일정/자원/요구사항/외부
    probability_level = Column(Integer, nullable=False, default=2)  # 1=LOW 2=MED 3=HIGH
    impact_level      = Column(Integer, nullable=False, default=2)  # 1=LOW 2=MED 3=HIGH
    risk_score        = Column(Integer, nullable=False, default=4)  # probability × impact
    risk_level        = Column(String(20), default=RiskLevel.MEDIUM)
    status            = Column(String(20), default=RiskStatus.OPEN)
    mitigation_plan   = Column(Text, nullable=True)
    contingency_plan  = Column(Text, nullable=True)
    owner             = Column(String(100), nullable=True)
    due_date          = Column(Date, nullable=True)
    converted_to_issue = Column(Boolean, default=False)
    created_at        = Column(DateTime, default=func.now())
    updated_at        = Column(DateTime, default=func.now(), onupdate=func.now())

    project           = relationship("SiProject", back_populates="risks")


# ── ORM: 마일스톤 & 산출물 ────────────────────────────────────────────────────

class ProjectMilestone(Base):
    """마일스톤 관리 — 단계 완료 기준점."""
    __tablename__ = "tb_project_milestone"

    id               = Column(Integer, primary_key=True, index=True)
    project_id       = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    phase            = Column(String(30), nullable=True)
    title            = Column(String(200), nullable=False)
    description      = Column(Text, nullable=True)
    planned_date     = Column(Date, nullable=False)
    actual_date      = Column(Date, nullable=True)
    is_completed     = Column(Boolean, default=False)
    completed_by     = Column(String(100), nullable=True)
    note             = Column(Text, nullable=True)
    created_at       = Column(DateTime, default=func.now())

    project          = relationship("SiProject", back_populates="milestones")
    deliverables     = relationship("ProjectDeliverable", back_populates="milestone")


class ProjectDeliverable(Base):
    """산출물 관리 — 단계별 제출 문서/결과물."""
    __tablename__ = "tb_project_deliverable"

    id             = Column(Integer, primary_key=True, index=True)
    project_id     = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    milestone_id   = Column(Integer, ForeignKey("tb_project_milestone.id"), nullable=True)
    phase          = Column(String(30), nullable=True)
    activity       = Column(String(50), nullable=True)
    title          = Column(String(200), nullable=False)
    doc_type       = Column(String(50), nullable=True)    # 계획서/명세서/설계서/보고서
    due_date       = Column(Date, nullable=True)
    submitted_date = Column(Date, nullable=True)
    is_submitted   = Column(Boolean, default=False)
    is_approved    = Column(Boolean, default=False)
    approved_by    = Column(String(100), nullable=True)
    file_path      = Column(String(512), nullable=True)   # 내부 경로 — API 노출 금지
    version        = Column(String(20), default="1.0")
    note           = Column(Text, nullable=True)
    created_at     = Column(DateTime, default=func.now())

    project        = relationship("SiProject", foreign_keys=[project_id])
    milestone      = relationship("ProjectMilestone", back_populates="deliverables")


# ── ORM: 산출물 관리 ──────────────────────────────────────────────────────────

class DeliverableStatus(str, Enum):
    PENDING   = "PENDING"    # 미제출
    SUBMITTED = "SUBMITTED"  # 제출됨
    REVIEWING = "REVIEWING"  # 검토중
    APPROVED  = "APPROVED"   # 승인
    REJECTED  = "REJECTED"   # 반려


class DeliverableType(str, Enum):
    DOCUMENT    = "DOCUMENT"     # 문서 (분석서, 설계서 등)
    CODE        = "CODE"         # 소스코드
    TEST_RESULT = "TEST_RESULT"  # 테스트 결과
    DESIGN      = "DESIGN"       # 설계도/UI
    REPORT      = "REPORT"       # 보고서
    MANUAL      = "MANUAL"       # 매뉴얼
    OTHER       = "OTHER"


class Deliverable(Base):
    """산출물(Deliverable) — WBS 항목·마일스톤별 제출 추적."""
    __tablename__ = "tb_deliverable"

    id              = Column(Integer, primary_key=True, index=True)
    project_id      = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    wbs_item_id     = Column(Integer, ForeignKey("tb_wbs_item.id"),   nullable=True)
    milestone_id    = Column(Integer, ForeignKey("tb_project_milestone.id"), nullable=True)
    # 식별
    name            = Column(String(200), nullable=False)
    description     = Column(Text, nullable=True)
    deliverable_type= Column(String(30), default=DeliverableType.DOCUMENT)
    # 상태
    status          = Column(String(20), default=DeliverableStatus.PENDING)
    version         = Column(String(20), default="1.0")
    # 일정
    due_date        = Column(Date, nullable=True)
    submitted_at    = Column(DateTime, nullable=True)
    submitted_by    = Column(String(100), nullable=True)
    # 검토
    reviewer        = Column(String(100), nullable=True)
    reviewed_at     = Column(DateTime, nullable=True)
    review_comment  = Column(Text, nullable=True)
    # 파일
    file_path       = Column(String(500), nullable=True)
    file_name       = Column(String(200), nullable=True)
    # 이력
    created_by      = Column(String(100), nullable=True)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())

    project  = relationship("SiProject", foreign_keys=[project_id])


class DeliverableOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id:              int
    project_id:      int
    wbs_item_id:     Optional[int]  = None
    milestone_id:    Optional[int]  = None
    name:            str
    deliverable_type:str
    status:          str
    version:         str
    due_date:        Optional[date] = None
    submitted_at:    Optional[datetime] = None
    submitted_by:    Optional[str]  = None
    reviewer:        Optional[str]  = None
    reviewed_at:     Optional[datetime] = None
    review_comment:  Optional[str]  = None
    file_name:       Optional[str]  = None
    created_at:      datetime


class DeliverableCreate(BaseModel):
    project_id:      int
    wbs_item_id:     Optional[int] = None
    milestone_id:    Optional[int] = None
    name:            str
    description:     Optional[str] = None
    deliverable_type:str = DeliverableType.DOCUMENT
    version:         str = "1.0"
    due_date:        Optional[date] = None
    reviewer:        Optional[str] = None


class DeliverableUpdate(BaseModel):
    name:            Optional[str] = None
    description:     Optional[str] = None
    status:          Optional[str] = None
    version:         Optional[str] = None
    due_date:        Optional[date] = None
    reviewer:        Optional[str] = None
    review_comment:  Optional[str] = None


# ── ORM: 변경 요청 ────────────────────────────────────────────────────────────

class ChangeRequest(Base):
    """변경 요청 (CR) — 범위/일정/예산/명세 변경 이력 관리."""
    __tablename__ = "tb_change_request"

    id              = Column(Integer, primary_key=True, index=True)
    cr_id           = Column(String(30), unique=True, nullable=False, index=True)
    project_id      = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    cr_type         = Column(String(20), nullable=False, default=CrType.SCOPE)
    title           = Column(String(200), nullable=False)
    description     = Column(Text, nullable=True)
    reason          = Column(Text, nullable=True)
    impact_scope    = Column(Text, nullable=True)
    impact_schedule = Column(Text, nullable=True)
    impact_budget   = Column(Text, nullable=True)
    status          = Column(String(20), default=CrStatus.DRAFT)
    requested_by    = Column(String(100), nullable=True)
    reviewed_by     = Column(String(100), nullable=True)
    approved_by     = Column(String(100), nullable=True)
    approved_at     = Column(DateTime, nullable=True)
    implemented_at  = Column(DateTime, nullable=True)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())

    project         = relationship("SiProject", back_populates="change_requests")


# ── ORM: 테스트 관리 ──────────────────────────────────────────────────────────

class SiTestPlan(Base):
    """테스트 계획 — 단위/통합/UAT 유형별 관리."""
    __tablename__ = "tb_si_test_plan"

    id             = Column(Integer, primary_key=True, index=True)
    project_id     = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    phase          = Column(String(30), nullable=True)
    test_type      = Column(String(30), nullable=False, default=TestType.INTEGRATION)
    plan_name      = Column(String(200), nullable=False)
    description    = Column(Text, nullable=True)
    planned_start  = Column(Date, nullable=True)
    planned_end    = Column(Date, nullable=True)
    actual_start   = Column(Date, nullable=True)
    actual_end     = Column(Date, nullable=True)
    total_cases    = Column(Integer, default=0)
    pass_count     = Column(Integer, default=0)
    fail_count     = Column(Integer, default=0)
    blocked_count  = Column(Integer, default=0)
    pass_rate      = Column(Integer, default=0)
    is_completed   = Column(Boolean, default=False)
    created_by     = Column(String(100), nullable=True)
    created_at     = Column(DateTime, default=func.now())
    updated_at     = Column(DateTime, default=func.now(), onupdate=func.now())

    project        = relationship("SiProject", back_populates="test_plans")
    test_cases     = relationship("SiTestCase", back_populates="test_plan",
                                   cascade="all, delete-orphan")


class SiTestCase(Base):
    """테스트 케이스 — RTM(TC→요구사항) 연결."""
    __tablename__ = "tb_si_test_case"

    id              = Column(Integer, primary_key=True, index=True)
    tc_id           = Column(String(30), unique=True, nullable=False, index=True)
    plan_id         = Column(Integer, ForeignKey("tb_si_test_plan.id"), nullable=False, index=True)
    req_id          = Column(Integer, ForeignKey("tb_si_requirement.id"), nullable=True)
    title           = Column(String(200), nullable=False)
    precondition    = Column(Text, nullable=True)
    test_steps      = Column(Text, nullable=True)         # JSON array
    expected_result = Column(Text, nullable=True)
    last_result     = Column(String(20), default=SiTestResult.NOT_RUN)
    assigned_to     = Column(String(100), nullable=True)
    priority        = Column(String(20), default="MEDIUM")
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())

    test_plan       = relationship("SiTestPlan", back_populates="test_cases")
    requirement     = relationship("SiRequirement", back_populates="test_cases")
    executions      = relationship("SiTestExecution", back_populates="test_case",
                                    cascade="all, delete-orphan")


class SiDefect(Base):
    """결함 관리 — 심각도별 OPEN→FIXED→VERIFIED 흐름."""
    __tablename__ = "tb_si_defect"

    id           = Column(Integer, primary_key=True, index=True)
    defect_id    = Column(String(30), unique=True, nullable=False, index=True)
    project_id   = Column(Integer, ForeignKey("tb_si_project.id"), nullable=False, index=True)
    tc_id        = Column(Integer, ForeignKey("tb_si_test_case.id"), nullable=True)
    req_id       = Column(Integer, ForeignKey("tb_si_requirement.id"), nullable=True)
    title        = Column(String(200), nullable=False)
    description  = Column(Text, nullable=True)
    severity     = Column(String(20), nullable=False, default=DefectSeverity.MEDIUM)
    status       = Column(String(20), nullable=False, default=DefectStatus.OPEN)
    phase        = Column(String(30), nullable=True)    # 발견 단계
    reported_by  = Column(String(100), nullable=True)
    assigned_to  = Column(String(100), nullable=True)
    fixed_by     = Column(String(100), nullable=True)
    fixed_at     = Column(DateTime, nullable=True)
    verified_by  = Column(String(100), nullable=True)
    verified_at  = Column(DateTime, nullable=True)
    build_found  = Column(String(50), nullable=True)
    build_fixed  = Column(String(50), nullable=True)
    note         = Column(Text, nullable=True)
    created_at   = Column(DateTime, default=func.now())
    updated_at   = Column(DateTime, default=func.now(), onupdate=func.now())


class SiTestExecution(Base):
    """테스트 실행 이력 — 동일 TC 반복 실행 추적."""
    __tablename__ = "tb_si_test_execution"

    id             = Column(Integer, primary_key=True, index=True)
    tc_id          = Column(Integer, ForeignKey("tb_si_test_case.id"), nullable=False, index=True)
    executed_by    = Column(String(100), nullable=True)
    executed_at    = Column(DateTime, default=func.now())
    result         = Column(String(20), nullable=False, default=SiTestResult.NOT_RUN)
    actual_result  = Column(Text, nullable=True)
    defect_id      = Column(Integer, ForeignKey("tb_si_defect.id"), nullable=True)
    note           = Column(Text, nullable=True)
    build_version  = Column(String(50), nullable=True)

    test_case      = relationship("SiTestCase", back_populates="executions")


# ── Pydantic Schemas: SiProject ───────────────────────────────────────────────

class SiProjectOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    project_code: str
    project_name: str
    inst_id: Optional[int]
    phase: str
    description: Optional[str]
    contract_amount: Optional[int]
    planned_start: Optional[date]
    planned_end: Optional[date]
    actual_start: Optional[date]
    actual_end: Optional[date]
    pm_user: Optional[str]
    dev_lead: Optional[str]
    customer_pm: Optional[str]
    team_members: Optional[str]
    budget_total: Optional[int]
    budget_used: int
    overall_progress: int
    health_status: str
    is_active: bool
    converted_to_sm: bool
    note: Optional[str]
    created_by: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]


class SiProjectCreate(BaseModel):
    project_name: str
    inst_id: Optional[int] = None
    description: Optional[str] = None
    contract_amount: Optional[int] = None
    planned_start: Optional[date] = None
    planned_end: Optional[date] = None
    pm_user: Optional[str] = None
    dev_lead: Optional[str] = None
    customer_pm: Optional[str] = None
    team_members: Optional[str] = None
    budget_total: Optional[int] = None
    note: Optional[str] = None


class SiProjectUpdate(BaseModel):
    project_name: Optional[str] = None
    inst_id: Optional[int] = None
    phase: Optional[ProjectPhase] = None
    description: Optional[str] = None
    contract_amount: Optional[int] = None
    planned_start: Optional[date] = None
    planned_end: Optional[date] = None
    actual_start: Optional[date] = None
    actual_end: Optional[date] = None
    pm_user: Optional[str] = None
    dev_lead: Optional[str] = None
    customer_pm: Optional[str] = None
    team_members: Optional[str] = None
    budget_total: Optional[int] = None
    budget_used: Optional[int] = None
    overall_progress: Optional[int] = None
    health_status: Optional[str] = None
    is_active: Optional[bool] = None
    note: Optional[str] = None


class SiPhaseChecklistOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    project_id: int
    phase: str
    activity: str
    activity_name: str
    is_done: bool
    done_by: Optional[str]
    done_at: Optional[datetime]
    note: Optional[str]


class SiPhaseChecklistUpdate(BaseModel):
    is_done: bool
    done_by: Optional[str] = None
    note: Optional[str] = None


# ── Pydantic Schemas: WBS ─────────────────────────────────────────────────────

class WbsItemOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    project_id: int
    parent_id: Optional[int]
    wbs_code: str
    title: str
    phase: Optional[str]
    activity: Optional[str]
    assignee: Optional[str]
    planned_start: Optional[date]
    planned_end: Optional[date]
    actual_start: Optional[date]
    actual_end: Optional[date]
    completion_pct: int
    status: str
    weight: int
    level: int
    is_leaf: bool
    deliverable: Optional[str]
    note: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]


class WbsItemCreate(BaseModel):
    parent_id: Optional[int] = None
    wbs_code: str
    title: str
    phase: Optional[ProjectPhase] = None
    activity: Optional[PhaseActivity] = None
    assignee: Optional[str] = None
    planned_start: Optional[date] = None
    planned_end: Optional[date] = None
    weight: int = 1
    deliverable: Optional[str] = None
    note: Optional[str] = None


class WbsItemUpdate(BaseModel):
    title: Optional[str] = None
    phase: Optional[ProjectPhase] = None
    activity: Optional[PhaseActivity] = None
    assignee: Optional[str] = None
    planned_start: Optional[date] = None
    planned_end: Optional[date] = None
    actual_start: Optional[date] = None
    actual_end: Optional[date] = None
    completion_pct: Optional[int] = None
    status: Optional[WbsStatus] = None
    weight: Optional[int] = None
    deliverable: Optional[str] = None
    note: Optional[str] = None


# ── Pydantic Schemas: 요구사항 ────────────────────────────────────────────────

class SiRequirementOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    req_id: str
    project_id: int
    req_type: str
    title: str
    description: Optional[str]
    source: Optional[str]
    priority: str
    status: str
    wbs_item_id: Optional[int]
    acceptance_criteria: Optional[str]
    created_by: Optional[str]
    confirmed_by: Optional[str]
    confirmed_at: Optional[datetime]
    created_at: datetime
    updated_at: Optional[datetime]


class SiRequirementCreate(BaseModel):
    req_type: ReqType = ReqType.FUNCTIONAL
    title: str
    description: Optional[str] = None
    source: Optional[str] = None
    priority: str = "MEDIUM"
    wbs_item_id: Optional[int] = None
    acceptance_criteria: Optional[str] = None
    created_by: Optional[str] = None


class SiRequirementUpdate(BaseModel):
    req_type: Optional[ReqType] = None
    title: Optional[str] = None
    description: Optional[str] = None
    source: Optional[str] = None
    priority: Optional[str] = None
    status: Optional[ReqStatus] = None
    wbs_item_id: Optional[int] = None
    acceptance_criteria: Optional[str] = None
    confirmed_by: Optional[str] = None


# ── Pydantic Schemas: 이슈 ────────────────────────────────────────────────────

class ProjectIssueOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    issue_id: str
    project_id: int
    wbs_item_id: Optional[int]
    issue_type: str
    title: str
    description: Optional[str]
    priority: str
    status: str
    raised_by: Optional[str]
    assigned_to: Optional[str]
    due_date: Optional[date]
    resolved_at: Optional[datetime]
    resolution: Optional[str]
    impact: Optional[str]
    from_risk_id: Optional[int]
    created_at: datetime
    updated_at: Optional[datetime]


class ProjectIssueCreate(BaseModel):
    wbs_item_id: Optional[int] = None
    issue_type: IssueType = IssueType.OTHER
    title: str
    description: Optional[str] = None
    priority: str = "MEDIUM"
    raised_by: Optional[str] = None
    assigned_to: Optional[str] = None
    due_date: Optional[date] = None
    impact: Optional[str] = None


class ProjectIssueUpdate(BaseModel):
    issue_type: Optional[IssueType] = None
    title: Optional[str] = None
    description: Optional[str] = None
    priority: Optional[str] = None
    status: Optional[IssueStatus] = None
    assigned_to: Optional[str] = None
    due_date: Optional[date] = None
    resolved_at: Optional[datetime] = None
    resolution: Optional[str] = None
    impact: Optional[str] = None


# ── Pydantic Schemas: 위험 ────────────────────────────────────────────────────

class ProjectRiskOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    risk_id: str
    project_id: int
    title: str
    description: Optional[str]
    category: Optional[str]
    probability_level: int
    impact_level: int
    risk_score: int
    risk_level: str
    status: str
    mitigation_plan: Optional[str]
    contingency_plan: Optional[str]
    owner: Optional[str]
    due_date: Optional[date]
    converted_to_issue: bool
    created_at: datetime
    updated_at: Optional[datetime]


class ProjectRiskCreate(BaseModel):
    title: str
    description: Optional[str] = None
    category: Optional[str] = None
    probability_level: int = 2
    impact_level: int = 2
    mitigation_plan: Optional[str] = None
    contingency_plan: Optional[str] = None
    owner: Optional[str] = None
    due_date: Optional[date] = None


class ProjectRiskUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    category: Optional[str] = None
    probability_level: Optional[int] = None
    impact_level: Optional[int] = None
    status: Optional[RiskStatus] = None
    mitigation_plan: Optional[str] = None
    contingency_plan: Optional[str] = None
    owner: Optional[str] = None
    due_date: Optional[date] = None


# ── Pydantic Schemas: 마일스톤/산출물 ────────────────────────────────────────

class ProjectMilestoneOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    project_id: int
    phase: Optional[str]
    title: str
    description: Optional[str]
    planned_date: date
    actual_date: Optional[date]
    is_completed: bool
    completed_by: Optional[str]
    note: Optional[str]
    created_at: datetime


class ProjectMilestoneCreate(BaseModel):
    phase: Optional[ProjectPhase] = None
    title: str
    description: Optional[str] = None
    planned_date: date
    note: Optional[str] = None


class ProjectMilestoneUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    planned_date: Optional[date] = None
    actual_date: Optional[date] = None
    is_completed: Optional[bool] = None
    completed_by: Optional[str] = None
    note: Optional[str] = None


class ProjectDeliverableOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    project_id: int
    milestone_id: Optional[int]
    phase: Optional[str]
    activity: Optional[str]
    title: str
    doc_type: Optional[str]
    due_date: Optional[date]
    submitted_date: Optional[date]
    is_submitted: bool
    is_approved: bool
    approved_by: Optional[str]
    version: str
    note: Optional[str]
    created_at: datetime


class ProjectDeliverableCreate(BaseModel):
    milestone_id: Optional[int] = None
    phase: Optional[ProjectPhase] = None
    activity: Optional[PhaseActivity] = None
    title: str
    doc_type: Optional[str] = None
    due_date: Optional[date] = None
    version: str = "1.0"
    note: Optional[str] = None


class ProjectDeliverableUpdate(BaseModel):
    title: Optional[str] = None
    doc_type: Optional[str] = None
    due_date: Optional[date] = None
    submitted_date: Optional[date] = None
    is_submitted: Optional[bool] = None
    is_approved: Optional[bool] = None
    approved_by: Optional[str] = None
    version: Optional[str] = None
    note: Optional[str] = None


# ── Pydantic Schemas: 변경 요청 ───────────────────────────────────────────────

class ChangeRequestOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    cr_id: str
    project_id: int
    cr_type: str
    title: str
    description: Optional[str]
    reason: Optional[str]
    impact_scope: Optional[str]
    impact_schedule: Optional[str]
    impact_budget: Optional[str]
    status: str
    requested_by: Optional[str]
    reviewed_by: Optional[str]
    approved_by: Optional[str]
    approved_at: Optional[datetime]
    implemented_at: Optional[datetime]
    created_at: datetime
    updated_at: Optional[datetime]


class ChangeRequestCreate(BaseModel):
    cr_type: CrType = CrType.SCOPE
    title: str
    description: Optional[str] = None
    reason: Optional[str] = None
    impact_scope: Optional[str] = None
    impact_schedule: Optional[str] = None
    impact_budget: Optional[str] = None
    requested_by: Optional[str] = None


class ChangeRequestUpdate(BaseModel):
    cr_type: Optional[CrType] = None
    title: Optional[str] = None
    description: Optional[str] = None
    reason: Optional[str] = None
    impact_scope: Optional[str] = None
    impact_schedule: Optional[str] = None
    impact_budget: Optional[str] = None
    status: Optional[CrStatus] = None
    reviewed_by: Optional[str] = None
    approved_by: Optional[str] = None
    approved_at: Optional[datetime] = None
    implemented_at: Optional[datetime] = None


# ── Pydantic Schemas: 테스트 ──────────────────────────────────────────────────

class SiTestPlanOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    project_id: int
    phase: Optional[str]
    test_type: str
    plan_name: str
    description: Optional[str]
    planned_start: Optional[date]
    planned_end: Optional[date]
    actual_start: Optional[date]
    actual_end: Optional[date]
    total_cases: int
    pass_count: int
    fail_count: int
    blocked_count: int
    pass_rate: int
    is_completed: bool
    created_by: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]


class SiTestPlanCreate(BaseModel):
    phase: Optional[ProjectPhase] = None
    test_type: TestType = TestType.INTEGRATION
    plan_name: str
    description: Optional[str] = None
    planned_start: Optional[date] = None
    planned_end: Optional[date] = None
    created_by: Optional[str] = None


class SiTestCaseOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    tc_id: str
    plan_id: int
    req_id: Optional[int]
    title: str
    precondition: Optional[str]
    test_steps: Optional[str]
    expected_result: Optional[str]
    last_result: str
    assigned_to: Optional[str]
    priority: str
    created_at: datetime
    updated_at: Optional[datetime]


class SiTestCaseCreate(BaseModel):
    req_id: Optional[int] = None
    title: str
    precondition: Optional[str] = None
    test_steps: Optional[str] = None
    expected_result: Optional[str] = None
    assigned_to: Optional[str] = None
    priority: str = "MEDIUM"


class SiTestExecutionOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    tc_id: int
    executed_by: Optional[str]
    executed_at: datetime
    result: str
    actual_result: Optional[str]
    defect_id: Optional[int]
    note: Optional[str]
    build_version: Optional[str]


class SiTestExecutionCreate(BaseModel):
    executed_by: Optional[str] = None
    result: SiTestResult
    actual_result: Optional[str] = None
    note: Optional[str] = None
    build_version: Optional[str] = None


class SiDefectOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    defect_id: str
    project_id: int
    tc_id: Optional[int]
    req_id: Optional[int]
    title: str
    description: Optional[str]
    severity: str
    status: str
    phase: Optional[str]
    reported_by: Optional[str]
    assigned_to: Optional[str]
    fixed_by: Optional[str]
    fixed_at: Optional[datetime]
    verified_by: Optional[str]
    verified_at: Optional[datetime]
    build_found: Optional[str]
    build_fixed: Optional[str]
    note: Optional[str]
    created_at: datetime
    updated_at: Optional[datetime]


class SiDefectCreate(BaseModel):
    tc_id: Optional[int] = None
    req_id: Optional[int] = None
    title: str
    description: Optional[str] = None
    severity: DefectSeverity = DefectSeverity.MEDIUM
    phase: Optional[str] = None
    reported_by: Optional[str] = None
    assigned_to: Optional[str] = None
    build_found: Optional[str] = None
    note: Optional[str] = None


class SiDefectUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    severity: Optional[DefectSeverity] = None
    status: Optional[DefectStatus] = None
    assigned_to: Optional[str] = None
    fixed_by: Optional[str] = None
    fixed_at: Optional[datetime] = None
    verified_by: Optional[str] = None
    verified_at: Optional[datetime] = None
    build_fixed: Optional[str] = None
    note: Optional[str] = None


# ══════════════════════════════════════════════════════════════════════════════
# ── AI 에이전트 (Paperclip 스타일 하트비트 기반) ──────────────────────────────
# 온프레미스 보안: 모든 LLM 추론은 Ollama(localhost:11434) 전용.
# 외부 AI/API 호출 금지 — core/llm_client.py 참조.
# ══════════════════════════════════════════════════════════════════════════════

class AgentRole(str, Enum):
    # ── 개발 조직 에이전트 (Phase 1 Paperclip) ──
    CEO             = "CEO"           # 총괄: 목표 분배·보고
    CTO             = "CTO"           # 기술 아키텍처·코드 리뷰
    DEVELOPER       = "DEVELOPER"     # 코드 생성·구현
    QA              = "QA"            # 테스트케이스·품질
    PM_AGENT        = "PM_AGENT"      # 일정·WBS 관리
    # ── 운영 자동화 에이전트 (Phase 3 런타임) ──
    INCIDENT_TRIAGE = "INCIDENT_TRIAGE"  # 인시던트 자동 분류·배정
    KB_CURATOR      = "KB_CURATOR"       # 해결 SR → KB 자동 생성
    SSL_WATCHER     = "SSL_WATCHER"      # SSL 만료 → SR 자동 생성
    WBS_MONITOR     = "WBS_MONITOR"      # WBS 지연 위험 분석
    PM_SUGGESTER    = "PM_SUGGESTER"     # PM 스케줄 미등록 서버 권고


class AgentStatus(str, Enum):
    IDLE    = "IDLE"    # 대기 (하트비트 사이)
    ACTIVE  = "ACTIVE"  # 하트비트로 깨어남
    WORKING = "WORKING" # 작업 처리 중
    ERROR   = "ERROR"   # 오류 발생
    PAUSED  = "PAUSED"  # 일시 중지 (관리자)


class LLMProvider(str, Enum):
    OLLAMA = "ollama"   # 로컬 Ollama (온프레미스, 기본값·권장)
    CLAUDE = "claude"   # Anthropic Claude API (개발·테스트 전용)
    OPENAI = "openai"   # OpenAI API (개발·테스트 전용)


class AgentTaskStatus(str, Enum):
    PENDING     = "PENDING"
    IN_PROGRESS = "IN_PROGRESS"
    COMPLETED   = "COMPLETED"
    FAILED      = "FAILED"
    CANCELLED   = "CANCELLED"


class AgentApprovalStatus(str, Enum):
    PENDING       = "PENDING"       # 사람 승인 대기
    APPROVED      = "APPROVED"      # 승인
    REJECTED      = "REJECTED"      # 거부
    AUTO_APPROVED = "AUTO_APPROVED" # 저위험 자동 승인


# ── ORM Models ───────────────────────────────────────────────────────────────

class AgentConfig(Base):
    """에이전트 설정 — 역할·LLM 모델·하트비트 스케줄 정의."""
    __tablename__ = "tb_agent_config"

    id             = Column(Integer, primary_key=True, index=True)
    name           = Column(String(100), nullable=False)
    role           = Column(String(30), nullable=False)          # AgentRole
    description    = Column(Text)
    llm_provider   = Column(String(20), default="ollama")        # LLMProvider
    llm_model      = Column(String(80), default="guardia-agent") # Ollama 모델명
    system_prompt  = Column(Text)
    heartbeat_cron = Column(String(50))   # cron 표현식 예: "*/15 * * * *"
    is_active      = Column(Boolean, default=True)
    status         = Column(String(20), default="IDLE")          # AgentStatus
    last_heartbeat = Column(DateTime)
    last_error     = Column(Text)
    total_tasks    = Column(Integer, default=0)
    total_tokens   = Column(Integer, default=0)
    created_by     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at     = Column(DateTime, default=func.now())
    updated_at     = Column(DateTime, onupdate=func.now())

    tasks     = relationship("AgentTask",    back_populates="agent",
                             cascade="all, delete-orphan")
    approvals = relationship("AgentApproval", back_populates="agent",
                             cascade="all, delete-orphan")


class AgentTask(Base):
    """에이전트 작업 실행 기록 — 입출력·토큰·상태."""
    __tablename__ = "tb_agent_task"

    id           = Column(Integer, primary_key=True, index=True)
    agent_id     = Column(Integer, ForeignKey("tb_agent_config.id"), nullable=False)
    title        = Column(String(200), nullable=False)
    description  = Column(Text)
    status       = Column(String(20), default="PENDING")  # AgentTaskStatus
    input_data   = Column(JSON)   # 에이전트에게 전달된 컨텍스트
    output_data  = Column(JSON)   # 에이전트가 생성한 결과
    tokens_used  = Column(Integer, default=0)
    started_at   = Column(DateTime)
    completed_at = Column(DateTime)
    created_at   = Column(DateTime, default=func.now())

    agent    = relationship("AgentConfig", back_populates="tasks")
    approval = relationship("AgentApproval", back_populates="task", uselist=False)


class AgentApproval(Base):
    """
    사람 검토·승인 대기 항목.
    CRITICAL 인시던트 배정, 코드 변경, SR 자동 생성 등
    고위험 액션은 사람이 승인·거부해야 실제 적용된다.
    """
    __tablename__ = "tb_agent_approval"

    id           = Column(Integer, primary_key=True, index=True)
    agent_id     = Column(Integer, ForeignKey("tb_agent_config.id"), nullable=False)
    task_id      = Column(Integer, ForeignKey("tb_agent_task.id"), nullable=True)
    action_type  = Column(String(50), nullable=False)
    # 예: TRIAGE_INCIDENT / CREATE_SR / CODE_CHANGE / CREATE_RISK / ASSIGN_ENGINEER
    action_data  = Column(JSON)    # 적용 예정 데이터
    status       = Column(String(20), default="PENDING")  # AgentApprovalStatus
    notes        = Column(Text)    # 승인·거부 사유
    requested_at = Column(DateTime, default=func.now())
    reviewed_by  = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    reviewed_at  = Column(DateTime, nullable=True)

    agent = relationship("AgentConfig", back_populates="approvals")
    task  = relationship("AgentTask",   back_populates="approval")


# ── Pydantic Schemas ──────────────────────────────────────────────────────────

class AgentConfigOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id:             int
    name:           str
    role:           str
    description:    Optional[str]
    llm_provider:   str
    llm_model:      str
    heartbeat_cron: Optional[str]
    is_active:      bool
    status:         str
    last_heartbeat: Optional[datetime]
    total_tasks:    int
    total_tokens:   int
    created_at:     datetime


class AgentConfigCreate(BaseModel):
    name:           str
    role:           AgentRole
    description:    Optional[str] = None
    llm_provider:   LLMProvider  = LLMProvider.OLLAMA
    llm_model:      str          = "guardia-agent"
    system_prompt:  Optional[str] = None
    heartbeat_cron: Optional[str] = None   # None = 수동 실행만
    is_active:      bool         = True


class AgentConfigUpdate(BaseModel):
    name:           Optional[str]          = None
    description:    Optional[str]          = None
    llm_model:      Optional[str]          = None
    system_prompt:  Optional[str]          = None
    heartbeat_cron: Optional[str]          = None
    is_active:      Optional[bool]         = None


class AgentTaskOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id:           int
    agent_id:     int
    title:        str
    status:       str
    input_data:   Optional[dict]
    output_data:  Optional[dict]
    tokens_used:  int
    started_at:   Optional[datetime]
    completed_at: Optional[datetime]
    created_at:   datetime


class AgentTaskCreate(BaseModel):
    agent_id:    int
    title:       str
    description: Optional[str] = None
    input_data:  Optional[dict] = None


class AgentApprovalOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id:           int
    agent_id:     int
    task_id:      Optional[int]
    action_type:  str
    action_data:  Optional[dict]
    status:       str
    notes:        Optional[str]
    requested_at: datetime
    reviewed_by:  Optional[int]
    reviewed_at:  Optional[datetime]


class AgentApprovalReview(BaseModel):
    approved: bool
    notes:    Optional[str] = None


class AgentOrgNode(BaseModel):
    """에이전트 조직도 노드 (Phase 4 대시보드용)."""
    id:       int
    name:     str
    role:     str
    status:   str
    children: List["AgentOrgNode"] = []
    model_config = ConfigDict(from_attributes=True)


# ══════════════════════════════════════════════════════════════════════════════
# B-3: 코드 리뷰 (Code Review Agent)
# ══════════════════════════════════════════════════════════════════════════════

class ReviewSeverity(str, Enum):
    CRITICAL = "CRITICAL"
    HIGH     = "HIGH"
    MEDIUM   = "MEDIUM"
    LOW      = "LOW"
    INFO     = "INFO"


class ReviewCategory(str, Enum):
    SECURITY     = "SECURITY"      # 보안 취약점
    PERFORMANCE  = "PERFORMANCE"   # 성능 문제
    CODE_QUALITY = "CODE_QUALITY"  # 코드 품질
    ARCHITECTURE = "ARCHITECTURE"  # 아키텍처 문제
    NAMING       = "NAMING"        # 네이밍 컨벤션
    TESTING      = "TESTING"       # 테스트 부족
    DOCUMENTATION= "DOCUMENTATION" # 문서화 부족


class CodeReview(Base):
    """코드 리뷰 결과 저장."""
    __tablename__ = "tb_code_review"

    id             = Column(Integer, primary_key=True, index=True)
    project_id     = Column(Integer, ForeignKey("tb_project.id"), nullable=True, index=True)
    vibe_session_id= Column(Integer, ForeignKey("tb_vibe_session.id"), nullable=True, index=True)
    sr_id          = Column(String(50), nullable=True, index=True)
    reviewer       = Column(String(100), default="code-review-agent")  # 리뷰어 (에이전트명)
    target_path    = Column(String(512), nullable=True)   # 리뷰 대상 경로
    tech_stack     = Column(String(50), nullable=True)
    # 리뷰 결과
    score          = Column(Integer, default=0)    # 0-100 종합 점수
    summary        = Column(Text, nullable=True)   # LLM 요약
    findings_json  = Column(Text, nullable=True)   # JSON: List[ReviewFinding]
    # 메타
    file_count     = Column(Integer, default=0)
    line_count     = Column(Integer, default=0)
    model_used     = Column(String(100), nullable=True)  # 사용된 LLM 모델
    duration_sec   = Column(Integer, nullable=True)
    status         = Column(String(20), default="PENDING")  # PENDING/RUNNING/DONE/FAILED
    error_msg      = Column(Text, nullable=True)
    requested_by   = Column(String(100), nullable=True)
    created_at     = Column(DateTime, default=func.now())
    completed_at   = Column(DateTime, nullable=True)


class CodeReviewOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    project_id: Optional[int]
    vibe_session_id: Optional[int]
    sr_id: Optional[str]
    reviewer: str
    target_path: Optional[str]
    tech_stack: Optional[str]
    score: int
    summary: Optional[str]
    findings_json: Optional[str]
    file_count: int
    line_count: int
    model_used: Optional[str]
    duration_sec: Optional[int]
    status: str
    error_msg: Optional[str]
    requested_by: Optional[str]
    created_at: datetime
    completed_at: Optional[datetime]


class CodeReviewRequest(BaseModel):
    """코드 리뷰 요청."""
    project_id: Optional[int] = None
    vibe_session_id: Optional[int] = None
    sr_id: Optional[str] = None
    target_path: Optional[str] = None   # 특정 파일/디렉토리만 리뷰 (None=project_dir 전체)
    focus: Optional[str] = None          # 리뷰 집중 포인트 (예: "security,performance")
    model: str = "codellama"             # Ollama 모델명


class AgentStatsOut(BaseModel):
    """에이전트 엔진 전체 통계."""
    total_agents:      int
    active_agents:     int
    total_tasks_today: int
    total_tokens_today: int
    pending_approvals: int
    llm_online:        bool


# ── 에이전트 간 메시지 (Priority 3 — 에이전트 메시지 전달) ────────────────────

class AgentMessageType(str, Enum):
    TASK_DELEGATION = "TASK_DELEGATION"   # CEO → CTO: 개발 태스크 위임
    STATUS_UPDATE   = "STATUS_UPDATE"     # CTO/Dev → CEO: 완료 보고
    ESCALATION      = "ESCALATION"        # 하위 에이전트 → 상위 에이전트


class AgentMessage(Base):
    """에이전트 간 메시지 전달 테이블."""
    __tablename__ = "tb_agent_message"

    id            = Column(Integer, primary_key=True, index=True)
    from_agent_id = Column(Integer, ForeignKey("tb_agent_config.id"), nullable=True, index=True)
    to_agent_id   = Column(Integer, ForeignKey("tb_agent_config.id"), nullable=False, index=True)
    message_type  = Column(String(30), nullable=False)
    subject       = Column(String(200), nullable=False)
    body          = Column(Text, nullable=True)
    metadata_json = Column(Text, nullable=True)   # JSON 문자열 저장
    is_read       = Column(Boolean, default=False)
    created_at    = Column(DateTime, default=func.now())
    read_at       = Column(DateTime, nullable=True)

    from_agent    = relationship("AgentConfig", foreign_keys=[from_agent_id])
    to_agent      = relationship("AgentConfig", foreign_keys=[to_agent_id])


class AgentMessageOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    from_agent_id: Optional[int]
    to_agent_id: int
    message_type: str
    subject: str
    body: Optional[str]
    metadata_json: Optional[str]
    is_read: bool
    created_at: datetime
    read_at: Optional[datetime]


class AgentMessageCreate(BaseModel):
    from_agent_id: Optional[int] = None
    to_agent_id: int
    message_type: AgentMessageType
    subject: str
    body: Optional[str] = None
    metadata_json: Optional[str] = None


# ══════════════════════════════════════════════════════════════════════════════
# B-1: AI 이상 탐지 (Anomaly Detection) 모델
# ══════════════════════════════════════════════════════════════════════════════

class AnomalySeverity(str, Enum):
    INFO     = "INFO"
    WARNING  = "WARNING"
    CRITICAL = "CRITICAL"


class AnomalyStatus(str, Enum):
    OPEN           = "OPEN"
    ACKNOWLEDGED   = "ACKNOWLEDGED"
    RESOLVED       = "RESOLVED"
    FALSE_POSITIVE = "FALSE_POSITIVE"


class MetricType(str, Enum):
    CPU_USAGE          = "CPU_USAGE"          # %
    MEMORY_USAGE       = "MEMORY_USAGE"       # %
    DISK_USAGE         = "DISK_USAGE"         # %
    RESPONSE_TIME      = "RESPONSE_TIME"      # ms
    ERROR_RATE         = "ERROR_RATE"         # per minute
    NETWORK_RX         = "NETWORK_RX"         # KB/s
    NETWORK_TX         = "NETWORK_TX"         # KB/s
    ACTIVE_CONNECTIONS = "ACTIVE_CONNECTIONS" # count
    HEAP_USAGE         = "HEAP_USAGE"         # % (JVM)
    THROUGHPUT         = "THROUGHPUT"         # req/s
    CUSTOM             = "CUSTOM"


class MetricSnapshot(Base):
    """시계열 메트릭 스냅샷 (이상 탐지 학습/검출 데이터)."""
    __tablename__ = "tb_metric_snapshot"

    id          = Column(Integer, primary_key=True, index=True)
    server_id   = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    source      = Column(String(100), nullable=False)   # 서버명 또는 앱명
    metric_type = Column(String(50), nullable=False)
    value       = Column(Float, nullable=False)
    unit        = Column(String(20), nullable=True)     # %, ms, KB/s, req/s 등
    tags        = Column(Text, nullable=True)           # JSON 태그 (inst_code 등)
    recorded_at = Column(DateTime, default=func.now(), index=True)


class AnomalyEvent(Base):
    """이상 탐지 이벤트."""
    __tablename__ = "tb_anomaly_event"

    id              = Column(Integer, primary_key=True, index=True)
    server_id       = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    source          = Column(String(100), nullable=False)
    metric_type     = Column(String(50), nullable=False)
    severity        = Column(String(20), default="WARNING")
    status          = Column(String(20), default="OPEN")
    title           = Column(String(200), nullable=False)
    description     = Column(Text, nullable=True)

    # 탐지 수치
    current_value   = Column(Float, nullable=False)
    baseline_mean   = Column(Float, nullable=True)
    baseline_std    = Column(Float, nullable=True)
    z_score         = Column(Float, nullable=True)
    threshold       = Column(Float, nullable=True)
    detect_method   = Column(String(30), nullable=True)  # ZSCORE/IQR/THRESHOLD/TREND

    # Ollama LLM 분석 (선택적)
    llm_analysis    = Column(Text, nullable=True)

    # 연결
    sr_id           = Column(String(20), nullable=True)  # 자동 생성 SR
    rule_name       = Column(String(100), nullable=True)  # 적용 룰명

    # 타임스탬프
    detected_at     = Column(DateTime, default=func.now(), index=True)
    acknowledged_at = Column(DateTime, nullable=True)
    resolved_at     = Column(DateTime, nullable=True)
    acknowledged_by = Column(String(100), nullable=True)

    server          = relationship("Server", foreign_keys=[server_id])


class AnomalyRule(Base):
    """이상 탐지 룰 설정 (임계값, 알림 조건)."""
    __tablename__ = "tb_anomaly_rule"

    id              = Column(Integer, primary_key=True, index=True)
    name            = Column(String(100), nullable=False, unique=True)
    description     = Column(String(300), nullable=True)
    source_pattern  = Column(String(100), nullable=True)   # 적용 소스 패턴 (None=전체)
    metric_type     = Column(String(50), nullable=False)
    method          = Column(String(20), default="ZSCORE")  # ZSCORE/IQR/THRESHOLD/TREND
    threshold       = Column(Float, nullable=True)          # THRESHOLD 방식: 정적 임계값
    z_threshold     = Column(Float, default=3.0)            # ZSCORE 방식: Z-score 임계
    iqr_factor      = Column(Float, default=1.5)            # IQR 방식: IQR 배율
    window_size     = Column(Integer, default=60)           # 기준 윈도우 샘플 수
    min_samples     = Column(Integer, default=10)           # 최소 필요 샘플 수
    severity        = Column(String(20), default="WARNING")
    auto_create_sr  = Column(Boolean, default=False)        # CRITICAL 시 SR 자동 생성
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


# ── B-1 Pydantic schemas ────────────────────────────────────────────────────

class MetricSnapshotIn(BaseModel):
    """메트릭 수집 요청."""
    source: str
    metric_type: str        # MetricType value
    value: float
    unit: Optional[str] = None
    tags: Optional[dict] = None
    server_id: Optional[int] = None


class MetricSnapshotOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    source: str
    metric_type: str
    value: float
    unit: Optional[str]
    tags: Optional[str]
    recorded_at: datetime


class AnomalyEventOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    source: str
    metric_type: str
    severity: str
    status: str
    title: str
    description: Optional[str]
    current_value: float
    baseline_mean: Optional[float]
    baseline_std: Optional[float]
    z_score: Optional[float]
    threshold: Optional[float]
    detect_method: Optional[str]
    llm_analysis: Optional[str]
    sr_id: Optional[str]
    rule_name: Optional[str]
    detected_at: datetime
    acknowledged_at: Optional[datetime]
    resolved_at: Optional[datetime]
    acknowledged_by: Optional[str]


class AnomalyRuleOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    description: Optional[str]
    source_pattern: Optional[str]
    metric_type: str
    method: str
    threshold: Optional[float]
    z_threshold: float
    iqr_factor: float
    window_size: int
    min_samples: int
    severity: str
    auto_create_sr: bool
    is_active: bool


class AnomalyRuleCreate(BaseModel):
    name: str
    description: Optional[str] = None
    source_pattern: Optional[str] = None
    metric_type: str
    method: str = "ZSCORE"
    threshold: Optional[float] = None
    z_threshold: float = 3.0
    iqr_factor: float = 1.5
    window_size: int = 60
    min_samples: int = 10
    severity: str = "WARNING"
    auto_create_sr: bool = False


class SimulateMetricIn(BaseModel):
    """이상 탐지 시뮬레이션 (테스트용)."""
    source: str = "test-server"
    metric_type: str = "CPU_USAGE"
    normal_count: int = 50     # 정상 데이터 포인트 수
    anomaly_value: float = 95.0  # 이상 값
    baseline_mean: float = 40.0
    baseline_std: float = 10.0
    include_llm: bool = False  # Ollama LLM 분석 포함 여부


# ==============================================================================
# B-2: 자연어 SR 접수 챗봇 모델
# ==============================================================================

class ChatSessionStatus(str, Enum):
    ACTIVE   = "ACTIVE"
    RESOLVED = "RESOLVED"
    ABANDONED = "ABANDONED"


class ChatIntentType(str, Enum):
    SR_CREATE        = "SR_CREATE"         # SR 생성 요청
    SR_QUERY         = "SR_QUERY"          # SR 조회
    DEPLOY_REQUEST   = "DEPLOY_REQUEST"    # 배포 요청
    INCIDENT_REPORT  = "INCIDENT_REPORT"   # 인시던트 보고
    GENERAL_INQUIRY  = "GENERAL_INQUIRY"   # 일반 문의
    CLARIFICATION    = "CLARIFICATION"     # 추가 정보 응답
    UNKNOWN          = "UNKNOWN"


class ChatSession(Base):
    """챗봇 대화 세션."""
    __tablename__ = "tb_chat_session"

    id           = Column(Integer, primary_key=True, index=True)
    session_key  = Column(String(64), unique=True, nullable=False, index=True)
    user_id      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    username     = Column(String(50), nullable=True)
    status       = Column(String(20), default="ACTIVE")
    context_json = Column(Text, nullable=True)   # 수집된 컨텍스트 JSON
    created_sr_id = Column(String(20), nullable=True)  # 자동 생성된 SR ID
    created_at   = Column(DateTime, default=func.now())
    updated_at   = Column(DateTime, default=func.now(), onupdate=func.now())

    messages     = relationship("ChatMessage", back_populates="session",
                                cascade="all, delete-orphan",
                                order_by="ChatMessage.created_at")


class ChatMessage(Base):
    """챗봇 대화 메시지."""
    __tablename__ = "tb_chat_message"

    id           = Column(Integer, primary_key=True, index=True)
    session_id   = Column(Integer, ForeignKey("tb_chat_session.id"), nullable=False)
    role         = Column(String(10), nullable=False)   # user | assistant | system
    content      = Column(Text, nullable=False)
    intent       = Column(String(30), nullable=True)    # 감지된 인텐트
    entities_json = Column(Text, nullable=True)         # 추출된 엔티티 JSON
    confidence   = Column(Float, nullable=True)         # 인텐트 확신도 0.0~1.0
    created_at   = Column(DateTime, default=func.now())

    session      = relationship("ChatSession", back_populates="messages")


# ── B-2 Pydantic schemas ────────────────────────────────────────────────────

class ChatMessageRequest(BaseModel):
    """챗봇 메시지 전송 요청."""
    message: str
    session_key: Optional[str] = None  # None이면 새 세션 생성
    username: Optional[str] = None


class ChatMessageOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    role: str
    content: str
    intent: Optional[str]
    entities_json: Optional[str]
    confidence: Optional[float]
    created_at: datetime


class ChatSessionOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    session_key: str
    username: Optional[str]
    status: str
    context_json: Optional[str]
    created_sr_id: Optional[str]
    created_at: datetime
    updated_at: datetime
    messages: Optional[List[ChatMessageOut]] = None


class ChatResponse(BaseModel):
    """챗봇 응답 전체."""
    session_key: str
    reply: str
    intent: str
    entities: dict
    action_taken: bool
    created_sr_id: Optional[str] = None
    needs_clarification: bool = False
    clarification_prompt: Optional[str] = None
    suggestions: Optional[List[str]] = None


# ==============================================================================
# B-5: 멀티 에이전트 협업 오케스트레이션 모델
# ==============================================================================

class WorkflowStatus(str, Enum):
    PENDING   = "PENDING"
    RUNNING   = "RUNNING"
    WAITING   = "WAITING"    # 승인 대기 등 외부 이벤트 대기
    COMPLETED = "COMPLETED"
    FAILED    = "FAILED"
    CANCELLED = "CANCELLED"


class WorkflowType(str, Enum):
    SR_TO_DEPLOY   = "SR_TO_DEPLOY"    # SR 접수 → 코드 리뷰 → 배포
    INCIDENT_RESP  = "INCIDENT_RESP"   # 인시던트 대응 → RCA → 복구
    CODE_REVIEW    = "CODE_REVIEW"     # 코드 리뷰 → 취약점 스캔 → 보고
    MAINTENANCE    = "MAINTENANCE"     # 유지보수 → 변경 관리
    CUSTOM         = "CUSTOM"


class WorkflowInstance(Base):
    """워크플로우 실행 인스턴스."""
    __tablename__ = "tb_workflow_instance"

    id            = Column(Integer, primary_key=True, index=True)
    workflow_type = Column(String(30), nullable=False)
    status        = Column(String(20), default="PENDING")
    title         = Column(String(200), nullable=False)
    sr_id         = Column(String(20), nullable=True)
    project_id    = Column(Integer, ForeignKey("tb_project.id"), nullable=True)
    triggered_by  = Column(String(100), nullable=True)
    context_json  = Column(Text, nullable=True)   # 워크플로우 컨텍스트 JSON
    current_step  = Column(Integer, default=0)
    total_steps   = Column(Integer, default=0)
    progress_pct  = Column(Integer, default=0)    # 0~100
    result_json   = Column(Text, nullable=True)   # 최종 결과
    error_msg     = Column(Text, nullable=True)
    started_at    = Column(DateTime, nullable=True)
    completed_at  = Column(DateTime, nullable=True)
    created_at    = Column(DateTime, default=func.now())

    steps         = relationship("WorkflowStep", back_populates="instance",
                                  cascade="all, delete-orphan",
                                  order_by="WorkflowStep.step_order")


class WorkflowStep(Base):
    """워크플로우 단계."""
    __tablename__ = "tb_workflow_step"

    id            = Column(Integer, primary_key=True, index=True)
    instance_id   = Column(Integer, ForeignKey("tb_workflow_instance.id"), nullable=False)
    step_order    = Column(Integer, nullable=False)
    agent_name    = Column(String(50), nullable=False)   # sr-manager, code-reviewer 등
    action        = Column(String(100), nullable=False)  # 수행할 액션
    status        = Column(String(20), default="PENDING")
    input_json    = Column(Text, nullable=True)          # 입력 파라미터
    output_json   = Column(Text, nullable=True)          # 출력 결과
    error_msg     = Column(Text, nullable=True)
    retry_count   = Column(Integer, default=0)
    started_at    = Column(DateTime, nullable=True)
    completed_at  = Column(DateTime, nullable=True)
    duration_sec  = Column(Integer, nullable=True)

    instance      = relationship("WorkflowInstance", back_populates="steps")


# ── B-5 Pydantic schemas ────────────────────────────────────────────────────

class WorkflowStepOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    step_order: int
    agent_name: str
    action: str
    status: str
    input_json: Optional[str]
    output_json: Optional[str]
    error_msg: Optional[str]
    retry_count: int
    started_at: Optional[datetime]
    completed_at: Optional[datetime]
    duration_sec: Optional[int]


class WorkflowInstanceOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    workflow_type: str
    status: str
    title: str
    sr_id: Optional[str]
    project_id: Optional[int]
    triggered_by: Optional[str]
    current_step: int
    total_steps: int
    progress_pct: int
    result_json: Optional[str]
    error_msg: Optional[str]
    started_at: Optional[datetime]
    completed_at: Optional[datetime]
    created_at: datetime
    steps: Optional[List[WorkflowStepOut]] = None


class WorkflowCreateRequest(BaseModel):
    workflow_type: str = "SR_TO_DEPLOY"
    title: str
    sr_id: Optional[str] = None
    project_id: Optional[int] = None
    context: Optional[dict] = None  # 초기 컨텍스트


# ════════════════════════════════════════════════════════════════════════════════
# C-1: CMDB 확장 — CI 관리, 관계, 변경 이력
# ════════════════════════════════════════════════════════════════════════════════

class CIStatus(str, Enum):
    PLANNED  = "PLANNED"
    ACTIVE   = "ACTIVE"
    INACTIVE = "INACTIVE"
    RETIRED  = "RETIRED"
    DISPOSED = "DISPOSED"


class CIType(str, Enum):
    SERVER    = "SERVER"
    NETWORK   = "NETWORK"
    STORAGE   = "STORAGE"
    SOFTWARE  = "SOFTWARE"
    SERVICE   = "SERVICE"
    DATABASE  = "DATABASE"
    MIDDLEWARE = "MIDDLEWARE"
    SECURITY  = "SECURITY"
    OTHER     = "OTHER"


class CIRelationType(str, Enum):
    DEPENDS_ON   = "DEPENDS_ON"    # A → B: A는 B에 의존
    PART_OF      = "PART_OF"       # A는 B의 구성 요소
    HOSTED_ON    = "HOSTED_ON"     # A는 B 위에서 실행
    CONNECTS_TO  = "CONNECTS_TO"   # A↔B: 네트워크 연결
    BACKS_UP     = "BACKS_UP"      # A가 B를 백업
    MONITORS     = "MONITORS"      # A가 B를 모니터링
    REPLACED_BY  = "REPLACED_BY"   # A는 B로 교체됨


class CIChangeType(str, Enum):
    CREATE        = "CREATE"
    UPDATE        = "UPDATE"
    STATUS_CHANGE = "STATUS_CHANGE"
    RETIRE        = "RETIRE"
    RELATION_ADD  = "RELATION_ADD"
    RELATION_DEL  = "RELATION_DEL"


# ── ORM ──────────────────────────────────────────────────────────────────────

class ConfigItem(Base):
    """형상 관리 항목 (CI)."""
    __tablename__ = "tb_ci"

    id            = Column(Integer, primary_key=True, index=True)
    ci_id         = Column(String(30), unique=True, nullable=False, index=True)  # CI-YYYYMMDD-NNNN
    name          = Column(String(200), nullable=False)
    ci_type       = Column(String(20), nullable=False, default="SERVER")
    category      = Column(String(50))          # 세분류 (예: WEB서버, WAS서버)
    status        = Column(String(20), default="ACTIVE")
    version       = Column(String(50))          # 소프트웨어 버전 등
    owner         = Column(String(100))         # 담당자 사번/이름
    location      = Column(String(200))         # 물리적 위치 / 데이터센터 랙
    vendor        = Column(String(100))
    model         = Column(String(100))
    serial_number = Column(String(100))
    install_date  = Column(Date)
    retire_date   = Column(Date)
    linked_server_id = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    inst_id       = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    sr_id         = Column(String(20))          # 등록/변경 관련 SR ID
    attributes_json = Column(Text)              # 유연한 추가 속성 (JSON)
    description   = Column(Text)
    created_by    = Column(String(50))
    created_at    = Column(DateTime, default=func.now())
    updated_at    = Column(DateTime, default=func.now(), onupdate=func.now())

    # relationships
    relations_from = relationship(
        "CIRelation",
        foreign_keys="CIRelation.from_ci_id",
        back_populates="from_ci",
        lazy="select",
    )
    relations_to = relationship(
        "CIRelation",
        foreign_keys="CIRelation.to_ci_id",
        back_populates="to_ci",
        lazy="select",
    )
    change_logs = relationship("CIChangeLog", back_populates="ci", lazy="select")


class CIRelation(Base):
    """CI 간 관계."""
    __tablename__ = "tb_ci_relation"

    id            = Column(Integer, primary_key=True, index=True)
    from_ci_id    = Column(Integer, ForeignKey("tb_ci.id"), nullable=False, index=True)
    to_ci_id      = Column(Integer, ForeignKey("tb_ci.id"), nullable=False, index=True)
    relation_type = Column(String(30), nullable=False)   # CIRelationType
    description   = Column(String(500))
    created_by    = Column(String(50))
    created_at    = Column(DateTime, default=func.now())

    from_ci = relationship("ConfigItem", foreign_keys=[from_ci_id], back_populates="relations_from")
    to_ci   = relationship("ConfigItem", foreign_keys=[to_ci_id],   back_populates="relations_to")


class CIChangeLog(Base):
    """CI 변경 이력 (불변 감사 로그)."""
    __tablename__ = "tb_ci_change_log"

    id          = Column(Integer, primary_key=True, index=True)
    ci_id_fk    = Column(Integer, ForeignKey("tb_ci.id"), nullable=False, index=True)
    ci_id_str   = Column(String(30))        # ci_id 문자열 (CI 삭제 후에도 조회 가능)
    change_type = Column(String(30), nullable=False)   # CIChangeType
    field_name  = Column(String(100))       # 변경된 필드명 (UPDATE 시)
    old_value   = Column(Text)
    new_value   = Column(Text)
    changed_by  = Column(String(50))
    changed_at  = Column(DateTime, default=func.now())
    sr_id       = Column(String(20))        # 변경 관련 SR
    note        = Column(String(500))

    ci = relationship("ConfigItem", back_populates="change_logs")


# ── Pydantic Schemas ─────────────────────────────────────────────────────────

class CIRelationOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    from_ci_id: int
    to_ci_id: int
    relation_type: str
    description: Optional[str]
    created_by: Optional[str]
    created_at: datetime


class CIChangeLogOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    ci_id_str: Optional[str]
    change_type: str
    field_name: Optional[str]
    old_value: Optional[str]
    new_value: Optional[str]
    changed_by: Optional[str]
    changed_at: datetime
    sr_id: Optional[str]
    note: Optional[str]


class ConfigItemOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    ci_id: str
    name: str
    ci_type: str
    category: Optional[str]
    status: str
    version: Optional[str]
    owner: Optional[str]
    location: Optional[str]
    vendor: Optional[str]
    model: Optional[str]
    serial_number: Optional[str]
    install_date: Optional[date]
    retire_date: Optional[date]
    linked_server_id: Optional[int]
    inst_id: Optional[int]
    sr_id: Optional[str]
    description: Optional[str]
    created_by: Optional[str]
    created_at: datetime
    updated_at: datetime


class ConfigItemCreate(BaseModel):
    name: str
    ci_type: str = "SERVER"
    category: Optional[str] = None
    status: str = "ACTIVE"
    version: Optional[str] = None
    owner: Optional[str] = None
    location: Optional[str] = None
    vendor: Optional[str] = None
    model: Optional[str] = None
    serial_number: Optional[str] = None
    install_date: Optional[date] = None
    retire_date: Optional[date] = None
    linked_server_id: Optional[int] = None
    inst_id: Optional[int] = None
    sr_id: Optional[str] = None
    attributes: Optional[dict] = None
    description: Optional[str] = None


class ConfigItemUpdate(BaseModel):
    name: Optional[str] = None
    ci_type: Optional[str] = None
    category: Optional[str] = None
    status: Optional[str] = None
    version: Optional[str] = None
    owner: Optional[str] = None
    location: Optional[str] = None
    vendor: Optional[str] = None
    model: Optional[str] = None
    serial_number: Optional[str] = None
    install_date: Optional[date] = None
    retire_date: Optional[date] = None
    linked_server_id: Optional[int] = None
    sr_id: Optional[str] = None
    attributes: Optional[dict] = None
    description: Optional[str] = None


class CIRelationCreate(BaseModel):
    from_ci_id: int
    to_ci_id: int
    relation_type: str
    description: Optional[str] = None


# ════════════════════════════════════════════════════════════════════════════════
# C-2: 변경 관리 CAB (Change Advisory Board)
# ════════════════════════════════════════════════════════════════════════════════

class RFCStatus(str, Enum):
    DRAFT     = "DRAFT"          # 초안
    SUBMITTED = "SUBMITTED"      # 제출됨 (CAB 검토 대기)
    IN_REVIEW = "IN_REVIEW"      # CAB 검토 중
    APPROVED  = "APPROVED"       # CAB 승인
    REJECTED  = "REJECTED"       # CAB 거부
    SCHEDULED = "SCHEDULED"      # 일정 확정
    IN_PROGRESS = "IN_PROGRESS"  # 변경 진행 중
    COMPLETED = "COMPLETED"      # 변경 완료
    FAILED    = "FAILED"         # 변경 실패/롤백
    WITHDRAWN = "WITHDRAWN"      # 철회


class ChangeType(str, Enum):
    STANDARD   = "STANDARD"    # 표준 변경 (사전 승인)
    NORMAL     = "NORMAL"      # 일반 변경 (CAB 필요)
    EMERGENCY  = "EMERGENCY"   # 긴급 변경 (e-CAB)
    MAJOR      = "MAJOR"       # 대규모 변경


class ChangeRisk(str, Enum):
    LOW    = "LOW"
    MEDIUM = "MEDIUM"
    HIGH   = "HIGH"
    CRITICAL = "CRITICAL"


class CABVoteResult(str, Enum):
    APPROVE  = "APPROVE"
    REJECT   = "REJECT"
    ABSTAIN  = "ABSTAIN"
    DEFER    = "DEFER"


# ── ORM ──────────────────────────────────────────────────────────────────────

class RFChange(Base):
    """변경 요청서 (Request for Change)."""
    __tablename__ = "tb_rfc"

    id              = Column(Integer, primary_key=True, index=True)
    rfc_id          = Column(String(30), unique=True, nullable=False, index=True)  # RFC-YYYYMMDD-NNNN
    title           = Column(String(300), nullable=False)
    description     = Column(Text)
    change_type     = Column(String(20), default="NORMAL")    # ChangeType
    risk_level      = Column(String(20), default="MEDIUM")    # ChangeRisk
    status          = Column(String(20), default="DRAFT")     # RFCStatus
    priority        = Column(String(20), default="MEDIUM")
    # 요청자 / 담당자
    requester_id    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    assignee_id     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    # 일정
    planned_start   = Column(DateTime)
    planned_end     = Column(DateTime)
    actual_start    = Column(DateTime)
    actual_end      = Column(DateTime)
    freeze_exempt   = Column(Boolean, default=False)  # 동결 기간 예외
    # 변경 내용
    change_plan     = Column(Text)        # 변경 계획 상세
    rollback_plan   = Column(Text)        # 롤백 계획
    test_plan       = Column(Text)        # 테스트 계획
    impact_analysis = Column(Text)        # 영향도 분석
    # 연관 정보
    sr_id           = Column(String(20))  # 관련 SR
    ci_ids_json     = Column(Text)        # 영향받는 CI ID 목록 (JSON)
    # 결과
    result_summary  = Column(Text)
    error_msg       = Column(Text)
    # 감사
    created_by      = Column(String(50))
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())
    submitted_at    = Column(DateTime)
    approved_at     = Column(DateTime)
    completed_at    = Column(DateTime)

    # relationships
    cab_votes     = relationship("CABVote",      back_populates="rfc", lazy="select")
    freeze_checks = relationship("FreezeWindow", back_populates="rfc", lazy="select", uselist=False)


class CABVote(Base):
    """CAB 위원 투표 기록."""
    __tablename__ = "tb_cab_vote"

    id          = Column(Integer, primary_key=True, index=True)
    rfc_id_fk   = Column(Integer, ForeignKey("tb_rfc.id"), nullable=False, index=True)
    voter_id    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    voter_name  = Column(String(100))
    vote        = Column(String(20), nullable=False)   # CABVoteResult
    comment     = Column(Text)
    voted_at    = Column(DateTime, default=func.now())
    is_final    = Column(Boolean, default=False)       # 최종 결정권자 여부

    rfc = relationship("RFChange", back_populates="cab_votes")


class FreezeWindow(Base):
    """변경 동결 기간 (Freeze Window)."""
    __tablename__ = "tb_freeze_window"

    id          = Column(Integer, primary_key=True, index=True)
    rfc_id_fk   = Column(Integer, ForeignKey("tb_rfc.id"), nullable=True)
    name        = Column(String(200), nullable=False)    # 동결 사유 (예: 연말 결산)
    start_dt    = Column(DateTime, nullable=False)
    end_dt      = Column(DateTime, nullable=False)
    scope       = Column(String(50), default="ALL")     # ALL / CRITICAL / NORMAL
    reason      = Column(Text)
    created_by  = Column(String(50))
    created_at  = Column(DateTime, default=func.now())
    is_active   = Column(Boolean, default=True)

    rfc = relationship("RFChange", back_populates="freeze_checks")


# ── Pydantic Schemas ─────────────────────────────────────────────────────────

class CABVoteOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    rfc_id_fk: int
    voter_name: Optional[str]
    vote: str
    comment: Optional[str]
    voted_at: datetime
    is_final: bool


class RFChangeOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    rfc_id: str
    title: str
    change_type: str
    risk_level: str
    status: str
    priority: str
    planned_start: Optional[datetime]
    planned_end: Optional[datetime]
    actual_start: Optional[datetime]
    actual_end: Optional[datetime]
    freeze_exempt: bool
    sr_id: Optional[str]
    created_by: Optional[str]
    created_at: datetime
    updated_at: datetime
    submitted_at: Optional[datetime]
    approved_at: Optional[datetime]
    completed_at: Optional[datetime]


class RFChangeCreate(BaseModel):
    title: str
    description: Optional[str] = None
    change_type: str = "NORMAL"
    risk_level: str = "MEDIUM"
    priority: str = "MEDIUM"
    planned_start: Optional[datetime] = None
    planned_end: Optional[datetime] = None
    freeze_exempt: bool = False
    change_plan: Optional[str] = None
    rollback_plan: Optional[str] = None
    test_plan: Optional[str] = None
    impact_analysis: Optional[str] = None
    sr_id: Optional[str] = None
    ci_ids: Optional[List[int]] = None


class RFChangeUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    change_type: Optional[str] = None
    risk_level: Optional[str] = None
    priority: Optional[str] = None
    planned_start: Optional[datetime] = None
    planned_end: Optional[datetime] = None
    freeze_exempt: Optional[bool] = None
    change_plan: Optional[str] = None
    rollback_plan: Optional[str] = None
    test_plan: Optional[str] = None
    impact_analysis: Optional[str] = None
    ci_ids: Optional[List[int]] = None


class CABVoteCreate(BaseModel):
    vote: str                 # APPROVE / REJECT / ABSTAIN / DEFER
    comment: Optional[str] = None
    is_final: bool = False


class FreezeWindowOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    start_dt: datetime
    end_dt: datetime
    scope: str
    reason: Optional[str]
    created_by: Optional[str]
    is_active: bool


class FreezeWindowCreate(BaseModel):
    name: str
    start_dt: datetime
    end_dt: datetime
    scope: str = "ALL"
    reason: Optional[str] = None


# ════════════════════════════════════════════════════════════════════════════════
# C-3: Problem Management
# ════════════════════════════════════════════════════════════════════════════════

class ProblemStatus(str, Enum):
    OPEN         = "OPEN"
    INVESTIGATING = "INVESTIGATING"
    RCA_DONE     = "RCA_DONE"        # 근본 원인 분석 완료
    WORKAROUND   = "WORKAROUND"      # 임시 해결책 적용
    RFC_RAISED   = "RFC_RAISED"      # 영구 수정 RFC 등록됨
    RESOLVED     = "RESOLVED"
    CLOSED       = "CLOSED"


class ProblemCategory(str, Enum):
    HARDWARE   = "HARDWARE"
    SOFTWARE   = "SOFTWARE"
    NETWORK    = "NETWORK"
    PROCESS    = "PROCESS"
    HUMAN      = "HUMAN"
    VENDOR     = "VENDOR"
    UNKNOWN    = "UNKNOWN"


class ProblemRecord(Base):
    """문제 레코드 (Problem Record)."""
    __tablename__ = "tb_problem"

    id              = Column(Integer, primary_key=True, index=True)
    problem_id      = Column(String(30), unique=True, nullable=False, index=True)  # PRB-YYYYMMDD-NNNN
    title           = Column(String(300), nullable=False)
    description     = Column(Text)
    status          = Column(String(30), default="OPEN")
    priority        = Column(String(20), default="MEDIUM")
    category        = Column(String(30), default="UNKNOWN")
    # 근본 원인 분석
    root_cause      = Column(Text)         # RCA 결과
    workaround      = Column(Text)         # 임시 해결 방법
    permanent_fix   = Column(Text)         # 영구 수정 계획
    known_error     = Column(Boolean, default=False)  # Known Error DB 등재 여부
    # 연관 정보
    related_sr_ids  = Column(Text)         # 관련 SR ID 목록 (JSON)
    rfc_id          = Column(String(30))   # 영구 수정 RFC
    ci_id           = Column(String(30))   # 영향 CI
    # 담당자
    assignee_id     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_by      = Column(String(50))
    # 이력
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())
    rca_completed_at = Column(DateTime)
    resolved_at     = Column(DateTime)
    closed_at       = Column(DateTime)
    # 영향도
    affected_users  = Column(Integer, default=0)
    incident_count  = Column(Integer, default=0)  # 관련 인시던트 수
    total_downtime_min = Column(Integer, default=0)  # 누적 다운타임 (분)


class ProblemNote(Base):
    """문제 레코드 활동 노트."""
    __tablename__ = "tb_problem_note"

    id          = Column(Integer, primary_key=True, index=True)
    problem_id_fk = Column(Integer, ForeignKey("tb_problem.id"), nullable=False, index=True)
    note_type   = Column(String(30), default="UPDATE")  # UPDATE / RCA / WORKAROUND / RESOLUTION
    content     = Column(Text, nullable=False)
    author      = Column(String(100))
    created_at  = Column(DateTime, default=func.now())


# ── Pydantic ─────────────────────────────────────────────────────────────────

class ProblemRecordOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    problem_id: str
    title: str
    status: str
    priority: str
    category: str
    root_cause: Optional[str]
    workaround: Optional[str]
    known_error: bool
    rfc_id: Optional[str]
    ci_id: Optional[str]
    created_by: Optional[str]
    created_at: datetime
    updated_at: datetime
    resolved_at: Optional[datetime]
    affected_users: int
    incident_count: int
    total_downtime_min: int


class ProblemRecordCreate(BaseModel):
    title: str
    description: Optional[str] = None
    priority: str = "MEDIUM"
    category: str = "UNKNOWN"
    related_sr_ids: Optional[List[str]] = None
    ci_id: Optional[str] = None
    affected_users: int = 0
    incident_count: int = 0
    total_downtime_min: int = 0


class ProblemNoteCreate(BaseModel):
    note_type: str = "UPDATE"   # UPDATE / RCA / WORKAROUND / RESOLUTION
    content: str


# ════════════════════════════════════════════════════════════════════════════════
# C-4: 용량 관리 대시보드
# ════════════════════════════════════════════════════════════════════════════════

class CapacityStatus(str, Enum):
    NORMAL   = "NORMAL"
    WARNING  = "WARNING"
    CRITICAL = "CRITICAL"
    OVERLOAD = "OVERLOAD"


class CapacityPlan(Base):
    """용량 계획 레코드."""
    __tablename__ = "tb_capacity_plan"

    id            = Column(Integer, primary_key=True, index=True)
    source        = Column(String(100), nullable=False)     # 서버/서비스명
    metric_type   = Column(String(30), nullable=False)      # CPU_USAGE / MEMORY_USAGE / DISK_USAGE
    current_value = Column(Float)
    capacity_max  = Column(Float)                           # 최대 용량
    threshold_warn = Column(Float)
    threshold_crit = Column(Float)
    status        = Column(String(20), default="NORMAL")
    growth_rate   = Column(Float)                           # 월 증가율 (%)
    forecast_3m   = Column(Float)                           # 3개월 후 예측값
    forecast_6m   = Column(Float)                           # 6개월 후 예측값
    forecast_12m  = Column(Float)                           # 12개월 후 예측값
    expansion_needed_at = Column(DateTime)                  # 확장 필요 예상 시점
    note          = Column(Text)
    inst_id       = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    updated_by    = Column(String(50))
    updated_at    = Column(DateTime, default=func.now(), onupdate=func.now())
    created_at    = Column(DateTime, default=func.now())


class CapacityPlanOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    source: str
    metric_type: str
    current_value: Optional[float]
    capacity_max: Optional[float]
    threshold_warn: Optional[float]
    threshold_crit: Optional[float]
    status: str
    growth_rate: Optional[float]
    forecast_3m: Optional[float]
    forecast_6m: Optional[float]
    forecast_12m: Optional[float]
    expansion_needed_at: Optional[datetime]
    note: Optional[str]
    updated_at: datetime


class CapacityPlanCreate(BaseModel):
    source: str
    metric_type: str
    current_value: Optional[float] = None
    capacity_max: Optional[float] = None
    threshold_warn: Optional[float] = None
    threshold_crit: Optional[float] = None
    growth_rate: Optional[float] = None
    note: Optional[str] = None
    inst_id: Optional[int] = None


# ════════════════════════════════════════════════════════════════════════════════
# C-5: 서비스 카탈로그
# ════════════════════════════════════════════════════════════════════════════════

class ServiceStatus(str, Enum):
    ACTIVE     = "ACTIVE"
    DRAFT      = "DRAFT"
    DEPRECATED = "DEPRECATED"
    RETIRED    = "RETIRED"


class ServiceItem(Base):
    """서비스 카탈로그 항목."""
    __tablename__ = "tb_service_catalog"

    id            = Column(Integer, primary_key=True, index=True)
    service_id    = Column(String(30), unique=True, nullable=False, index=True)  # SVC-NNNN
    name          = Column(String(200), nullable=False)
    category      = Column(String(50))                   # 카테고리 (배포, DB관리, 보안 등)
    description   = Column(Text)
    status        = Column(String(20), default="ACTIVE")
    # SLA 정보
    sla_response_h  = Column(Float, default=8.0)         # 응답 SLA (시간)
    sla_resolve_h   = Column(Float, default=24.0)        # 해결 SLA (시간)
    sla_availability = Column(Float, default=99.5)       # 가용성 SLA (%)
    # 요청 안내
    request_template = Column(Text)                      # SR 요청 템플릿
    approval_required = Column(Boolean, default=False)   # 승인 필요 여부
    auto_assignee   = Column(String(50))                 # 자동 배정 담당자 사번
    estimated_hours = Column(Float)                      # 예상 처리 시간
    # 비용
    charge_per_request = Column(Float, default=0.0)      # 요청당 비용 (0=무료)
    currency        = Column(String(10), default="KRW")
    # 통계
    request_count   = Column(Integer, default=0)
    avg_resolve_h   = Column(Float)                      # 실제 평균 해결 시간
    satisfaction    = Column(Float)                      # 평균 만족도 (1-5)
    # 메타
    owner           = Column(String(100))
    version         = Column(String(20), default="1.0")
    tags            = Column(String(500))
    created_by      = Column(String(50))
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())


class ServiceItemOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    service_id: str
    name: str
    category: Optional[str]
    description: Optional[str]
    status: str
    sla_response_h: float
    sla_resolve_h: float
    sla_availability: float
    approval_required: bool
    estimated_hours: Optional[float]
    charge_per_request: float
    request_count: int
    avg_resolve_h: Optional[float]
    satisfaction: Optional[float]
    owner: Optional[str]
    version: str
    tags: Optional[str]
    created_at: datetime


class ServiceItemCreate(BaseModel):
    name: str
    category: Optional[str] = None
    description: Optional[str] = None
    status: str = "ACTIVE"
    sla_response_h: float = 8.0
    sla_resolve_h: float = 24.0
    sla_availability: float = 99.5
    request_template: Optional[str] = None
    approval_required: bool = False
    auto_assignee: Optional[str] = None
    estimated_hours: Optional[float] = None
    charge_per_request: float = 0.0
    owner: Optional[str] = None
    tags: Optional[str] = None


class ServiceItemUpdate(BaseModel):
    name: Optional[str] = None
    category: Optional[str] = None
    description: Optional[str] = None
    status: Optional[str] = None
    sla_response_h: Optional[float] = None
    sla_resolve_h: Optional[float] = None
    approval_required: Optional[bool] = None
    estimated_hours: Optional[float] = None
    owner: Optional[str] = None
    tags: Optional[str] = None


# ── 자율 운영 자동처리 ────────────────────────────────────────────────────────

class AutoActionStatus(str, Enum):
    AUTO_DONE        = "AUTO_DONE"        # 자동 완료
    PENDING_APPROVAL = "PENDING_APPROVAL" # 승인 대기
    APPROVED         = "APPROVED"         # 승인됨
    REJECTED         = "REJECTED"         # 거부됨
    EXPIRED          = "EXPIRED"          # 시간 만료
    FAILED           = "FAILED"           # 실행 실패


class AutoAction(Base):
    """자율 운영 작업 이력 및 승인 큐."""
    __tablename__ = "tb_auto_action"

    id           = Column(Integer, primary_key=True, index=True)
    action_id    = Column(String(30), unique=True, nullable=False, index=True)
    # ACT-XXXXXXXX
    action_type  = Column(String(50), nullable=False)
    # sr_classify | kb_answer | server_restart | deploy_prd | dr_failover 등
    description  = Column(Text)
    target       = Column(String(200))       # 대상 서버/SR/서비스
    risk_level   = Column(String(20), default="HIGH")
    # LOW | MEDIUM | HIGH | CRITICAL
    status       = Column(String(30), default=AutoActionStatus.PENDING_APPROVAL)
    payload      = Column(JSON, default=dict) # 실행에 필요한 파라미터
    result       = Column(JSON, default=dict) # 실행 결과
    requested_by = Column(String(100))
    approved_by  = Column(String(100))
    approved_at  = Column(DateTime)
    comment      = Column(Text)
    processed_at = Column(DateTime)
    expires_at   = Column(DateTime)          # 승인 만료 시각
    created_at   = Column(DateTime, default=func.now())


# ── DR 자동화 ──────────────────────────────────────────────────────────────────

class DRScenario(Base):
    """DR 시나리오 정의."""
    __tablename__ = "tb_dr_scenario"

    id                = Column(Integer, primary_key=True, index=True)
    name              = Column(String(100), nullable=False)
    scenario_type     = Column(String(30), default="SERVER_FAILURE")
    # SITE_FAILURE | SERVER_FAILURE | DATA_CORRUPTION
    primary_server_id = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    standby_server_id = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    rto_minutes       = Column(Integer, default=240)   # 목표 복구 시간 (분)
    rpo_minutes       = Column(Integer, default=60)    # 목표 복구 시점 (분)
    failover_steps    = Column(JSON, default=list)     # 실행 단계 목록
    healthcheck_url   = Column(String(255))
    last_test_at      = Column(DateTime)
    last_test_result  = Column(String(20))             # PASS | FAIL | PARTIAL
    is_active         = Column(Boolean, default=True)
    created_at        = Column(DateTime, default=func.now())

    tests             = relationship("DRTest", back_populates="scenario",
                                     cascade="all, delete-orphan")


class DRTest(Base):
    """DR 복구 테스트 실행 기록."""
    __tablename__ = "tb_dr_test"

    id            = Column(Integer, primary_key=True, index=True)
    scenario_id   = Column(Integer, ForeignKey("tb_dr_scenario.id"), nullable=False)
    test_type     = Column(String(20), default="RECOVERY")
    # BACKUP_VERIFY | RECOVERY | FAILOVER_SIM
    status        = Column(String(20), default="RUNNING")
    # RUNNING | PASS | FAIL | PARTIAL
    rto_actual    = Column(Integer)          # 실제 복구 시간 (분)
    rpo_actual    = Column(Integer)          # 실제 복구 시점 (분)
    result_detail = Column(JSON, default=dict)
    started_at    = Column(DateTime, default=func.now())
    completed_at  = Column(DateTime)
    triggered_by  = Column(String(100))

    scenario      = relationship("DRScenario", back_populates="tests")


# ── 네트워크 장비 관리 ─────────────────────────────────────────────────────────

class NetworkDevice(Base):
    """네트워크 장비 (스위치/라우터/방화벽/LB)."""
    __tablename__ = "tb_network_device"

    id             = Column(Integer, primary_key=True, index=True)
    device_name    = Column(String(100), nullable=False)
    device_type    = Column(String(30))    # SWITCH | ROUTER | FIREWALL | LOAD_BALANCER
    vendor         = Column(String(30))    # CISCO | HUAWEI | JUNIPER | PIOLINK | SECUI | RADWARE
    model          = Column(String(100))
    os_type        = Column(String(30))    # cisco_ios | huawei_vrp | junos | linux
    ip_addr        = Column(String(45))    # NOT exposed in API
    ssh_user       = Column(String(50))    # NOT exposed
    ssh_pw_enc     = Column(Text)          # AES-256-GCM, NEVER exposed
    ssh_port       = Column(Integer, default=22)
    location       = Column(String(200))
    inst_id        = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    is_active      = Column(Boolean, default=True)
    last_backup_at = Column(DateTime)
    created_at     = Column(DateTime, default=func.now())

    backups        = relationship("NetworkConfigBackup", back_populates="device",
                                  cascade="all, delete-orphan")


class NetworkConfigBackup(Base):
    """네트워크 장비 설정 백업."""
    __tablename__ = "tb_network_backup"

    id            = Column(Integer, primary_key=True, index=True)
    device_id     = Column(Integer, ForeignKey("tb_network_device.id"), nullable=False)
    config_text   = Column(Text)           # 설정 전문
    config_hash   = Column(String(64))     # SHA-256
    backup_type   = Column(String(20), default="MANUAL")
    # SCHEDULED | MANUAL | PRE_CHANGE
    backed_up_at  = Column(DateTime, default=func.now())
    backed_up_by  = Column(String(100))

    device        = relationship("NetworkDevice", back_populates="backups")


# ── CSAP 공공기관 보안 점검 ────────────────────────────────────────────────────

class CSAPCheckResult(Base):
    """CSAP/ISMS-P 점검 결과."""
    __tablename__ = "tb_csap_result"

    id             = Column(Integer, primary_key=True, index=True)
    scan_id        = Column(String(50), nullable=False, index=True)
    # 배치 ID: CSAP-YYYYMMDD-HHMMSS
    inst_id        = Column(Integer, ForeignKey("tb_inst_meta.id"), nullable=True)
    item_id        = Column(String(20), nullable=False)   # M-01, T-03 등
    category       = Column(String(20))                   # 관리적 | 기술적 | 물리적 | 운영
    item_name      = Column(String(200))
    severity       = Column(String(20))                   # HIGH | MEDIUM | LOW
    status         = Column(String(20))
    # PASS | FAIL | PARTIAL | MANUAL_REQUIRED | N_A
    finding        = Column(Text)
    evidence       = Column(JSON, default=dict)           # 자동 수집 증적 (마스킹)
    recommendation = Column(Text)
    scanned_at     = Column(DateTime, default=func.now())


# ── 개방망 API Key ─────────────────────────────────────────────────────────────

# ── 스크랩핑 ────────────────────────────────────────────────────────────────

class ScrapingTarget(Base):
    """스크랩 대상 URL 등록 테이블."""
    __tablename__ = "tb_scraping_target"

    id           = Column(Integer, primary_key=True, index=True)
    name         = Column(String(100), nullable=False)
    url          = Column(String(500), nullable=False)
    selector     = Column(String(200))          # CSS 셀렉터 (옵션)
    schedule     = Column(String(50))           # cron (옵션)
    is_active    = Column(Boolean, default=True)
    last_scraped = Column(DateTime, nullable=True)
    note         = Column(Text)
    created_by   = Column(String(50))
    created_at   = Column(DateTime, default=func.now())
    updated_at   = Column(DateTime, default=func.now(), onupdate=func.now())

    results = relationship("ScrapingResult", back_populates="target",
                           cascade="all, delete-orphan")


class ScrapingResult(Base):
    """스크랩 결과 저장 테이블."""
    __tablename__ = "tb_scraping_result"

    id           = Column(Integer, primary_key=True, index=True)
    target_id    = Column(Integer, ForeignKey("tb_scraping_target.id"), nullable=True)
    title        = Column(String(300))
    content      = Column(Text)          # 정제된 본문 (HTML)
    plain_text   = Column(Text)          # 텍스트 본문 (검색용)
    url          = Column(String(500), nullable=False)
    source_html  = Column(Text)          # 원본 HTML (원복용)
    status       = Column(String(20), default="DRAFT")  # DRAFT/PUBLISHED/DELETED/FAILED
    scraped_at   = Column(DateTime, default=func.now())
    published_at = Column(DateTime, nullable=True)
    deleted_at   = Column(DateTime, nullable=True)
    published_by = Column(String(50), nullable=True)
    messenger_room = Column(String(50), default="ops")
    meta         = Column(JSON, default=dict)
    error_msg    = Column(Text, nullable=True)
    scraped_by   = Column(String(50), default="system")
    created_at   = Column(DateTime, default=func.now())

    target = relationship("ScrapingTarget", back_populates="results")


class ScrapingTargetOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    name: str
    url: str
    selector: Optional[str]
    schedule: Optional[str]
    is_active: bool
    last_scraped: Optional[datetime]
    note: Optional[str]
    created_by: Optional[str]
    created_at: datetime


class ScrapingTargetCreate(BaseModel):
    name: str
    url: str
    selector: Optional[str] = None
    schedule: Optional[str] = None
    is_active: bool = True
    note: Optional[str] = None


class ScrapingResultOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: int
    target_id: Optional[int]
    title: Optional[str]
    plain_text: Optional[str]
    url: str
    status: str
    scraped_at: datetime
    published_at: Optional[datetime]
    deleted_at: Optional[datetime]
    published_by: Optional[str]
    messenger_room: Optional[str]
    meta: Optional[dict]
    error_msg: Optional[str]
    scraped_by: Optional[str]
    created_at: datetime


class APIKey(Base):
    """외부 시스템 연동용 API Key (개방망 전용)."""
    __tablename__ = "tb_api_key"

    id           = Column(Integer, primary_key=True, index=True)
    name         = Column(String(100), nullable=False)          # 키 이름 (ex: "카카오워크 봇")
    key_hash     = Column(String(64), unique=True, nullable=False, index=True)  # SHA-256
    scopes       = Column(String(200), default="read")          # read,write,admin,webhook
    allowed_ips  = Column(String(500), default="")              # "1.2.3.4,5.6.7.8" 빈칸=전체허용
    is_active    = Column(Boolean, default=True)
    use_count    = Column(Integer, default=0)
    last_used_at = Column(DateTime, nullable=True)
    expires_at   = Column(DateTime, nullable=True)
    created_by   = Column(String(50), nullable=True)
    created_at   = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── GUARDiA 확장 모델 (v3) — RAG / Jira / KPI / Workflow ─────────────────────
# ══════════════════════════════════════════════════════════════════════════════

class RAGFeedback(Base):
    """RAG 검색 품질 피드백 — Learning Loop 훈련 데이터."""
    __tablename__ = "tb_rag_feedback"

    id         = Column(Integer, primary_key=True, index=True)
    user_id    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    query      = Column(Text, nullable=False)
    doc_id     = Column(Integer, nullable=True)
    rating     = Column(Integer, nullable=False)   # 1~5
    comment    = Column(Text, nullable=True)
    created_at = Column(DateTime, default=func.now())


class JiraConfig(Base):
    """테넌트별 Jira 연동 설정 (API 토큰 암호화 저장)."""
    __tablename__ = "tb_jira_config"

    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    base_url        = Column(String(500), nullable=False)
    email           = Column(String(200), nullable=False)
    api_token_enc   = Column(Text, nullable=False)          # AES-256-GCM 암호화
    project_key     = Column(String(50), nullable=False)
    status_mapping  = Column(Text, nullable=True)           # JSON
    auto_sync       = Column(Boolean, default=True)
    webhook_secret  = Column(String(200), nullable=True)
    is_active       = Column(Boolean, default=True)
    last_synced_at  = Column(DateTime, nullable=True)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())


class JiraSyncMapping(Base):
    """SR ↔ Jira Issue 매핑."""
    __tablename__ = "tb_jira_sync_mapping"

    id              = Column(Integer, primary_key=True, index=True)
    sr_id           = Column(Integer, ForeignKey("tb_sr_request.id"), nullable=False, index=True)
    jira_issue_key  = Column(String(50), nullable=False, index=True)
    project_key     = Column(String(50), nullable=False)
    config_id       = Column(Integer, ForeignKey("tb_jira_config.id"), nullable=False)
    synced_at       = Column(DateTime, nullable=True)
    created_at      = Column(DateTime, default=func.now())


class KPIDefinition(Base):
    """KPI 정의 — 테넌트별 커스터마이즈."""
    __tablename__ = "tb_kpi_definition"

    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(Integer, nullable=False, index=True)
    name         = Column(String(100), nullable=False)
    display_name = Column(String(200), nullable=False)
    description  = Column(Text, nullable=True)
    unit         = Column(String(20), nullable=False)
    direction    = Column(String(20), nullable=False)  # HIGHER_BETTER | LOWER_BETTER
    target       = Column(Float, nullable=False)
    period       = Column(String(10), nullable=False, default="MONTHLY")
    is_active    = Column(Boolean, default=True)
    last_run_at  = Column(DateTime, nullable=True)
    created_at   = Column(DateTime, default=func.now())


class KPIValue(Base):
    """KPI 계산값 이력."""
    __tablename__ = "tb_kpi_value"

    id             = Column(Integer, primary_key=True, index=True)
    kpi_id         = Column(Integer, ForeignKey("tb_kpi_definition.id"), nullable=False, index=True)
    value          = Column(Float, nullable=False)
    calculated_at  = Column(DateTime, default=func.now(), index=True)


class AutoWorkflowRule(Base):
    """자율 워크플로우 규칙 정의."""
    __tablename__ = "tb_auto_workflow_rule"

    id               = Column(Integer, primary_key=True, index=True)
    name             = Column(String(200), nullable=False)
    description      = Column(Text, nullable=True)
    trigger_type     = Column(String(50), nullable=False, index=True)
    conditions_json  = Column(Text, nullable=True)   # JSON 조건식
    actions_json     = Column(Text, nullable=False)  # JSON 액션 목록
    approval_required = Column(Boolean, default=False)
    max_daily_runs   = Column(Integer, default=100)
    cron_expr        = Column(String(100), nullable=True)
    is_active        = Column(Boolean, default=True)
    last_run_at      = Column(DateTime, nullable=True)
    created_by       = Column(Integer, nullable=True)
    created_at       = Column(DateTime, default=func.now())
    updated_at       = Column(DateTime, default=func.now(), onupdate=func.now())


class AutoWorkflowRun(Base):
    """자율 워크플로우 실행 이력."""
    __tablename__ = "tb_auto_workflow_run"

    id              = Column(Integer, primary_key=True, index=True)
    rule_id         = Column(Integer, ForeignKey("tb_auto_workflow_rule.id"), nullable=False, index=True)
    trigger_payload = Column(Text, nullable=True)    # JSON
    status          = Column(String(20), nullable=False, default="PENDING")  # RUNNING|SUCCESS|FAILED
    result_json     = Column(Text, nullable=True)
    error_message   = Column(Text, nullable=True)
    started_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


# ── GUARDiA 확장 v3 P2 — K8s / SSO / Slack / WhiteLabel ──────────────────────

class K8sCluster(Base):
    """Kubernetes 클러스터 등록 (SSH 경유 관리)."""
    __tablename__ = "tb_k8s_cluster"

    id               = Column(Integer, primary_key=True, index=True)
    tenant_id        = Column(Integer, nullable=False, index=True)
    name             = Column(String(100), nullable=False)
    description      = Column(Text, nullable=True)
    ssh_server_id    = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False)
    namespace        = Column(String(100), default="default")
    kubeconfig_path  = Column(String(500), default="/root/.kube/config")
    is_active        = Column(Boolean, default=True)
    created_at       = Column(DateTime, default=func.now())


class SSOConfig(Base):
    """SSO 통합 인증 설정 (SAML/OIDC/OAuth2)."""
    __tablename__ = "tb_sso_config"

    id                  = Column(Integer, primary_key=True, index=True)
    tenant_id           = Column(Integer, nullable=False, index=True)
    name                = Column(String(100), nullable=False)
    provider_type       = Column(String(20), nullable=False)  # SAML|OIDC|OAUTH2
    idp_metadata_url    = Column(String(500), nullable=True)
    idp_sso_url         = Column(String(500), nullable=True)
    idp_cert            = Column(Text, nullable=True)
    client_id           = Column(String(200), nullable=True)
    client_secret_enc   = Column(Text, nullable=True)           # AES-256-GCM 암호화
    discovery_url       = Column(String(500), nullable=True)
    scopes              = Column(String(200), default="openid email profile")
    attribute_mapping   = Column(Text, nullable=True)           # JSON
    default_role        = Column(String(20), default="ENGINEER")
    is_active           = Column(Boolean, default=True)
    created_at          = Column(DateTime, default=func.now())


class SSOSession(Base):
    """SSO 로그인 세션 추적."""
    __tablename__ = "tb_sso_session"

    id          = Column(Integer, primary_key=True, index=True)
    user_id     = Column(Integer, ForeignKey("tb_user.id"), nullable=False)
    config_id   = Column(Integer, ForeignKey("tb_sso_config.id"), nullable=False)
    state       = Column(String(100), nullable=True, index=True)
    created_at  = Column(DateTime, default=func.now())
    expires_at  = Column(DateTime, nullable=True)


class SlackConfig(Base):
    """Slack 연동 설정."""
    __tablename__ = "tb_slack_config"

    id                  = Column(Integer, primary_key=True, index=True)
    tenant_id           = Column(Integer, nullable=False, unique=True, index=True)
    name                = Column(String(100), nullable=False)
    webhook_url         = Column(String(500), nullable=False)
    signing_secret      = Column(String(200), nullable=True)
    default_channel     = Column(String(100), default="#guardia-ops")
    notify_sr_create    = Column(Boolean, default=True)
    notify_incident     = Column(Boolean, default=True)
    notify_deploy       = Column(Boolean, default=True)
    notify_sla_breach   = Column(Boolean, default=True)
    is_active           = Column(Boolean, default=True)
    created_at          = Column(DateTime, default=func.now())
    updated_at          = Column(DateTime, default=func.now(), onupdate=func.now())


class TenantBranding(Base):
    """테넌트 화이트라벨 브랜딩 설정."""
    __tablename__ = "tb_tenant_branding"

    id                  = Column(Integer, primary_key=True, index=True)
    tenant_id           = Column(Integer, nullable=False, unique=True, index=True)
    company_name        = Column(String(200), nullable=True)
    logo_url            = Column(String(500), nullable=True)
    logo_dark_url       = Column(String(500), nullable=True)
    favicon_url         = Column(String(500), nullable=True)
    primary_color       = Column(String(7), nullable=True)      # #RRGGBB
    secondary_color     = Column(String(7), nullable=True)
    accent_color        = Column(String(7), nullable=True)
    font_family         = Column(String(200), nullable=True)
    login_bg_color      = Column(String(7), nullable=True)
    header_bg_color     = Column(String(7), nullable=True)
    custom_domain       = Column(String(200), nullable=True)
    footer_text         = Column(String(500), nullable=True)
    email_header_html   = Column(Text, nullable=True)
    email_footer_html   = Column(Text, nullable=True)
    created_at          = Column(DateTime, default=func.now())
    updated_at          = Column(DateTime, default=func.now(), onupdate=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── GUARDiA 확장 v3 P3 — Learning / Container / NCloud / Billing / ServiceNow
# ══════════════════════════════════════════════════════════════════════════════

class LearningRun(Base):
    """AI 학습 실행 이력."""
    __tablename__ = "tb_learning_run"
    id              = Column(Integer, primary_key=True, index=True)
    triggered_by    = Column(Integer, nullable=True)
    sample_count    = Column(Integer, default=0)
    samples_used    = Column(Integer, default=0)
    model_name      = Column(String(200), nullable=True)
    status          = Column(String(20), default="PENDING")  # PENDING|RUNNING|SUCCESS|FAILED
    error_message   = Column(Text, nullable=True)
    started_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


class ContainerAlertRule(Base):
    """컨테이너 알림 규칙."""
    __tablename__ = "tb_container_alert_rule"
    id                  = Column(Integer, primary_key=True, index=True)
    tenant_id           = Column(Integer, nullable=False, index=True)
    name                = Column(String(200), nullable=False)
    server_id           = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False)
    container_name      = Column(String(200), nullable=True)
    alert_on_stopped    = Column(Boolean, default=True)
    alert_on_high_cpu   = Column(Boolean, default=True)
    cpu_threshold       = Column(Float, default=90.0)
    alert_on_high_mem   = Column(Boolean, default=True)
    mem_threshold       = Column(Float, default=90.0)
    auto_sr             = Column(Boolean, default=True)
    is_active           = Column(Boolean, default=True)
    created_at          = Column(DateTime, default=func.now())


class ContainerAlertLog(Base):
    """컨테이너 알림 이력."""
    __tablename__ = "tb_container_alert_log"
    id              = Column(Integer, primary_key=True, index=True)
    rule_id         = Column(Integer, ForeignKey("tb_container_alert_rule.id"), nullable=False)
    alert_type      = Column(String(50), nullable=False)
    container_name  = Column(String(200), nullable=True)
    severity        = Column(String(20), nullable=False)
    message         = Column(Text, nullable=True)
    detected_at     = Column(DateTime, default=func.now())


class NCloudConfig(Base):
    """NCloud API 설정."""
    __tablename__ = "tb_ncloud_config"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, unique=True, index=True)
    access_key      = Column(String(200), nullable=False)
    secret_key_enc  = Column(Text, nullable=False)
    region          = Column(String(20), default="KR")
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class Subscription(Base):
    """테넌트 구독 정보."""
    __tablename__ = "tb_subscription"
    id                  = Column(Integer, primary_key=True, index=True)
    tenant_id           = Column(Integer, nullable=False, unique=True, index=True)
    plan                = Column(String(50), nullable=False, default="COMMUNITY")
    billing_cycle       = Column(String(20), default="MONTHLY")
    status              = Column(String(20), default="ACTIVE")
    is_trial            = Column(Boolean, default=False)
    start_date          = Column(DateTime, nullable=True)
    next_billing_date   = Column(DateTime, nullable=True)
    is_active           = Column(Boolean, default=True)
    created_at          = Column(DateTime, default=func.now())
    updated_at          = Column(DateTime, default=func.now(), onupdate=func.now())


class Invoice(Base):
    """청구서."""
    __tablename__ = "tb_invoice"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    plan            = Column(String(50), nullable=True)
    period          = Column(String(10), nullable=False)  # YYYY-MM
    amount          = Column(Integer, default=0)
    servers_used    = Column(Integer, default=0)
    users_used      = Column(Integer, default=0)
    sr_count        = Column(Integer, default=0)
    status          = Column(String(20), default="DRAFT")  # DRAFT|ISSUED|PAID
    generated_by    = Column(Integer, nullable=True)
    created_at      = Column(DateTime, default=func.now())


class ServiceNowConfig(Base):
    """ServiceNow 연동 설정."""
    __tablename__ = "tb_servicenow_config"
    id                  = Column(Integer, primary_key=True, index=True)
    tenant_id           = Column(Integer, nullable=False, unique=True, index=True)
    instance_url        = Column(String(500), nullable=False)
    username            = Column(String(200), nullable=False)
    password_enc        = Column(Text, nullable=False)
    assignment_group    = Column(String(200), nullable=True)
    is_active           = Column(Boolean, default=True)
    created_at          = Column(DateTime, default=func.now())


class ServiceNowMapping(Base):
    """SR ↔ ServiceNow Incident 매핑."""
    __tablename__ = "tb_servicenow_mapping"
    id              = Column(Integer, primary_key=True, index=True)
    sr_id           = Column(Integer, ForeignKey("tb_sr_request.id"), nullable=False)
    snow_number     = Column(String(50), nullable=False)
    config_id       = Column(Integer, ForeignKey("tb_servicenow_config.id"), nullable=False)
    synced_at       = Column(DateTime, default=func.now())


class ERPConfig(Base):
    """ERP / 그룹웨어 연동 설정."""
    __tablename__ = "tb_erp_config"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    name            = Column(String(100), nullable=False)
    base_url        = Column(String(500), nullable=False)
    erp_type        = Column(String(50), default="generic")
    api_key_enc     = Column(Text, nullable=True)
    username        = Column(String(200), nullable=True)
    password_enc    = Column(Text, nullable=True)
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class KakaoConfig(Base):
    """카카오 알림톡 설정."""
    __tablename__ = "tb_kakao_config"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, unique=True, index=True)
    apikey          = Column(String(200), nullable=False)
    userid          = Column(String(100), nullable=False)
    senderkey_enc   = Column(Text, nullable=False)
    sender          = Column(String(20), nullable=False)
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class KakaoNotifyLog(Base):
    """카카오 발송 이력."""
    __tablename__ = "tb_kakao_notify_log"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    template_code   = Column(String(100), nullable=False)
    receiver_count  = Column(Integer, default=0)
    success         = Column(Boolean, default=False)
    result_json     = Column(Text, nullable=True)
    sent_at         = Column(DateTime, default=func.now())


class ReportRecord(Base):
    """생성된 보고서 이력."""
    __tablename__ = "tb_report_record"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    template        = Column(String(50), nullable=False)
    period_start    = Column(DateTime, nullable=True)
    period_end      = Column(DateTime, nullable=True)
    format          = Column(String(10), default="excel")
    file_size       = Column(Integer, default=0)
    status          = Column(String(20), default="DONE")
    generated_by    = Column(Integer, nullable=True)
    created_at      = Column(DateTime, default=func.now())


class BenchmarkContrib(Base):
    """익명 벤치마킹 기여 데이터."""
    __tablename__ = "tb_benchmark_contrib"
    id                  = Column(Integer, primary_key=True, index=True)
    completion_rate     = Column(Float, nullable=True)
    mttr_hours          = Column(Float, nullable=True)
    sla_compliance      = Column(Float, nullable=True)
    sr_volume_band      = Column(String(20), nullable=True)  # LOW|MEDIUM|HIGH
    contributed_at      = Column(DateTime, default=func.now())


class ReportSchedule(Base):
    """자동 보고서 발송 스케줄."""
    __tablename__ = "tb_report_schedule"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(Integer, nullable=False, index=True)
    template    = Column(String(50), nullable=False)
    cron        = Column(String(100), nullable=False)
    email       = Column(String(200), nullable=False)
    format      = Column(String(10), default="excel")
    is_active   = Column(Boolean, default=True)
    created_at  = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── GUARDiA 고급 확장 — CMDB자동발견 / NL쿼리 / 드리프트 / 멀티클라우드 / 공공
# ══════════════════════════════════════════════════════════════════════════════

class DiscoveryScanJob(Base):
    __tablename__ = "tb_discovery_scan_job"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    network_range   = Column(String(50), nullable=False)
    ssh_user        = Column(String(100), default="root")
    include_snmp    = Column(Boolean, default=False)
    auto_register   = Column(Boolean, default=False)
    status          = Column(String(20), default="QUEUED")
    host_count      = Column(Integer, default=0)
    discovered_count = Column(Integer, default=0)
    result_summary  = Column(Text, nullable=True)
    error_message   = Column(Text, nullable=True)
    created_by      = Column(Integer, nullable=True)
    created_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


class CMDBAutoDiscovery(Base):
    __tablename__ = "tb_cmdb_autodiscovery"
    id              = Column(Integer, primary_key=True, index=True)
    job_id          = Column(Integer, ForeignKey("tb_discovery_scan_job.id"), nullable=False, index=True)
    ip_addr         = Column(String(50), nullable=False)
    status          = Column(String(20), default="UNREACHABLE")
    discovered_data = Column(JSON, nullable=True)
    in_cmdb         = Column(Boolean, default=False)
    discovered_at   = Column(DateTime, default=func.now())


class ServiceDependency(Base):
    __tablename__ = "tb_service_dependency"
    id               = Column(Integer, primary_key=True, index=True)
    upstream_ci_id   = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False, index=True)
    downstream_ci_id = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False, index=True)
    dependency_type  = Column(String(50), nullable=False)
    port             = Column(Integer, nullable=True)
    protocol         = Column(String(10), default="TCP")
    discovered_at    = Column(DateTime, default=func.now())


class SNMPConfig(Base):
    __tablename__ = "tb_snmp_config"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    name            = Column(String(100), nullable=False)
    community_enc   = Column(Text, nullable=False)
    version         = Column(Integer, default=2)
    ip_ranges       = Column(JSON, nullable=True)
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class SNMPDevice(Base):
    __tablename__ = "tb_snmp_device"
    id               = Column(Integer, primary_key=True, index=True)
    config_id        = Column(Integer, ForeignKey("tb_snmp_config.id"), nullable=False)
    ip_addr          = Column(String(50), nullable=False)
    sysname          = Column(String(200), nullable=True)
    description      = Column(Text, nullable=True)
    device_type      = Column(String(50), default="UNKNOWN")
    location         = Column(String(200), nullable=True)
    interface_count  = Column(Integer, default=0)
    discovered_at    = Column(DateTime, default=func.now())


class ServerInventory(Base):
    __tablename__ = "tb_server_inventory"
    id           = Column(Integer, primary_key=True, index=True)
    server_id    = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False, index=True)
    data         = Column(JSON, nullable=True)
    collected_at = Column(DateTime, default=func.now())


class QueryHistory(Base):
    __tablename__ = "tb_query_history"
    id            = Column(Integer, primary_key=True, index=True)
    user_id       = Column(Integer, ForeignKey("tb_user.id"), nullable=False, index=True)
    question      = Column(Text, nullable=False)
    generated_sql = Column(Text, nullable=True)
    description   = Column(Text, nullable=True)
    row_count     = Column(Integer, default=0)
    is_favorite   = Column(Boolean, default=False)
    is_shared     = Column(Boolean, default=False)
    executed_at   = Column(DateTime, default=func.now())


class AssistantSession(Base):
    __tablename__ = "tb_assistant_session"
    id         = Column(Integer, primary_key=True, index=True)
    user_id    = Column(Integer, ForeignKey("tb_user.id"), nullable=False, index=True)
    title      = Column(String(200), nullable=True)
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())


class AssistantMessage(Base):
    __tablename__ = "tb_assistant_message"
    id         = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("tb_assistant_session.id"), nullable=False, index=True)
    role       = Column(String(20), nullable=False)
    content    = Column(Text, nullable=False)
    created_at = Column(DateTime, default=func.now())


class GoldenConfig(Base):
    __tablename__ = "tb_golden_config"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(Integer, nullable=False, index=True)
    name        = Column(String(200), nullable=False)
    server_type = Column(String(50), nullable=False)
    description = Column(Text, nullable=True)
    items_json  = Column(Text, nullable=True)
    version     = Column(String(20), default="1.0")
    is_active   = Column(Boolean, default=True)
    created_at  = Column(DateTime, default=func.now())


class DriftResult(Base):
    __tablename__ = "tb_drift_result"
    id                 = Column(Integer, primary_key=True, index=True)
    server_id          = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False, index=True)
    config_id          = Column(Integer, ForeignKey("tb_golden_config.id"), nullable=True)
    total_checks       = Column(Integer, default=0)
    compliant_count    = Column(Integer, default=0)
    non_compliant_count = Column(Integer, default=0)
    compliance_pct     = Column(Float, nullable=True)
    results_json       = Column(Text, nullable=True)
    scanned_at         = Column(DateTime, default=func.now())


class AutoRemediationJob(Base):
    __tablename__ = "tb_auto_remediation_job"
    id               = Column(Integer, primary_key=True, index=True)
    drift_result_id  = Column(Integer, ForeignKey("tb_drift_result.id"), nullable=True)
    server_id        = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False)
    item_key         = Column(String(100), nullable=False)
    fix_cmd          = Column(Text, nullable=False)
    rollback_cmd     = Column(Text, nullable=True)
    status           = Column(String(30), default="PENDING_APPROVAL")
    result_message   = Column(Text, nullable=True)
    requested_by     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    approved_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at       = Column(DateTime, default=func.now())
    executed_at      = Column(DateTime, nullable=True)


class MultiCloudConfig(Base):
    __tablename__ = "tb_multicloud_config"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    name            = Column(String(100), nullable=False)
    provider_type   = Column(String(50), nullable=False)
    region          = Column(String(50), default="kr-1")
    access_key      = Column(String(200), nullable=False)
    secret_key_enc  = Column(Text, nullable=False)
    extra_config    = Column(Text, nullable=True)
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class AWSConfig(Base):
    __tablename__ = "tb_aws_config"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, unique=True, index=True)
    name            = Column(String(100), nullable=False)
    access_key_id   = Column(String(200), nullable=False)
    secret_key_enc  = Column(Text, nullable=False)
    region          = Column(String(50), default="ap-northeast-2")
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class NarasajangConfig(Base):
    __tablename__ = "tb_narasajang_config"
    id                  = Column(Integer, primary_key=True, index=True)
    tenant_id           = Column(Integer, nullable=False, unique=True, index=True)
    api_key_enc         = Column(Text, nullable=False)
    institution_code    = Column(String(50), nullable=True)
    is_active           = Column(Boolean, default=True)
    created_at          = Column(DateTime, default=func.now())


class PublicApiConfig(Base):
    __tablename__ = "tb_public_api_config"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(Integer, nullable=False, index=True)
    api_id      = Column(String(100), nullable=False)
    name        = Column(String(200), nullable=False)
    endpoint    = Column(String(500), nullable=False)
    api_key_enc = Column(Text, nullable=False)
    is_active   = Column(Boolean, default=True)
    created_at  = Column(DateTime, default=func.now())


class MigrationChecklist(Base):
    __tablename__ = "tb_migration_checklist"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(Integer, nullable=False, index=True)
    item_key    = Column(String(100), nullable=False)
    completed   = Column(Boolean, default=False)
    note        = Column(Text, nullable=True)
    created_at  = Column(DateTime, default=func.now())
    updated_at  = Column(DateTime, default=func.now(), onupdate=func.now())


class KCloudConfig(Base):
    __tablename__ = "tb_kcloud_config"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    csp_id          = Column(String(50), nullable=False)
    csp_name        = Column(String(100), nullable=False)
    access_key      = Column(String(200), nullable=False)
    secret_key_enc  = Column(Text, nullable=False)
    region          = Column(String(50), default="KR")
    account_type    = Column(String(20), default="gov")
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class NetworkZone(Base):
    __tablename__ = "tb_network_zone"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(Integer, nullable=False, index=True)
    name        = Column(String(100), nullable=False)
    zone_type   = Column(String(30), nullable=False)
    description = Column(Text, nullable=True)
    ip_ranges   = Column(JSON, nullable=True)
    created_at  = Column(DateTime, default=func.now())


class NetworkPolicy(Base):
    __tablename__ = "tb_network_policy"
    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(Integer, nullable=False, index=True)
    src_zone_id  = Column(Integer, ForeignKey("tb_network_zone.id"), nullable=False)
    dst_zone_id  = Column(Integer, ForeignKey("tb_network_zone.id"), nullable=False)
    protocol     = Column(String(10), default="TCP")
    dst_port     = Column(Integer, nullable=True)
    action       = Column(String(10), default="DENY")
    description  = Column(Text, nullable=True)
    created_by   = Column(Integer, nullable=True)
    created_at   = Column(DateTime, default=func.now())


class ProcurementRecord(Base):
    """조달 계약 이력 — 나라장터·전자조달 연계."""
    __tablename__ = "tb_procurement_record"
    id               = Column(Integer, primary_key=True, index=True)
    tenant_id        = Column(Integer, nullable=False, index=True)
    contract_no      = Column(String(50), nullable=True)
    contract_name    = Column(String(300), nullable=False)
    supplier         = Column(String(200), nullable=True)
    amount           = Column(BigInteger, default=0)
    category         = Column(String(100), nullable=True)
    start_date       = Column(DateTime, nullable=True)
    end_date         = Column(DateTime, nullable=True)
    linked_sr_ids    = Column(JSON, nullable=True)
    status           = Column(String(30), default="ACTIVE")
    inspection_result = Column(String(20), nullable=True)
    inspection_date  = Column(DateTime, nullable=True)
    inspection_by    = Column(String(100), nullable=True)
    created_at       = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── Upstage OCR 연동 모델
# ══════════════════════════════════════════════════════════════════════════════

class UpstageOCRConfig(Base):
    """Upstage Document AI API 설정."""
    __tablename__ = "tb_upstage_ocr_config"
    tenant_id   = Column(Integer, primary_key=True, index=True)
    api_key_enc = Column(Text, nullable=False)          # AES-256-GCM 암호화
    model       = Column(String(50), default="document-parse")
    daily_limit = Column(Integer, default=1000)
    is_active   = Column(Boolean, default=True)
    created_at  = Column(DateTime, default=func.now())
    updated_at  = Column(DateTime, default=func.now(), onupdate=func.now())


class OCRHistory(Base):
    """OCR 처리 이력."""
    __tablename__ = "tb_ocr_history"
    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(Integer, nullable=False, index=True)
    filename     = Column(String(300), nullable=False)
    file_size    = Column(Integer, default=0)
    ocr_type     = Column(String(30), nullable=False)   # PARSE | EXTRACT | QA
    schema_used  = Column(Text, nullable=True)
    result_json  = Column(Text, nullable=True)          # 결과 요약 (최대 5000자)
    linked_to    = Column(String(50), nullable=True)
    linked_id    = Column(Integer, nullable=True)
    pages        = Column(Integer, default=1)
    tokens_used  = Column(Integer, default=0)
    status       = Column(String(20), default="SUCCESS")
    created_by   = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at   = Column(DateTime, default=func.now())


class DocWorkflowJob(Base):
    """문서 워크플로우 작업 이력."""
    __tablename__ = "tb_doc_workflow_job"
    id               = Column(Integer, primary_key=True, index=True)
    tenant_id        = Column(Integer, nullable=False, index=True)
    workflow_type    = Column(String(50), nullable=False)
    filename         = Column(String(300), nullable=True)
    template_id      = Column(Integer, nullable=True)
    status           = Column(String(20), default="PROCESSING")
    extracted_data   = Column(JSON, nullable=True)
    linked_table     = Column(String(50), nullable=True)
    linked_record_id = Column(Integer, nullable=True)
    error_message    = Column(Text, nullable=True)
    created_by       = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at       = Column(DateTime, default=func.now())
    completed_at     = Column(DateTime, nullable=True)


class DocTemplate(Base):
    """문서 추출 템플릿."""
    __tablename__ = "tb_doc_template"
    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(Integer, nullable=False, index=True)
    name         = Column(String(200), nullable=False)
    description  = Column(Text, nullable=True)
    schema_json  = Column(Text, nullable=False)
    workflow     = Column(String(50), nullable=True)
    builtin_key  = Column(String(100), nullable=True)
    is_builtin   = Column(Boolean, default=False)
    is_active    = Column(Boolean, default=True)
    created_at   = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── GUARDiA 기능 개선 v4 — 앱배포QR / 배치SSH / 자산QR / 스마트알림
# ══════════════════════════════════════════════════════════════════════════════

class AppVersion(Base):
    """모바일 앱 배포 버전 관리."""
    __tablename__ = "tb_app_version"
    id              = Column(Integer, primary_key=True, index=True)
    version         = Column(String(20), nullable=False)
    platform        = Column(String(20), default="Android")
    file_path       = Column(String(500), nullable=True)
    file_size_mb    = Column(Float, default=0.0)
    android_url     = Column(String(1000), nullable=True)
    ios_url         = Column(String(1000), nullable=True)
    qr_image_path   = Column(String(500), nullable=True)
    qr_data         = Column(Text, nullable=True)           # base64 PNG
    landing_token   = Column(String(64), nullable=True, unique=True)
    download_count  = Column(Integer, default=0)
    is_latest       = Column(Boolean, default=False)
    release_notes   = Column(Text, nullable=True)
    uploaded_by     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class AppDownloadLog(Base):
    """앱 다운로드 이력."""
    __tablename__ = "tb_app_download_log"
    id          = Column(Integer, primary_key=True, index=True)
    version_id  = Column(Integer, ForeignKey("tb_app_version.id"), nullable=False)
    platform    = Column(String(20), nullable=True)
    user_agent  = Column(String(500), nullable=True)
    ip_hash     = Column(String(64), nullable=True)
    downloaded_at = Column(DateTime, default=func.now())


class BatchSSHJob(Base):
    """다중 서버 동시 SSH 실행 작업."""
    __tablename__ = "tb_batch_ssh_job"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    name            = Column(String(200), nullable=False)
    command         = Column(Text, nullable=False)
    server_ids      = Column(JSON, nullable=False)
    timeout_sec     = Column(Integer, default=30)
    status          = Column(String(20), default="PENDING")
    results         = Column(JSON, nullable=True)
    total_count     = Column(Integer, default=0)
    success_count   = Column(Integer, default=0)
    fail_count      = Column(Integer, default=0)
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


class AssetQRToken(Base):
    """서버 자산 QR 토큰."""
    __tablename__ = "tb_asset_qr_token"
    id          = Column(Integer, primary_key=True, index=True)
    server_id   = Column(Integer, ForeignKey("tb_server_info.id"), nullable=False, unique=True)
    token       = Column(String(36), nullable=False, unique=True, index=True)
    qr_image_path = Column(String(500), nullable=True)
    scan_count  = Column(Integer, default=0)
    last_scanned_at = Column(DateTime, nullable=True)
    created_by  = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at  = Column(DateTime, default=func.now())


class AssetQRScanLog(Base):
    """자산 QR 스캔 이력."""
    __tablename__ = "tb_asset_qr_scan_log"
    id          = Column(Integer, primary_key=True, index=True)
    token_id    = Column(Integer, ForeignKey("tb_asset_qr_token.id"), nullable=False)
    scanned_by  = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    checkin     = Column(Boolean, default=False)
    note        = Column(Text, nullable=True)
    scanned_at  = Column(DateTime, default=func.now())


class SmartNotifyRule(Base):
    """스마트 알림 규칙."""
    __tablename__ = "tb_smart_notify_rule"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(Integer, nullable=False, index=True)
    name            = Column(String(200), nullable=False)
    enabled         = Column(Boolean, default=True)
    conditions      = Column(JSON, nullable=True)
    channels        = Column(JSON, nullable=True)
    silence_start   = Column(String(5), nullable=True)
    silence_end     = Column(String(5), nullable=True)
    digest_mode     = Column(Boolean, default=False)
    priority_filter = Column(String(20), nullable=True)
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())
    updated_at      = Column(DateTime, default=func.now(), onupdate=func.now())


class NotifyLog(Base):
    """알림 발송 이력."""
    __tablename__ = "tb_notify_log"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(Integer, nullable=False, index=True)
    rule_id     = Column(Integer, ForeignKey("tb_smart_notify_rule.id"), nullable=True)
    channel     = Column(String(50), nullable=False)
    recipient   = Column(String(200), nullable=True)
    subject     = Column(String(300), nullable=True)
    body        = Column(Text, nullable=True)
    success     = Column(Boolean, default=False)
    error_msg   = Column(Text, nullable=True)
    sent_at     = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── GUARDiA 차세대 확장 — AIOps 2.0 / Zero Trust / IDP / GreenOps+Edge
# ── 트렌드 근거: 2026년 웹 크롤링 (Gartner, Forrester, EU CRA, 국정원 N²SF)
# ══════════════════════════════════════════════════════════════════════════════

# ── Agentic AIOps ─────────────────────────────────────────────────────────────

class AgentRun(Base):
    """에이전트 태스크 실행 이력."""
    __tablename__ = "tb_agent_run"
    id              = Column(Integer, primary_key=True, index=True)
    task            = Column(Text, nullable=False)
    status          = Column(String(30), default="PENDING")  # RUNNING|DONE|STOPPED|PENDING_APPROVAL
    result          = Column(Text, nullable=True)
    context_json    = Column(Text, nullable=True)
    tenant_id       = Column(Integer, nullable=False, default=1)
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    approved_by     = Column(Integer, nullable=True)
    created_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


class AgentToolCall(Base):
    """에이전트 도구 호출 로그."""
    __tablename__ = "tb_agent_tool_call"
    id          = Column(Integer, primary_key=True, index=True)
    run_id      = Column(Integer, ForeignKey("tb_agent_run.id"), nullable=False)
    step        = Column(Integer, default=0)
    tool_name   = Column(String(100), nullable=False)
    params_json = Column(Text, nullable=True)
    result      = Column(Text, nullable=True)
    called_at   = Column(DateTime, default=func.now())


class RemediationRule(Base):
    """자율 교정 규칙."""
    __tablename__ = "tb_remediation_rule"
    id                  = Column(Integer, primary_key=True, index=True)
    name                = Column(String(200), nullable=False)
    trigger_type        = Column(String(50), nullable=False)  # ANOMALY|THRESHOLD|MANUAL
    condition_json      = Column(Text, default="{}")
    action_template     = Column(Text, nullable=True)
    auto_approve        = Column(Boolean, default=False)
    severity_threshold  = Column(String(20), default="HIGH")
    is_active           = Column(Boolean, default=True)
    created_by          = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at          = Column(DateTime, default=func.now())


class RemediationHistory(Base):
    """교정 실행 이력."""
    __tablename__ = "tb_remediation_history"
    id              = Column(Integer, primary_key=True, index=True)
    rule_id         = Column(Integer, ForeignKey("tb_remediation_rule.id"), nullable=True)
    server_id       = Column(Integer, nullable=True)
    trigger_data    = Column(Text, nullable=True)
    status          = Column(String(30), default="DIAGNOSING")
    diagnosis       = Column(Text, nullable=True)
    action_taken    = Column(Text, nullable=True)
    result          = Column(Text, nullable=True)
    triggered_by    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    approved_by     = Column(Integer, nullable=True)
    created_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


class OtelTrace(Base):
    """OpenTelemetry 트레이스 레코드."""
    __tablename__ = "tb_otel_trace"
    id          = Column(Integer, primary_key=True, index=True)
    trace_id    = Column(String(64), nullable=False, unique=True, index=True)
    service     = Column(String(200), nullable=False)
    start_time  = Column(DateTime, nullable=True)
    created_at  = Column(DateTime, default=func.now())


class OtelSpan(Base):
    """OpenTelemetry 스팬 레코드."""
    __tablename__ = "tb_otel_span"
    id              = Column(Integer, primary_key=True, index=True)
    trace_id        = Column(String(64), ForeignKey("tb_otel_trace.trace_id"), nullable=False, index=True)
    span_id         = Column(String(64), nullable=False, unique=True)
    parent_span_id  = Column(String(64), nullable=True)
    service         = Column(String(200), nullable=False)
    operation       = Column(String(300), nullable=False)
    start_time      = Column(DateTime, nullable=True)
    end_time        = Column(DateTime, nullable=True)
    duration_ms     = Column(Integer, default=0)
    status          = Column(String(20), default="OK")
    attributes      = Column(Text, nullable=True)


class MLModel(Base):
    """AI 모델 보안 스캔 이력."""
    __tablename__ = "tb_ml_model"
    id              = Column(Integer, primary_key=True, index=True)
    name            = Column(String(200), nullable=False)
    scan_status     = Column(String(30), default="UNKNOWN")  # CLEAN|VULNERABLE|APPROVED
    vulnerability   = Column(Text, nullable=True)
    approved_by     = Column(Integer, nullable=True)
    scanned_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    scanned_at      = Column(DateTime, default=func.now())


# ── Zero Trust + Supply Chain ─────────────────────────────────────────────────

class ZTNAPolicy(Base):
    """ZTNA 접근 정책."""
    __tablename__ = "tb_ztna_policy"
    id                      = Column(Integer, primary_key=True, index=True)
    name                    = Column(String(200), nullable=False)
    resource                = Column(String(300), nullable=False, index=True)
    allowed_roles           = Column(Text, default="[]")
    require_mfa             = Column(Boolean, default=True)
    min_trust_score         = Column(Integer, default=70)
    allowed_ips             = Column(Text, default="[]")
    require_device_compliant = Column(Boolean, default=True)
    is_active               = Column(Boolean, default=True)
    created_by              = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at              = Column(DateTime, default=func.now())


class ZTNADevice(Base):
    """등록된 디바이스 (Zero Trust)."""
    __tablename__ = "tb_ztna_device"
    id              = Column(Integer, primary_key=True, index=True)
    device_name     = Column(String(200), nullable=False)
    device_type     = Column(String(50), nullable=False)  # PC|MOBILE|SERVER
    os_name         = Column(String(100), nullable=True)
    os_version      = Column(String(50), nullable=True)
    antivirus_active = Column(Boolean, default=False)
    disk_encrypted  = Column(Boolean, default=False)
    last_patch_days = Column(Integer, default=999)
    is_compliant    = Column(Boolean, default=False)
    registered_by   = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    registered_at   = Column(DateTime, default=func.now())


class ZTNAViolation(Base):
    """ZTNA 정책 위반 로그."""
    __tablename__ = "tb_ztna_violation"
    id          = Column(Integer, primary_key=True, index=True)
    user_id     = Column(Integer, nullable=True)
    resource    = Column(String(300), nullable=False)
    reason      = Column(Text, nullable=True)
    source_ip   = Column(String(50), nullable=True)
    trust_score = Column(Integer, default=0)
    created_at  = Column(DateTime, default=func.now())


class SBOMRecord(Base):
    """SBOM 레코드."""
    __tablename__ = "tb_sbom_record"
    id              = Column(Integer, primary_key=True, index=True)
    server_id       = Column(Integer, nullable=False, index=True)
    format          = Column(String(20), default="CycloneDX")
    spec_version    = Column(String(10), default="1.4")
    component_count = Column(Integer, default=0)
    status          = Column(String(20), default="PENDING")
    generated_by    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class SBOMComponent(Base):
    """SBOM 컴포넌트 (패키지·라이브러리)."""
    __tablename__ = "tb_sbom_component"
    id              = Column(Integer, primary_key=True, index=True)
    sbom_id         = Column(Integer, ForeignKey("tb_sbom_record.id"), nullable=False, index=True)
    name            = Column(String(200), nullable=False)
    version         = Column(String(100), nullable=False)
    component_type  = Column(String(50), default="library")
    ecosystem       = Column(String(50), nullable=True)
    purl            = Column(String(500), nullable=True)
    cve_ids         = Column(Text, nullable=True)


class N2SFAssessment(Base):
    """N²SF 준수 평가."""
    __tablename__ = "tb_n2sf_assessment"
    id              = Column(Integer, primary_key=True, index=True)
    system_name     = Column(String(200), nullable=False)
    zone            = Column(String(1), nullable=False)  # A|B|C
    total_items     = Column(Integer, default=0)
    passed_items    = Column(Integer, default=0)
    score           = Column(Integer, default=0)
    checked_items   = Column(Text, nullable=True)  # JSON 배열
    notes           = Column(Text, nullable=True)
    assessed_by     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class N2SFZone(Base):
    """N²SF 보안 구역 등록."""
    __tablename__ = "tb_n2sf_zone"
    id          = Column(Integer, primary_key=True, index=True)
    zone        = Column(String(1), nullable=False)
    system_name = Column(String(200), nullable=False)
    classified_by = Column(Integer, nullable=True)
    created_at  = Column(DateTime, default=func.now())


# ── IDP ───────────────────────────────────────────────────────────────────────

class IDPComponent(Base):
    """IDP 소프트웨어 카탈로그 컴포넌트."""
    __tablename__ = "tb_idp_component"
    id              = Column(Integer, primary_key=True, index=True)
    name            = Column(String(200), nullable=False, unique=True)
    display_name    = Column(String(200), nullable=True)
    component_type  = Column(String(50), default="service")
    description     = Column(Text, nullable=True)
    language        = Column(String(50), nullable=True)
    framework       = Column(String(100), nullable=True)
    gitea_repo      = Column(String(500), nullable=True)
    ci_job          = Column(String(200), nullable=True)
    docs_url        = Column(String(500), nullable=True)
    owner_team      = Column(String(200), nullable=True)
    tags            = Column(Text, nullable=True)
    lifecycle       = Column(String(30), default="production")
    dependencies    = Column(Text, nullable=True)
    registered_by   = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class IDPTemplate(Base):
    """IDP Golden Path 템플릿."""
    __tablename__ = "tb_idp_template"
    id              = Column(Integer, primary_key=True, index=True)
    name            = Column(String(200), nullable=False, unique=True)
    description     = Column(Text, nullable=True)
    language        = Column(String(50), nullable=True)
    framework       = Column(String(100), nullable=True)
    variables       = Column(Text, nullable=True)   # JSON 배열
    files           = Column(Text, nullable=True)   # JSON 배열
    content_json    = Column(Text, default="{}")
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class IDPProvisionRequest(Base):
    """IDP 인프라 프로비저닝 요청."""
    __tablename__ = "tb_idp_provision_request"
    id              = Column(Integer, primary_key=True, index=True)
    resource_type   = Column(String(50), nullable=False)
    params          = Column(Text, nullable=True)
    justification   = Column(Text, nullable=True)
    team            = Column(String(200), nullable=True)
    status          = Column(String(30), default="PENDING")
    result          = Column(Text, nullable=True)
    requested_by    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    approved_by     = Column(Integer, nullable=True)
    created_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


# ── GreenOps + Edge ───────────────────────────────────────────────────────────

class CarbonRecord(Base):
    """탄소 배출 기록."""
    __tablename__ = "tb_carbon_record"
    id              = Column(Integer, primary_key=True, index=True)
    server_id       = Column(Integer, nullable=False, index=True)
    watt            = Column(Float, nullable=False)
    hours           = Column(Float, default=1.0)
    pue             = Column(Float, default=1.5)
    carbon_kg       = Column(Float, nullable=False)
    grid_factor     = Column(Float, default=0.4593)
    recorded_by     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    recorded_at     = Column(DateTime, default=func.now())


class GreenOpsConfig(Base):
    """GreenOps 서버별 설정 (베이스라인)."""
    __tablename__ = "tb_greenops_config"
    id              = Column(Integer, primary_key=True, index=True)
    server_id       = Column(Integer, nullable=False, unique=True)
    watt_baseline   = Column(Float, nullable=False)
    pue             = Column(Float, default=1.5)
    note            = Column(Text, nullable=True)
    set_by          = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class OptimizationRec(Base):
    """에너지 최적화 권고."""
    __tablename__ = "tb_optimization_rec"
    id                  = Column(Integer, primary_key=True, index=True)
    rec_type            = Column(String(50), nullable=False)
    description         = Column(Text, nullable=False)
    saving_kwh          = Column(Float, default=0.0)
    saving_carbon_kg    = Column(Float, default=0.0)
    status              = Column(String(20), default="PENDING")
    applied_at          = Column(DateTime, nullable=True)
    created_by          = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    applied_by          = Column(Integer, nullable=True)
    created_at          = Column(DateTime, default=func.now())


class CarbonSchedule(Base):
    """Carbon-aware 배치 스케줄."""
    __tablename__ = "tb_carbon_schedule"
    id              = Column(Integer, primary_key=True, index=True)
    job_name        = Column(String(200), nullable=False)
    job_command     = Column(Text, nullable=False)
    server_id       = Column(Integer, nullable=False)
    preferred_hour  = Column(Integer, nullable=False)
    carbon_factor   = Column(Float, nullable=True)
    status          = Column(String(20), default="SCHEDULED")
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class EdgeDevice(Base):
    """Edge/IoT 디바이스."""
    __tablename__ = "tb_edge_device"
    id              = Column(Integer, primary_key=True, index=True)
    name            = Column(String(200), nullable=False)
    device_type     = Column(String(50), nullable=False)
    location        = Column(String(300), nullable=True)
    protocol        = Column(String(20), default="HTTP")
    device_token    = Column(String(36), nullable=True, unique=True)
    meta_json       = Column(Text, nullable=True)
    status          = Column(String(20), default="REGISTERED")
    last_seen       = Column(DateTime, nullable=True)
    registered_by   = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class EdgeMetric(Base):
    """Edge 디바이스 텔레메트리."""
    __tablename__ = "tb_edge_metric"
    id          = Column(Integer, primary_key=True, index=True)
    device_id   = Column(Integer, ForeignKey("tb_edge_device.id"), nullable=False, index=True)
    metrics_json = Column(Text, nullable=True)
    recorded_at = Column(DateTime, default=func.now())


class EdgeAlert(Base):
    """Edge 디바이스 알림."""
    __tablename__ = "tb_edge_alert"
    id          = Column(Integer, primary_key=True, index=True)
    device_id   = Column(Integer, ForeignKey("tb_edge_device.id"), nullable=False, index=True)
    alert_type  = Column(String(50), nullable=False)
    message     = Column(Text, nullable=True)
    severity    = Column(String(20), default="WARNING")
    created_at  = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── GUARDiA Brain — AI 지능화 엔진 (2026-06-03 크롤링 기반)
# ── Mem0 메모리 / oh-my-codex 스킬 / ICLR 2026 자기개선 / LoRA 파인튜닝
# ══════════════════════════════════════════════════════════════════════════════

class AgentMemory(Base):
    """영구 에이전트 메모리 (Mem0-style pgvector)."""
    __tablename__ = "tb_agent_memory"
    id              = Column(Integer, primary_key=True, index=True)
    session_id      = Column(String(100), nullable=True, index=True)
    memory_type     = Column(String(20), default="EPISODIC")  # EPISODIC|SEMANTIC|PROCEDURAL
    content         = Column(Text, nullable=False)
    embedding       = Column(Text, nullable=True)              # JSON 배열 (768차원)
    metadata_json   = Column(Text, nullable=True)
    confidence      = Column(Float, default=0.5)
    access_count    = Column(Integer, default=0)
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())
    expires_at      = Column(DateTime, nullable=True)


class AgentSession(Base):
    """에이전트 세션 (컨텍스트 지속성)."""
    __tablename__ = "tb_agent_session"
    id              = Column(Integer, primary_key=True, index=True)
    session_id      = Column(String(100), nullable=False, unique=True, index=True)
    agent_name      = Column(String(200), nullable=False)
    context_json    = Column(Text, nullable=True)
    memory_refs_json = Column(Text, nullable=True)
    user_id         = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    started_at      = Column(DateTime, default=func.now())
    last_active     = Column(DateTime, default=func.now())


class KGNode(Base):
    """지식 그래프 노드 (서버·장애·해결책·기관)."""
    __tablename__ = "tb_kg_node"
    id              = Column(Integer, primary_key=True, index=True)
    node_type       = Column(String(50), nullable=False, index=True)
    name            = Column(String(300), nullable=False)
    properties_json = Column(Text, nullable=True)
    created_at      = Column(DateTime, default=func.now())


class KGEdge(Base):
    """지식 그래프 엣지 (시간적 관계)."""
    __tablename__ = "tb_kg_edge"
    id              = Column(Integer, primary_key=True, index=True)
    from_node_id    = Column(Integer, ForeignKey("tb_kg_node.id"), nullable=False, index=True)
    to_node_id      = Column(Integer, ForeignKey("tb_kg_node.id"), nullable=False)
    edge_type       = Column(String(50), nullable=False)
    weight          = Column(Float, default=1.0)
    valid_from      = Column(DateTime, default=func.now())
    valid_until     = Column(DateTime, nullable=True)
    confidence      = Column(Float, default=0.5)
    notes           = Column(Text, nullable=True)


class MinedPattern(Base):
    """자동 감지된 운영 패턴."""
    __tablename__ = "tb_mined_pattern"
    id              = Column(Integer, primary_key=True, index=True)
    pattern_type    = Column(String(50), default="COMMAND_SEQ")
    trigger         = Column(String(200), nullable=False)
    action_sequence = Column(Text, nullable=True)
    occurrence_count = Column(Integer, default=1)
    last_seen       = Column(DateTime, default=func.now())
    status          = Column(String(30), default="PENDING")
    context         = Column(Text, nullable=True)
    generated_code  = Column(Text, nullable=True)


class Skill(Base):
    """운영 스킬 레지스트리."""
    __tablename__ = "tb_skill"
    id              = Column(Integer, primary_key=True, index=True)
    name            = Column(String(200), nullable=False, unique=True)
    description     = Column(Text, nullable=True)
    category        = Column(String(50), default="GENERAL")
    code_template   = Column(Text, nullable=True)
    parameters_schema = Column(Text, nullable=True)
    created_from    = Column(String(20), default="MANUAL")  # MANUAL|MINED|MARKETPLACE|BUILTIN
    success_count   = Column(Integer, default=0)
    fail_count      = Column(Integer, default=0)
    is_active       = Column(Boolean, default=True)
    created_at      = Column(DateTime, default=func.now())


class SkillExecution(Base):
    """스킬 실행 로그."""
    __tablename__ = "tb_skill_execution"
    id              = Column(Integer, primary_key=True, index=True)
    skill_id        = Column(Integer, ForeignKey("tb_skill.id"), nullable=True)
    input_params    = Column(Text, nullable=True)
    result          = Column(Text, nullable=True)
    success         = Column(Boolean, default=False)
    duration_ms     = Column(Integer, default=0)
    executed_by     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    executed_at     = Column(DateTime, default=func.now())


class FeedbackSample(Base):
    """LoRA 파인튜닝 학습 데이터."""
    __tablename__ = "tb_feedback_sample"
    id              = Column(Integer, primary_key=True, index=True)
    question        = Column(Text, nullable=False)
    ollama_response = Column(Text, nullable=True)
    approved_answer = Column(Text, nullable=False)
    label_type      = Column(String(20), default="POSITIVE")  # POSITIVE|NEGATIVE|CORRECTED
    quality_score   = Column(Float, default=0.8)
    domain          = Column(String(50), default="general")
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class FinetuneJob(Base):
    """LoRA 파인튜닝 작업."""
    __tablename__ = "tb_finetune_job"
    id              = Column(Integer, primary_key=True, index=True)
    base_model      = Column(String(100), nullable=False)
    dataset_size    = Column(Integer, default=0)
    epochs          = Column(Integer, default=3)
    status          = Column(String(20), default="QUEUED")
    output_model    = Column(String(200), nullable=True)
    loss_history_json = Column(Text, nullable=True)
    notes           = Column(Text, nullable=True)
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())
    finished_at     = Column(DateTime, nullable=True)


class EvalResult(Base):
    """모델 평가 결과."""
    __tablename__ = "tb_eval_result"
    id              = Column(Integer, primary_key=True, index=True)
    model_name      = Column(String(200), nullable=False)
    job_id          = Column(Integer, ForeignKey("tb_finetune_job.id"), nullable=True)
    accuracy        = Column(Float, nullable=True)
    hallucination_rate = Column(Float, nullable=True)
    latency_ms      = Column(Integer, nullable=True)
    eval_date       = Column(DateTime, default=func.now())
    promoted        = Column(Boolean, default=False)


class ObserveSession(Base):
    """관찰형 AI 세션."""
    __tablename__ = "tb_observe_session"
    id              = Column(Integer, primary_key=True, index=True)
    agent_name      = Column(String(200), nullable=False)
    context         = Column(Text, nullable=True)
    patterns_found  = Column(Integer, default=0)
    skills_created  = Column(Integer, default=0)
    user_id         = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    start_time      = Column(DateTime, default=func.now())
    end_time        = Column(DateTime, nullable=True)


class AIMetric(Base):
    """AI 엔진 성능 메트릭."""
    __tablename__ = "tb_ai_metric"
    id          = Column(Integer, primary_key=True, index=True)
    metric_type = Column(String(50), nullable=False)
    value       = Column(Float, nullable=False)
    model_name  = Column(String(200), nullable=True)
    measured_at = Column(DateTime, default=func.now())


class PluginRecord(Base):
    """설치된 Claude 플러그인."""
    __tablename__ = "tb_plugin_record"
    id           = Column(Integer, primary_key=True, index=True)
    name         = Column(String(200), nullable=False, unique=True)
    version      = Column(String(50), nullable=True)
    description  = Column(Text, nullable=True)
    is_installed = Column(Boolean, default=True)
    usage_count  = Column(Integer, default=0)
    installed_at = Column(DateTime, default=func.now())
    last_updated = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── 디자인 AI + 스마트 UX (2026-06-03 크롤링 기반)
# ══════════════════════════════════════════════════════════════════════════════

class DesignSR(Base):
    """디자인 SR AI 자동 처리 이력."""
    __tablename__ = "tb_design_sr"
    id              = Column(Integer, primary_key=True, index=True)
    sr_id           = Column(Integer, nullable=True)
    design_type     = Column(String(30), nullable=False)  # CSS_CHANGE|ICON_REQUEST|UI_REVIEW|LAYOUT
    requirement     = Column(Text, nullable=False)
    screenshot_path = Column(String(500), nullable=True)
    ai_analysis     = Column(Text, nullable=True)
    generated_code  = Column(Text, nullable=True)
    status          = Column(String(20), default="PENDING")
    resolved_by_ai  = Column(Boolean, default=False)
    created_by      = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class GeneratedIcon(Base):
    """AI 생성 SVG 아이콘."""
    __tablename__ = "tb_generated_icon"
    id            = Column(Integer, primary_key=True, index=True)
    name          = Column(String(200), nullable=False)
    description   = Column(Text, nullable=True)
    svg_code      = Column(Text, nullable=False)
    category      = Column(String(50), default="custom")
    primary_color = Column(String(10), default="#003366")
    size          = Column(Integer, default=24)
    created_by    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at    = Column(DateTime, default=func.now())


class CSSGeneration(Base):
    """CSS 자동 생성 이력."""
    __tablename__ = "tb_css_generation"
    id               = Column(Integer, primary_key=True, index=True)
    requirement      = Column(Text, nullable=False)
    generated_css    = Column(Text, nullable=True)
    tailwind_classes = Column(Text, nullable=True)
    applied          = Column(Boolean, default=False)
    created_by       = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at       = Column(DateTime, default=func.now())


class CommandHistory(Base):
    """음성/명령 사용 이력 (다음명령 학습용)."""
    __tablename__ = "tb_command_history"
    id        = Column(Integer, primary_key=True, index=True)
    user_id   = Column(Integer, ForeignKey("tb_user.id"), nullable=False, index=True)
    command   = Column(String(200), nullable=False)
    room      = Column(String(100), default="general")
    success   = Column(Boolean, default=True)
    used_at   = Column(DateTime, default=func.now())


# ── 부모 역할 4가지: 건강검진·성장일지·미래준비·독립지원 ──────────────────

class HealthCheckResult(Base):
    """건강검진 — 전체 테스트 실행 이력."""
    __tablename__ = "tb_health_check_result"
    id             = Column(Integer, primary_key=True, index=True)
    triggered_by   = Column(String(100), default="schedule")
    total          = Column(Integer, default=69)
    passed         = Column(Integer, default=0)
    failed         = Column(Integer, default=0)
    success        = Column(Boolean, default=False)
    duration_sec   = Column(Float, default=0.0)
    output_summary = Column(Text, nullable=True)
    created_at     = Column(DateTime, default=func.now())


class ChangelogEntry(Base):
    """성장일지 — 변경이력 자동 기록."""
    __tablename__ = "tb_changelog_entry"
    id          = Column(Integer, primary_key=True, index=True)
    version     = Column(String(20), nullable=False)
    category    = Column(String(50), default="feat")
    title       = Column(String(300), nullable=False)
    description = Column(Text, nullable=True)
    files       = Column(Text, nullable=True)
    commit_hash = Column(String(40), nullable=True)
    author      = Column(String(100), nullable=True)
    created_at  = Column(DateTime, default=func.now())


class PushDevice(Base):
    """미래 준비 — 푸시알림 디바이스 토큰."""
    __tablename__ = "tb_push_device"
    id          = Column(Integer, primary_key=True, index=True)
    user_id     = Column(Integer, ForeignKey("tb_user.id"), nullable=False, index=True)
    token       = Column(String(500), nullable=False, unique=True)
    platform    = Column(String(20), default="android")
    active      = Column(Boolean, default=True)
    registered_at = Column(DateTime, default=func.now())
    last_used_at  = Column(DateTime, nullable=True)


class PushLog(Base):
    """미래 준비 — 푸시알림 발송 이력."""
    __tablename__ = "tb_push_log"
    id         = Column(Integer, primary_key=True, index=True)
    device_id  = Column(Integer, ForeignKey("tb_push_device.id"), nullable=True)
    title      = Column(String(200), nullable=False)
    body       = Column(Text, nullable=False)
    category   = Column(String(50), default="general")
    success    = Column(Boolean, default=True)
    error_msg  = Column(Text, nullable=True)
    sent_at    = Column(DateTime, default=func.now())


class I18nEntry(Base):
    """미래 준비 — 다국어 번역 데이터."""
    __tablename__ = "tb_i18n_entry"
    id          = Column(Integer, primary_key=True, index=True)
    key         = Column(String(300), nullable=False, index=True)
    locale      = Column(String(10), nullable=False, index=True)
    value       = Column(Text, nullable=False)
    namespace   = Column(String(100), default="common")
    verified    = Column(Boolean, default=False)
    created_at  = Column(DateTime, default=func.now())
    updated_at  = Column(DateTime, onupdate=func.now())


class AutoFinetuneJob(Base):
    """미래 준비 — LoRA 자동 파인튜닝 작업."""
    __tablename__ = "tb_auto_finetune_job"
    id           = Column(Integer, primary_key=True, index=True)
    model_name   = Column(String(100), default="llama3")
    dataset_size = Column(Integer, default=0)
    status       = Column(String(20), default="pending")
    loss         = Column(Float, nullable=True)
    epochs       = Column(Integer, default=3)
    output_path  = Column(String(500), nullable=True)
    error_msg    = Column(Text, nullable=True)
    started_at   = Column(DateTime, nullable=True)
    finished_at  = Column(DateTime, nullable=True)
    created_at   = Column(DateTime, default=func.now())


class AutonomousAction(Base):
    """독립 지원 — 자율 실행 이력."""
    __tablename__ = "tb_autonomous_action"
    id           = Column(Integer, primary_key=True, index=True)
    trigger_type = Column(String(50), nullable=False)
    trigger_data = Column(Text, nullable=True)
    action_type  = Column(String(50), nullable=False)
    action_cmd   = Column(Text, nullable=True)
    result       = Column(Text, nullable=True)
    success      = Column(Boolean, default=False)
    approved_by  = Column(String(100), nullable=True)
    executed_at  = Column(DateTime, default=func.now())


class SelfReport(Base):
    """독립 지원 — 자율 주간 보고서."""
    __tablename__ = "tb_self_report"
    id              = Column(Integer, primary_key=True, index=True)
    period          = Column(String(20), nullable=False)
    sr_total        = Column(Integer, default=0)
    sr_auto_handled = Column(Integer, default=0)
    health_score    = Column(Float, default=0.0)
    auto_heals      = Column(Integer, default=0)
    incidents       = Column(Integer, default=0)
    summary         = Column(Text, nullable=True)
    sent_to         = Column(String(200), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class IndependenceScore(Base):
    """독립 지원 — 자립도 점수 추적."""
    __tablename__ = "tb_independence_score"
    id              = Column(Integer, primary_key=True, index=True)
    score           = Column(Float, nullable=False)
    dimension       = Column(String(50), default="overall")
    details         = Column(Text, nullable=True)
    target_score    = Column(Float, default=85.0)
    measured_at     = Column(DateTime, default=func.now())


# ── 자율 비용 최적화 (AutonomousCostOps) ───────────────────────────────────────

class CostAIAnalysis(Base):
    """AI 비용 분석 결과 저장."""
    __tablename__ = "tb_cost_ai_analysis"
    id              = Column(Integer, primary_key=True, index=True)
    period          = Column(String(20))
    total_cost      = Column(Float, default=0.0)
    breakdown       = Column(Text, nullable=True)   # JSON
    ai_insights     = Column(Text, nullable=True)
    waste_detected  = Column(Text, nullable=True)   # JSON
    created_at      = Column(DateTime, default=func.now())


class CostRecommendation(Base):
    """AI 비용 절감 권고 항목."""
    __tablename__ = "tb_cost_recommendation"
    id              = Column(Integer, primary_key=True, index=True)
    category        = Column(String(50))     # server|license|cloud
    title           = Column(String(300))
    description     = Column(Text, nullable=True)
    estimated_saving= Column(Float, default=0.0)   # 만원/월
    risk_level      = Column(String(20), default="LOW")
    auto_applicable = Column(Boolean, default=False)
    status          = Column(String(20), default="pending")
    created_at      = Column(DateTime, default=func.now())


class CostForecast(Base):
    """AI 비용 예측 데이터."""
    __tablename__ = "tb_cost_forecast"
    id              = Column(Integer, primary_key=True, index=True)
    forecast_date   = Column(DateTime)
    predicted_cost  = Column(Float, default=0.0)
    confidence      = Column(Float, default=0.0)
    factors         = Column(Text, nullable=True)   # JSON
    created_at      = Column(DateTime, default=func.now())


# ── Digital Twin ────────────────────────────────────────────────────────────────

class DigitalTwinServer(Base):
    """디지털 트윈 — 서버 가상 복제본."""
    __tablename__ = "tb_digital_twin_server"

    id            = Column(Integer, primary_key=True, index=True)
    server_id     = Column(Integer, ForeignKey("tb_server_info.id"), nullable=True)
    server_name   = Column(String(200), nullable=False)
    twin_state    = Column(Text, nullable=True)   # JSON 직렬화 (트윈 상태)
    real_state    = Column(Text, nullable=True)   # JSON 직렬화 (실서버 수집 상태)
    diff          = Column(Text, nullable=True)   # JSON 직렬화 (차이점)
    last_sync_at  = Column(DateTime, nullable=True)
    created_at    = Column(DateTime, default=func.now())

    server        = relationship("Server", foreign_keys=[server_id])


class TwinSimulation(Base):
    """디지털 트윈 — 장애/변경 시뮬레이션 결과."""
    __tablename__ = "tb_twin_simulation"

    id            = Column(Integer, primary_key=True, index=True)
    sim_type      = Column(String(50), nullable=False)   # failure | change
    target        = Column(String(200))
    scenario      = Column(Text, nullable=True)          # JSON
    result        = Column(Text, nullable=True)          # JSON
    risk_score    = Column(Float, default=0.0)
    created_at    = Column(DateTime, default=func.now())


class TwinSnapshot(Base):
    """디지털 트윈 — 상태 스냅샷 이력."""
    __tablename__ = "tb_twin_snapshot"

    id            = Column(Integer, primary_key=True, index=True)
    label         = Column(String(200))
    state         = Column(Text, nullable=True)          # JSON
    created_at    = Column(DateTime, default=func.now())


# ── 공급망 보안 (Supply Chain Security) ─────────────────────────────────────

class SCSScan(Base):
    """공급망 스캔 이력."""
    __tablename__ = "tb_scs_scan"

    id             = Column(Integer, primary_key=True, index=True)
    scan_type      = Column(String(50), default="dependency")
    target         = Column(String(200))
    status         = Column(String(20), default="completed")
    findings_count = Column(Integer, default=0)
    critical_count = Column(Integer, default=0)
    high_count     = Column(Integer, default=0)
    report         = Column(Text, nullable=True)   # JSON
    created_at     = Column(DateTime, default=func.now())


class SupplyChainVulnerability(Base):
    """공급망 취약점 레코드."""
    __tablename__ = "tb_supply_chain_vulnerability"

    id              = Column(Integer, primary_key=True, index=True)
    cve_id          = Column(String(50), nullable=True, index=True)
    package         = Column(String(200))
    version         = Column(String(50), nullable=True)
    fixed_version   = Column(String(50), nullable=True)
    severity        = Column(String(20), default="MEDIUM")  # CRITICAL|HIGH|MEDIUM|LOW
    cvss_score      = Column(Float, default=0.0)
    description     = Column(Text, nullable=True)
    patch_available = Column(Boolean, default=False)
    status          = Column(String(20), default="open")    # open|patched|accepted
    created_at      = Column(DateTime, default=func.now())


class SLSAAssessment(Base):
    """SLSA 레벨 평가 이력."""
    __tablename__ = "tb_slsa_assessment"

    id           = Column(Integer, primary_key=True, index=True)
    level        = Column(Integer, default=0)   # 0~3
    requirements = Column(Text, nullable=True)  # JSON 각 레벨 요구사항
    gaps         = Column(Text, nullable=True)  # JSON 미충족 항목
    score        = Column(Float, default=0.0)
    created_at   = Column(DateTime, default=func.now())


# ── Pydantic 스키마 (공급망 보안) ─────────────────────────────────────────────

class SCSScanOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id:             int
    scan_type:      str
    target:         Optional[str]
    status:         str
    findings_count: int
    critical_count: int
    high_count:     int
    created_at:     datetime


class SupplyChainVulnerabilityOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id:              int
    cve_id:          Optional[str]
    package:         str
    version:         Optional[str]
    fixed_version:   Optional[str]
    severity:        str
    cvss_score:      float
    description:     Optional[str]
    patch_available: bool
    status:          str
    created_at:      datetime


class SLSAAssessmentOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id:           int
    level:        int
    requirements: Optional[str]
    gaps:         Optional[str]
    score:        float
    created_at:   datetime


# ── Digital Twin Pydantic Schemas ───────────────────────────────────────────────

class DigitalTwinServerOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    server_id: Optional[int]
    server_name: str
    twin_state: Optional[str]
    real_state: Optional[str]
    diff: Optional[str]
    last_sync_at: Optional[datetime]
    created_at: datetime


class TwinSimulationOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    sim_type: str
    target: Optional[str]
    scenario: Optional[str]
    result: Optional[str]
    risk_score: float
    created_at: datetime


class TwinSnapshotOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    label: Optional[str]
    state: Optional[str]
    created_at: datetime


# ════════════════════════════════════════════════════════════════════════════════
# 예측 용량 계획 (Predictive Capacity Planning)
# ════════════════════════════════════════════════════════════════════════════════

class CapacityForecast(Base):
    """AI 예측 용량 레코드."""
    __tablename__ = "tb_capacity_forecast"

    id              = Column(Integer, primary_key=True, index=True)
    server_name     = Column(String(200), nullable=True)
    metric          = Column(String(50), default="cpu")      # cpu|memory|disk
    forecast_days   = Column(Integer, default=30)
    current_value   = Column(Float, default=0.0)
    predicted_value = Column(Float, default=0.0)             # 예측값 (%)
    confidence      = Column(Float, default=0.75)
    trend           = Column(String(20), default="stable")   # increasing|stable|decreasing
    created_at      = Column(DateTime, default=func.now())


class CapacityRecommendation(Base):
    """용량 증설·감축 권고."""
    __tablename__ = "tb_capacity_recommendation"

    id              = Column(Integer, primary_key=True, index=True)
    server_name     = Column(String(200), nullable=True)
    rec_type        = Column(String(50), default="scale_up")  # scale_up|scale_down|add_server
    urgency         = Column(String(20), default="60days")    # immediate|30days|60days|90days
    reason          = Column(Text, nullable=True)
    estimated_cost  = Column(Float, default=0.0)              # 만원
    status          = Column(String(20), default="pending")   # pending|approved|rejected
    approved_by     = Column(String(100), nullable=True)
    created_at      = Column(DateTime, default=func.now())


class BudgetCycle(Base):
    """공공기관 예산 사이클."""
    __tablename__ = "tb_budget_cycle"

    id              = Column(Integer, primary_key=True, index=True)
    year            = Column(Integer)
    quarter         = Column(Integer, default=1)              # 1~4
    budget_infra    = Column(Float, default=0.0)
    budget_license  = Column(Float, default=0.0)
    budget_cloud    = Column(Float, default=0.0)
    spent           = Column(Float, default=0.0)
    forecast_spend  = Column(Float, default=0.0)
    status          = Column(String(20), default="planning")  # planning|active|closed
    created_at      = Column(DateTime, default=func.now())


# ── Pydantic Schemas ─────────────────────────────────────────────────────────

class CapacityForecastOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    server_name: Optional[str]
    metric: str
    forecast_days: int
    current_value: float
    predicted_value: float
    confidence: float
    trend: str
    created_at: datetime


class CapacityRecommendationOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    server_name: Optional[str]
    rec_type: str
    urgency: str
    reason: Optional[str]
    estimated_cost: float
    status: str
    approved_by: Optional[str]
    created_at: datetime


class BudgetCycleOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    year: int
    quarter: int
    budget_infra: float
    budget_license: float
    budget_cloud: float
    spent: float
    forecast_spend: float
    status: str
    created_at: datetime


class BudgetCycleCreate(BaseModel):
    year: int
    quarter: int = 1
    budget_infra: float = 0.0
    budget_license: float = 0.0
    budget_cloud: float = 0.0
    spent: float = 0.0
    forecast_spend: float = 0.0
    status: str = "planning"


# ══════════════════════════════════════════════════════════════════════════════
# ── AI 거버넌스 — 편향 감사 / 공공기관 AI 윤리 / XAI 설명
# ── 기반: 2세대 확장 (guardia-extend2-orchestrator)
# ══════════════════════════════════════════════════════════════════════════════

class AIModelAudit(Base):
    """AI 모델 편향·공정성·투명성 감사 결과."""
    __tablename__ = "tb_ai_model_audit"

    id             = Column(Integer, primary_key=True, index=True)
    model_name     = Column(String(100), nullable=False, index=True)
    audit_type     = Column(String(50), default="bias")
    # bias | fairness | transparency
    bias_score     = Column(Float, default=0.0)
    # 0.0(공정) ~ 1.0(편향)
    findings       = Column(Text, nullable=True)       # JSON: 감사 상세 결과
    recommendation = Column(Text, nullable=True)       # 개선 권고 사항
    created_by     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at     = Column(DateTime, default=func.now())


class AIEthicsCheck(Base):
    """공공기관 AI 윤리 체크리스트 점검 결과."""
    __tablename__ = "tb_ai_ethics_check"

    id          = Column(Integer, primary_key=True, index=True)
    checklist   = Column(Text, nullable=True)
    # JSON: {"target_system": "...", "items": [{id, category, item, status, weight}, ...]}
    passed      = Column(Integer, default=0)    # 통과 항목 수
    failed      = Column(Integer, default=0)    # 실패 항목 수
    score       = Column(Float, default=0.0)    # 가중 점수 0.0 ~ 100.0
    created_by  = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at  = Column(DateTime, default=func.now())


class AIDecisionLog(Base):
    """XAI — AI 결정 설명 및 신뢰도 로그."""
    __tablename__ = "tb_ai_decision_log"

    id          = Column(Integer, primary_key=True, index=True)
    context     = Column(Text, nullable=True)      # 결정 컨텍스트 (최대 2000자)
    decision    = Column(Text, nullable=True)      # AI가 내린 결정 (최대 1000자)
    explanation = Column(Text, nullable=True)      # Ollama 생성 설명 (최대 4000자)
    confidence  = Column(Float, default=0.0)       # 설명 신뢰도 0.0 ~ 1.0
    created_at  = Column(DateTime, default=func.now())


# ── 대화형 운영 AI ─────────────────────────────────────────────────────────────

class ConvOpsSession(Base):
    """대화형 자연어 운영 명령 세션."""
    __tablename__ = "tb_conv_ops_session"

    id            = Column(Integer, primary_key=True, index=True)
    user_input    = Column(Text, nullable=False)
    parsed_intent = Column(Text, nullable=True)    # JSON: {intent, params, confidence}
    steps         = Column(Text, nullable=True)    # JSON: [{action, result, status}, ...]
    summary       = Column(Text, nullable=True)
    success       = Column(Boolean, default=False)
    created_by    = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    created_at    = Column(DateTime, default=func.now())


# ── UX 분석 이벤트 ─────────────────────────────────────────────────────────────

class UXEvent(Base):
    """UX 행동 이벤트 수집 테이블."""
    __tablename__ = "tb_ux_event"

    id          = Column(Integer, primary_key=True, index=True)
    event_type  = Column(String(50))               # click | pageview | error | scroll
    page        = Column(String(200))
    element     = Column(String(200), nullable=True)
    duration_ms = Column(Integer, nullable=True)
    user_id     = Column(Integer, ForeignKey("tb_user.id"), nullable=True)
    session_id  = Column(String(100))
    extra       = Column(Text, nullable=True)       # JSON: 추가 메타데이터
    created_at  = Column(DateTime, default=func.now())


# ── 자율 패치 관리 ───────────────────────────────────────────────────────────────

class PatchPlan(Base):
    """패치 계획 — CVE 기반 자동 생성, 승인 후 SSH 실행."""
    __tablename__ = "tb_patch_plan"

    id               = Column(Integer, primary_key=True, index=True)
    cve_id           = Column(String(50), nullable=True)
    severity         = Column(String(20), default="MEDIUM")   # CRITICAL|HIGH|MEDIUM|LOW
    affected_servers = Column(Text, nullable=True)            # JSON array of server IDs
    patch_cmd        = Column(Text, nullable=True)
    rollback_cmd     = Column(Text, nullable=True)
    status           = Column(String(20), default="pending")  # pending|approved|executing|done|failed|rolled_back
    approved_by      = Column(String(100), nullable=True)
    approved_at      = Column(DateTime, nullable=True)
    executed_at      = Column(DateTime, nullable=True)
    executed_by      = Column(String(100), nullable=True)
    result_log       = Column(Text, nullable=True)            # JSON execution results
    created_by       = Column(String(100), nullable=True)
    created_at       = Column(DateTime, default=func.now())
    updated_at       = Column(DateTime, default=func.now(), onupdate=func.now())


# ── GRC 자동화 ─────────────────────────────────────────────────────────────────

class GRCPolicy(Base):
    """GRC 보안 정책 — Ollama 초안 생성, 버전 관리."""
    __tablename__ = "tb_grc_policy"

    id             = Column(Integer, primary_key=True, index=True)
    title          = Column(String(300), nullable=False)
    category       = Column(String(50), default="security")   # security|privacy|compliance|operational
    content        = Column(Text, nullable=True)
    version        = Column(String(20), default="1.0")
    status         = Column(String(20), default="draft")       # draft|review|approved|deprecated
    effective_date = Column(DateTime, nullable=True)
    owner          = Column(String(100), nullable=True)
    created_by     = Column(String(100), nullable=True)
    created_at     = Column(DateTime, default=func.now())
    updated_at     = Column(DateTime, default=func.now(), onupdate=func.now())


class RiskItem(Base):
    """리스크 항목 — 5x5 매트릭스, AI 완화 전략."""
    __tablename__ = "tb_risk_item"

    id          = Column(Integer, primary_key=True, index=True)
    title       = Column(String(300), nullable=False)
    category    = Column(String(50), default="operational")   # operational|security|compliance|financial
    likelihood  = Column(Integer, default=3)                  # 1~5
    impact      = Column(Integer, default=3)                  # 1~5
    risk_score  = Column(Float, default=9.0)                  # likelihood * impact
    risk_level  = Column(String(20), default="MEDIUM")        # CRITICAL|HIGH|MEDIUM|LOW
    mitigation  = Column(Text, nullable=True)
    owner       = Column(String(100), nullable=True)
    status      = Column(String(20), default="open")          # open|mitigating|closed|accepted
    created_by  = Column(String(100), nullable=True)
    created_at  = Column(DateTime, default=func.now())
    updated_at  = Column(DateTime, default=func.now(), onupdate=func.now())


# ── Auto Remediation Runbook ────────────────────────────────────────────────────

class RemediationRunbook(Base):
    """자동 복구 런북 — 장애 유형별 표준 복구 절차 정의."""
    __tablename__ = "tb_remediation_runbook"

    id              = Column(Integer, primary_key=True, index=True)
    name            = Column(String(300), nullable=False)
    trigger_pattern = Column(Text, nullable=True)    # 트리거 패턴 (키워드/조건)
    steps           = Column(Text, nullable=True)    # JSON 배열: 단계별 명령
    auto_execute    = Column(Boolean, default=False) # True: 즉시 실행, False: 승인 후 실행
    created_at      = Column(DateTime, default=func.now())

    sessions        = relationship("RemediationSession", back_populates="runbook")


class RemediationSession(Base):
    """자동 복구 세션 — 런북 실행 이력."""
    __tablename__ = "tb_remediation_session"

    id           = Column(Integer, primary_key=True, index=True)
    runbook_id   = Column(Integer, ForeignKey("tb_remediation_runbook.id"), nullable=True)
    trigger_data = Column(Text, nullable=True)  # 트리거 이벤트 원문 (JSON)
    step_results = Column(Text, nullable=True)  # 각 단계 실행 결과 (JSON)
    status       = Column(String(20), default="running")  # running|completed|failed|escalated
    success      = Column(Boolean, nullable=True)
    created_at   = Column(DateTime, default=func.now())

    runbook      = relationship("RemediationRunbook", back_populates="sessions")


# ── Policy Engine ───────────────────────────────────────────────────────────────

class PolicyRule(Base):
    """정책 규칙 — 공공기관 IT 표준 보안/운영 정책."""
    __tablename__ = "tb_policy_rule"

    id             = Column(Integer, primary_key=True, index=True)
    name           = Column(String(300), nullable=False)
    category       = Column(String(50), default="security")  # security|access|patch|backup|operation
    condition      = Column(Text, nullable=True)   # JSON 평가 조건
    severity       = Column(String(20), default="MEDIUM")    # CRITICAL|HIGH|MEDIUM|LOW
    auto_remediate = Column(Boolean, default=False)          # 자동 교정 여부
    active         = Column(Boolean, default=True)
    created_at     = Column(DateTime, default=func.now())

    violations     = relationship("PolicyViolation", back_populates="rule")


class PolicyViolation(Base):
    """정책 위반 사례 — 평가 결과로 생성되는 위반 이력."""
    __tablename__ = "tb_policy_violation"

    id            = Column(Integer, primary_key=True, index=True)
    rule_id       = Column(Integer, ForeignKey("tb_policy_rule.id"), nullable=True)
    target        = Column(String(200), nullable=False)  # 위반 대상 (서버명/계정 등)
    detail        = Column(Text, nullable=True)
    status        = Column(String(20), default="open")   # open|remediated|accepted|false_positive
    remediated_at = Column(DateTime, nullable=True)
    created_at    = Column(DateTime, default=func.now())

    rule          = relationship("PolicyRule", back_populates="violations")


# ══════════════════════════════════════════════════════════════════════════════
# ── ChatOps 확장 (chatops_extended.py)
# ── 채널별 webhook, 인터랙티브, 브로드캐스트, 통계
# ══════════════════════════════════════════════════════════════════════════════

class ChatOpsCommand(Base):
    """ChatOps 명령 실행 이력 — 채널별 webhook 수신 기록."""
    __tablename__ = "tb_chatops_command"

    id         = Column(Integer, primary_key=True, index=True)
    channel    = Column(String(50))   # kakao|slack|internal
    command    = Column(String(200))
    args       = Column(Text, nullable=True)
    user_id    = Column(String(100))
    response   = Column(Text, nullable=True)
    success    = Column(Boolean, default=True)
    created_at = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── 예측 장애 방지 (predictive_failure.py)
# ── 전조 신호 감지 → 패턴 분석 → 예방 조치 실행
# ══════════════════════════════════════════════════════════════════════════════

class FailureSignal(Base):
    """장애 전조 신호 — cpu_spike|mem_leak|disk_full|error_rate."""
    __tablename__ = "tb_failure_signal"

    id                = Column(Integer, primary_key=True, index=True)
    server_name       = Column(String(200))
    signal_type       = Column(String(50))   # cpu_spike|mem_leak|disk_full|error_rate
    value             = Column(Float)
    threshold         = Column(Float)
    risk_score        = Column(Float, default=0.0)
    predicted_failure = Column(String(100), nullable=True)
    created_at        = Column(DateTime, default=func.now())


class PreventionAction(Base):
    """예방 조치 실행 이력."""
    __tablename__ = "tb_prevention_action"

    id          = Column(Integer, primary_key=True, index=True)
    signal_id   = Column(Integer, ForeignKey("tb_failure_signal.id"), nullable=True)
    action_type = Column(String(50))
    action_cmd  = Column(Text, nullable=True)
    success     = Column(Boolean, default=False)
    created_at  = Column(DateTime, default=func.now())


# ══════════════════════════════════════════════════════════════════════════════
# ── 5세대 확장 모델 (2026-06-06)
# ══════════════════════════════════════════════════════════════════════════════

# ── 통합 옵저버빌리티 (observability.py) ───────────────────────────────────────

class OtelTraceExt(Base):
    __tablename__ = "tb_otel_trace_ext"
    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(String(50), nullable=False, index=True)
    trace_id     = Column(String(100), nullable=False, index=True)
    service_name = Column(String(200))
    spans        = Column(JSON)
    duration_ms  = Column(Float)
    status       = Column(String(20))
    root_cause   = Column(Text, nullable=True)
    created_at   = Column(DateTime, default=func.now(), index=True)


class SLODefinition(Base):
    __tablename__ = "tb_slo"
    id               = Column(Integer, primary_key=True, index=True)
    tenant_id        = Column(String(50), nullable=False, index=True)
    service_name     = Column(String(200))
    metric_type      = Column(String(50))
    target_pct       = Column(Float, default=99.9)
    window_days      = Column(Integer, default=30)
    current_value    = Column(Float, nullable=True)
    error_budget_pct = Column(Float, nullable=True)
    created_at       = Column(DateTime, default=func.now())


class SignalLink(Base):
    __tablename__ = "tb_signal_link"
    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(String(50), nullable=False, index=True)
    trace_id     = Column(String(100), nullable=True, index=True)
    metric_ref   = Column(String(200), nullable=True)
    log_ref      = Column(String(200), nullable=True)
    service_name = Column(String(200), nullable=True)
    correlation  = Column(JSON, nullable=True)
    created_at   = Column(DateTime, default=func.now(), index=True)


# ── 레거시 현대화 (legacy_modernization.py) ────────────────────────────────────

class LegacySystem(Base):
    __tablename__ = "tb_legacy_system"
    id                 = Column(Integer, primary_key=True, index=True)
    tenant_id          = Column(String(50), nullable=False, index=True)
    name               = Column(String(200), nullable=False)
    os_name            = Column(String(100))
    os_version         = Column(String(50))
    middleware         = Column(String(200))
    eol_date           = Column(Date, nullable=True)
    risk_score         = Column(Float, default=0.0)
    migration_strategy = Column(String(50))
    migration_status   = Column(String(50), default="NOT_STARTED")
    tech_debt_score    = Column(Float, default=0.0)
    notes              = Column(Text)
    created_at         = Column(DateTime, default=func.now())
    updated_at         = Column(DateTime, default=func.now(), onupdate=func.now())


# ── AI-SOC 보안운영센터 (ai_soc.py) ───────────────────────────────────────────

class SecurityEvent(Base):
    __tablename__ = "tb_soc_event"
    id             = Column(Integer, primary_key=True, index=True)
    tenant_id      = Column(String(50), nullable=False, index=True)
    event_type     = Column(String(100))
    source_ip      = Column(String(50))
    severity       = Column(String(20))
    description    = Column(Text)
    raw_log        = Column(Text)
    status         = Column(String(30), default="OPEN")
    correlation_id = Column(String(100))
    created_at     = Column(DateTime, default=func.now(), index=True)


class SOARPlaybook(Base):
    __tablename__ = "tb_soar_playbook"
    id                = Column(Integer, primary_key=True, index=True)
    tenant_id         = Column(String(50), nullable=False, index=True)
    name              = Column(String(200))
    trigger_condition = Column(JSON)
    actions           = Column(JSON)
    is_active         = Column(Boolean, default=True)
    run_count         = Column(Integer, default=0)
    created_at        = Column(DateTime, default=func.now())


class ThreatIntel(Base):
    __tablename__ = "tb_threat_intel"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(String(50), nullable=False, index=True)
    ioc_type    = Column(String(30))
    ioc_value   = Column(String(500))
    threat_type = Column(String(100))
    confidence  = Column(Float, default=0.5)
    source      = Column(String(200))
    expires_at  = Column(DateTime, nullable=True)
    created_at  = Column(DateTime, default=func.now())


# ── 시민 접점 포털 (citizen_portal.py) ────────────────────────────────────────

class CitizenReport(Base):
    __tablename__ = "tb_citizen_report"
    id                 = Column(Integer, primary_key=True, index=True)
    tenant_id          = Column(String(50), nullable=False, index=True)
    ticket_id          = Column(String(50), unique=True, nullable=False, index=True)
    reporter_name      = Column(String(100))
    reporter_contact   = Column(String(255))
    issue_description  = Column(Text)
    category           = Column(String(30), default="etc")
    location           = Column(String(200))
    photo_url          = Column(String(500))
    contact_method     = Column(String(20), default="none")
    status             = Column(String(30), default="RECEIVED")
    sr_id              = Column(Integer, nullable=True)
    satisfaction_score = Column(Integer, nullable=True)
    notify_subscribed  = Column(Boolean, default=False)
    created_at         = Column(DateTime, default=func.now())
    updated_at         = Column(DateTime, default=func.now(), onupdate=func.now())


class SatisfactionSurvey(Base):
    __tablename__ = "tb_satisfaction"
    id                = Column(Integer, primary_key=True, index=True)
    citizen_report_id = Column(Integer, ForeignKey("tb_citizen_report.id"), nullable=False, index=True)
    score             = Column(Integer, nullable=False)
    comment           = Column(Text, nullable=True)
    submitted_at      = Column(DateTime, default=func.now())


class ServiceStatusPage(Base):
    __tablename__ = "tb_service_status"
    id             = Column(Integer, primary_key=True, index=True)
    tenant_id      = Column(String(50), nullable=False, index=True)
    service_name   = Column(String(200), nullable=False)
    current_status = Column(String(30), default="operational")
    message        = Column(Text, nullable=True)
    updated_at     = Column(DateTime, default=func.now(), onupdate=func.now())


# ── 데이터 거버넌스 (data_governance.py) ──────────────────────────────────────

class DataLineage(Base):
    __tablename__ = "tb_data_lineage"
    id             = Column(Integer, primary_key=True, index=True)
    tenant_id      = Column(String(50), nullable=False, index=True)
    source_system  = Column(String(200))
    source_table   = Column(String(200))
    target_system  = Column(String(200))
    target_table   = Column(String(200))
    transformation = Column(Text)
    pii_involved   = Column(Boolean, default=False)
    created_at     = Column(DateTime, default=func.now())


class DataRetentionPolicy(Base):
    __tablename__ = "tb_data_retention"
    id            = Column(Integer, primary_key=True, index=True)
    tenant_id     = Column(String(50), nullable=False, index=True)
    table_name    = Column(String(200))
    retention_days = Column(Integer)
    action        = Column(String(30), default="DELETE")
    last_enforced = Column(DateTime, nullable=True)
    is_active     = Column(Boolean, default=True)
    created_at    = Column(DateTime, default=func.now())


class PIIScanResult(Base):
    __tablename__ = "tb_pii_scan"
    id              = Column(Integer, primary_key=True, index=True)
    tenant_id       = Column(String(50), nullable=False, index=True)
    scan_target     = Column(String(500))
    pii_types_found = Column(JSON)
    match_count     = Column(Integer, default=0)
    risk_level      = Column(String(20))
    created_at      = Column(DateTime, default=func.now())


# ── 하네스 빌더 (harness_builder.py) ──────────────────────────────────────────

class HarnessAgent(Base):
    __tablename__ = "tb_harness_agent"
    id               = Column(Integer, primary_key=True, index=True)
    tenant_id        = Column(String(50), nullable=False, index=True)
    name             = Column(String(200), nullable=False)
    role             = Column(String(200))
    skills           = Column(JSON)
    trigger_keywords = Column(JSON)
    system_prompt    = Column(Text)
    is_active        = Column(Boolean, default=True)
    run_count        = Column(Integer, default=0)
    last_run_at      = Column(DateTime, nullable=True)
    created_at       = Column(DateTime, default=func.now())


class HarnessRunHistory(Base):
    __tablename__ = "tb_harness_run"
    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(String(50), nullable=False, index=True)
    agent_id    = Column(Integer, ForeignKey("tb_harness_agent.id"), index=True)
    prompt      = Column(Text)
    result      = Column(Text)
    status      = Column(String(30), default="SUCCESS")
    duration_ms = Column(Integer, default=0)
    run_by      = Column(String(100))
    created_at  = Column(DateTime, default=func.now())


class HarnessSkill(Base):
    __tablename__ = "tb_harness_skill"
    id         = Column(Integer, primary_key=True, index=True)
    tenant_id  = Column(String(50), nullable=False, index=True)
    name       = Column(String(200))
    skill_type = Column(String(50))
    content_md = Column(Text)
    is_active  = Column(Boolean, default=True)
    created_at = Column(DateTime, default=func.now())


# ── tmux 세션 관리 (tmux_sessions.py) ─────────────────────────────────────────

class TmuxSession(Base):
    __tablename__ = "tb_tmux_session"
    id            = Column(Integer, primary_key=True, index=True)
    tenant_id     = Column(String(50), nullable=False, index=True)
    server_id     = Column(Integer, nullable=True)
    session_name  = Column(String(200))
    status        = Column(String(30), default="ACTIVE")
    owner         = Column(String(100))
    shared_users  = Column(JSON)
    output_buffer = Column(Text)
    created_at    = Column(DateTime, default=func.now())
    last_activity = Column(DateTime, default=func.now())


class TmuxCommand(Base):
    __tablename__ = "tb_tmux_command"
    id         = Column(Integer, primary_key=True, index=True)
    session_id = Column(Integer, ForeignKey("tb_tmux_session.id"), index=True)
    command    = Column(Text)
    sent_by    = Column(String(100))
    created_at = Column(DateTime, default=func.now())


# ════════════════════════════════════════════════════════════════════════════════
# ── 모바일 100기능 백엔드 모델 (Mobile API) ─────────────────────────────────────
# ════════════════════════════════════════════════════════════════════════════════

class AlertRule(Base):
    """알림 규칙 — 서버/서비스/SR 지표 임계치 기반 노코드 알림 정의 (#45)."""
    __tablename__ = "tb_alert_rule"

    id          = Column(Integer, primary_key=True, index=True)
    tenant_id   = Column(String(50), nullable=False, index=True)
    target_type = Column(String(20), nullable=False)   # server | service | sr
    target_id   = Column(String(100))
    metric      = Column(String(30), nullable=False)   # cpu | memory | disk | sla
    threshold   = Column(Float, nullable=False)
    operator    = Column(String(5), default=">")        # > | < | =
    channel     = Column(String(20), default="inapp")   # push | inapp | sms
    enabled     = Column(Boolean, default=True)
    created_by  = Column(String(100))
    created_at  = Column(DateTime, default=func.now())
    updated_at  = Column(DateTime, default=func.now(), onupdate=func.now())


class InventoryPart(Base):
    """부품 재고 — 현장 서비스 교체 부품 관리 (#62)."""
    __tablename__ = "tb_inventory_part"

    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(String(50), nullable=False, index=True)
    name         = Column(String(200), nullable=False)
    model        = Column(String(100))
    quantity     = Column(Integer, default=0)
    min_quantity = Column(Integer, default=1)
    location     = Column(String(200))
    created_at   = Column(DateTime, default=func.now())
    updated_at   = Column(DateTime, default=func.now(), onupdate=func.now())


class SRChatMessage(Base):
    """SR 채팅 메시지 — SR별 실시간 협업 대화 (#98)."""
    __tablename__ = "tb_sr_chat_message"

    id         = Column(Integer, primary_key=True, index=True)
    task_id    = Column(String(30), nullable=False, index=True)   # SRRequest.sr_id
    sender_id  = Column(String(100), nullable=False)
    content    = Column(Text, nullable=False)
    msg_type   = Column(String(20), default="text")   # text | image | sr_update
    read_by    = Column(Text)   # JSON list of usernames who read
    created_at = Column(DateTime, default=func.now(), index=True)


class SRSubscription(Base):
    """SR 구독/팔로우 — 사용자가 SR 변경 알림을 받도록 구독 (#14)."""
    __tablename__ = "tb_sr_subscription"

    id         = Column(Integer, primary_key=True, index=True)
    task_id    = Column(String(30), nullable=False, index=True)   # SRRequest.sr_id
    username   = Column(String(100), nullable=False, index=True)
    created_at = Column(DateTime, default=func.now())


class SRRating(Base):
    """SR 완료 만족도 평가 (#53)."""
    __tablename__ = "tb_sr_rating"

    id         = Column(Integer, primary_key=True, index=True)
    task_id    = Column(String(30), nullable=False, index=True)
    rater      = Column(String(100), nullable=False)
    score      = Column(Integer, nullable=False)   # 1~5
    comment    = Column(Text)
    created_at = Column(DateTime, default=func.now())


class SRSignature(Base):
    """현장 전자서명 — 작업 완료 현장 확인 서명 (#70)."""
    __tablename__ = "tb_sr_signature"

    id            = Column(Integer, primary_key=True, index=True)
    task_id       = Column(String(30), nullable=False, index=True)
    signed_by     = Column(String(100), nullable=False)
    signature_b64 = Column(Text, nullable=False)   # base64 PNG 서명 데이터
    created_at    = Column(DateTime, default=func.now())


class SRCheckin(Base):
    """현장 체크인 — 엔지니어 현장 도착 GPS 기록 (#93)."""
    __tablename__ = "tb_sr_checkin"

    id         = Column(Integer, primary_key=True, index=True)
    task_id    = Column(String(30), nullable=False, index=True)
    username   = Column(String(100), nullable=False)
    lat        = Column(Float)
    lng        = Column(Float)
    created_at = Column(DateTime, default=func.now())


class UserDevice(Base):
    """등록 디바이스 — 모바일 앱 디바이스/푸시 토큰 관리 (#33)."""
    __tablename__ = "tb_user_device"

    id           = Column(Integer, primary_key=True, index=True)
    username     = Column(String(100), nullable=False, index=True)
    device_name  = Column(String(150))
    device_type  = Column(String(30))   # android | ios | web
    push_token   = Column(String(300))
    last_seen_at = Column(DateTime, default=func.now())
    created_at   = Column(DateTime, default=func.now())


class LoginEvent(Base):
    """보안 이벤트 로그 — 로그인 성공/실패/디바이스 등록 등 (#34)."""
    __tablename__ = "tb_login_event"

    id          = Column(Integer, primary_key=True, index=True)
    username    = Column(String(100), index=True)
    event_type  = Column(String(40), nullable=False)   # LOGIN_SUCCESS | LOGIN_FAIL | DEVICE_ADDED | ...
    detail      = Column(Text)
    ip_addr_hash= Column(String(64))   # SHA-256, 원본 미저장
    created_at  = Column(DateTime, default=func.now(), index=True)


# ── jasper-report-dev: JasperReports 호환 문서 자동생성 (JRXML→PDF/Excel, 자바 미설치) ──

class JasperTemplate(Base):
    """JRXML 템플릿 메타데이터 + 원문 — 산출물·회의록·보고서 카테고리별 등록."""
    __tablename__ = "tb_jasper_template"

    id            = Column(Integer, primary_key=True, index=True)
    tenant_id     = Column(Integer, index=True, default=1)
    name          = Column(String(200), nullable=False)
    category      = Column(String(30), nullable=False)   # DELIVERABLE|MEETING_MINUTES|REPORT
    jrxml_content = Column(Text)          # JRXML XML 원문 (밴드/필드 정의)
    field_mapping = Column(JSON)          # {"sr_count": "report.sr_total", ...} ITSM 데이터 -> 템플릿 필드
    output_format = Column(String(10), default="PDF")  # PDF|EXCEL
    is_builtin    = Column(Boolean, default=False)      # 내장 시드 템플릿 여부
    created_at    = Column(DateTime, default=func.now())


class JasperGenerationJob(Base):
    """문서 생성 작업 이력 — 산출물/회의록/보고서 PDF·Excel 생성 결과 보존."""
    __tablename__ = "tb_jasper_job"

    id           = Column(Integer, primary_key=True, index=True)
    tenant_id    = Column(Integer, index=True, default=1)
    template_id  = Column(Integer, index=True)
    category     = Column(String(30))     # DELIVERABLE|MEETING_MINUTES|REPORT
    title        = Column(String(300))
    data_source  = Column(JSON)           # 렌더링에 사용된 데이터 스냅샷
    status       = Column(String(20), default="PENDING")  # PENDING|DONE|FAILED
    output_format= Column(String(10), default="PDF")      # PDF|EXCEL
    output_path  = Column(String(500), nullable=True)
    file_size    = Column(Integer, nullable=True)
    error_msg    = Column(Text, nullable=True)
    requested_by = Column(Integer, nullable=True)
    created_at   = Column(DateTime, default=func.now())


# ── bid-watcher-dev: 나라장터 당일 입찰정보 크롤링 + 참가 의사결정 워크플로우 ──────

class BidWatch(Base):
    """나라장터 당일 입찰정보 — 참가 의사결정 관리 단위 (SI/SM 프로젝트 한정 수집)."""
    __tablename__ = "tb_bid_watch"

    id              = Column(Integer, primary_key=True, index=True)
    bid_no          = Column(String(50), unique=True, index=True)  # 나라장터 공고번호 (중복수집 방지)
    title           = Column(String(400))
    category        = Column(String(100))     # 사업분류명
    institution     = Column(String(200))     # 발주기관
    budget          = Column(BigInteger, nullable=True)
    announce_date   = Column(Date, nullable=True)
    deadline_date   = Column(Date, nullable=True)
    source_url      = Column(String(500))     # 나라장터 원본 공고 링크
    attachments     = Column(JSON)            # [{name, doc_type, url}, ...]
    status          = Column(String(20), default="NEW")  # NEW|JOIN|HOLD|DELETED
    memo            = Column(Text, nullable=True)
    decided_by      = Column(Integer, nullable=True)
    decided_at      = Column(DateTime, nullable=True)
    collected_at    = Column(DateTime, default=func.now())


class BidWatchAssignee(Base):
    """입찰워처 알림 담당자 — 신규 SI/SM 입찰 수집 시 메일 발송 대상 (관리시스템에서 지정·관리)."""
    __tablename__ = "tb_bid_watch_assignee"

    id           = Column(Integer, primary_key=True, index=True)
    tenant_code  = Column(String(50), index=True)
    name         = Column(String(100))
    email        = Column(String(200))
    active       = Column(Boolean, default=True)
    created_by   = Column(Integer, nullable=True)
    created_at   = Column(DateTime, default=func.now())


# ── perf-test-studio-dev: Playwright MCP 기반 jMeter 성능테스트 스크립트 자동작성 ──

class PerfTestScenario(Base):
    """성능테스트 시나리오 — 재사용 가능한 등록 단위 (시나리오→입력값→녹화→jmx 파이프라인)."""
    __tablename__ = "tb_perf_scenario"

    id            = Column(Integer, primary_key=True, index=True)
    tenant_id     = Column(Integer, index=True, default=1)
    name          = Column(String(200))
    description   = Column(Text, nullable=True)
    target_url    = Column(String(500))
    steps         = Column(JSON)        # [{action, selector, value, think_time}, ...]
    users         = Column(Integer, default=10)
    duration_sec  = Column(Integer, default=30)
    ramp_up_sec   = Column(Integer, default=5)
    status        = Column(String(20), default="DRAFT")  # DRAFT|RECORDED|SCRIPT_READY|JMX_READY
    created_by    = Column(Integer, nullable=True)
    created_at    = Column(DateTime, default=func.now())
    updated_at    = Column(DateTime, default=func.now(), onupdate=func.now())


class PerfTestInputData(Base):
    """파라미터화 입력값 (엑셀 업로드 결과) — CSV Data Set Config 원천."""
    __tablename__ = "tb_perf_input_data"

    id            = Column(Integer, primary_key=True, index=True)
    scenario_id   = Column(Integer, index=True)
    column_names  = Column(JSON)         # ["username","password","search_keyword",...]
    rows          = Column(JSON)         # [["user1","pw1","검색어1"], ...]
    uploaded_at   = Column(DateTime, default=func.now())


class PerfTestRecording(Base):
    """Playwright MCP 녹화 결과 + 변환된 jMeter 스크립트(.jmx)."""
    __tablename__ = "tb_perf_recording"

    id              = Column(Integer, primary_key=True, index=True)
    scenario_id     = Column(Integer, index=True)
    action_log      = Column(JSON)       # Playwright 액션 시퀀스 [{type,url,selector,value,timestamp}]
    generated_jmx   = Column(Text, nullable=True)
    recorded_at     = Column(DateTime, default=func.now())
