import os
import re
import math
import pwd
import grp
from datetime import datetime
from collections import Counter, defaultdict
from html import escape

# ---------------------------------------------
#  LOG FILE PATHS
# ---------------------------------------------
log_paths = {
    "LIVE":      "/var/log/snort/alerts/alert_fast.txt",
    "TUESDAY":   "/var/log/snort/pcap_results/tuesday/alert_fast.txt",
    "WEDNESDAY": "/var/log/snort/pcap_results/wednesday/alert_fast.txt",
    "FRIDAY":    "/var/log/snort/pcap_results/friday/alert_fast.txt"
}

SESSION_ORDER = ["LIVE", "TUESDAY", "WEDNESDAY", "FRIDAY"]

# ---------------------------------------------
#  OUTPUT PATHS
# ---------------------------------------------
report_dir = "/home/osboxes/nids-reports"
detail_dir = os.path.join(report_dir, "details")
os.makedirs(report_dir, exist_ok=True)
os.makedirs(detail_dir, exist_ok=True)

report_file = os.path.join(report_dir, "report_all.html")
PAGE_SIZE = 1000

# ---------------------------------------------
#  SEVERITY MAP
# ---------------------------------------------
severity_map = {
    1000001: "LOW",
    1000002: "MEDIUM",
    1000003: "MEDIUM",
    1000004: "MEDIUM",
    1000005: "MEDIUM",
    1000006: "MEDIUM",
    1000007: "MEDIUM",
    1000008: "HIGH",
    1000009: "HIGH",
    1000010: "HIGH",
    1000011: "HIGH",
    1000012: "HIGH",
    1000013: "MEDIUM",
    1000014: "HIGH",
    1000015: "MEDIUM",
    1000016: "MEDIUM",
    1000017: "LOW",
    1000018: "LOW",
    1000019: "BLOCKED",
    1000020: "BLOCKED",
    1000021: "BLOCKED",
    1000022: "BLOCKED",
    1000023: "BLOCKED",
    1000024: "BLOCKED",
    1000025: "BLOCKED",
}

# ---------------------------------------------
#  RULE COVERAGE TABLE
# ---------------------------------------------
rule_coverage = {
    # NIDS ALERT RULES
    1000001: {"name": "ICMP Ping Detected",               "attack": "Ping / Reconnaissance",       "protocol": "ICMP",  "mode": "NIDS"},
    1000002: {"name": "Possible SYN Scan",                "attack": "Port Scanning",                "protocol": "TCP",   "mode": "NIDS"},
    1000003: {"name": "Suspicious HTTP Admin Probe",      "attack": "Web Probe",                    "protocol": "HTTP",  "mode": "NIDS"},
    1000004: {"name": "Possible TCP Portscan (burst)",    "attack": "Port Scanning",                "protocol": "TCP",   "mode": "NIDS"},
    1000005: {"name": "Possible Xmas Scan",               "attack": "Port Scanning",                "protocol": "TCP",   "mode": "NIDS"},
    1000006: {"name": "Possible NULL Scan",               "attack": "Port Scanning",                "protocol": "TCP",   "mode": "NIDS"},
    1000007: {"name": "Possible UDP Scan (burst)",        "attack": "Port Scanning",                "protocol": "UDP",   "mode": "NIDS"},
    1000008: {"name": "Possible SYN Flood (rate)",        "attack": "Denial of Service",            "protocol": "TCP",   "mode": "NIDS"},
    1000009: {"name": "Possible ICMP Flood (rate)",       "attack": "Denial of Service",            "protocol": "ICMP",  "mode": "NIDS"},
    1000010: {"name": "Possible DNS Query Flood (rate)",  "attack": "Denial of Service",            "protocol": "UDP",   "mode": "NIDS"},
    1000011: {"name": "Possible SSH Brute Force",         "attack": "Credential / Brute Force",     "protocol": "TCP",   "mode": "NIDS"},
    1000012: {"name": "Possible RDP Brute Force",         "attack": "Credential / Brute Force",     "protocol": "TCP",   "mode": "NIDS"},
    1000013: {"name": "HTTP Directory Traversal Probe",   "attack": "Web Application Attack",       "protocol": "HTTP",  "mode": "NIDS"},
    1000014: {"name": "Possible SQL Injection Attempt",   "attack": "Web Application Attack",       "protocol": "HTTP",  "mode": "NIDS"},
    1000015: {"name": "HTTP Possible XSS Probe",          "attack": "Web Application Attack",       "protocol": "HTTP",  "mode": "NIDS"},
    1000016: {"name": "HTTP Upload Endpoint Probe",       "attack": "Web Application Attack",       "protocol": "HTTP",  "mode": "NIDS"},
    1000017: {"name": "Telnet Connection Attempt",        "attack": "Unauthorised Access",          "protocol": "TCP",   "mode": "NIDS"},
    1000018: {"name": "SMB Connection Attempt",           "attack": "Unauthorised Access",          "protocol": "TCP",   "mode": "NIDS"},
    # IPS DROP RULES
    1000019: {"name": "ICMP Flood BLOCKED",               "attack": "Denial of Service (Blocked)",  "protocol": "ICMP",  "mode": "IPS"},
    1000020: {"name": "SSH Brute Force BLOCKED",          "attack": "Credential Attack (Blocked)",  "protocol": "TCP",   "mode": "IPS"},
    1000021: {"name": "SYN Flood BLOCKED",                "attack": "Denial of Service (Blocked)",  "protocol": "TCP",   "mode": "IPS"},
    1000022: {"name": "DNS Flood BLOCKED",                "attack": "Denial of Service (Blocked)",  "protocol": "UDP",   "mode": "IPS"},
    1000023: {"name": "RDP Brute Force BLOCKED",          "attack": "Credential Attack (Blocked)",  "protocol": "TCP",   "mode": "IPS"},
    1000024: {"name": "SQL Injection BLOCKED",            "attack": "Web Application (Blocked)",    "protocol": "HTTP",  "mode": "IPS"},
    1000025: {"name": "XSS Probe BLOCKED",                "attack": "Web Application (Blocked)",    "protocol": "HTTP",  "mode": "IPS"},
}

# ---------------------------------------------
#  ALERT LINE REGEX
# ---------------------------------------------
alert_re = re.compile(
    r'^(?P<timestamp>\d{2}/\d{2}-\d{2}:\d{2}:\d{2}\.\d+)'
    r'\s+(?P<action>\[drop\]\s+)?'
    r'\[\*\*\]\s+\[(?P<gid>\d+):(?P<sid>\d+):(?P<rev>\d+)\]'
    r'\s+"(?P<msg>.+?)"\s+\[\*\*\].*?'
    r'\{(?P<proto>[A-Z0-9]+)\}\s+'
    r'(?P<src_raw>\S+)\s+->\s+(?P<dst_raw>\S+)'
)

# ---------------------------------------------
#  PERMISSION HELPERS
# ---------------------------------------------
def fix_permissions(path, user="osboxes", group="osboxes"):
    try:
        uid = pwd.getpwnam(user).pw_uid
        gid = grp.getgrnam(group).gr_gid
        os.chown(path, uid, gid)
    except Exception:
        pass
    try:
        os.chmod(path, 0o755 if os.path.isdir(path) else 0o644)
    except Exception:
        pass

def fix_tree_permissions(root_path, user="osboxes", group="osboxes"):
    for current_root, dirs, files in os.walk(root_path):
        fix_permissions(current_root, user, group)
        for d in dirs:
            fix_permissions(os.path.join(current_root, d), user, group)
        for f in files:
            fix_permissions(os.path.join(current_root, f), user, group)

# ---------------------------------------------
#  PARSING
# ---------------------------------------------
def parse_endpoint(endpoint):
    endpoint = endpoint.strip()
    if endpoint == "-" or not endpoint:
        return "-", "-"
    if endpoint.startswith("[") and "]:" in endpoint:
        host, port = endpoint.rsplit("]:", 1)
        return host + "]", port if port.isdigit() else "-"
    if "." in endpoint and endpoint.count(":") == 1:
        host, port = endpoint.rsplit(":", 1)
        if port.isdigit():
            return host, port
    if "." in endpoint:
        return endpoint, "-"
    return endpoint, "-"

def parse_alert_file(path, session):
    alerts = []
    if not os.path.exists(path):
        print(f"[WARN] {session}: file not found -> {path}", flush=True)
        return alerts
    print(f"[INFO] Parsing {session}: {path}", flush=True)
    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        for line_num, line in enumerate(f, 1):
            line = line.strip()
            if not line:
                continue
            m = alert_re.search(line)
            if not m:
                continue
            sid      = int(m.group("sid"))
            action   = m.group("action")
            is_drop  = action is not None and "drop" in action.lower()
            src_ip, src_port = parse_endpoint(m.group("src_raw"))
            dst_ip, dst_port = parse_endpoint(m.group("dst_raw"))

            if is_drop or sid in range(1000019, 1000026):
                sev = "BLOCKED"
            else:
                sev = severity_map.get(sid, "UNKNOWN")

            alerts.append({
                "session":   session,
                "timestamp": m.group("timestamp"),
                "sid":       sid,
                "message":   m.group("msg"),
                "severity":  sev,
                "action":    "DROP" if is_drop else "ALERT",
                "protocol":  m.group("proto"),
                "src_ip":    src_ip,
                "src_port":  src_port,
                "dst_ip":    dst_ip,
                "dst_port":  dst_port
            })
            if line_num % 50000 == 0:
                print(f"[INFO] {session}: read {line_num} lines, matched {len(alerts)} alerts", flush=True)
    print(f"[INFO] Finished {session}: {len(alerts)} alerts parsed", flush=True)
    return alerts

# ---------------------------------------------
#  SHARED STYLES
# ---------------------------------------------
def base_style():
    return """
<style>
    * { box-sizing: border-box; }
    body {
        font-family: Arial, sans-serif;
        background: #f4f6f8;
        color: #222;
        margin: 0;
        padding: 0;
    }
    .container {
        width: 95%;
        max-width: 1400px;
        margin: 20px auto;
    }
    .header {
        background: linear-gradient(135deg, #1f3b73, #2b6cb0);
        color: white;
        padding: 24px 30px;
        border-radius: 14px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.12);
        margin-bottom: 20px;
    }
    .header h1 { margin: 0 0 8px 0; font-size: 30px; }
    .header p  { margin: 0; opacity: 0.92; font-size: 14px; }
    .section {
        background: white;
        border-radius: 14px;
        padding: 20px;
        margin-bottom: 20px;
        box-shadow: 0 3px 10px rgba(0,0,0,0.08);
    }
    .section h2 {
        margin-top: 0;
        color: #1f3b73;
        border-bottom: 2px solid #e5e7eb;
        padding-bottom: 8px;
    }
    .section.ips-section {
        border-left: 6px solid #7c3aed;
    }
    .section.ips-section h2 {
        color: #7c3aed;
    }
    .cards {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
        gap: 14px;
        margin-top: 15px;
    }
    .card {
        background: #f8fafc;
        border-left: 6px solid #2b6cb0;
        border-radius: 10px;
        padding: 14px 16px;
    }
    .card.blocked { border-left-color: #7c3aed; }
    .card .label { font-size: 13px; color: #555; margin-bottom: 6px; }
    .card .value { font-size: 28px; font-weight: bold; color: #111827; }
    table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 12px;
        font-size: 14px;
        background: white;
    }
    th, td {
        border: 1px solid #d1d5db;
        padding: 10px 8px;
        text-align: left;
        vertical-align: top;
    }
    th {
        background: #1f2937;
        color: white;
        position: sticky;
        top: 0;
    }
    tr:nth-child(even) { background: #f9fafb; }
    tr:hover           { background: #eef6ff; }
    tr.drop-row        { background: #faf5ff; }
    tr.drop-row:hover  { background: #ede9fe; }
    .badge {
        display: inline-block;
        padding: 4px 10px;
        border-radius: 999px;
        font-size: 12px;
        font-weight: bold;
        color: white;
    }
    .high    { background: #dc2626; }
    .medium  { background: #d97706; }
    .low     { background: #16a34a; }
    .blocked { background: #7c3aed; }
    .unknown { background: #6b7280; }
    .action-drop  { background: #7c3aed; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; }
    .action-alert { background: #e5e7eb; color: #374151; padding: 2px 8px; border-radius: 4px; font-size: 11px; }
    .mode-ips  { background: #7c3aed; color: white; padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; }
    .mode-nids { background: #2b6cb0; color: white; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
    .small-note { color: #6b7280; font-size: 13px; margin-top: 8px; }
    .table-wrap { overflow-x: auto; }
    .nav-buttons { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px; }
    .btn {
        display: inline-block;
        padding: 10px 14px;
        background: #2b6cb0;
        color: white;
        text-decoration: none;
        border-radius: 8px;
        font-size: 14px;
        font-weight: bold;
    }
    .btn:hover { background: #1f4f85; }
    .btn.secondary { background: #4b5563; }
    .btn.secondary:hover { background: #374151; }
    .page-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
    .page-links a {
        text-decoration: none;
        padding: 8px 12px;
        border-radius: 8px;
        background: #e5e7eb;
        color: #111827;
        font-weight: bold;
    }
    .page-links a.active { background: #2b6cb0; color: white; }
    .generated-note { font-size: 13px; color: #9ca3af; margin-top: 6px; }
</style>
"""

def severity_badge(sev):
    sev_esc = escape(sev)
    cls = sev.lower()
    return f'<span class="badge {cls}">{sev_esc}</span>'

def action_badge(action):
    if action == "DROP":
        return '<span class="action-drop">&#x26D4; DROP</span>'
    return '<span class="action-alert">ALERT</span>'

def mode_badge(mode):
    if mode == "IPS":
        return '<span class="mode-ips">IPS</span>'
    return '<span class="mode-nids">NIDS</span>'

# ---------------------------------------------
#  MAIN REPORT
# ---------------------------------------------
def build_main_report(all_alerts, alerts_by_session, session_totals,
                      severity_totals, sid_totals, src_ip_totals, generated_at):
    parts = []

    blocked_total = severity_totals.get("BLOCKED", 0)
    drop_alerts   = [a for a in all_alerts if a["action"] == "DROP"]

    parts.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>NIDS and IPS Alert Report</title>
{base_style()}
</head>
<body>
<div class="container">

<div class="header">
    <h1>NIDS and IPS Alert Report</h1>
    <p>Snort 3 running on Ubuntu VM. Covers live traffic and PCAP replay sessions from CICIDS2017.</p>
    <p class="generated-note">Report generated: {escape(generated_at)}</p>
</div>
""")

    # Log file status
    parts.append("""
<div class="section">
    <h2>Log File Status</h2>
    <div class="table-wrap">
        <table>
            <tr><th>Session</th><th>Path</th><th>Status</th><th>Alerts Parsed</th></tr>
""")
    for session in SESSION_ORDER:
        path   = log_paths[session]
        status = "FOUND" if os.path.exists(path) else "MISSING"
        count  = session_totals.get(session, 0)
        parts.append(
            f"<tr><td>{escape(session)}</td><td>{escape(path)}</td>"
            f"<td>{escape(status)}</td><td>{count:,}</td></tr>"
        )
    parts.append("</table></div></div>")

    # Overall summary cards
    total_alerts = len(all_alerts)
    parts.append(f"""
<div class="section">
    <h2>Overall Summary</h2>
    <div class="cards">
        <div class="card"><div class="label">Total Alerts</div><div class="value">{total_alerts:,}</div></div>
        <div class="card"><div class="label">LIVE</div><div class="value">{session_totals.get("LIVE", 0):,}</div></div>
        <div class="card"><div class="label">TUESDAY</div><div class="value">{session_totals.get("TUESDAY", 0):,}</div></div>
        <div class="card"><div class="label">WEDNESDAY</div><div class="value">{session_totals.get("WEDNESDAY", 0):,}</div></div>
        <div class="card"><div class="label">FRIDAY</div><div class="value">{session_totals.get("FRIDAY", 0):,}</div></div>
        <div class="card"><div class="label">HIGH Severity</div><div class="value">{severity_totals.get("HIGH", 0):,}</div></div>
        <div class="card"><div class="label">MEDIUM Severity</div><div class="value">{severity_totals.get("MEDIUM", 0):,}</div></div>
        <div class="card"><div class="label">LOW Severity</div><div class="value">{severity_totals.get("LOW", 0):,}</div></div>
        <div class="card blocked"><div class="label">&#x26D4; IPS BLOCKED</div><div class="value">{blocked_total:,}</div></div>
    </div>
</div>
""")

    # Alerts by severity
    parts.append("""
<div class="section">
    <h2>Alerts by Severity</h2>
    <div class="table-wrap">
        <table>
            <tr><th>Severity</th><th>Count</th><th>% of Total</th></tr>
""")
    for sev in ["HIGH", "MEDIUM", "LOW", "BLOCKED", "UNKNOWN"]:
        count = severity_totals.get(sev, 0)
        pct   = f"{(count / total_alerts * 100):.1f}%" if total_alerts else "0%"
        parts.append(f"<tr><td>{severity_badge(sev)}</td><td>{count:,}</td><td>{pct}</td></tr>")
    parts.append("</table></div></div>")

    # IPS Blocked Alerts
    parts.append("""
<div class="section ips-section">
    <h2>&#x26D4; IPS Blocked Alerts</h2>
    <p class="small-note">These are alerts from the IPS DROP rules (SIDs 1000019-1000025).
    Traffic matched by these rules was actively dropped by Snort.
    Only shows up in LIVE mode when running with --daq afpacket -Q.</p>
    <div class="table-wrap">
        <table>
            <tr><th>SID</th><th>Rule Name</th><th>Attack Type</th><th>Protocol</th><th>Count</th></tr>
""")
    ips_sids = range(1000019, 1000026)
    has_ips  = False
    for sid in ips_sids:
        count = sid_totals.get(sid, 0)
        if count > 0:
            has_ips = True
            info = rule_coverage.get(sid, {})
            parts.append(
                f"<tr class='drop-row'>"
                f"<td>{sid}</td>"
                f"<td>{escape(info.get('name', 'Unknown'))}</td>"
                f"<td>{escape(info.get('attack', '-'))}</td>"
                f"<td>{escape(info.get('protocol', '-'))}</td>"
                f"<td>{count:,}</td>"
                f"</tr>"
            )
    if not has_ips:
        parts.append("<tr><td colspan='5' style='color:#6b7280;text-align:center;'>"
                     "No IPS blocks found. Run Snort in IPS mode (-Q flag) to enable dropping.</td></tr>")
    parts.append("</table></div></div>")

    # Full Rule Coverage Table
    parts.append("""
<div class="section">
    <h2>Full Rule Coverage (NIDS + IPS)</h2>
    <p class="small-note">All 25 custom rules: 18 NIDS alert rules and 7 IPS DROP rules.</p>
    <div class="table-wrap">
        <table>
            <tr><th>SID</th><th>Mode</th><th>Rule Name</th><th>Attack Type</th><th>Protocol</th><th>Severity</th><th>Alerts Triggered</th></tr>
""")
    for sid in sorted(rule_coverage.keys()):
        info  = rule_coverage[sid]
        sev   = severity_map.get(sid, "UNKNOWN")
        count = sid_totals.get(sid, 0)
        row_cls = "drop-row" if info["mode"] == "IPS" else ""
        parts.append(
            f"<tr class='{row_cls}'>"
            f"<td>{sid}</td>"
            f"<td>{mode_badge(info['mode'])}</td>"
            f"<td>{escape(info['name'])}</td>"
            f"<td>{escape(info['attack'])}</td>"
            f"<td>{escape(info['protocol'])}</td>"
            f"<td>{severity_badge(sev)}</td>"
            f"<td>{count:,}</td>"
            f"</tr>"
        )
    parts.append("</table></div></div>")

    # Alert Frequency by SID
    parts.append("""
<div class="section">
    <h2>Alert Frequency by SID</h2>
    <div class="table-wrap">
        <table>
            <tr><th>SID</th><th>Mode</th><th>Rule Name</th><th>Severity</th><th>Count</th></tr>
""")
    for sid, count in sid_totals.most_common():
        info = rule_coverage.get(sid, {})
        name = info.get("name", f"SID {sid}")
        mode = info.get("mode", "")
        sev  = severity_map.get(sid, "UNKNOWN")
        row_cls = "drop-row" if mode == "IPS" else ""
        parts.append(
            f"<tr class='{row_cls}'>"
            f"<td>{sid}</td>"
            f"<td>{mode_badge(mode) if mode else ''}</td>"
            f"<td>{escape(name)}</td>"
            f"<td>{severity_badge(sev)}</td>"
            f"<td>{count:,}</td>"
            f"</tr>"
        )
    parts.append("</table></div></div>")

    # Top Source IPs
    parts.append("""
<div class="section">
    <h2>Top 15 Source IPs</h2>
    <div class="table-wrap">
        <table>
            <tr><th>Source IP</th><th>Count</th><th>% of Total</th></tr>
""")
    for ip, count in src_ip_totals.most_common(15):
        pct = f"{(count / total_alerts * 100):.1f}%" if total_alerts else "0%"
        parts.append(f"<tr><td>{escape(ip)}</td><td>{count:,}</td><td>{pct}</td></tr>")
    parts.append("</table></div></div>")

    # Per-Session Breakdown
    parts.append("""
<div class="section">
    <h2>Per-Session Breakdown</h2>
    <div class="table-wrap">
        <table>
            <tr><th>Session</th><th>Total</th><th>HIGH</th><th>MEDIUM</th><th>LOW</th><th>BLOCKED (IPS)</th><th>UNKNOWN</th></tr>
""")
    for session in SESSION_ORDER:
        s_alerts = alerts_by_session[session]
        s_sev    = Counter(a["severity"] for a in s_alerts)
        parts.append(
            f"<tr>"
            f"<td>{escape(session)}</td>"
            f"<td>{len(s_alerts):,}</td>"
            f"<td>{s_sev.get('HIGH', 0):,}</td>"
            f"<td>{s_sev.get('MEDIUM', 0):,}</td>"
            f"<td>{s_sev.get('LOW', 0):,}</td>"
            f"<td><b style='color:#7c3aed'>{s_sev.get('BLOCKED', 0):,}</b></td>"
            f"<td>{s_sev.get('UNKNOWN', 0):,}</td>"
            f"</tr>"
        )
    parts.append("</table></div></div>")

    # Navigation to detail pages
    parts.append("""
<div class="section">
    <h2>Browse Detailed Alerts</h2>
    <p class="small-note">Alert data split into pages of 1000 rows each.</p>
    <div class="nav-buttons">
""")
    for session in SESSION_ORDER:
        total = len(alerts_by_session[session])
        pages = max(1, math.ceil(total / PAGE_SIZE))
        first_page = f"details/{session.lower()}_page_1.html"
        parts.append(
            f'<a class="btn" href="{escape(first_page)}">'
            f'{escape(session)} ({total:,} alerts, {pages} pages)</a>'
        )
    parts.append("</div></div>")
    parts.append("</div></body></html>")

    with open(report_file, "w", encoding="utf-8") as f:
        f.write("".join(parts))
    fix_permissions(report_file)
    print(f"[INFO] Main report written: {report_file}", flush=True)

# ---------------------------------------------
#  SESSION DETAIL PAGES
# ---------------------------------------------
def build_session_pages(session, alerts):
    total       = len(alerts)
    total_pages = max(1, math.ceil(total / PAGE_SIZE))

    for page_num in range(1, total_pages + 1):
        start       = (page_num - 1) * PAGE_SIZE
        end         = min(start + PAGE_SIZE, total)
        page_alerts = alerts[start:end]
        filename    = os.path.join(detail_dir, f"{session.lower()}_page_{page_num}.html")

        parts = []
        parts.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{escape(session)} Alerts - Page {page_num}</title>
{base_style()}
</head>
<body>
<div class="container">

<div class="header">
    <h1>{escape(session)} Alert Details</h1>
    <p>Showing alerts {start + 1:,} to {end:,} of {total:,}. Page {page_num} of {total_pages}.</p>
</div>

<div class="section">
    <h2>Navigation</h2>
    <div class="nav-buttons">
        <a class="btn secondary" href="../report_all.html">&#8592; Main Report</a>
""")
        if page_num > 1:
            parts.append(f'<a class="btn" href="{session.lower()}_page_{page_num - 1}.html">&#8592; Previous</a>')
        if page_num < total_pages:
            parts.append(f'<a class="btn" href="{session.lower()}_page_{page_num + 1}.html">Next &#8594;</a>')

        parts.append('</div><div class="page-links">')
        page_window = 10
        start_page  = max(1, page_num - page_window // 2)
        end_page    = min(total_pages, start_page + page_window - 1)
        start_page  = max(1, end_page - page_window + 1)
        for p in range(start_page, end_page + 1):
            cls = "active" if p == page_num else ""
            parts.append(f'<a class="{cls}" href="{session.lower()}_page_{p}.html">{p}</a>')
        parts.append("</div></div>")

        parts.append("""
<div class="section">
    <h2>Alert Table</h2>
    <div class="table-wrap">
        <table>
            <tr>
                <th>Action</th>
                <th>Session</th>
                <th>Timestamp</th>
                <th>SID</th>
                <th>Mode</th>
                <th>Rule Name</th>
                <th>Message</th>
                <th>Severity</th>
                <th>Protocol</th>
                <th>Source IP</th>
                <th>Src Port</th>
                <th>Dest IP</th>
                <th>Dst Port</th>
            </tr>
""")
        for a in page_alerts:
            info      = rule_coverage.get(a["sid"], {})
            rule_name = info.get("name", f"SID {a['sid']}")
            mode      = info.get("mode", "")
            row_cls   = "drop-row" if a["action"] == "DROP" else ""
            parts.append(
                f"<tr class='{row_cls}'>"
                f"<td>{action_badge(a['action'])}</td>"
                f"<td>{escape(a['session'])}</td>"
                f"<td>{escape(a['timestamp'])}</td>"
                f"<td>{a['sid']}</td>"
                f"<td>{mode_badge(mode) if mode else ''}</td>"
                f"<td>{escape(rule_name)}</td>"
                f"<td>{escape(a['message'])}</td>"
                f"<td>{severity_badge(a['severity'])}</td>"
                f"<td>{escape(a['protocol'])}</td>"
                f"<td>{escape(a['src_ip'])}</td>"
                f"<td>{escape(a['src_port'])}</td>"
                f"<td>{escape(a['dst_ip'])}</td>"
                f"<td>{escape(a['dst_port'])}</td>"
                f"</tr>"
            )
        parts.append("</table></div></div></div></body></html>")

        with open(filename, "w", encoding="utf-8") as f:
            f.write("".join(parts))
        fix_permissions(filename)

# ---------------------------------------------
#  MAIN ENTRY POINT
# ---------------------------------------------
def main():
    generated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(f"[INFO] Starting report generation at {generated_at}", flush=True)

    fix_permissions(report_dir)
    fix_permissions(detail_dir)

    all_alerts        = []
    alerts_by_session = defaultdict(list)
    session_totals    = Counter()
    severity_totals   = Counter()
    sid_totals        = Counter()
    src_ip_totals     = Counter()

    for session in SESSION_ORDER:
        alerts = parse_alert_file(log_paths[session], session)
        alerts_by_session[session].extend(alerts)
        all_alerts.extend(alerts)
        for a in alerts:
            session_totals[a["session"]]   += 1
            severity_totals[a["severity"]] += 1
            sid_totals[a["sid"]]           += 1
            src_ip_totals[a["src_ip"]]     += 1

    print(f"[INFO] Total parsed alerts   : {len(all_alerts):,}", flush=True)
    print(f"[INFO] IPS blocked alerts    : {severity_totals.get('BLOCKED', 0):,}", flush=True)

    for session in SESSION_ORDER:
        alerts_by_session[session].sort(key=lambda x: x["timestamp"])

    print("[INFO] Writing main HTML report...", flush=True)
    build_main_report(
        all_alerts, alerts_by_session,
        session_totals, severity_totals,
        sid_totals, src_ip_totals,
        generated_at
    )

    print("[INFO] Writing paginated detail pages...", flush=True)
    for session in SESSION_ORDER:
        print(f"[INFO]   Building pages for {session}...", flush=True)
        build_session_pages(session, alerts_by_session[session])

    fix_tree_permissions(report_dir)

    print("[INFO] Done.", flush=True)
    print(f"[INFO] Main report : {report_file}", flush=True)
    print(f"[INFO] Detail pages: {detail_dir}", flush=True)
    print(f"[INFO] Generated at: {generated_at}", flush=True)

if __name__ == "__main__":
    main()
