"""
GUARDiA ITSM — SSH 실행 엔진
asyncssh 기반 비동기 원격 명령 실행.

보안 원칙:
- root 직접 접속 금지 (ssh_user != root 강제)
- 위험 명령어 패턴 차단 (BLOCKED_PATTERNS)
- AES-256-GCM 자격증명 복호화
- 모든 실행 감사 로그 기록
- 서버 IP, 계정, 비밀번호 응답에 포함 금지
"""
from __future__ import annotations

import asyncio
import base64
import logging
import os
import re
from datetime import datetime
from typing import Optional

logger = logging.getLogger(__name__)

# ── 위험 명령어 차단 패턴 ──────────────────────────────────────────────────────
BLOCKED_PATTERNS: list[re.Pattern] = [
    re.compile(p, re.IGNORECASE) for p in [
        r"rm\s+-[rf]+\s+/",          # rm -rf /
        r"rm\s+-[rf]+\s+\*",         # rm -rf *
        r"mkfs(\.[a-z0-9]+)?\s",      # mkfs
        r"dd\s+if=",                  # dd if=
        r">\s*/dev/(sd|hd|vd|nvme)",  # 블록 디바이스 덮어쓰기
        r">\s*/dev/zero",
        r"shutdown\s",                # shutdown
        r"reboot(\s|$)",              # reboot
        r"halt(\s|$)",                # halt
        r"poweroff(\s|$)",            # poweroff
        r"init\s+[06]",               # init 0 / init 6
        r"chmod\s+[0-9]*777\s+/",     # chmod 777 /
        r"passwd\s+root",             # root 비밀번호 변경
        r"usermod\s+.*root",          # root 계정 수정
        r"visudo",                    # sudoers 편집
        r"iptables\s+-F",             # 방화벽 전체 삭제
        r"firewall-cmd\s+--remove",   # firewall-cmd 규칙 삭제
        r"systemctl\s+(disable|mask)\s+(firewall|iptables|sshd)",
        r"setenforce\s+0",            # SELinux 비활성화
        r"rm\s+.*authorized_keys",    # SSH 키 삭제
        r"echo\s+.*>+\s*/etc/shadow", # shadow 파일 덮어쓰기
        r"curl\s+.*\|\s*(bash|sh)",   # curl | bash 패턴
        r"wget\s+.*-O\s*-\s*\|",      # wget | 파이프
        r"base64\s+-d.*\|.*sh",       # base64 decode | sh
        r"python.*exec\(",            # 동적 코드 실행
        r":(){ :|:& };:",             # fork bomb
    ]
]


def _validate_command(cmd: str) -> tuple[bool, str]:
    """
    명령어 안전성 검증.
    Returns: (is_safe, reason)
    """
    for pattern in BLOCKED_PATTERNS:
        if pattern.search(cmd):
            return False, f"차단된 명령어 패턴: {pattern.pattern}"
    # 다중 명령어 내 각 부분도 검사
    for part in re.split(r"[;&|]+", cmd):
        part = part.strip()
        for pattern in BLOCKED_PATTERNS:
            if pattern.search(part):
                return False, f"차단된 명령어 패턴 (조합): {pattern.pattern}"
    return True, ""


def _decrypt_password(enc_b64: str) -> str:
    """
    AES-256-GCM 암호화된 비밀번호 복호화.
    형식: base64(nonce[12] + ciphertext + tag[16])
    """
    try:
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM
        key_b64 = os.environ.get("AES_SECRET_KEY", "")
        if not key_b64:
            raise ValueError("AES_SECRET_KEY 환경변수 미설정")
        key = base64.b64decode(key_b64)
        data = base64.b64decode(enc_b64)
        nonce, ct = data[:12], data[12:]
        return AESGCM(key).decrypt(nonce, ct, None).decode()
    except Exception as e:
        logger.error("비밀번호 복호화 실패: %s", str(e)[:50])
        raise RuntimeError("자격증명 복호화 실패") from e


class SSHResult:
    """SSH 실행 결과 (보안 필드 미포함)."""
    def __init__(self, success: bool, stdout: str, stderr: str,
                 exit_code: int, elapsed: float, error: str = ""):
        self.success   = success
        self.stdout    = stdout[:10000]  # 최대 10KB
        self.stderr    = stderr[:2000]   # 최대 2KB
        self.exit_code = exit_code
        self.elapsed   = round(elapsed, 2)
        self.error     = error           # 연결/실행 오류 요약

    def to_dict(self) -> dict:
        return {
            "success":   self.success,
            "stdout":    self.stdout,
            "stderr":    self.stderr,
            "exit_code": self.exit_code,
            "elapsed":   self.elapsed,
            "error":     self.error,
        }


async def exec_command(
    server_name: str,
    command: str,
    timeout: int = 300,
    *,
    db=None,
    actor: str = "GUARDiA-AI",
    sr_id: Optional[str] = None,
) -> SSHResult:
    """
    지정 서버에 SSH 접속 후 명령어 실행.

    Args:
        server_name: tb_server.server_name
        command:     실행할 셸 명령어
        timeout:     실행 타임아웃 (초)
        db:          AsyncSession (None 이면 새 세션 생성)
        actor:       감사 로그 기록자
        sr_id:       연관 SR ID (감사 로그용)
    """
    import asyncssh
    from sqlalchemy import select
    from database import SessionLocal
    from models import Server, AuditLog, compute_log_hash

    # ── 1. 명령어 안전성 검증 ──────────────────────────────
    is_safe, reason = _validate_command(command)
    if not is_safe:
        logger.warning("[SSH] 차단된 명령: %s — %s", command[:80], reason)
        await _write_audit_safe(
            db, sr_id, actor, "SSH_BLOCKED",
            f"차단: {reason} | CMD: {command[:100]}"
        )
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0,
            error=f"보안 정책에 의해 차단된 명령입니다: {reason}"
        )

    # ── 2. 서버 자격증명 조회 ──────────────────────────────
    own_db = db is None
    _db = SessionLocal() if own_db else db
    server_obj = None
    try:
        r = await _db.execute(
            select(Server).where(Server.server_name == server_name)
        )
        server_obj = r.scalars().first()
    finally:
        if own_db:
            await _db.close()

    if not server_obj:
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0,
            error=f"서버를 찾을 수 없습니다: {server_name}"
        )

    # root 직접 접속 차단
    ssh_user = getattr(server_obj, "ssh_user", "") or ""
    if ssh_user.strip() == "root":
        await _write_audit_safe(
            db, sr_id, actor, "SSH_BLOCKED",
            f"root 직접 접속 차단: {server_name}"
        )
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0,
            error="root 직접 SSH 접속은 보안 정책에 의해 금지됩니다."
        )

    # 비밀번호 복호화
    enc_pw = getattr(server_obj, "os_pw_enc", "") or ""
    try:
        password = _decrypt_password(enc_pw) if enc_pw else ""
    except RuntimeError as e:
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0, error=str(e)
        )

    ip_addr  = getattr(server_obj, "ip_addr",  "")
    ssh_port = getattr(server_obj, "ssh_port", 22) or 22

    # ── 3. 감사 로그 — 실행 시작 ─────────────────────────
    await _write_audit_safe(
        db, sr_id, actor, "SSH_EXEC_START",
        f"서버: {server_name} | CMD: {command[:200]}"
    )

    # ── 4. asyncssh 실행 ─────────────────────────────────
    start = asyncio.get_event_loop().time()
    try:
        connect_timeout = int(os.environ.get("SSH_CONNECT_TIMEOUT", "10"))
        known_hosts = os.environ.get("SSH_KNOWN_HOSTS", None)

        conn_kwargs: dict = {
            "host":             ip_addr,
            "port":             ssh_port,
            "username":         ssh_user,
            "connect_timeout":  connect_timeout,
        }
        if password:
            conn_kwargs["password"] = password
        # 운영환경: known_hosts 검증 / 개발환경: 비활성화
        if known_hosts:
            conn_kwargs["known_hosts"] = known_hosts
        else:
            conn_kwargs["known_hosts"] = None  # 개발/테스트용

        async with asyncssh.connect(**conn_kwargs) as conn:
            result = await asyncio.wait_for(
                conn.run(command, check=False),
                timeout=timeout
            )

        elapsed = asyncio.get_event_loop().time() - start
        stdout = (result.stdout or "").strip()
        stderr = (result.stderr or "").strip()
        exit_code = result.exit_status or 0
        success = (exit_code == 0)

        # ── 5. 감사 로그 — 실행 완료 ─────────────────────
        summary = stdout[:200] if success else stderr[:200]
        await _write_audit_safe(
            db, sr_id, actor, "SSH_EXEC_DONE",
            f"서버: {server_name} | exit={exit_code} | {summary}"
        )

        return SSHResult(
            success=success,
            stdout=stdout,
            stderr=stderr,
            exit_code=exit_code,
            elapsed=elapsed,
        )

    except asyncio.TimeoutError:
        elapsed = asyncio.get_event_loop().time() - start
        await _write_audit_safe(
            db, sr_id, actor, "SSH_EXEC_TIMEOUT",
            f"서버: {server_name} | timeout={timeout}s"
        )
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=elapsed,
            error=f"명령 실행 타임아웃 ({timeout}초)"
        )
    except Exception as e:
        elapsed = asyncio.get_event_loop().time() - start
        # 에러 메시지에서 IP/계정 제거 후 로깅
        safe_err = _sanitize_error(str(e))
        logger.error("[SSH] 실행 오류 — 서버: %s | %s", server_name, safe_err)
        await _write_audit_safe(
            db, sr_id, actor, "SSH_EXEC_ERROR",
            f"서버: {server_name} | 오류: {safe_err}"
        )
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=elapsed,
            error=f"SSH 연결/실행 오류 (서버 관리자에게 문의)"
        )


async def exec_script(
    server_name: str,
    script_path: str,
    env_vars: Optional[dict] = None,
    timeout: int = 600,
    *,
    db=None,
    actor: str = "GUARDiA-AI",
    sr_id: Optional[str] = None,
) -> SSHResult:
    """
    지정 서버에서 SM 쉘 스크립트 실행.
    서버에 스크립트를 전송 후 실행 — SCP + bash.
    """
    import asyncssh
    from sqlalchemy import select
    from database import SessionLocal
    from models import Server

    # 스크립트 경로 안전성 검사
    safe_path = os.path.realpath(script_path)
    scripts_root = os.path.realpath(
        os.path.join(os.path.dirname(__file__), "..", "scripts", "sm")
    )
    if not safe_path.startswith(scripts_root):
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0,
            error="허용되지 않은 스크립트 경로입니다."
        )
    if not os.path.isfile(safe_path):
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0,
            error=f"스크립트 파일이 존재하지 않습니다: {os.path.basename(script_path)}"
        )

    # 서버 정보 조회
    own_db = db is None
    _db = SessionLocal() if own_db else db
    server_obj = None
    try:
        r = await _db.execute(
            select(Server).where(Server.server_name == server_name)
        )
        server_obj = r.scalars().first()
    finally:
        if own_db:
            await _db.close()

    if not server_obj:
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0,
            error=f"서버를 찾을 수 없습니다: {server_name}"
        )

    ssh_user = getattr(server_obj, "ssh_user", "") or ""
    if ssh_user.strip() == "root":
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=0.0,
            error="root 직접 SSH 접속은 보안 정책에 의해 금지됩니다."
        )

    enc_pw  = getattr(server_obj, "os_pw_enc", "") or ""
    ip_addr = getattr(server_obj, "ip_addr",   "") or ""
    ssh_port = getattr(server_obj, "ssh_port", 22) or 22
    try:
        password = _decrypt_password(enc_pw) if enc_pw else ""
    except RuntimeError as e:
        return SSHResult(success=False, stdout="", stderr="",
                         exit_code=-1, elapsed=0.0, error=str(e))

    connect_timeout = int(os.environ.get("SSH_CONNECT_TIMEOUT", "10"))
    known_hosts = os.environ.get("SSH_KNOWN_HOSTS", None)

    conn_kwargs: dict = {
        "host": ip_addr, "port": ssh_port, "username": ssh_user,
        "connect_timeout": connect_timeout,
        "known_hosts": known_hosts,
    }
    if password:
        conn_kwargs["password"] = password

    remote_tmp = f"/tmp/guardia_sm_{os.path.basename(script_path)}"
    env_str = " ".join(f"{k}={v}" for k, v in (env_vars or {}).items())

    await _write_audit_safe(
        db, sr_id, actor, "SSH_SCRIPT_START",
        f"서버: {server_name} | 스크립트: {os.path.basename(script_path)}"
    )

    start = asyncio.get_event_loop().time()
    try:
        async with asyncssh.connect(**conn_kwargs) as conn:
            # SCP 전송
            async with conn.start_sftp_client() as sftp:
                await sftp.put(safe_path, remote_tmp)
            # 실행 권한 + 실행
            run_cmd = f"chmod +x {remote_tmp} && {env_str} bash {remote_tmp}; rm -f {remote_tmp}"
            result = await asyncio.wait_for(
                conn.run(run_cmd, check=False),
                timeout=timeout
            )

        elapsed = asyncio.get_event_loop().time() - start
        stdout = (result.stdout or "").strip()
        stderr = (result.stderr or "").strip()
        exit_code = result.exit_status or 0

        await _write_audit_safe(
            db, sr_id, actor, "SSH_SCRIPT_DONE",
            f"서버: {server_name} | script: {os.path.basename(script_path)} | exit={exit_code}"
        )
        return SSHResult(
            success=(exit_code == 0),
            stdout=stdout, stderr=stderr,
            exit_code=exit_code, elapsed=elapsed
        )

    except asyncio.TimeoutError:
        elapsed = asyncio.get_event_loop().time() - start
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=elapsed,
            error=f"스크립트 실행 타임아웃 ({timeout}초)"
        )
    except Exception as e:
        elapsed = asyncio.get_event_loop().time() - start
        safe_err = _sanitize_error(str(e))
        return SSHResult(
            success=False, stdout="", stderr="",
            exit_code=-1, elapsed=elapsed,
            error=f"SSH 스크립트 실행 오류: {safe_err}"
        )


def _sanitize_error(msg: str) -> str:
    """에러 메시지에서 IP/계정/비밀번호 패턴 제거."""
    msg = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "<IP>", msg)
    msg = re.sub(r"password[=:\s]+\S+", "password=<hidden>", msg, flags=re.IGNORECASE)
    msg = re.sub(r"user(?:name)?[=:\s]+\S+", "user=<hidden>", msg, flags=re.IGNORECASE)
    return msg[:200]


async def _write_audit_safe(db, sr_id, actor, action, detail):
    """감사 로그 안전 기록 — 독립 세션 사용."""
    try:
        from database import SessionLocal
        from models import AuditLog, compute_log_hash
        from sqlalchemy import select

        async with SessionLocal() as _db:
            r = await _db.execute(
                select(AuditLog)
                .where(AuditLog.sr_id == sr_id)
                .order_by(AuditLog.id.desc())
                .limit(1)
            )
            last = r.scalars().first()
            prev_hash = last.log_hash if last else None
            ts = datetime.now().isoformat()
            log_hash = compute_log_hash(prev_hash, actor, action, detail, ts)
            _db.add(AuditLog(
                sr_id=sr_id, actor=actor, action=action,
                detail=detail, prev_hash=prev_hash, log_hash=log_hash
            ))
            await _db.commit()
    except Exception as e:
        logger.warning("[SSH] 감사 로그 기록 실패: %s", str(e)[:80])
