"""
GUARDiA 준수성 자동 점검 엔진

1. 시큐어코딩 (행안부 SW 보안약점 기준 + OWASP Top 10)
2. 웹 접근성 (WCAG 2.1 / KWCAG 2.1 — 한국형)
3. 개인정보보호법 (PIPA — 개인정보 식별자 탐지)
"""
from __future__ import annotations

import ast
import re
import os
import logging
from datetime import datetime
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

# ── 1. 시큐어코딩 점검 ───────────────────────────────────────────────────────

# 행안부 SW 보안약점 주요 패턴
SECURE_CODING_RULES = [
    # SQL 인젝션
    {
        "id":       "SC-001",
        "category": "SQL 인젝션",
        "severity": "CRITICAL",
        "pattern":  r'execute\s*\(\s*f["\'].*\{|execute\s*\(\s*".*%.*%|execute\s*\(\s*\'.*\'\.format\(',
        "message":  "SQL 문자열 직접 포맷팅 — 파라미터 바인딩 사용 필요",
    },
    # XSS
    {
        "id":       "SC-002",
        "category": "XSS",
        "severity": "HIGH",
        "pattern":  r'innerHTML\s*=\s*(?!"|\')|\.html\s*\(',
        "message":  "innerHTML 직접 삽입 — textContent 또는 escapeHtml 함수 사용 필요",
    },
    # 하드코딩 비밀번호/키
    {
        "id":       "SC-003",
        "category": "하드코딩 자격증명",
        "severity": "CRITICAL",
        "pattern":  r'(?i)(password|passwd|secret|api_key|apikey|token)\s*=\s*["\'][^"\']{6,}["\']',
        "message":  "하드코딩된 자격증명 — 환경변수 또는 시크릿 관리자 사용 필요",
    },
    # OS 명령어 인젝션
    {
        "id":       "SC-004",
        "category": "OS 명령어 인젝션",
        "severity": "CRITICAL",
        "pattern":  r'os\.system\s*\(|subprocess\.call\s*\(.*shell\s*=\s*True|eval\s*\(',
        "message":  "shell=True 또는 eval 사용 — 입력값 검증 필수",
    },
    # 경로 조작
    {
        "id":       "SC-005",
        "category": "경로 조작",
        "severity": "HIGH",
        "pattern":  r'open\s*\(\s*.*\+|open\s*\(\s*f["\']',
        "message":  "사용자 입력 기반 파일 경로 — Path.resolve().relative_to() 검증 필수",
    },
    # 정보 노출
    {
        "id":       "SC-006",
        "category": "민감 정보 노출",
        "severity": "MEDIUM",
        "pattern":  r'traceback\.print_exc\(\)|print\s*\(.*exception|logger\.(info|debug).*password',
        "message":  "예외 스택트레이스 직접 출력 — 상세 오류 메시지 은닉 필요",
    },
    # CSRF 위험 (GET으로 상태 변경)
    {
        "id":       "SC-007",
        "category": "CSRF",
        "severity": "MEDIUM",
        "pattern":  r'@router\.get\s*\([^\)]+\)\s*\nasync def.*(delete|remove|drop|truncate)',
        "message":  "GET 메서드로 데이터 변경 — POST/DELETE 사용 및 CSRF 토큰 적용 필요",
    },
    # 취약한 암호화
    {
        "id":       "SC-008",
        "category": "취약 암호화",
        "severity": "HIGH",
        "pattern":  r'md5|sha1\s*\(|DES\.|RC4\.',
        "message":  "취약한 해시/암호화 알고리즘 — SHA-256 이상 또는 AES-256-GCM 사용 필요",
    },
]

# ── 2. 웹 접근성 점검 (HTML 기반) ────────────────────────────────────────────

ACCESSIBILITY_RULES = [
    {
        "id":       "WA-001",
        "category": "대체 텍스트",
        "level":    "A",
        "pattern":  r'<img(?![^>]*alt=)[^>]*>',
        "message":  "img 요소에 alt 속성 없음 — 시각 장애인 스크린리더 접근 불가",
    },
    {
        "id":       "WA-002",
        "category": "색상 대비",
        "level":    "AA",
        "pattern":  r'color:\s*#(?:aaa|999|bbb|ccc|ddd|eee|f{3,6})',
        "message":  "낮은 색상 대비 — 4.5:1 이상 비율 필요 (WCAG 2.1 AA)",
    },
    {
        "id":       "WA-003",
        "category": "키보드 접근성",
        "level":    "A",
        "pattern":  r'onclick="[^"]*"(?![^>]*tabindex)',
        "message":  "onclick만 있는 요소 — tabindex + onkeydown 추가 또는 <button> 사용",
    },
    {
        "id":       "WA-004",
        "category": "폼 레이블",
        "level":    "A",
        "pattern":  r'<input(?![^>]*aria-label|[^>]*id=)[^>]*>',
        "message":  "input 요소에 label/aria-label 없음 — 폼 접근성 미준수",
    },
    {
        "id":       "WA-005",
        "category": "언어 설정",
        "level":    "A",
        "pattern":  r'<html(?![^>]*lang=)',
        "message":  "html 요소에 lang 속성 없음 — <html lang=\"ko\"> 필요",
    },
    {
        "id":       "WA-006",
        "category": "포커스 표시",
        "level":    "AA",
        "pattern":  r'outline:\s*none|outline:\s*0',
        "message":  "포커스 표시 제거 — 키보드 사용자 탐색 불가, :focus-visible 활용 권장",
    },
    {
        "id":       "WA-007",
        "category": "ARIA 역할",
        "level":    "A",
        "pattern":  r'<div\s+onclick|<span\s+onclick',
        "message":  "div/span에 onclick — <button role=\"button\"> 또는 role+tabindex 추가 필요",
    },
]

# ── 3. 개인정보보호법 위반 탐지 ──────────────────────────────────────────────

PIPA_RULES = [
    {
        "id":       "PI-001",
        "category": "주민등록번호",
        "severity": "CRITICAL",
        "pattern":  r'\d{6}[-–]\d{7}|\d{13}',
        "message":  "주민등록번호 패턴 감지 — 법적 수집 제한 항목 (개인정보보호법 제24조)",
    },
    {
        "id":       "PI-002",
        "category": "신용카드번호",
        "severity": "CRITICAL",
        "pattern":  r'\b(?:\d{4}[-\s]?){3}\d{4}\b',
        "message":  "신용카드번호 패턴 감지 — 마스킹 처리 필요",
    },
    {
        "id":       "PI-003",
        "category": "이메일 수집",
        "severity": "MEDIUM",
        "pattern":  r'email.*=.*Column.*String|Column\(.*email',
        "message":  "이메일 DB 저장 — 수집 동의 여부 및 암호화 저장 확인 필요",
    },
    {
        "id":       "PI-004",
        "category": "비밀번호 평문",
        "severity": "CRITICAL",
        "pattern":  r'password.*=.*Column(?!.*hash)|password_hash\s*=\s*None',
        "message":  "비밀번호 평문 저장 의심 — bcrypt/argon2 해시 저장 필수",
    },
    {
        "id":       "PI-005",
        "category": "전화번호",
        "severity": "LOW",
        "pattern":  r'01[016789][-–]?\d{3,4}[-–]?\d{4}',
        "message":  "전화번호 패턴 감지 — 불필요한 수집 여부 검토 필요",
    },
    {
        "id":       "PI-006",
        "category": "로그에 개인정보",
        "severity": "HIGH",
        "pattern":  r'logger\.(info|debug|warning).*(?:email|phone|name|password)',
        "message":  "로그에 개인정보 기록 의심 — 로그 마스킹 처리 필요",
    },
]


# ── 점검 실행 ────────────────────────────────────────────────────────────────

def scan_file(filepath: str, content: str) -> list[dict]:
    """단일 파일에 대한 전체 점검 수행."""
    ext = Path(filepath).suffix.lower()
    findings = []

    rule_sets = []
    if ext in (".py",):
        rule_sets = [SECURE_CODING_RULES, PIPA_RULES]
    if ext in (".html", ".css", ".js"):
        rule_sets.append(ACCESSIBILITY_RULES)
    if ext in (".html", ".js"):
        rule_sets.append(SECURE_CODING_RULES)  # XSS 등
    if ext in (".py", ".js", ".html"):
        rule_sets.append(PIPA_RULES)

    # 중복 제거
    seen_ids: set = set()
    for rules in rule_sets:
        for rule in rules:
            if rule["id"] in seen_ids:
                continue
            seen_ids.add(rule["id"])
            try:
                matches = list(re.finditer(rule["pattern"], content, re.IGNORECASE | re.MULTILINE))
                for m in matches:
                    line_no = content[:m.start()].count("\n") + 1
                    findings.append({
                        "file":      filepath,
                        "line":      line_no,
                        "rule_id":   rule["id"],
                        "category":  rule["category"],
                        "severity":  rule.get("severity", rule.get("level", "INFO")),
                        "message":   rule["message"],
                        "snippet":   content[max(0, m.start()-20):m.end()+20].strip()[:100],
                    })
            except re.error:
                pass

    return findings


async def scan_project(base_dir: str = None) -> dict[str, Any]:
    """GUARDiA 프로젝트 전체 준수성 점검."""
    if not base_dir:
        base_dir = str(Path(__file__).parent.parent)

    target_dirs = [
        Path(base_dir) / "routers",
        Path(base_dir) / "core",
        Path(base_dir) / "static",
    ]

    all_findings = []
    scanned = 0

    for tdir in target_dirs:
        if not tdir.exists():
            continue
        for fpath in tdir.rglob("*"):
            if fpath.suffix not in (".py", ".html", ".js", ".css"):
                continue
            if fpath.name.startswith("."):
                continue
            try:
                content = fpath.read_text(encoding="utf-8", errors="ignore")
                findings = scan_file(str(fpath.relative_to(base_dir)), content)
                all_findings.extend(findings)
                scanned += 1
            except Exception:
                pass

    # 집계
    by_severity: dict = {}
    by_category: dict = {}
    for f in all_findings:
        sev = f["severity"]
        cat = f["category"]
        by_severity[sev] = by_severity.get(sev, 0) + 1
        by_category[cat] = by_category.get(cat, 0) + 1

    critical = by_severity.get("CRITICAL", 0)
    high     = by_severity.get("HIGH", 0)
    risk_level = "CRITICAL" if critical > 0 else "HIGH" if high > 0 else "MEDIUM" if all_findings else "LOW"

    return {
        "scan_time":   datetime.utcnow().isoformat(),
        "scanned_files": scanned,
        "total_findings": len(all_findings),
        "risk_level":  risk_level,
        "by_severity": by_severity,
        "by_category": by_category,
        "findings":    all_findings[:200],  # 최대 200건
        "summary": {
            "secure_coding":   sum(1 for f in all_findings if f["rule_id"].startswith("SC")),
            "accessibility":   sum(1 for f in all_findings if f["rule_id"].startswith("WA")),
            "privacy":         sum(1 for f in all_findings if f["rule_id"].startswith("PI")),
        }
    }
