"""
D-5: 불변 감사 로그 (Immutable Audit Log with Hash Chain)

기능:
  1. SHA-256 해시 체인 — 각 로그가 이전 로그의 해시를 포함
  2. 체인 무결성 검증 — 임의 변조 즉시 탐지
  3. 범용 감사 기록 — SR, RFC, PAM, LDAP, 취약점 등 모든 이벤트
  4. 심각도 분류 — INFO / WARN / ERROR / CRITICAL
  5. 기간별 체인 스냅샷 — 날짜 기준 체인 분리 검증
  6. 감사 로그 내보내기 — CSV/JSON (관리자 전용)
  7. IP 해시 저장 — 원본 IP 저장 금지, SHA-256만 보관

엔드포인트:
  GET  /api/audit            — 감사 로그 목록
  POST /api/audit/record     — 감사 이벤트 수동 기록
  GET  /api/audit/verify     — 해시 체인 무결성 검증
  GET  /api/audit/verify/{from_id}/{to_id} — 범위 검증
  GET  /api/audit/stats      — 감사 통계
  GET  /api/audit/export     — 로그 내보내기 (ADMIN)
  GET  /api/audit/{log_id}   — 단일 로그 상세
  GET  /api/audit/entity/{entity_type}/{entity_id} — 엔티티별 감사 이력

보안:
  - 로그 수정/삭제 절대 불가 (hash chain으로 변조 탐지)
  - IP 주소는 SHA-256 해시로만 저장
  - 스택트레이스 노출 금지
"""
from __future__ import annotations

import csv
import hashlib
import io
import json
import logging
from datetime import datetime, timedelta
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from sqlalchemy import select, and_, func, desc
from sqlalchemy.ext.asyncio import AsyncSession

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

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

# ── 심각도 정의 ───────────────────────────────────────────────────────────────
SEVERITY_LEVELS = {"INFO": 0, "WARN": 1, "ERROR": 2, "CRITICAL": 3}


# ── 해시 체인 헬퍼 ────────────────────────────────────────────────────────────

async def _get_last_hash(db: AsyncSession) -> Optional[str]:
    """마지막 감사 로그 해시 조회 (체인 연결용)."""
    last = (await db.execute(
        select(AuditLog.log_hash).order_by(AuditLog.id.desc()).limit(1)
    )).scalar()
    return last


async def append_audit_log(
    db: AsyncSession,
    actor: str,
    action: str,
    detail: str = "",
    sr_id: Optional[str] = None,
    entity_type: Optional[str] = None,
    entity_id: Optional[str] = None,
    severity: str = "INFO",
    client_ip: Optional[str] = None,
) -> AuditLog:
    """
    감사 로그 체인에 새 항목 추가.
    이전 로그의 hash를 포함하여 불변 체인 형성.
    """
    prev_hash = await _get_last_hash(db)
    now = datetime.utcnow()
    ts = now.isoformat()

    # IP는 SHA-256 해시로만 저장 (원본 절대 금지)
    ip_hash = hashlib.sha256(client_ip.encode()).hexdigest() if client_ip else None

    log_hash = compute_log_hash(prev_hash, actor, action, detail, ts)

    log = AuditLog(
        sr_id        = sr_id,
        actor        = actor,
        action       = action,
        detail       = detail,
        prev_hash    = prev_hash,
        log_hash     = log_hash,
        created_at   = now,
        entity_type  = entity_type,
        entity_id    = entity_id,
        severity     = severity,
        ip_addr_hash = ip_hash,
    )
    db.add(log)
    await db.flush()
    return log


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

class AuditRecordIn(BaseModel):
    action:      str
    detail:      Optional[str] = ""
    sr_id:       Optional[str] = None
    entity_type: Optional[str] = None
    entity_id:   Optional[str] = None
    severity:    str = "INFO"


class ChainVerifyResult(BaseModel):
    total:        int
    intact:       bool
    broken_at_id: Optional[int]
    verified_at:  str
    chain_start:  Optional[int]
    chain_end:    Optional[int]


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

@router.get("", response_model=List[AuditLogOut])
async def list_audit_logs(
    sr_id:       Optional[str] = Query(None),
    entity_type: Optional[str] = Query(None),
    entity_id:   Optional[str] = Query(None),
    actor:       Optional[str] = Query(None),
    severity:    Optional[str] = Query(None),
    action:      Optional[str] = Query(None),
    from_dt:     Optional[str] = Query(None, description="ISO 날짜 (예: 2024-01-01)"),
    to_dt:       Optional[str] = Query(None),
    skip:        int = Query(0, ge=0),
    limit:       int = Query(100, ge=1, le=500),
    db:          AsyncSession = Depends(get_db),
    _u:          User = Depends(get_current_user),
):
    """감사 로그 목록 조회 (최신순)."""
    conditions = []
    if sr_id:        conditions.append(AuditLog.sr_id        == sr_id)
    if entity_type:  conditions.append(AuditLog.entity_type  == entity_type.upper())
    if entity_id:    conditions.append(AuditLog.entity_id    == entity_id)
    if actor:        conditions.append(AuditLog.actor.ilike(f"%{actor}%"))
    if severity:     conditions.append(AuditLog.severity     == severity.upper())
    if action:       conditions.append(AuditLog.action.ilike(f"%{action}%"))
    if from_dt:
        try:
            conditions.append(AuditLog.created_at >= datetime.fromisoformat(from_dt))
        except ValueError:
            raise HTTPException(400, f"from_dt 형식 오류: {from_dt}")
    if to_dt:
        try:
            conditions.append(AuditLog.created_at <= datetime.fromisoformat(to_dt))
        except ValueError:
            raise HTTPException(400, f"to_dt 형식 오류: {to_dt}")

    q = (
        select(AuditLog)
        .where(and_(*conditions) if conditions else True)
        .order_by(AuditLog.id.desc())
        .offset(skip).limit(limit)
    )
    return (await db.execute(q)).scalars().all()


@router.post("/record", status_code=201)
async def record_event(
    body: AuditRecordIn,
    request: Request,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """감사 이벤트 수동 기록 (API / 내부 시스템 호출)."""
    if body.severity.upper() not in SEVERITY_LEVELS:
        raise HTTPException(400, f"유효하지 않은 심각도: {body.severity}")

    # 클라이언트 IP 수집 (해시로만 저장)
    client_ip = request.client.host if request.client else None

    log = await append_audit_log(
        db,
        actor        = current_user.username,
        action       = body.action,
        detail       = body.detail or "",
        sr_id        = body.sr_id,
        entity_type  = body.entity_type,
        entity_id    = body.entity_id,
        severity     = body.severity.upper(),
        client_ip    = client_ip,
    )
    await db.commit()

    return {
        "log_id":    log.id,
        "log_hash":  log.log_hash,
        "prev_hash": log.prev_hash,
        "action":    log.action,
        "created_at": log.created_at.isoformat(),
        "message":   "감사 로그가 체인에 추가되었습니다.",
    }


@router.get("/verify", response_model=ChainVerifyResult)
async def verify_chain(
    from_id: Optional[int] = Query(None),
    to_id:   Optional[int] = Query(None),
    db:      AsyncSession = Depends(get_db),
    _u:      User = Depends(get_current_user),
):
    """SHA-256 해시 체인 무결성 전체 검증."""
    q = select(AuditLog).order_by(AuditLog.id)
    if from_id:
        q = q.where(AuditLog.id >= from_id)
    if to_id:
        q = q.where(AuditLog.id <= to_id)

    logs = (await db.execute(q)).scalars().all()

    broken_at: Optional[int] = None
    prev_hash_expected: Optional[str] = None

    for i, log in enumerate(logs):
        # 1) 체인 연결 검증 (prev_hash가 이전 로그의 log_hash와 일치해야 함)
        if i == 0:
            # 첫 로그: from_id가 없으면 prev_hash는 None이어야 함
            if from_id is None and log.prev_hash is not None:
                broken_at = log.id
                break
        else:
            if log.prev_hash != prev_hash_expected:
                broken_at = log.id
                break

        # 2) log_hash 재계산 검증
        expected = compute_log_hash(
            log.prev_hash,
            log.actor or "",
            log.action,
            log.detail or "",
            log.created_at.isoformat() if log.created_at else "",
        )
        if expected != log.log_hash:
            broken_at = log.id
            break

        prev_hash_expected = log.log_hash

    return ChainVerifyResult(
        total        = len(logs),
        intact       = broken_at is None,
        broken_at_id = broken_at,
        verified_at  = datetime.utcnow().isoformat(),
        chain_start  = logs[0].id if logs else None,
        chain_end    = logs[-1].id if logs else None,
    )


@router.get("/verify/{from_id}/{to_id}", response_model=ChainVerifyResult)
async def verify_chain_range(
    from_id: int,
    to_id:   int,
    db:      AsyncSession = Depends(get_db),
    _u:      User = Depends(get_current_user),
):
    """특정 ID 범위 해시 체인 검증."""
    if from_id > to_id:
        raise HTTPException(400, "from_id는 to_id보다 작아야 합니다.")
    return await verify_chain(from_id, to_id, db, _u)


@router.get("/stats")
async def audit_stats(
    days: int = Query(30, ge=1, le=365),
    db:   AsyncSession = Depends(get_db),
    _u:   User = Depends(get_current_user),
):
    """감사 로그 통계."""
    since = datetime.utcnow() - timedelta(days=days)

    total = (await db.execute(
        select(func.count()).select_from(AuditLog)
    )).scalar() or 0

    recent = (await db.execute(
        select(func.count()).select_from(AuditLog)
        .where(AuditLog.created_at >= since)
    )).scalar() or 0

    # 심각도별 집계
    sev_rows = (await db.execute(
        select(AuditLog.severity, func.count())
        .where(AuditLog.created_at >= since)
        .group_by(AuditLog.severity)
    )).all()
    by_severity = {row[0] or "INFO": row[1] for row in sev_rows}

    # 엔티티 유형별 집계
    ent_rows = (await db.execute(
        select(AuditLog.entity_type, func.count())
        .where(AuditLog.created_at >= since)
        .group_by(AuditLog.entity_type)
    )).all()
    by_entity = {row[0] or "UNKNOWN": row[1] for row in ent_rows}

    # 최근 CRITICAL/ERROR 이벤트
    alerts = (await db.execute(
        select(AuditLog)
        .where(and_(
            AuditLog.created_at >= since,
            AuditLog.severity.in_(["CRITICAL", "ERROR"]),
        ))
        .order_by(AuditLog.id.desc())
        .limit(5)
    )).scalars().all()

    return {
        "total_logs":   total,
        "recent_logs":  recent,
        "period_days":  days,
        "by_severity":  by_severity,
        "by_entity":    by_entity,
        "recent_alerts": [
            {"id": a.id, "action": a.action, "severity": a.severity,
             "actor": a.actor, "created_at": a.created_at.isoformat()}
            for a in alerts
        ],
    }


@router.get("/export")
async def export_audit_logs(
    from_dt: Optional[str] = Query(None),
    to_dt:   Optional[str] = Query(None),
    fmt:     str = Query("json", enum=["json", "csv"]),
    db:      AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """감사 로그 내보내기 (ADMIN 전용)."""
    if current_user.role != UserRole.ADMIN:
        raise HTTPException(403, "ADMIN 권한이 필요합니다.")

    conditions = []
    if from_dt:
        try:
            conditions.append(AuditLog.created_at >= datetime.fromisoformat(from_dt))
        except ValueError:
            raise HTTPException(400, f"from_dt 형식 오류: {from_dt}")
    if to_dt:
        try:
            conditions.append(AuditLog.created_at <= datetime.fromisoformat(to_dt))
        except ValueError:
            raise HTTPException(400, f"to_dt 형식 오류: {to_dt}")

    q = (
        select(AuditLog)
        .where(and_(*conditions) if conditions else True)
        .order_by(AuditLog.id)
        .limit(10000)
    )
    logs = (await db.execute(q)).scalars().all()

    if fmt == "csv":
        output = io.StringIO()
        writer = csv.writer(output)
        writer.writerow(["id", "created_at", "actor", "action", "detail",
                         "entity_type", "entity_id", "severity",
                         "log_hash", "prev_hash"])
        for log in logs:
            writer.writerow([
                log.id,
                log.created_at.isoformat() if log.created_at else "",
                log.actor or "",
                log.action,
                log.detail or "",
                log.entity_type or "",
                log.entity_id or "",
                log.severity or "INFO",
                log.log_hash or "",
                log.prev_hash or "",
            ])
        output.seek(0)
        return StreamingResponse(
            iter([output.getvalue()]),
            media_type="text/csv",
            headers={"Content-Disposition": "attachment; filename=audit_log.csv"},
        )
    else:
        data = [
            {
                "id":          log.id,
                "created_at":  log.created_at.isoformat() if log.created_at else None,
                "actor":       log.actor,
                "action":      log.action,
                "detail":      log.detail,
                "entity_type": log.entity_type,
                "entity_id":   log.entity_id,
                "severity":    log.severity,
                "log_hash":    log.log_hash,
                "prev_hash":   log.prev_hash,
            }
            for log in logs
        ]
        return StreamingResponse(
            iter([json.dumps(data, ensure_ascii=False, default=str)]),
            media_type="application/json",
            headers={"Content-Disposition": "attachment; filename=audit_log.json"},
        )


@router.get("/entity/{entity_type}/{entity_id}", response_model=List[AuditLogOut])
async def get_entity_audit_trail(
    entity_type: str,
    entity_id:   str,
    limit:       int = Query(50, ge=1, le=200),
    db:          AsyncSession = Depends(get_db),
    _u:          User = Depends(get_current_user),
):
    """특정 엔티티(SR, RFC, PAM 세션 등)의 전체 감사 이력."""
    q = (
        select(AuditLog)
        .where(and_(
            AuditLog.entity_type == entity_type.upper(),
            AuditLog.entity_id   == entity_id,
        ))
        .order_by(AuditLog.id.desc())
        .limit(limit)
    )
    return (await db.execute(q)).scalars().all()


@router.get("/{log_id}", response_model=AuditLogOut)
async def get_audit_log(
    log_id: int,
    db:     AsyncSession = Depends(get_db),
    _u:     User = Depends(get_current_user),
):
    """단일 감사 로그 상세 (해시 포함)."""
    log = (await db.execute(
        select(AuditLog).where(AuditLog.id == log_id)
    )).scalars().first()
    if not log:
        raise HTTPException(404, f"감사 로그 {log_id}를 찾을 수 없습니다.")
    return log
