"""
D-4: 보안 취약점 자동 스캔 API 라우터

엔드포인트:
  POST /api/vuln/scan              — 대상 서버 스캔 시작 (비동기)
  GET  /api/vuln/scans             — 스캔 이력 조회
  GET  /api/vuln/scans/{scan_id}   — 스캔 결과 상세
  POST /api/vuln/quick-check       — 빠른 단일 포트/서비스 점검
  GET  /api/vuln/cve/{cve_id}      — CVE 상세 정보
  POST /api/vuln/cvss              — CVSS 점수 계산
  GET  /api/vuln/stats             — 취약점 통계 요약
  GET  /api/vuln/policies          — 스캔 정책 조회
"""
from __future__ import annotations

import asyncio
import logging
from datetime import datetime
from typing import Dict, List, Optional

from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession

from core.auth import get_current_user
from database import get_db
from models import User, UserRole

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/vuln", tags=["vuln_scan"])

# ── 스캔 결과 인메모리 스토어 ──────────────────────────────────────────────────
_scan_results: Dict[str, Dict] = {}
_scan_queue: List[str] = []   # 진행 중 scan_id


# ── Pydantic 스키마 ──────────────────────────────────────────────────────────

class ScanRequestIn(BaseModel):
    host:          str
    ports:         Optional[List[int]] = None
    include_llm:   bool = False
    timeout:       float = 1.0
    sr_id:         Optional[str] = None    # 연관 SR
    note:          Optional[str] = None

class QuickCheckIn(BaseModel):
    host:  str
    port:  int
    service: Optional[str] = None

class CVSSCalcIn(BaseModel):
    attack_vector: str = "NETWORK"  # NETWORK|ADJACENT|LOCAL|PHYSICAL
    complexity:    str = "LOW"       # LOW|HIGH
    privileges:    str = "NONE"      # NONE|LOW|HIGH
    impact:        str = "HIGH"      # NONE|LOW|MEDIUM|HIGH


# ── 백그라운드 스캔 실행기 ─────────────────────────────────────────────────────

async def _run_scan_bg(scan_id: str, host: str, ports, include_llm: bool,
                       timeout: float, requester: str):
    """백그라운드에서 스캔을 실행하고 결과를 저장."""
    from core.vuln_scan import run_vulnerability_scan

    _scan_results[scan_id]["status"] = "RUNNING"
    try:
        result = await run_vulnerability_scan(host, ports, include_llm, timeout)
        result["scan_id"]   = scan_id
        result["requester"] = requester
        result["status"]    = "COMPLETED"
        _scan_results[scan_id].update(result)
        logger.info("스캔 완료: %s → risk=%s score=%d",
                    scan_id, result["risk_level"], result["risk_score"])
    except Exception as e:
        _scan_results[scan_id]["status"] = "FAILED"
        _scan_results[scan_id]["error"]  = str(e)[:100]
        logger.error("스캔 실패: %s — %s", scan_id, str(e)[:80])
    finally:
        if scan_id in _scan_queue:
            _scan_queue.remove(scan_id)


# ── 엔드포인트 ───────────────────────────────────────────────────────────────

@router.post("/scan", status_code=202)
async def start_scan(
    body: ScanRequestIn,
    background_tasks: BackgroundTasks,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """
    취약점 스캔 시작 (202 Accepted — 비동기 실행).
    보안: PM/ADMIN만 스캔 가능, 스캔 대상 기록 필수.
    """
    if current_user.role not in (UserRole.ADMIN, UserRole.PM):
        raise HTTPException(403, "PM/ADMIN 권한이 필요합니다.")
    if not body.host:
        raise HTTPException(400, "host는 필수입니다.")
    if body.timeout > 5.0:
        raise HTTPException(400, "timeout은 최대 5초입니다.")

    # scan_id 생성
    import hashlib
    scan_id = hashlib.sha256(
        f"{body.host}:{datetime.utcnow().isoformat()}:{current_user.username}".encode()
    ).hexdigest()[:12]

    _scan_results[scan_id] = {
        "scan_id":    scan_id,
        "host":       body.host,
        "status":     "QUEUED",
        "requester":  current_user.username,
        "requested_at": datetime.utcnow().isoformat(),
        "sr_id":      body.sr_id,
        "note":       body.note,
    }
    _scan_queue.append(scan_id)

    background_tasks.add_task(
        _run_scan_bg,
        scan_id, body.host, body.ports,
        body.include_llm, body.timeout,
        current_user.username,
    )

    logger.info("스캔 요청: %s → %s by %s", scan_id, body.host, current_user.username)
    return {
        "scan_id":  scan_id,
        "status":   "QUEUED",
        "message":  "스캔이 시작되었습니다. GET /api/vuln/scans/{scan_id}로 결과를 확인하세요.",
        "host":     body.host,
    }


@router.get("/scans")
async def list_scans(
    status:   Optional[str] = Query(None),
    host:     Optional[str] = Query(None),
    limit:    int = Query(20, ge=1, le=100),
    offset:   int = Query(0, ge=0),
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """스캔 이력 조회."""
    results = list(_scan_results.values())

    # ENGINEER는 본인 스캔만
    if current_user.role == UserRole.ENGINEER:
        results = [r for r in results if r.get("requester") == current_user.username]

    if status:
        results = [r for r in results if r.get("status") == status.upper()]
    if host:
        results = [r for r in results if host in r.get("host", "")]

    results_sorted = sorted(
        results,
        key=lambda x: x.get("requested_at", ""),
        reverse=True,
    )
    return {
        "total":   len(results_sorted),
        "scans":   results_sorted[offset: offset + limit],
        "running": len(_scan_queue),
    }


@router.get("/scans/{scan_id}")
async def get_scan_result(
    scan_id: str,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """스캔 결과 상세 조회."""
    r = _scan_results.get(scan_id)
    if not r:
        raise HTTPException(404, f"스캔 {scan_id}를 찾을 수 없습니다.")
    if current_user.role == UserRole.ENGINEER and r.get("requester") != current_user.username:
        raise HTTPException(403, "본인 스캔만 조회할 수 있습니다.")
    return r


@router.post("/quick-check")
async def quick_check(
    body: QuickCheckIn,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """
    단일 포트/서비스 빠른 점검.
    """
    if current_user.role not in (UserRole.ADMIN, UserRole.PM, UserRole.ENGINEER):
        raise HTTPException(403, "로그인 사용자만 접근 가능합니다.")

    from core.vuln_scan import _scan_port, _grab_banner, check_version_vulns, DANGER_PORTS

    is_open = _scan_port(body.host, body.port, timeout=1.0)
    result = {
        "host":    body.host,
        "port":    body.port,
        "service": body.service,
        "is_open": is_open,
        "banner":  None,
        "vulns":   [],
        "risk":    "UNKNOWN",
        "checked_at": datetime.utcnow().isoformat(),
    }

    if is_open:
        banner = _grab_banner(body.host, body.port, timeout=2.0)
        result["banner"] = banner
        if banner:
            result["vulns"] = check_version_vulns(banner, body.service or "")
        result["risk"] = "HIGH" if body.port in DANGER_PORTS else "LOW"

    return result


@router.get("/cve/{cve_id}")
async def get_cve_info(
    cve_id: str,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """CVE 상세 정보 조회 (내부 DB)."""
    from core.vuln_scan import VULN_VERSION_PATTERNS

    cve_upper = cve_id.upper()
    matches = [
        {
            "cve_id":      cve,
            "service":     svc,
            "pattern":     pat,
            "severity":    sev,
            "description": desc,
        }
        for svc, pat, cve, sev, desc in VULN_VERSION_PATTERNS
        if cve.upper() == cve_upper
    ]

    if not matches:
        raise HTTPException(404, f"{cve_id}는 내부 DB에 없습니다.")
    return matches[0]


@router.post("/cvss")
async def calculate_cvss(
    body: CVSSCalcIn,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """CVSS v3.1 단순화 점수 계산."""
    from core.vuln_scan import calculate_cvss_simplified

    score = calculate_cvss_simplified(
        body.attack_vector, body.complexity,
        body.privileges, body.impact,
    )
    severity = (
        "CRITICAL" if score >= 9.0 else
        "HIGH"     if score >= 7.0 else
        "MEDIUM"   if score >= 4.0 else
        "LOW"      if score > 0.0  else
        "NONE"
    )
    return {
        "score":         score,
        "severity":      severity,
        "attack_vector": body.attack_vector,
        "complexity":    body.complexity,
        "privileges":    body.privileges,
        "impact":        body.impact,
        "note":          "단순화된 CVSS v3.1 근사 계산입니다.",
    }


@router.get("/stats")
async def vuln_stats(
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """취약점 스캔 통계."""
    if current_user.role not in (UserRole.ADMIN, UserRole.PM):
        raise HTTPException(403, "PM/ADMIN 권한이 필요합니다.")

    completed = [r for r in _scan_results.values() if r.get("status") == "COMPLETED"]
    total_vulns = sum(len(r.get("vulnerabilities", [])) for r in completed)
    total_configs = sum(len(r.get("config_issues", [])) for r in completed)

    sev_totals: Dict[str, int] = {}
    for r in completed:
        for sev, cnt in (r.get("severity_summary") or {}).items():
            sev_totals[sev] = sev_totals.get(sev, 0) + cnt

    avg_risk = (
        sum(r.get("risk_score", 0) for r in completed) / len(completed)
        if completed else 0
    )

    return {
        "total_scans":     len(_scan_results),
        "completed_scans": len(completed),
        "running_scans":   len(_scan_queue),
        "total_vulns":     total_vulns,
        "total_config_issues": total_configs,
        "severity_totals": sev_totals,
        "avg_risk_score":  round(avg_risk, 1),
    }


@router.get("/policies")
async def get_scan_policies(
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """스캔 정책 목록."""
    from core.vuln_scan import _DANGER_PATTERNS, DANGER_PORTS
    return {
        "scan_policies": [
            {"name": "스캔 권한",      "value": "PM/ADMIN만 가능"},
            {"name": "위험 포트",      "value": sorted(DANGER_PORTS)},
            {"name": "타임아웃 최대",  "value": "5초"},
            {"name": "외부 DB 조회",   "value": "금지 (내부망 전용)"},
            {"name": "root 계정 사용", "value": "금지"},
            {"name": "LLM 분석",       "value": "내부 Ollama sLLM만 허용"},
        ],
        "cve_db_count": len(
            __import__("core.vuln_scan", fromlist=["VULN_VERSION_PATTERNS"]).VULN_VERSION_PATTERNS
        ),
        "danger_port_count": len(DANGER_PORTS),
    }


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

class PatchUpdateIn(BaseModel):
    cve_id:     str
    patch_note: Optional[str] = None
    patched_at: Optional[str] = None
    status:     str = "PATCHED"  # PATCHED|WONTFIX|MITIGATED


@router.get("/patches")
async def list_patches(
    skip: int = 0,
    limit: int = 100,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """패치 이력 목록 조회."""
    from models import VulnPatchRecord
    from sqlalchemy import select, desc
    rows = (await db.execute(
        select(VulnPatchRecord).order_by(desc(VulnPatchRecord.created_at)).offset(skip).limit(limit)
    )).scalars().all()
    return [
        {
            "id":         r.id,
            "scan_id":    r.scan_id,
            "cve_id":     r.cve_id,
            "cvss_score": r.cvss_score,
            "severity":   r.severity,
            "status":     r.status,
            "patch_note": r.patch_note,
            "patched_at": r.patched_at.isoformat() if r.patched_at else None,
            "patched_by": r.patched_by,
            "created_at": r.created_at.isoformat(),
        }
        for r in rows
    ]


@router.post("/scans/{scan_id}/patch", status_code=201)
async def mark_patch(
    scan_id: str,
    body: PatchUpdateIn,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """취약점 패치 완료 기록."""
    from models import VulnPatchRecord
    from sqlalchemy import select

    if current_user.role == UserRole.CUSTOMER:
        raise HTTPException(403, "패치 기록은 ADMIN/PM/ENGINEER만 가능합니다.")

    patched_at = None
    if body.patched_at:
        try:
            patched_at = datetime.fromisoformat(body.patched_at)
        except ValueError:
            patched_at = datetime.utcnow()
    else:
        patched_at = datetime.utcnow()

    # 기존 레코드 확인
    existing = (await db.execute(
        select(VulnPatchRecord).where(
            VulnPatchRecord.scan_id == scan_id,
            VulnPatchRecord.cve_id == body.cve_id,
        )
    )).scalars().first()

    if existing:
        existing.status     = body.status
        existing.patch_note = body.patch_note
        existing.patched_at = patched_at
        existing.patched_by = current_user.username
        existing.updated_at = datetime.utcnow()
    else:
        existing = VulnPatchRecord(
            scan_id    = scan_id,
            cve_id     = body.cve_id,
            status     = body.status,
            patch_note = body.patch_note,
            patched_at = patched_at,
            patched_by = current_user.username,
        )
        db.add(existing)

    await db.commit()
    return {"message": f"CVE {body.cve_id} 패치 상태를 {body.status}로 기록했습니다."}


@router.get("/patch-stats")
async def patch_stats(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """패치 현황 통계."""
    from models import VulnPatchRecord
    from sqlalchemy import select, func as sqlfunc, case

    rows = (await db.execute(select(VulnPatchRecord))).scalars().all()
    total   = len(rows)
    patched = sum(1 for r in rows if r.status == "PATCHED")
    open_   = sum(1 for r in rows if r.status == "OPEN")
    by_sev  = {}
    for r in rows:
        sev = r.severity or "UNKNOWN"
        by_sev.setdefault(sev, {"total": 0, "patched": 0})
        by_sev[sev]["total"] += 1
        if r.status == "PATCHED":
            by_sev[sev]["patched"] += 1

    return {
        "total_vulns":     total,
        "patched":         patched,
        "open":            open_,
        "patch_rate_pct":  round(patched / total * 100, 1) if total else 0.0,
        "by_severity":     by_sev,
    }


@router.get("/overdue-patches")
async def overdue_patches(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """30일 이상 미패치 취약점 목록."""
    from models import VulnPatchRecord
    from sqlalchemy import select
    cutoff = datetime.utcnow().replace(hour=0, minute=0, second=0) - __import__("datetime").timedelta(days=30)
    rows = (await db.execute(
        select(VulnPatchRecord).where(
            VulnPatchRecord.status == "OPEN",
            VulnPatchRecord.created_at <= cutoff,
        ).order_by(VulnPatchRecord.created_at)
    )).scalars().all()
    return [
        {
            "id":         r.id,
            "cve_id":     r.cve_id,
            "cvss_score": r.cvss_score,
            "severity":   r.severity,
            "created_at": r.created_at.isoformat(),
            "overdue_days": (datetime.utcnow() - r.created_at).days,
        }
        for r in rows
    ]
