/* ============================================================
   SARIX — THE BENCH
   Interactive scanner playground. Pick a scanner, watch Sarix
   verify its raw alerts one at a time. Stats animate. Each row
   expands to show the full reasoning.
   ============================================================ */

const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ============================================================
   i18n hook — reads window.__sarixLang and re-renders on change
   ============================================================ */
function useT() {
  const [lang, setLang] = useState(window.__sarixLang || 'en');
  useEffect(() => {
    const onChange = () => setLang(window.__sarixLang || 'en');
    window.addEventListener('sarixlangchange', onChange);
    return () => window.removeEventListener('sarixlangchange', onChange);
  }, []);
  return useCallback((key, fallback) => {
    if (window.sarixI18n) {
      const v = window.sarixI18n.get(key);
      if (v != null) return v;
    }
    return fallback;
  }, [lang]);
}

/* ============================================================
   DATA — five scanners with realistic mock SARIF feeds
   ============================================================ */
const SCANNERS = [
  {
    id: "semgrep",
    name: "Semgrep",
    desc: "Pattern-based, runs in CI",
    raw: 47,
    avgTime: 2.8,
    runs: [
      { id: "CVE-2024-1234", file: "app.py:142", sev: "HIGH", v: "exp",
        note: "SQL injection — input reaches raw query, no validation upstream",
        why: "request.json['customer_id'] flows into a raw cursor.execute() at line 142 with no schema validation, no parameter binding. Reachable from the /api/customers/* route handler."
      },
      { id: "B608", file: "models/auth.py:88", sev: "HIGH", v: "dis",
        note: "Pydantic-validated 3 frames up — unreachable",
        why: "The string concatenation flagged here only fires after the AuthRequest Pydantic model has coerced user_id to int (validators/auth.py:14). The path is unreachable with attacker-controlled strings."
      },
      { id: "B404", file: "ops.py:88", sev: "MED", v: "fix",
        note: "subprocess shell=True — patch quotes via shlex",
        why: "shell=True is used with f-string interpolation of repo_name. The fix replaces it with shlex.split() + shell=False. Patch written to .sarix/patches/B404.diff."
      },
      { id: "B501", file: "client.py:33", sev: "HIGH", v: "exp",
        note: "TLS verification disabled in production import path",
        why: "requests.get(..., verify=False) on line 33. Used by the payment-webhook handler. Allows MITM on outbound calls."
      },
      { id: "B201", file: "app.py:8", sev: "HIGH", v: "exp",
        note: "Flask debug=True ships in production WSGI",
        why: "DEBUG=True is read from env with no guard. The dockerfile sets ENV=production but the flag isn't gated. Werkzeug debugger exposes RCE."
      },
      { id: "B303", file: "hash.py:14", sev: "MED", v: "dis",
        note: "MD5 used for cache key, not authentication",
        why: "hashlib.md5 here keys a Redis LRU cache. No security boundary. False positive — pattern matches but use is benign."
      },
      { id: "B311", file: "token.py:52", sev: "MED", v: "fix",
        note: "random.choice for token generation — patch uses secrets",
        why: "Mersenne Twister output is predictable. Patch swaps to secrets.token_urlsafe(32). 8-line diff."
      },
      { id: "B105", file: "token.py:31", sev: "LOW", v: "fix",
        note: "Hardcoded SECRET_KEY default — patch enforces env var",
        why: "Default fallback value visible in source. Patch removes the default and raises ConfigError on import if SARIX_SECRET unset."
      },
    ],
  },
  {
    id: "codeql",
    name: "CodeQL",
    desc: "Dataflow, GitHub-native",
    raw: 108,
    avgTime: 3.4,
    runs: [
      { id: "py/sql-injection", file: "queries/orders.py:201", sev: "HIGH", v: "exp",
        note: "User input concatenated into ORDER BY clause",
        why: "Query string built from request.GET['sort'] without an allowlist. ORDER BY can't be parameterized — patch uses a fixed mapping dict."
      },
      { id: "py/path-injection", file: "upload.py:18", sev: "MED", v: "fix",
        note: "User-supplied filename joined to disk path — patch normalizes",
        why: "os.path.join(UPLOAD_DIR, request.files['f'].filename) is vulnerable to ../ traversal. Patch resolves via Path.resolve() and asserts is_relative_to(UPLOAD_DIR)."
      },
      { id: "py/clear-text-storage", file: "settings.py:14", sev: "HIGH", v: "exp",
        note: "Plaintext DB password in committed .env.example",
        why: "Default value visible in git history. Patch rotates the credential and replaces with placeholder."
      },
      { id: "py/xxe", file: "parser.py:30", sev: "HIGH", v: "dis",
        note: "lxml configured with resolve_entities=False — safe",
        why: "The XMLParser at line 12 sets resolve_entities=False and no_network=True. The flagged parse call inherits these defaults. False positive."
      },
      { id: "py/insecure-cookie", file: "auth.py:121", sev: "MED", v: "fix",
        note: "Missing secure / samesite flags — patch adds both",
        why: "set_cookie() call without secure=True, samesite='Strict'. Patch adds both. Session cookie remains backward-compatible."
      },
      { id: "py/url-redirection", file: "router.py:24", sev: "HIGH", v: "exp",
        note: "Open redirect — next= param trusted unfiltered",
        why: "redirect(request.args['next']) at line 24. No same-origin check. Patch validates against an allowlist of internal routes."
      },
      { id: "py/insecure-randomness", file: "token.py:52", sev: "MED", v: "fix",
        note: "random.SystemRandom not used — patch swaps to secrets",
        why: "token = ''.join(random.choices(string.ascii, k=32)). Predictable. Patch uses secrets.token_urlsafe(24)."
      },
      { id: "py/regex-injection", file: "search.py:12", sev: "LOW", v: "fix",
        note: "User input compiled into regex without escape",
        why: "re.compile(request.args['q']). DoS vector via complex regex. Patch wraps in re.escape() before compile."
      },
    ],
  },
  {
    id: "bandit",
    name: "Bandit",
    desc: "Python AST, security focus",
    raw: 62,
    avgTime: 1.9,
    runs: [
      { id: "B102", file: "migrate.py:44", sev: "HIGH", v: "exp",
        note: "exec() over migration string — controlled by env",
        why: "exec(open(MIGRATION_DIR / fname).read()) where fname comes from an environment variable. If env is attacker-influenced (some PaaS providers), RCE."
      },
      { id: "B301", file: "cache.py:201", sev: "HIGH", v: "exp",
        note: "pickle.load from untrusted Redis blob",
        why: "Redis cache is shared with a public worker. Pickle deserialization on any cached payload = RCE primitive. Patch switches to msgpack."
      },
      { id: "B403", file: "cache.py:1", sev: "LOW", v: "dis",
        note: "Pickle imported but used only on trusted internal blobs",
        why: "After B301 is patched (msgpack), this import is dead code. Already on the cleanup list. Not a finding in current state."
      },
      { id: "B201", file: "app.py:8", sev: "HIGH", v: "exp",
        note: "Flask debug=True read from env without prod guard",
        why: "DEBUG = bool(os.getenv('DEBUG', '1')). Default to true if env unset. Werkzeug debugger exposes /console RCE."
      },
      { id: "B105", file: "auth.py:120", sev: "LOW", v: "fix",
        note: "Hardcoded password default — patch raises if unset",
        why: "Default 'changeme' fallback. Patch removes the default and fails fast on missing AUTH_SECRET env."
      },
      { id: "B107", file: "config.py:88", sev: "LOW", v: "dis",
        note: "Hardcoded password — test fixture only",
        why: "Used by pytest fixtures in conftest.py only. Not packaged into the built wheel. Verified via build manifest."
      },
      { id: "B506", file: "config.py:6", sev: "HIGH", v: "exp",
        note: "yaml.load on user-supplied config",
        why: "yaml.load(open(args.config)) where args.config is a CLI flag passed by orchestration. PyYAML resolves !!python/object — RCE. Patch uses yaml.safe_load."
      },
      { id: "B608", file: "queries.py:55", sev: "MED", v: "fix",
        note: "SQL string concat — patch parameterizes",
        why: "f-string built into raw cursor.execute. Patch swaps to a parameterized query with named placeholders. 4-line diff."
      },
    ],
  },
  {
    id: "trivy",
    name: "Trivy",
    desc: "Container & dep scanning",
    raw: 134,
    avgTime: 1.1,
    runs: [
      { id: "CVE-2024-1198", file: "package-lock.json:441", sev: "MED", v: "exp",
        note: "tar-stream RCE — used in upload pipeline",
        why: "tar-stream@2.2.0 vulnerable to path traversal during extraction. Imported by ./services/upload.js which extracts user-supplied archives. Reachable on the hot path."
      },
      { id: "CVE-2023-9981", file: "package-lock.json:982", sev: "LOW", v: "dis",
        note: "Transitive lodash — only loaded in test bundle",
        why: "lodash@4.17.20 is a dev dep via @testing/library. Webpack production config excludes it. Not in any shipped bundle."
      },
      { id: "CVE-2024-5712", file: "parser.py:30", sev: "HIGH", v: "dis",
        note: "XXE — configured safely already",
        why: "Same XMLParser config check as the CodeQL finding. resolve_entities=False at construction time."
      },
      { id: "CVE-2024-7891", file: "search.py:12", sev: "MED", v: "fix",
        note: "ReDoS in regex — patch bounds the engine",
        why: "Catastrophic backtracking on /^(a+)+$/-style patterns. Patch wraps re.match with a 100ms timeout via signal."
      },
      { id: "CVE-2025-0044", file: "infra/iam.tf:14", sev: "HIGH", v: "exp",
        note: "S3 bucket policy: public read on private bucket",
        why: "Principal: '*' with s3:GetObject on bucket containing PII exports. Patch restricts to specific role ARNs."
      },
      { id: "CVE-2025-0210", file: "Dockerfile:8", sev: "MED", v: "fix",
        note: "Docker privileged mode — patch drops to specific caps",
        why: "--privileged in compose file. Patch swaps to cap_add: [SYS_ADMIN] which is the only capability actually needed."
      },
      { id: "CVE-2025-1144", file: "Dockerfile:1", sev: "MED", v: "fix",
        note: "Base image python:3.11 has 11 known CVEs — patch bumps",
        why: "Patch updates FROM python:3.11-slim → python:3.12-slim. Drops 11 transitive CVEs in apt packages."
      },
      { id: "CVE-2024-3782", file: "webhook.py:60", sev: "MED", v: "dis",
        note: "SSRF — outbound allowlist already enforced",
        why: "WEBHOOK_ALLOWED_HOSTS env applied in middleware. Outbound calls validated against the list before httpx.get is reached."
      },
    ],
  },
  {
    id: "snyk",
    name: "Snyk Code",
    desc: "AI-assisted, IDE-integrated",
    raw: 38,
    avgTime: 2.2,
    runs: [
      { id: "python/SqlInjection", file: "queries.py:55", sev: "HIGH", v: "exp",
        note: "Direct concat in raw cursor",
        why: "f-string in cursor.execute call. Reachable from authenticated routes. Patch parameterizes via :var placeholders."
      },
      { id: "python/HardcodedCred", file: "auth.py:121", sev: "HIGH", v: "exp",
        note: "JWT signing key literal in source",
        why: "JWT_SECRET = 'sarix-dev' on line 121. Committed to public history. Patch rotates secret and reads from vault."
      },
      { id: "python/InsecureHash", file: "hash.py:14", sev: "MED", v: "dis",
        note: "MD5 for cache key — no security boundary",
        why: "Same context as the Semgrep B303 finding. Used for an internal cache LRU. False positive."
      },
      { id: "python/PathTraversal", file: "upload.py:18", sev: "MED", v: "fix",
        note: "Filename joined to disk path — patch normalizes",
        why: "Same as CodeQL finding. Patch uses Path.resolve() with is_relative_to assertion."
      },
      { id: "python/DisabledTls", file: "client.py:33", sev: "HIGH", v: "exp",
        note: "verify=False on outbound requests",
        why: "Same as Semgrep B501. Used by webhook delivery. Patch removes the kwarg."
      },
      { id: "python/UnsafeYaml", file: "config.py:6", sev: "HIGH", v: "fix",
        note: "yaml.load on config file — patch uses safe_load",
        why: "Same as Bandit B506. Patch swaps to yaml.safe_load()."
      },
      { id: "python/CmdInjection", file: "ops.py:88", sev: "MED", v: "fix",
        note: "subprocess shell=True with interpolation — patch uses shlex",
        why: "Same as Semgrep B404. Patch wraps args in shlex.split()."
      },
    ],
  },
];

/* ============================================================
   ANIMATED COUNTER hook
   ============================================================ */
function useCount(value, dur = 480) {
  const [display, setDisplay] = useState(value);
  const prevRef = useRef(value);
  useEffect(() => {
    const start = prevRef.current;
    const end = value;
    if (start === end) return;
    const t0 = performance.now();
    let raf;
    const tick = (t) => {
      const k = Math.min(1, (t - t0) / dur);
      const eased = 1 - Math.pow(1 - k, 3);
      setDisplay(Math.round(start + (end - start) * eased));
      if (k < 1) raf = requestAnimationFrame(tick);
      else prevRef.current = end;
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [value, dur]);
  return display;
}

/* ============================================================
   STAT
   ============================================================ */
function Stat({ label, value, tone, big }) {
  const v = useCount(value);
  return (
    <div className={`bs-stat ${tone || ""} ${big ? "big" : ""}`}>
      <div className="bs-stat-v">{v}</div>
      <div className="bs-stat-l">{label}</div>
    </div>
  );
}

/* ============================================================
   ALERT ROW — expands on click
   ============================================================ */
function AlertRow({ run, index, scannerId }) {
  const t = useT();
  const [open, setOpen] = useState(false);
  if (!run) return null;
  const verdictKey = run.v === 'exp' ? 'b.exp' : run.v === 'dis' ? 'b.dis' : 'b.fix';
  const verdictFallback = run.v === 'exp' ? 'EXPLOITABLE' : run.v === 'dis' ? 'DISMISSED' : 'FIX PROPOSED';
  return (
    <div
      className={`bs-row bs-${run.v} ${open ? "open" : ""}`}
      style={{ animationDelay: `${index * 60}ms` }}
      data-key={`${scannerId}-${index}`}
    >
      <button className="bs-row-head" onClick={() => setOpen((o) => !o)}>
        <span className="bs-mark">{run.v === "exp" ? "✗" : run.v === "dis" ? "✓" : "◆"}</span>
        <span className="bs-id">{run.id}</span>
        <span className="bs-file">{run.file}</span>
        <span className="bs-sev">{run.sev}</span>
        <span className="bs-verdict">
          {t(verdictKey, verdictFallback)}
        </span>
        <span className="bs-chev">{open ? "−" : "+"}</span>
      </button>
      <div className="bs-row-body" style={{ maxHeight: open ? 240 : 0 }}>
        <div className="bs-row-inner">
          <div className="bs-note">{run.note}</div>
          <div className="bs-why"><span className="bs-why-l">{t('b.reasoning', '→ reasoning')}</span>{run.why}</div>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   BENCH — main interactive playground
   ============================================================ */
function Bench() {
  const t = useT();
  const [scannerId, setScannerId] = useState("semgrep");
  const [verified, setVerified] = useState([]);
  const [running, setRunning] = useState(false);
  const [runKey, setRunKey] = useState(0);

  const scanner = useMemo(() => SCANNERS.find((s) => s.id === scannerId), [scannerId]);

  // On scanner change OR re-run, animate verifications appearing one by one
  useEffect(() => {
    setVerified([]);
    setRunning(true);
    let cancel = false;
    const run = async () => {
      // initial "loading" pause
      await new Promise((r) => setTimeout(r, 280));
      for (let i = 0; i < scanner.runs.length; i++) {
        if (cancel) return;
        await new Promise((r) => setTimeout(r, 380));
        if (cancel) return;
        setVerified((arr) => [...arr, i]);
      }
      if (!cancel) setRunning(false);
    };
    run();
    return () => { cancel = true; };
  }, [scannerId, runKey, scanner.runs.length]);

  // Live stats
  const stats = useMemo(() => {
    let exp = 0, dis = 0, fix = 0;
    verified.forEach((i) => {
      const r = scanner.runs[i];
      if (r.v === "exp") exp++;
      else if (r.v === "dis") dis++;
      else fix++;
    });
    return {
      total: scanner.raw,
      done: verified.length,
      exp, dis, fix,
      kept: exp + fix,
      dropped: dis,
    };
  }, [verified, scanner]);

  const progress = (verified.length / scanner.runs.length) * 100;

  return (
    <div className="bs">
      {/* HEADER caption */}
      <div className="bs-caption">
        <span>FIG. 02 —</span>
        <span className="fig">{t('b.cap', 'Pick a scanner. Watch Sarix verify its output, line by line.')}</span>
        <span className="dashed"></span>
        <span>~/repo $</span>
      </div>

      {/* SCANNER picker — top row of cards */}
      <div className="bs-scanners">
        {SCANNERS.map((s) => (
          <button
            key={s.id}
            className={`bs-card ${scannerId === s.id ? "active" : ""}`}
            onClick={() => setScannerId(s.id)}
          >
            <div className="bs-card-name">{s.name}</div>
            <div className="bs-card-meta">
              <span>{s.raw} {t('b.alerts','alerts')}</span>
              <span>· {s.avgTime}{t('b.avg','s avg')}</span>
            </div>
            <div className="bs-card-desc">{t(`sc.${s.id}.desc`, s.desc)}</div>
            <div className="bs-card-hover">{t('b.run','RUN →')}</div>
          </button>
        ))}
      </div>

      {/* MAIN: terminal + stats */}
      <div className="bs-grid">
        {/* Terminal */}
        <div className="bs-term">
          <div className="bs-term-bar">
            <div className="bs-dots"><span></span><span></span><span></span></div>
            <div className="bs-term-title">$ sarix verify {scannerId}.sarif</div>
            <div className="bs-status">
              {running ? <><span className="bs-pulse"></span> {t('b.running','RUNNING')}</> : <>✓ {t('b.done','DONE')} · {scanner.avgTime}{t('b.perAlert','s/alert')}</>}
            </div>
          </div>

          {/* Progress bar */}
          <div className="bs-progress">
            <div className="bs-progress-fill" style={{ width: `${progress}%` }}></div>
            <span className="bs-progress-l">{verified.length} / {scanner.runs.length} {t('b.progress','verifications')}</span>
          </div>

          <div className="bs-term-body">
            {!running && verified.length === 0 && (
              <div className="bs-empty">{t('b.empty','Click a scanner above to run.')}</div>
            )}
            {verified.filter(i => i < scanner.runs.length).map((i) => (
              <AlertRow key={`${scannerId}-${runKey}-${i}`} run={scanner.runs[i]} index={i} scannerId={scannerId} />
            ))}
            {running && (
              <div className="bs-loading">
                <span className="bs-load-dot"></span>
                <span className="bs-load-dot"></span>
                <span className="bs-load-dot"></span>
                <span className="bs-load-text">{t('b.tracing','Tracing dataflow…')}</span>
              </div>
            )}
          </div>
        </div>

        {/* Stats panel */}
        <div className="bs-side">
          <div className="bs-side-h">
            <h4>{t('b.liveVerdict','Live verdict')}</h4>
            <button className="bs-rerun" onClick={() => setRunKey((k) => k + 1)} disabled={running}>
              {t('b.runAgain','▶ Run again')}
            </button>
          </div>

          <Stat label={t('b.raw','Raw alerts in')} value={stats.total} big />
          <Stat label={t('b.real','Real findings')} value={stats.kept} tone="orange" />
          <Stat label={t('b.dismissed','Dismissed as noise')} value={stats.dropped} tone="green" />
          <Stat label={t('b.patches','Patches proposed')} value={stats.fix} tone="ink" />

          <div className="bs-divider"></div>

          <div className="bs-ratio">
            <div className="bs-ratio-h">{t('b.signal','SIGNAL RATIO')}</div>
            <div className="bs-ratio-bar">
              <div className="bs-ratio-real" style={{ width: `${(stats.kept / scanner.runs.length) * 100 || 0}%` }}></div>
              <div className="bs-ratio-noise" style={{ width: `${(stats.dropped / scanner.runs.length) * 100 || 0}%` }}></div>
            </div>
            <div className="bs-ratio-foot">
              <span><b style={{color:'var(--accent)'}}>●</b> {t('b.realDot','real')}</span>
              <span><b style={{color:'var(--good)'}}>●</b> {t('b.noiseDot','noise')}</span>
            </div>
          </div>

          <div className="bs-divider"></div>

          <div className="bs-asks">
            <div className="bs-ask">
              <span className="bs-ask-l">{t('b.files','Files opened')}</span>
              <span className="bs-ask-v">{Math.min(verified.length + 1, scanner.runs.length)}</span>
            </div>
            <div className="bs-ask">
              <span className="bs-ask-l">{t('b.calls','Call sites traced')}</span>
              <span className="bs-ask-v">{verified.length * 4}</span>
            </div>
            <div className="bs-ask">
              <span className="bs-ask-l">{t('b.cost','Avg cost / alert')}</span>
              <span className="bs-ask-v">$0.003</span>
            </div>
          </div>
        </div>
      </div>

      {/* Foot */}
      <div className="bs-foot">
        <div>
          <span className="bs-foot-l">{t('b.tipL','TIP —')}</span>
          <span className="bs-foot-t">{t('b.tipT', "Click any row to read Sarix's full reasoning. Switch scanners up top to see how the verdict shape changes per tool.")}</span>
        </div>
        <a href="#pricing" className="bs-foot-cta" dangerouslySetInnerHTML={{__html: t('b.tipCta','Run it on your repo <span>→</span>')}}></a>
      </div>
    </div>
  );
}

/* ============================================================
   MOUNT
   ============================================================ */
const root = document.getElementById("bench-root");
if (root) {
  ReactDOM.createRoot(root).render(<Bench />);
}
