"""외부 AI 전송 전 PII 마스킹 파이프라인 — sanitize 게이트.

redact.py(런타임 로그 마스킹)와 보완적 관계:
- redact.py: 런타임 로그 출력에서 자격증명 자동 마스킹
- sanitize_gate.py: 외부 AI(Codex/Gemini) 전달 전 한국 PII 마스킹

Usage:
    from utils.sanitize_gate import sanitize_text, should_sanitize
    if should_sanitize(level):
        masked_text, detections = sanitize_text(content)
        report = generate_sanitize_report(detections)
"""

import re
from pathlib import Path
from typing import Any

# ---------------------------------------------------------------------------
# PII 마스킹 패턴 정의
# ---------------------------------------------------------------------------
SANITIZE_PATTERNS: dict[str, dict[str, Any]] = {
    "rrn": {
        "description": "주민등록번호",
        "pattern": re.compile(r"\d{6}[-]?\d{7}"),
        "replacement": "[RRN-REDACTED]",
    },
    "phone": {
        "description": "전화번호",
        "pattern": re.compile(r"01[016789]-?\d{3,4}-?\d{4}"),
        "replacement": "[PHONE-REDACTED]",
    },
    "email": {
        "description": "이메일",
        "pattern": re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"),
        "replacement": "[EMAIL-REDACTED]",
    },
    "apikey": {
        "description": "API 키",
        "pattern": re.compile(r"(?:sk-|ghp_|AKIA|AIza)[A-Za-z0-9_\-]{10,}"),
        "replacement": "[APIKEY-REDACTED]",
    },
    "account": {
        "description": "계좌번호",
        # 주민번호 패턴(6-7)과 구별: 앞자리 3~4자리, 중간 2~6자리, 뒷자리 4~6자리
        # 단, 주민번호(6-7) 형식과 겹치지 않도록 중간 자리를 2~6으로 제한
        "pattern": re.compile(r"\d{3,4}-\d{2,6}-\d{4,6}"),
        "replacement": "[ACCOUNT-REDACTED]",
    },
    "policy": {
        "description": "보험 증권번호",
        "pattern": re.compile(r"[A-Z]{1,3}\d{8,12}"),
        "replacement": "[POLICY-REDACTED]",
    },
}

# 주민번호 패턴이 계좌번호보다 먼저 처리되어야 하므로 우선순위 순서 정의
_PATTERN_ORDER = ["rrn", "apikey", "phone", "email", "account", "policy"]


def sanitize_text(text: str) -> tuple[str, list[dict]]:
    """텍스트에서 PII를 마스킹하고 (마스킹된 텍스트, 감지된 항목 리스트)를 반환합니다.

    Args:
        text: PII 마스킹 대상 텍스트

    Returns:
        (마스킹된 텍스트, 감지된 항목 리스트)
        각 감지 항목은 {"type": str, "description": str, "original": str, "replacement": str} 형식
    """
    detections: list[dict] = []
    result = text

    for key in _PATTERN_ORDER:
        config = SANITIZE_PATTERNS[key]
        pattern: re.Pattern = config["pattern"]
        replacement: str = config["replacement"]
        description: str = config["description"]

        def _replace_and_record(
            m: re.Match, _key: str = key, _desc: str = description, _repl: str = replacement
        ) -> str:
            detections.append(
                {
                    "type": _key,
                    "description": _desc,
                    "original": m.group(0),
                    "replacement": _repl,
                }
            )
            return _repl

        result = pattern.sub(_replace_and_record, result)

    return result, detections


def sanitize_file_content(filepath: str) -> tuple[str, list[dict]]:
    """파일 내용을 읽어 sanitize_text를 적용합니다.

    Args:
        filepath: 읽을 파일 경로

    Returns:
        (마스킹된 텍스트, 감지된 항목 리스트)

    Raises:
        FileNotFoundError: 파일이 존재하지 않을 때
        OSError: 파일 읽기 실패 시
    """
    content = Path(filepath).read_text(encoding="utf-8")
    return sanitize_text(content)


def should_sanitize(level: int) -> bool:
    """Lv.3 이상인 경우만 True를 반환합니다.

    Args:
        level: 작업 레벨 (int). 3 이상이면 sanitize 게이트 활성화.

    Returns:
        level >= 3이면 True, 그 외 False
    """
    return level >= 3


def generate_sanitize_report(detections: list[dict]) -> str:
    """감지 결과를 markdown 형식의 리포트로 변환합니다.

    Args:
        detections: sanitize_text()가 반환한 감지 항목 리스트

    Returns:
        markdown 형식의 리포트 문자열
    """
    if not detections:
        return "## Sanitize 게이트 리포트\n\n감지된 PII 없음. 외부 AI 전달 안전."

    lines = [
        "## Sanitize 게이트 리포트",
        "",
        f"총 {len(detections)}건의 PII 감지 및 마스킹 완료.",
        "",
        "| # | 유형 | 설명 | 원본 (일부) | 치환값 |",
        "|---|------|------|------------|--------|",
    ]

    for i, item in enumerate(detections, 1):
        original = item.get("original", "")
        # 원본은 앞 3자리만 표시하고 나머지 마스킹 (개인정보 보호)
        if len(original) > 3:
            preview = original[:3] + "*" * (len(original) - 3)
        else:
            preview = "*" * len(original)
        lines.append(
            f"| {i} | `{item.get('type', '')}` | {item.get('description', '')} "
            f"| `{preview}` | `{item.get('replacement', '')}` |"
        )

    lines.extend(
        [
            "",
            "> PII가 마스킹된 텍스트만 외부 AI에 전달하세요.",
        ]
    )

    return "\n".join(lines)
