"""Aturan deteksi murni (tanpa I/O). Hanya mencocokkan pola teks, tidak pernah mengeksekusi payload.

Semua hasil bersifat 'suspected' (dugaan). Pola input saja tidak membuktikan serangan berhasil.
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from urllib.parse import unquote_plus

RISK_ORDER = {"normal": 0, "warning": 1, "high": 2, "critical": 3}

DEFAULT_SETTINGS = {
    "brute_force_threshold": 5,
    "brute_force_window_min": 10,
    "port_scan_threshold": 15,
    "port_scan_window_sec": 60,
    "traffic_spike_factor": 3.0,
    "traffic_spike_min": 100,
    "sensor_offline_sec": 180,
    "work_hours_start": 6,
    "work_hours_end": 22,
}


@dataclass
class Finding:
    category: str
    risk: str
    summary: str
    indicators: list[str] = field(default_factory=list)
    recommendation: str = ""
    ip: str | None = None
    endpoint: str | None = None
    verification: str = "suspected"


def normalize(text: str) -> str:
    """Decode URL berlapis (maks 3x) agar payload ter-encode tetap terbaca."""
    out = text or ""
    for _ in range(3):
        decoded = unquote_plus(out)
        if decoded == out:
            break
        out = decoded
    return out.lower()


_SQLI = [
    ("union select", re.compile(r"\bunion\b\s+(all\s+)?\bselect\b")),
    ("tautologi OR/AND 1=1", re.compile(r"\b(or|and)\b\s+['\"]?\d+['\"]?\s*=\s*['\"]?\d+")),
    ("tautologi string", re.compile(r"\b(or|and)\b\s+['\"][^'\"]*['\"]\s*=\s*['\"]")),
    ("komentar SQL setelah kutip", re.compile(r"['\"]\s*(--|#|/\*)")),
    ("fungsi time-based", re.compile(r"\b(sleep|benchmark|pg_sleep|waitfor\s+delay)\b\s*\(?")),
    ("akses information_schema", re.compile(r"information_schema|sysobjects|pg_catalog")),
    ("stacked query berbahaya", re.compile(r";\s*(drop|alter|truncate|insert|update|delete)\b")),
    ("error SQL pada log", re.compile(r"you have an error in your sql syntax|sqlstate\[|unclosed quotation mark|ora-\d{5}")),
]
_XSS = [
    ("tag <script>", re.compile(r"<\s*script")),
    ("event handler inline", re.compile(r"\bon(error|load|click|mouseover|focus)\s*=")),
    ("skema javascript:", re.compile(r"javascript\s*:")),
    ("tag iframe/object/embed", re.compile(r"<\s*(iframe|object|embed)")),
    ("akses document.cookie", re.compile(r"document\s*\.\s*(cookie|location)")),
    ("svg onload", re.compile(r"<\s*svg[^>]*on\w+\s*=")),
]
_TRAVERSAL = [
    ("../ atau ..\\", re.compile(r"\.\.[/\\]")),
    ("berkas sistem sensitif", re.compile(r"(/etc/(passwd|shadow)|boot\.ini|win\.ini|/proc/self)")),
    ("null byte", re.compile(r"\x00")),
]


def _match(patterns, text: str) -> list[str]:
    return [name for name, rx in patterns if rx.search(text)]


def detect_sqli(text: str) -> list[str]:
    return _match(_SQLI, normalize(text))


def detect_xss(text: str) -> list[str]:
    return _match(_XSS, normalize(text))


def detect_path_traversal(text: str) -> list[str]:
    return _match(_TRAVERSAL, normalize(text))


def analyze_http_request(path: str, query: str = "", body: str = "", ip: str | None = None) -> list[Finding]:
    """Periksa satu request HTTP. Mengembalikan 0..n temuan (dugaan)."""
    target = f"{path or ''}?{query or ''} {body or ''}"
    endpoint = (path or "")[:255]
    findings: list[Finding] = []
    sq, xs, tr = detect_sqli(target), detect_xss(target), detect_path_traversal(target)
    if sq:
        findings.append(Finding(
            "sql_injection", "high" if len(sq) > 1 else "warning",
            f"Pola SQL Injection mencurigakan pada {endpoint}", sq,
            "Pastikan query memakai prepared statement, periksa log aplikasi/DB, pertimbangkan blokir IP sementara.",
            ip, endpoint))
    if xs:
        findings.append(Finding(
            "xss", "high" if len(xs) > 1 else "warning",
            f"Pola XSS mencurigakan pada {endpoint}", xs,
            "Pastikan output di-escape, terapkan Content-Security-Policy, tinjau input yang tersimpan.",
            ip, endpoint))
    if tr:
        findings.append(Finding(
            "path_traversal", "high",
            f"Percobaan akses jalur di luar direktori pada {endpoint}", tr,
            "Validasi path terhadap whitelist direktori, tinjau hak akses berkas server.",
            ip, endpoint))
    return findings


def check_brute_force(failed_count: int, threshold: int, ip: str | None, window_min: int,
                      username: str | None = None) -> Finding | None:
    if failed_count < threshold:
        return None
    risk = "critical" if failed_count >= threshold * 3 else "high"
    who = f" untuk akun '{username}'" if username else ""
    return Finding(
        "brute_force", risk,
        f"{failed_count} login gagal{who} dalam {window_min} menit dari {ip or 'sumber tidak diketahui'}",
        [f"failed_logins={failed_count}", f"threshold={threshold}"],
        "Blokir/ratelimit IP, paksa reset password jika ada login sukses setelahnya, aktifkan MFA.",
        ip, "/login")


def check_port_scan(records: list[tuple[int, datetime]], ip: str, now: datetime,
                    threshold: int, window_sec: int) -> Finding | None:
    """records: (dst_port, waktu) untuk satu IP sumber."""
    since = now - timedelta(seconds=window_sec)
    ports = {p for p, t in records if t >= since}
    if len(ports) < threshold:
        return None
    return Finding(
        "port_scan", "high",
        f"{ip} menyentuh {len(ports)} port berbeda dalam {window_sec} detik (dugaan port scan)",
        [f"distinct_ports={len(ports)}", f"threshold={threshold}"],
        "Tinjau aturan firewall, blokir IP jika bukan pemindaian internal yang sah.", ip)


def check_traffic_spike(current: int, history: list[int], factor: float, minimum: int) -> Finding | None:
    """current: request pada menit berjalan; history: jumlah per menit sebelumnya."""
    if current < minimum:
        return None
    baseline = (sum(history) / len(history)) if history else 0.0
    if baseline and current < baseline * factor:
        return None
    ratio = f"{current / baseline:.1f}x" if baseline else "tanpa baseline"
    return Finding(
        "traffic_spike", "warning" if baseline else "warning",
        f"Lonjakan trafik: {current} request/menit ({ratio} dari rata-rata {baseline:.0f})",
        [f"current={current}", f"baseline={baseline:.1f}", f"factor={factor}"],
        "Periksa sumber IP teratas, aktifkan rate limiting/WAF bila lonjakan tidak wajar.")


def check_file_integrity(path: str, baseline_hash: str | None, current_hash: str | None) -> tuple[str, Finding | None]:
    """Mengembalikan (status, temuan). status: ok|modified|missing|new."""
    if current_hash is None:
        return "missing", Finding("file_tampering", "high", f"Berkas penting hilang: {path}",
                                  ["status=missing"], "Pulihkan dari backup dan selidiki penyebab penghapusan.")
    if baseline_hash is None:
        return "new", None
    if baseline_hash != current_hash:
        return "modified", Finding(
            "file_tampering", "critical", f"Berkas penting berubah: {path}",
            [f"baseline={baseline_hash[:12]}…", f"current={current_hash[:12]}…"],
            "Bandingkan dengan backup/versi resmi. Jika perubahan sah, perbarui baseline; jika tidak, isolasi sistem.")
    return "ok", None


def check_admin_activity(action: str, when: datetime, start_hour: int, end_hour: int,
                         ip: str | None, known_ips: set[str] | None = None) -> Finding | None:
    reasons = []
    if not (start_hour <= when.hour < end_hour):
        reasons.append(f"di luar jam kerja ({when.hour:02d}:00 UTC)")
    if known_ips is not None and ip and ip not in known_ips:
        reasons.append("dari IP yang belum pernah dipakai admin")
    if not reasons:
        return None
    return Finding("unusual_admin", "warning", f"Aktivitas administratif tidak biasa '{action}': " + "; ".join(reasons),
                   reasons, "Konfirmasi dengan administrator yang bersangkutan.", ip)


def check_sensor_silent(last_seen: datetime | None, now: datetime, offline_sec: int) -> bool:
    return last_seen is None or (now - last_seen).total_seconds() > offline_sec


def check_system_metrics(cpu: float | None, mem: float | None, disk: float | None) -> Finding | None:
    ind = []
    if cpu is not None and cpu >= 90:
        ind.append(f"cpu={cpu:.0f}%")
    if mem is not None and mem >= 90:
        ind.append(f"mem={mem:.0f}%")
    if disk is not None and disk >= 90:
        ind.append(f"disk={disk:.0f}%")
    if not ind:
        return None
    return Finding("system_health", "warning", "Penggunaan sumber daya sangat tinggi: " + ", ".join(ind), ind,
                   "Periksa proses/layanan yang memakai sumber daya dan kapasitas disk.")


_AUTH_FAIL = re.compile(r"failed password|authentication failure|invalid user|4625|logon failure", re.I)


def classify_syslog(message: str) -> str:
    return "warning" if _AUTH_FAIL.search(message or "") else "normal"
