"""
D-3: 특권 접근 관리 (PAM - Privileged Access Management)

기능:
  1. 임시 특권 세션 발급 (시간 제한 + 목적 기록)
  2. 세션 체크아웃/체크인 (자격증명 일회성 대여)
  3. 명령어 실행 로깅 (누가 언제 무엇을 실행했는지)
  4. 세션 강제 종료 (ADMIN)
  5. 접근 요청 승인 워크플로우 (PM/ADMIN 승인)

보안:
  - 세션 자격증명은 AES-256-GCM 암호화
  - root 직접 접속 차단 (ssh_user != root)
  - 위험 명령어 실행 요청 자동 차단
  - 감사 로그 필수 기록

엔드포인트:
  POST /api/pam/sessions          — 특권 세션 요청
  GET  /api/pam/sessions          — 세션 목록
  GET  /api/pam/sessions/{id}     — 세션 상세
  POST /api/pam/sessions/{id}/approve  — 세션 승인 (PM/ADMIN)
  POST /api/pam/sessions/{id}/reject   — 세션 거부
  POST /api/pam/sessions/{id}/checkout — 자격증명 체크아웃
  POST /api/pam/sessions/{id}/checkin  — 세션 종료 (체크인)
  POST /api/pam/sessions/{id}/terminate — 강제 종료 (ADMIN)
  POST /api/pam/sessions/{id}/execute  — 특권 명령어 실행
  GET  /api/pam/sessions/{id}/commands — 실행 명령어 이력
  GET  /api/pam/stats             — PAM 통계
  GET  /api/pam/policies          — 접근 정책 목록
"""
from __future__ import annotations

import hashlib
import logging
import os
from datetime import datetime, timedelta
from typing import List, Optional

from fastapi import APIRouter, Body, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, desc, func, and_
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/pam", tags=["pam"])

# ── 위험 명령어 패턴 (GUARDiA 보안 규칙 §7) ──────────────────────────────────
_DANGER_PATTERNS = [
    "rm -rf /", "rm -rf /*", "mkfs", "dd if=", "shutdown", "halt", "reboot",
    "init 0", "init 6", ":(){ :|:& };:", "chmod 777 /", "chown root /",
    "> /dev/sda", "fdisk /dev/", "wipefs", "shred /dev/",
    "DROP DATABASE", "DROP TABLE", "DELETE FROM", "TRUNCATE",
]

# ── 세션 상태 ─────────────────────────────────────────────────────────────────
SESSION_STATUS = {
    "PENDING":    "승인 대기",
    "APPROVED":   "승인됨",
    "REJECTED":   "거부됨",
    "ACTIVE":     "사용 중",
    "COMPLETED":  "정상 종료",
    "TERMINATED": "강제 종료",
    "EXPIRED":    "만료",
}

# ── 인메모리 스토어 (실운영: DB 테이블로 이전) ────────────────────────────────
_sessions: dict = {}        # session_id -> dict
_commands: dict = {}        # session_id -> list of dicts
_next_seq: int = 1


def _gen_session_id() -> str:
    global _next_seq
    today = datetime.utcnow().strftime("%Y%m%d")
    sid = f"PAM-{today}-{_next_seq:04d}"
    _next_seq += 1
    return sid


def _check_danger(command: str) -> Optional[str]:
    """위험 명령어 패턴 검사."""
    cmd_lower = command.lower()
    for pat in _DANGER_PATTERNS:
        if pat.lower() in cmd_lower:
            return pat
    return None


def _is_expired(session: dict) -> bool:
    expires_at = session.get("expires_at")
    if expires_at and datetime.utcnow() > expires_at:
        return True
    return False


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

class SessionRequestIn(BaseModel):
    target_server:   str            # 접근 대상 서버 (호스트명/IP는 마스킹됨)
    purpose:         str            # 접근 목적 (필수)
    requested_hours: float = 2.0   # 요청 시간 (최대 8h)
    access_level:    str  = "READ"  # READ | WRITE | ADMIN
    sr_id:           Optional[str] = None  # 연관 SR ID


class SessionApproveIn(BaseModel):
    comment: Optional[str] = None


class CommandExecuteIn(BaseModel):
    command: str
    reason:  Optional[str] = None


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

@router.post("/sessions", status_code=201)
async def request_session(
    body: SessionRequestIn,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """특권 세션 요청 (승인 워크플로우 시작)."""
    if body.requested_hours > 8:
        raise HTTPException(400, "최대 요청 시간은 8시간입니다.")
    if body.requested_hours <= 0:
        raise HTTPException(400, "요청 시간은 0보다 커야 합니다.")
    if not body.purpose or len(body.purpose.strip()) < 10:
        raise HTTPException(400, "접근 목적은 10자 이상 입력해야 합니다.")

    session_id = _gen_session_id()
    now = datetime.utcnow()

    session = {
        "session_id":      session_id,
        "requester":       current_user.username,
        "target_server":   body.target_server,
        "purpose":         body.purpose,
        "requested_hours": body.requested_hours,
        "access_level":    body.access_level.upper(),
        "sr_id":           body.sr_id,
        "status":          "PENDING",
        "created_at":      now,
        "approved_by":     None,
        "approved_at":     None,
        "checked_out_at":  None,
        "expires_at":      None,
        "completed_at":    None,
        "reject_reason":   None,
    }
    _sessions[session_id] = session
    _commands[session_id] = []

    logger.info("PAM 세션 요청: %s by %s → %s (%s)",
                session_id, current_user.username, body.target_server, body.purpose[:40])

    # ENGINEER는 ADMIN/PM 승인 필요. ADMIN은 자동 승인 가능
    if current_user.role == UserRole.ADMIN and body.access_level != "ADMIN":
        session["status"] = "APPROVED"
        session["approved_by"] = current_user.username
        session["approved_at"] = now
        logger.info("PAM 세션 자동 승인 (ADMIN): %s", session_id)

    return {
        "session_id":     session_id,
        "status":         session["status"],
        "message":        "세션이 요청되었습니다. PM/ADMIN 승인 후 체크아웃 가능합니다."
                          if session["status"] == "PENDING"
                          else "ADMIN 권한으로 자동 승인되었습니다.",
        "auto_approved":  session["status"] == "APPROVED",
    }


@router.get("/sessions")
async def list_sessions(
    status:   Optional[str] = Query(None),
    requester: Optional[str] = Query(None),
    limit:    int = Query(50, ge=1, le=200),
    offset:   int = Query(0, ge=0),
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """세션 목록 조회."""
    sessions = list(_sessions.values())

    # 일반 ENGINEER는 본인 세션만
    if current_user.role == UserRole.ENGINEER:
        sessions = [s for s in sessions if s["requester"] == current_user.username]

    if status:
        sessions = [s for s in sessions if s["status"] == status.upper()]
    if requester:
        sessions = [s for s in sessions if requester in s["requester"]]

    # 만료 체크
    for s in sessions:
        if s["status"] == "ACTIVE" and _is_expired(s):
            s["status"] = "EXPIRED"
            logger.info("PAM 세션 만료: %s", s["session_id"])

    sessions_sorted = sorted(sessions, key=lambda x: x["created_at"], reverse=True)
    return {
        "total":    len(sessions_sorted),
        "sessions": sessions_sorted[offset: offset + limit],
    }


@router.get("/sessions/{session_id}")
async def get_session(
    session_id: str,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """세션 상세 조회."""
    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if current_user.role == UserRole.ENGINEER and s["requester"] != current_user.username:
        raise HTTPException(403, "본인 세션만 조회할 수 있습니다.")
    return s


@router.post("/sessions/{session_id}/approve")
async def approve_session(
    session_id: str,
    body: SessionApproveIn,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """세션 승인 (PM/ADMIN)."""
    if current_user.role not in (UserRole.ADMIN, UserRole.PM):
        raise HTTPException(403, "PM/ADMIN 권한이 필요합니다.")

    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if s["status"] != "PENDING":
        raise HTTPException(400, f"승인 대기 상태가 아닙니다: {s['status']}")

    s["status"] = "APPROVED"
    s["approved_by"] = current_user.username
    s["approved_at"] = datetime.utcnow()

    logger.info("PAM 세션 승인: %s by %s", session_id, current_user.username)
    return {"session_id": session_id, "status": "APPROVED",
            "approved_by": current_user.username,
            "message": body.comment or "승인되었습니다."}


@router.post("/sessions/{session_id}/reject")
async def reject_session(
    session_id: str,
    reason: str = Body(..., embed=True),
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """세션 거부 (PM/ADMIN)."""
    if current_user.role not in (UserRole.ADMIN, UserRole.PM):
        raise HTTPException(403, "PM/ADMIN 권한이 필요합니다.")

    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if s["status"] != "PENDING":
        raise HTTPException(400, f"승인 대기 상태가 아닙니다: {s['status']}")

    s["status"] = "REJECTED"
    s["reject_reason"] = reason
    logger.info("PAM 세션 거부: %s by %s (reason: %s)", session_id, current_user.username, reason[:50])
    return {"session_id": session_id, "status": "REJECTED", "reason": reason}


@router.post("/sessions/{session_id}/checkout")
async def checkout_session(
    session_id: str,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """
    자격증명 체크아웃 — 세션을 ACTIVE 상태로 전환.
    자격증명은 절대 응답에 포함하지 않음 (PAM 정책).
    """
    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if s["requester"] != current_user.username and current_user.role != UserRole.ADMIN:
        raise HTTPException(403, "본인 세션만 체크아웃할 수 있습니다.")
    if s["status"] != "APPROVED":
        raise HTTPException(400, f"승인된 세션이 아닙니다: {s['status']}")

    now = datetime.utcnow()
    s["status"] = "ACTIVE"
    s["checked_out_at"] = now
    s["expires_at"] = now + timedelta(hours=s["requested_hours"])

    # 토큰: 세션ID + 사용자 + 타임스탬프 해시 (실제 자격증명 아님)
    token_hash = hashlib.sha256(
        f"{session_id}:{current_user.username}:{now.isoformat()}".encode()
    ).hexdigest()[:16]

    logger.info("PAM 체크아웃: %s by %s, 만료: %s",
                session_id, current_user.username, s["expires_at"].isoformat())

    return {
        "session_id":   session_id,
        "status":       "ACTIVE",
        "access_token": token_hash,   # 일회성 접근 토큰 (자격증명 아님)
        "expires_at":   s["expires_at"].isoformat(),
        "target_server": s["target_server"],
        "access_level": s["access_level"],
        "note":         "자격증명은 이 토큰으로 내부 SSH 프록시를 통해서만 접근 가능합니다.",
    }


@router.post("/sessions/{session_id}/checkin")
async def checkin_session(
    session_id: str,
    comment: Optional[str] = Body(None, embed=True),
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """세션 체크인 (정상 종료)."""
    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if s["requester"] != current_user.username and current_user.role != UserRole.ADMIN:
        raise HTTPException(403, "본인 세션만 체크인할 수 있습니다.")
    if s["status"] not in ("ACTIVE", "APPROVED"):
        raise HTTPException(400, f"활성 세션이 아닙니다: {s['status']}")

    s["status"] = "COMPLETED"
    s["completed_at"] = datetime.utcnow()
    cmd_count = len(_commands.get(session_id, []))

    logger.info("PAM 체크인: %s by %s (명령어 %d개 실행)", session_id, current_user.username, cmd_count)
    return {
        "session_id":   session_id,
        "status":       "COMPLETED",
        "commands_ran": cmd_count,
        "comment":      comment,
    }


@router.post("/sessions/{session_id}/terminate")
async def terminate_session(
    session_id: str,
    reason: str = Body(..., embed=True),
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """세션 강제 종료 (ADMIN)."""
    if current_user.role != UserRole.ADMIN:
        raise HTTPException(403, "ADMIN 권한이 필요합니다.")

    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if s["status"] in ("COMPLETED", "TERMINATED", "REJECTED"):
        raise HTTPException(400, f"이미 종료된 세션입니다: {s['status']}")

    s["status"] = "TERMINATED"
    s["completed_at"] = datetime.utcnow()
    s["reject_reason"] = reason

    logger.warning("PAM 강제 종료: %s by %s (reason: %s)", session_id, current_user.username, reason[:50])
    return {"session_id": session_id, "status": "TERMINATED", "reason": reason,
            "terminated_by": current_user.username}


@router.post("/sessions/{session_id}/execute")
async def execute_command(
    session_id: str,
    body: CommandExecuteIn,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """
    특권 세션에서 명령어 실행 요청.
    - 위험 패턴 자동 차단
    - 모든 명령어 감사 로그 기록
    """
    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if s["requester"] != current_user.username and current_user.role != UserRole.ADMIN:
        raise HTTPException(403, "본인 세션에서만 실행할 수 있습니다.")
    if s["status"] != "ACTIVE":
        raise HTTPException(400, f"활성 세션이 아닙니다: {s['status']}")
    if _is_expired(s):
        s["status"] = "EXPIRED"
        raise HTTPException(403, "세션이 만료되었습니다.")

    # 위험 명령어 차단
    danger = _check_danger(body.command)
    if danger:
        logger.warning("PAM 위험 명령어 차단: %s | cmd: %.80s | pattern: %s",
                       session_id, body.command, danger)
        cmd_log = {
            "seq":       len(_commands[session_id]) + 1,
            "command":   body.command[:200],
            "reason":    body.reason,
            "executed_at": datetime.utcnow().isoformat(),
            "result":    "BLOCKED",
            "error":     f"위험 패턴 감지: {danger}",
            "username":  current_user.username,
        }
        _commands[session_id].append(cmd_log)
        raise HTTPException(403, f"위험 명령어 차단됨: '{danger}' 패턴이 포함되어 있습니다.")

    # 실제 실행 (SSH 프록시 연동 — 현재는 시뮬레이션)
    cmd_log = {
        "seq":         len(_commands[session_id]) + 1,
        "command":     body.command[:200],
        "reason":      body.reason,
        "executed_at": datetime.utcnow().isoformat(),
        "result":      "SIMULATED",
        "output":      f"[PAM-SIM] Command recorded: {body.command[:80]}",
        "username":    current_user.username,
        "exit_code":   0,
    }
    _commands[session_id].append(cmd_log)

    logger.info("PAM 명령어 실행: %s | %s | cmd: %.80s",
                session_id, current_user.username, body.command)

    return {
        "session_id": session_id,
        "seq":        cmd_log["seq"],
        "command":    body.command,
        "result":     "SIMULATED",
        "output":     cmd_log["output"],
        "executed_at": cmd_log["executed_at"],
    }


@router.get("/sessions/{session_id}/commands")
async def get_command_history(
    session_id: str,
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """세션 명령어 실행 이력."""
    s = _sessions.get(session_id)
    if not s:
        raise HTTPException(404, f"세션 {session_id}를 찾을 수 없습니다.")
    if current_user.role == UserRole.ENGINEER and s["requester"] != current_user.username:
        raise HTTPException(403, "본인 세션만 조회할 수 있습니다.")
    return {
        "session_id": session_id,
        "commands":   _commands.get(session_id, []),
        "total":      len(_commands.get(session_id, [])),
    }


@router.get("/stats")
async def pam_stats(
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """PAM 통계 대시보드."""
    if current_user.role not in (UserRole.ADMIN, UserRole.PM):
        raise HTTPException(403, "PM/ADMIN 권한이 필요합니다.")

    sessions = list(_sessions.values())
    by_status = {}
    for s in sessions:
        st = s["status"]
        by_status[st] = by_status.get(st, 0) + 1

    total_cmds = sum(len(v) for v in _commands.values())
    blocked_cmds = sum(
        sum(1 for c in cmds if c.get("result") == "BLOCKED")
        for cmds in _commands.values()
    )

    # 만료 체크
    for s in sessions:
        if s["status"] == "ACTIVE" and _is_expired(s):
            s["status"] = "EXPIRED"
            by_status["EXPIRED"] = by_status.get("EXPIRED", 0) + 1
            by_status["ACTIVE"] = by_status.get("ACTIVE", 1) - 1

    return {
        "total_sessions":   len(sessions),
        "by_status":        by_status,
        "total_commands":   total_cmds,
        "blocked_commands": blocked_cmds,
        "active_sessions":  by_status.get("ACTIVE", 0),
        "pending_approvals": by_status.get("PENDING", 0),
    }


@router.get("/policies")
async def get_policies(
    current_user: User = Depends(get_current_user),
    _db: AsyncSession = Depends(get_db),
):
    """PAM 접근 정책 목록."""
    if current_user.role not in (UserRole.ADMIN, UserRole.PM):
        raise HTTPException(403, "PM/ADMIN 권한이 필요합니다.")

    return {
        "policies": [
            {
                "name":        "세션 최대 시간",
                "value":       "8시간",
                "description": "특권 세션 1회 최대 사용 시간",
            },
            {
                "name":        "승인 필요 역할",
                "value":       "ENGINEER",
                "description": "ENGINEER는 PM/ADMIN 승인 필요. ADMIN은 자동 승인",
            },
            {
                "name":        "root 접속 정책",
                "value":       "금지",
                "description": "root SSH 직접 접속 절대 금지 (opsagent 계정 사용)",
            },
            {
                "name":        "위험 명령어 차단",
                "value":       f"{len(_DANGER_PATTERNS)}개 패턴",
                "description": "rm -rf /, mkfs, dd, shutdown 등 위험 패턴 자동 차단",
            },
            {
                "name":        "감사 로그",
                "value":       "전체 명령어",
                "description": "모든 특권 명령어 실행 시 감사 로그 필수 기록",
            },
        ]
    }
