#!/usr/bin/env python3
import sys
import io
import re
import argparse
import html
import json
from pathlib import Path
from datetime import datetime

# Force UTF-8 output to handle non-ASCII paths (e.g. Cyrillic usernames)
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

HEADER_RE = re.compile(r"^=== Results for (.+?) ===\s*$")
SECRET_LINE_RE = re.compile(r"^\[secret(/low)?\]")


def parse_results_streaming(filepath):
    """
    Generator that yields blocks one at a time by reading the file line-by-line.
    Each block is: {"url": "...", "secret_lines": [...], "has_secrets": bool}
    """
    current_url = None
    current_lines = []

    def flush():
        nonlocal current_url, current_lines
        if current_url is None:
            return None
        secret_lines = [l.strip() for l in current_lines if SECRET_LINE_RE.match(l.strip())]
        block = {
            "url": current_url.strip(),
            "secret_lines": secret_lines,
            "has_secrets": bool(secret_lines),
        }
        current_url = None
        current_lines = []
        return block

    with open(filepath, "r", encoding="utf-8", errors="replace") as fh:
        for line in fh:
            line = line.rstrip("\n\r")
            m = HEADER_RE.match(line)
            if m:
                block = flush()
                if block is not None:
                    yield block
                current_url = m.group(1)
                current_lines = []
            else:
                if current_url is not None:
                    current_lines.append(line)

    block = flush()
    if block is not None:
        yield block


def write_html_streaming(blocks_iter, out_fh, input_name="input"):
    """
    Two-pass approach for large files:
      Pass 1: iterate blocks, collect them in a list (only metadata, not raw text).
      Pass 2: stream JSON array into the HTML file piece by piece.

    This avoids holding the full JSON string in memory — we write each
    element individually instead of json.dumps(entire_list).
    """
    # Collect parsed blocks — this is the essential data we need and is much
    # smaller than the raw input file (we already discarded non-secret lines).
    blocks = list(blocks_iter)

    total_blocks = len(blocks)
    blocks_with_secrets = sum(1 for b in blocks if b["has_secrets"])
    total_secret_lines = sum(len(b["secret_lines"]) for b in blocks if b["has_secrets"])

    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    # We'll write the HTML in parts, streaming the JSON array element-by-element
    # so we never build the full JSON string in memory.

    w = out_fh.write

    # --- Part 1: HTML head + CSS + body header (with dynamic stats) ---
    w(f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Secrets Report</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    :root {{
      --bg: #0f172a;
      --panel: #111827;
      --muted: #9ca3af;
      --text: #e5e7eb;
      --accent: #60a5fa;
      --accent-2: #34d399;
      --danger: #f87171;
      --border: rgba(255,255,255,0.08);
      --shadow: 0 10px 30px rgba(0,0,0,0.35);
      --radius: 14px;
      --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
      --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans";
    }}
    body {{
      margin: 0;
      font-family: var(--sans);
      background:
        radial-gradient(1200px 600px at 10% 10%, rgba(96,165,250,0.12), transparent),
        radial-gradient(1200px 600px at 90% 20%, rgba(52,211,153,0.10), transparent),
        var(--bg);
      color: var(--text);
    }}
    .container {{
      max-width: 1100px;
      margin: 0 auto;
      padding: 28px 18px 60px;
    }}
    header {{
      display: flex;
      flex-direction: column;
      gap: 10px;
      margin-bottom: 18px;
    }}
    h1 {{
      font-size: clamp(22px, 3vw, 30px);
      margin: 0;
    }}
    .meta {{
      color: var(--muted);
      font-size: 12px;
    }}

    .summary {{
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 10px;
      margin: 16px 0 22px;
    }}
    .summary .box {{
      background:
        linear-gradient(180deg, rgba(255,255,255,0.02), transparent),
        var(--panel);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 14px 14px 12px;
      box-shadow: var(--shadow);
    }}
    .label {{
      color: var(--muted);
      font-size: 11px;
      text-transform: uppercase;
      letter-spacing: 0.08em;
    }}
    .value {{
      font-size: 22px;
      font-weight: 650;
      margin-top: 4px;
    }}

    .toolbar-sticky {{
      position: sticky;
      top: 0;
      z-index: 100;
      padding: 12px 0;
    }}
    .toolbar {{
      display: flex;
      gap: 10px;
      align-items: center;
      flex-wrap: wrap;
      background:
        linear-gradient(180deg, rgba(255,255,255,0.02), transparent),
        var(--panel);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      box-shadow: var(--shadow);
      padding: 12px 14px;
      margin-bottom: 12px;
    }}
    input[type="search"] {{
      flex: 1;
      min-width: 220px;
      background: var(--panel);
      border: 1px solid var(--border);
      color: var(--text);
      padding: 10px 12px;
      border-radius: 10px;
      outline: none;
    }}
    .toggle {{
      display: inline-flex;
      align-items: center;
      gap: 8px;
      background: var(--panel);
      border: 1px solid var(--border);
      padding: 8px 10px;
      border-radius: 10px;
      font-size: 12px;
      color: var(--muted);
      white-space: nowrap;
    }}
    .toggle input {{
      cursor: pointer;
      transform: translateY(1px);
    }}

    button {{
      background: rgba(96,165,250,0.12);
      border: 1px solid rgba(96,165,250,0.35);
      color: var(--text);
      padding: 9px 12px;
      border-radius: 10px;
      cursor: pointer;
      font-size: 12px;
      white-space: nowrap;
    }}
    button:hover {{
      background: rgba(96,165,250,0.2);
    }}

    .status {{
      display: flex;
      gap: 10px;
      align-items: center;
      color: var(--muted);
      font-size: 11px;
      margin: 6px 0 14px;
    }}
    .dot {{
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: rgba(52,211,153,0.6);
      display: inline-block;
    }}

    .cards {{
      display: flex;
      flex-direction: column;
      gap: 12px;
    }}

    details.card {{
      background:
        linear-gradient(180deg, rgba(255,255,255,0.02), transparent),
        var(--panel);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      box-shadow: var(--shadow);
      overflow: hidden;
    }}
    details.card summary {{
      list-style: none;
      cursor: pointer;
      padding: 14px 14px;
      display: grid;
      grid-template-columns: auto 1fr auto;
      gap: 10px;
      align-items: center;
    }}
    details.card summary::-webkit-details-marker {{
      display: none;
    }}

    .badge {{
      display: inline-flex;
      align-items: center;
      justify-content: center;
      font-size: 10px;
      padding: 3px 7px;
      border-radius: 999px;
      background: rgba(52,211,153,0.12);
      border: 1px solid rgba(52,211,153,0.35);
      color: #c7f9e8;
      white-space: nowrap;
    }}

    a.url {{
      font-size: 13px;
      color: var(--accent);
      text-decoration: none;
      word-break: break-all;
    }}
    a.url:hover {{
      text-decoration: underline;
    }}

    .right-pack {{
      display: inline-flex;
      align-items: center;
      gap: 8px;
      justify-self: end;
    }}

    .visited-wrap {{
      display: inline-flex;
      align-items: center;
      gap: 6px;
      font-size: 10px;
      color: var(--muted);
      background: rgba(255,255,255,0.03);
      border: 1px solid var(--border);
      padding: 4px 8px;
      border-radius: 999px;
      white-space: nowrap;
    }}
    .visited-wrap input {{
      cursor: pointer;
      transform: translateY(1px);
    }}

    .count {{
      font-size: 11px;
      color: var(--muted);
      white-space: nowrap;
    }}

    .card-body {{
      border-top: 1px solid var(--border);
      padding: 12px 16px 16px;
    }}

    ul.secrets {{
      margin: 0;
      padding-left: 18px;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }}
    .secret-item {{
      display: flex;
      gap: 8px;
      align-items: center;
      flex-wrap: wrap;
    }}
    code {{
      font-family: var(--mono);
      font-size: 11.5px;
      background: rgba(248,113,113,0.08);
      border: 1px solid rgba(248,113,113,0.25);
      padding: 2px 6px;
      border-radius: 6px;
      color: #ffd7d7;
      word-break: break-all;
    }}
    .src-link {{
      font-size: 10px;
      color: #ffd7d7;
      text-decoration: none;
      border: 1px solid rgba(248,113,113,0.25);
      padding: 2px 6px;
      border-radius: 999px;
      background: rgba(248,113,113,0.06);
    }}
    .src-link:hover {{
      text-decoration: underline;
    }}

    .no-secrets-note {{
      color: var(--muted);
      font-size: 12px;
    }}

    .empty {{
      background: var(--panel);
      border: 1px dashed var(--border);
      border-radius: var(--radius);
      padding: 18px;
      color: var(--muted);
    }}

    @media (max-width: 700px) {{
      .summary {{ grid-template-columns: 1fr; }}
      details.card summary {{ grid-template-columns: 1fr; }}
      .right-pack {{ justify-self: start; }}
    }}
  </style>
</head>
<body>
  <div class="container">
    <header>
      <h1>Secrets Report</h1>
      <div class="meta">
        Source: {html.escape(input_name)} &nbsp;•&nbsp; Generated: {now}
      </div>
    </header>

    <section class="summary">
      <div class="box">
        <div class="label">Total result blocks found</div>
        <div class="value">{total_blocks}</div>
      </div>
      <div class="box">
        <div class="label">Blocks with secrets</div>
        <div class="value">{blocks_with_secrets}</div>
      </div>
      <div class="box">
        <div class="label">Total secret lines</div>
        <div class="value">{total_secret_lines}</div>
      </div>
    </section>

    <div class="toolbar-sticky">
      <div class="toolbar">
        <input id="search" type="search" placeholder="Filter by URL or secret text..." />
        <input id="exclude" type="search" placeholder="Exclude findings containing (comma-separated)..." />

        <label class="toggle">
          <input id="onlySecrets" type="checkbox" checked />
          Show only secret entries
        </label>

        <label class="toggle">
          <input id="hideVisited" type="checkbox" />
          Hide visited
        </label>

        <label class="toggle" title="Hide false positives (Cloudflare tokens, code patterns, placeholder values)">
          <input id="smartFilter" type="checkbox" />
          Smart filter
        </label>

        <label class="toggle" title="Show only findings that contain email addresses">
          <input id="emailsOnly" type="checkbox" />
          Emails only
        </label>

        <button id="expandAll">Expand rendered</button>
        <button id="collapseAll">Collapse rendered</button>
        <button id="clearVisited">Clear visited</button>
      </div>
    </div>

    <div class="status">
      <span class="dot"></span>
      <span id="statusText">Ready</span>
    </div>

    <main class="cards" id="cards">
      <div class="empty" id="emptyBox" style="display:none;">No matching entries.</div>
    </main>
  </div>

  <script>
    const DATA = [""")

    # --- Part 2: Stream JSON array elements one at a time ---
    # We must escape '</' to '<\/' in JSON output, otherwise a secret line
    # containing '</script>' will break the HTML parser out of the script tag.
    for idx, block in enumerate(blocks):
        if idx > 0:
            w(",")
        chunk = json.dumps(block, ensure_ascii=False)
        w(chunk.replace("</", "<\\/"))

    # --- Part 3: Close the JSON array and write the rest of the JS + HTML ---
    w(r"""];

    const cardsContainer = document.getElementById('cards');
    const emptyBox = document.getElementById('emptyBox');
    const searchEl = document.getElementById('search');
    const excludeEl = document.getElementById('exclude');
    const onlySecretsEl = document.getElementById('onlySecrets');
    const hideVisitedEl = document.getElementById('hideVisited');
    const smartFilterEl = document.getElementById('smartFilter');
    const emailsOnlyEl = document.getElementById('emailsOnly');
    const statusText = document.getElementById('statusText');
    const expandAllBtn = document.getElementById('expandAll');
    const collapseAllBtn = document.getElementById('collapseAll');
    const clearVisitedBtn = document.getElementById('clearVisited');

    let renderToken = 0;

    // ---------------- Visited state ----------------
    const VISITED_KEY = "secrets_report_visited_v1";

    function loadVisited() {
      try {
        const raw = localStorage.getItem(VISITED_KEY);
        const arr = raw ? JSON.parse(raw) : [];
        return new Set(Array.isArray(arr) ? arr : []);
      } catch {
        return new Set();
      }
    }

    function saveVisited(set) {
      localStorage.setItem(VISITED_KEY, JSON.stringify(Array.from(set)));
    }

    let visitedSet = loadVisited();

    function applyVisitedUI(root = document) {
      root.querySelectorAll('details.card').forEach(card => {
        const url = card.dataset.url;
        const box = card.querySelector('.visited-box');
        if (!box) return;
        box.checked = visitedSet.has(url);
      });
    }

    // ---------------- Helpers ----------------
    function escapeHtml(str) {
      return str.replace(/[&<>"']/g, s => {
        switch (s) {
          case '&': return '&amp;';
          case '<': return '&lt;';
          case '>': return '&gt;';
          case '"': return '&quot;';
          case "'": return '&#39;';
          default: return s;
        }
      });
    }

    function parseSecretLine(line, sourceUrl) {
      // Strip trailing [https://...] or [http://...] bracketed URLs
      const cleaned = line.trim().replace(/\s*\[https?:\/\/[^\]]*\]\s*$/, '');
      const escaped = escapeHtml(cleaned);
      const urlEsc = escapeHtml(sourceUrl);
      return `
        <li class="secret-item">
          <code>${escaped}</code>
          <a class="src-link" href="${urlEsc}" target="_blank" rel="noopener noreferrer">source</a>
        </li>
      `;
    }

    function buildCardHTML(item, index) {
      const urlEsc = escapeHtml(item.url);
      const badge = `#${index + 1}`;
      const count = item.secret_lines.length;

      let bodyHtml = '';
      if (item.has_secrets) {
        const secrets = item.secret_lines.map(l => parseSecretLine(l, item.url)).join('');
        bodyHtml = `<ul class="secrets">${secrets}</ul>`;
      } else {
        bodyHtml = `<div class="no-secrets-note">No secrets detected.</div>`;
      }

      const openAttr = item.has_secrets ? 'open' : '';

      return `
        <details class="card" data-url="${urlEsc}" data-has-secrets="${item.has_secrets}" ${openAttr}>
          <summary>
            <span class="badge">${badge}</span>
            <a class="url" href="${urlEsc}" target="_blank" rel="noopener noreferrer">${urlEsc}</a>
            <span class="right-pack">
              <label class="visited-wrap">
                <input class="visited-box" type="checkbox" />
                Visited
              </label>
              <span class="count">${count} secret(s)</span>
            </span>
          </summary>
          <div class="card-body">
            ${bodyHtml}
          </div>
        </details>
      `;
    }

    // Smart filter: regex-based false-positive patterns
    const FALSE_POSITIVE_PATTERNS = [
      // Cloudflare challenge tokens
      /\/cdn-cgi\/challenge-platform\//,
      /\d+\.\d+:\d+:[a-zA-Z0-9_-]+ivzz/,
      // i18n/localization keys
      /["']secret:[a-z]+_[a-z]+_[a-z]+["']/,
      // Code patterns
      /[a-zA-Z]+Key\s*=\s*[a-z]\.define\(/,
      /[a-zA-Z]+Key\s*=\s*function/,
      /[a-zA-Z]+Secret\s*=\s*function/,
      /PrivateKey\s*=\s*[a-z]\.define\(/,
      /PublicKey\s*=\s*[a-z]\.define\(/,
      // Common test/placeholder values
      /["']?password["']?\s*[:=]\s*["']?(test|demo|example|placeholder|changeme|password|123456)["']?/i,
      /["']?secret["']?\s*[:=]\s*["']?(test|demo|example|placeholder)["']?/i,
      // SDK/Library code
      /tokenize\s*:\s*function/,
      /getToken\s*:\s*[a-z]$/,
      /setPassword\s*:\s*function/,
    ];

    // JS noise values — if the extracted value is *only* one of these (or starts
    // with them followed by typical JS syntax), the finding is worthless.
    const JS_NOISE_VALUES = new Set([
      'function', 'function(', 'function (', 'async function',
      'undefined', 'null', 'true', 'false', 'NaN', 'Infinity',
      'this', 'window', 'document', 'self', 'globalThis',
      'void 0', 'void(0)', '{}', '[]', 'new ', 'class ',
      'typeof ', 'instanceof ', 'return ', 'throw ', 'delete ',
      'Object', 'Array', 'String', 'Number', 'Boolean',
      'Promise', 'Symbol', 'Map', 'Set', 'WeakMap', 'WeakSet',
      'console', 'module', 'exports', 'require', 'import',
      'prototype', '__proto__',
    ]);

    // Email detection regex
    const EMAIL_RE = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/;

    // Regex to strip the [secret] or [secret/low] prefix and grab key = value
    const SECRET_KV_RE = /^\[secret(?:\/low)?\]\s*(.+)/i;

    function extractKeyValue(line) {
      const m = line.match(SECRET_KV_RE);
      if (!m) return null;
      // Strip trailing [https://...] or [http://...] bracketed URL first
      let rest = m[1].trim().replace(/\s*\[https?:\/\/[^\]]*\]\s*$/, '');
      // Strip outer quotes from the whole payload: 'key:value' -> key:value
      rest = rest.replace(/^["']+|["']+$/g, '');
      // Try key=value, key:value, key: value, key =value
      const sepIdx = rest.search(/\s*[:=]\s*/);
      if (sepIdx === -1) return { key: rest, value: '' };
      const key = rest.slice(0, sepIdx).trim();
      const value = rest.slice(sepIdx).replace(/^[\s:=]+/, '').trim();
      return { key, value };
    }

    function isFalsePositive(line) {
      // 1. Regex-based patterns
      for (const pat of FALSE_POSITIVE_PATTERNS) {
        if (pat.test(line)) return true;
      }

      // 2. Key/value based checks
      const kv = extractKeyValue(line);
      if (kv) {
        // Remove quotes around key for length check
        const bareKey = kv.key.replace(/^["']+|["']+$/g, '');
        // Filter out keys shorter than 3 characters
        if (bareKey.length < 3) return true;

        // Remove surrounding quotes from value
        const bareVal = kv.value.replace(/^["']+|["']+$/g, '').trim();
        // Filter out values shorter than 3 characters (single letters, flags, etc.)
        if (bareVal.length < 3) return true;

        // Filter out values that are JS noise
        const valLower = bareVal.toLowerCase();
        for (const noise of JS_NOISE_VALUES) {
          if (valLower === noise.toLowerCase()) return true;
          // Also catch values that start with JS keywords like "function(" or "function foo()"
          if (valLower.startsWith(noise.toLowerCase() + '(') ||
              valLower.startsWith(noise.toLowerCase() + ' ')) return true;
        }
        // Catch anonymous/arrow functions: (a,b)=>{...}, ()=>{}, (a)=>...
        if (/^\(.*\)\s*=>/.test(bareVal) || /^\(.*\)\s*\{/.test(bareVal)) return true;
        // Catch: function keyword anywhere at the start
        if (/^(async\s+)?function\b/.test(bareVal)) return true;
      }

      return false;
    }

    function getFilteredData() {
      const q = searchEl.value.toLowerCase().trim();
      const exRaw = excludeEl.value.toLowerCase().trim();
      const exTerms = exRaw ? exRaw.split(',').map(t => t.trim()).filter(Boolean) : [];
      const onlySecrets = onlySecretsEl.checked;
      const hideVisited = hideVisitedEl.checked;
      const smartFilter = smartFilterEl.checked;
      const emailsOnly = emailsOnlyEl.checked;

      const results = [];
      for (const item of DATA) {
        const urlLower = item.url.toLowerCase();

        if (onlySecrets && !item.has_secrets) continue;
        if (hideVisited && visitedSet.has(item.url)) continue;

        if (q) {
          const matchesSearch = urlLower.includes(q) ||
            (item.has_secrets && item.secret_lines.some(s => s.toLowerCase().includes(q)));
          if (!matchesSearch) continue;
        }

        // Apply filters at the finding level
        let lines = item.secret_lines;

        if (exTerms.length > 0 && item.has_secrets) {
          lines = lines.filter(s => {
            const lower = s.toLowerCase();
            return !exTerms.some(term => lower.includes(term));
          });
        }

        if (smartFilter && item.has_secrets) {
          lines = lines.filter(s => !isFalsePositive(s));
        }

        if (emailsOnly) {
          lines = lines.filter(s => EMAIL_RE.test(s));
          if (lines.length === 0) continue;
        }

        if ((exTerms.length > 0 || smartFilter || emailsOnly) && item.has_secrets) {
          if (lines.length === 0) continue;
          results.push({
            url: item.url,
            secret_lines: lines,
            has_secrets: true,
          });
        } else {
          results.push(item);
        }
      }
      return results;
    }

    function clearCards() {
      const nodes = Array.from(cardsContainer.querySelectorAll('details.card'));
      nodes.forEach(n => n.remove());
    }

    function setStatus(msg) {
      statusText.textContent = msg;
    }

    let scheduleRender = null;

    function wireVisitedForRenderedCards() {
      cardsContainer.querySelectorAll('details.card').forEach(card => {
        if (card.dataset.visitedWired) return;
        card.dataset.visitedWired = "1";

        const url = card.dataset.url;
        const box = card.querySelector('.visited-box');
        const link = card.querySelector('a.url');

        if (box) {
          box.addEventListener('change', () => {
            if (box.checked) visitedSet.add(url);
            else visitedSet.delete(url);
            saveVisited(visitedSet);

            if (hideVisitedEl.checked && scheduleRender) {
              scheduleRender();
            }
          });
        }

        if (link) {
          link.addEventListener('click', () => {
            visitedSet.add(url);
            saveVisited(visitedSet);
            if (box) box.checked = true;

            if (hideVisitedEl.checked && scheduleRender) {
              scheduleRender();
            }
          });
        }
      });
    }

    function renderIncrementally(items) {
      const myToken = ++renderToken;
      clearCards();

      if (items.length === 0) {
        emptyBox.style.display = '';
        setStatus('No matches');
        return;
      }
      emptyBox.style.display = 'none';

      const chunkSize = 200;
      let i = 0;

      setStatus(`Rendering 0 / ${items.length}`);

      function step() {
        if (myToken !== renderToken) return;

        const end = Math.min(i + chunkSize, items.length);
        let htmlChunk = '';
        for (; i < end; i++) {
          htmlChunk += buildCardHTML(items[i], i);
        }
        cardsContainer.insertAdjacentHTML('beforeend', htmlChunk);

        applyVisitedUI(cardsContainer);
        wireVisitedForRenderedCards();

        setStatus(`Rendering ${end} / ${items.length}`);

        if (i < items.length) {
          requestAnimationFrame(step);
        } else {
          const totalEntries = DATA.filter(d => d.has_secrets).length;
          const suffix = items.length < totalEntries ? ` (${totalEntries} total, ${totalEntries - items.length} filtered out)` : '';
          setStatus(`Done \u2022 Showing ${items.length} entries${suffix}`);
        }
      }

      requestAnimationFrame(step);
    }

    // ---------------- Debounce search/render ----------------
    let searchTimer = null;
    scheduleRender = function() {
      if (searchTimer) clearTimeout(searchTimer);
      searchTimer = setTimeout(() => {
        renderIncrementally(getFilteredData());
      }, 120);
    };

    searchEl.addEventListener('input', scheduleRender);
    excludeEl.addEventListener('input', scheduleRender);
    onlySecretsEl.addEventListener('change', scheduleRender);
    hideVisitedEl.addEventListener('change', scheduleRender);
    smartFilterEl.addEventListener('change', scheduleRender);
    emailsOnlyEl.addEventListener('change', scheduleRender);

    expandAllBtn.addEventListener('click', () => {
      document.querySelectorAll('details.card').forEach(d => d.open = true);
    });

    collapseAllBtn.addEventListener('click', () => {
      document.querySelectorAll('details.card').forEach(d => d.open = false);
    });

    clearVisitedBtn.addEventListener('click', () => {
      visitedSet = new Set();
      saveVisited(visitedSet);
      applyVisitedUI(cardsContainer);

      if (hideVisitedEl.checked) {
        scheduleRender();
      } else {
        setStatus("Visited cleared");
      }
    });

    // Initial render
    renderIncrementally(getFilteredData());
  </script>
</body>
</html>
""")

    return total_blocks, blocks_with_secrets


def main():
    ap = argparse.ArgumentParser(
        description="Re-organize secret scanner results into a neat HTML report (fast, visited tracking + hide visited filter)."
    )
    ap.add_argument("-i", "--input", required=True, help="Input text file with results.")
    ap.add_argument("-o", "--output", required=True, help="Output HTML file.")
    ap.add_argument(
        "--max-blocks",
        type=int,
        default=2000,
        help="Max blocks per HTML file before splitting into chunks. Default: 2000.",
    )
    args = ap.parse_args()

    in_path = Path(args.input)
    out_path = Path(args.output)
    max_blocks = args.max_blocks

    # Collect all blocks first to decide if we need chunking
    all_blocks = list(parse_results_streaming(in_path))
    total_blocks = len(all_blocks)

    if total_blocks <= max_blocks:
        # Single file — no chunking needed
        with open(out_path, "w", encoding="utf-8") as out_fh:
            _, blocks_with_secrets = write_html_streaming(
                iter(all_blocks), out_fh, input_name=in_path.name
            )
        print(f"[+] Parsed blocks: {total_blocks}")
        print(f"[+] Blocks with secrets: {blocks_with_secrets}")
        print(f"[+] Wrote HTML: {out_path}")
    else:
        # Split into multiple chunk files
        stem = out_path.stem
        suffix = out_path.suffix or ".html"
        parent = out_path.parent
        chunk_num = 0
        written_files = []

        for i in range(0, total_blocks, max_blocks):
            chunk_num += 1
            chunk_blocks = all_blocks[i : i + max_blocks]
            chunk_path = parent / f"{stem}_part{chunk_num}{suffix}"

            chunk_label = f"{in_path.name} (part {chunk_num}, blocks {i+1}-{i+len(chunk_blocks)})"
            with open(chunk_path, "w", encoding="utf-8") as out_fh:
                write_html_streaming(iter(chunk_blocks), out_fh, input_name=chunk_label)
            written_files.append(chunk_path)
            print(f"[+] Wrote chunk: {chunk_path} ({len(chunk_blocks)} blocks)")

        total_secrets = sum(1 for b in all_blocks if b["has_secrets"])
        print(f"[+] Parsed blocks: {total_blocks}")
        print(f"[+] Blocks with secrets: {total_secrets}")
        print(f"[+] Split into {chunk_num} HTML files")


if __name__ == "__main__":
    main()
