import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  DEFAULT_BENCHMARK_SETTINGS,
  DEFAULT_GROWTH_SETTINGS,
} from "../regex/analysis/analysis-limits";
import {
  runGrowthAnalysis,
  runRegexBenchmark,
} from "../regex/analysis/analysis-runner";
import {
  AnalysisSupervisor,
  type AnalysisWorkerClient,
} from "../regex/analysis/AnalysisSupervisor";
import type {
  AnalysisProgress,
  GrowthAnalysisResult,
  GrowthSettings,
  MetricStatistics,
  RegexBenchmarkResult,
  RegexRiskFinding,
} from "../regex/analysis/analysis.types";
import { analyseStaticRisk } from "../regex/analysis/static-risk";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../regex/execution/request-limits";
import type { RegexFlavourId } from "../regex/model/flavour";
import type { RegexSyntaxResult, SourceRange } from "../regex/model/syntax";
import "./AnalysisPanel.css";

type AnalysisView = "risk" | "benchmark";
type ActiveRun = "benchmark" | "growth";

export interface AnalysisPanelProps {
  readonly active: boolean;
  readonly flavour: RegexFlavourId;
  readonly pattern: string;
  readonly flags: readonly string[];
  readonly subject: string;
  readonly replacement: string;
  readonly scanAll: boolean;
  readonly syntax?: RegexSyntaxResult;
  readonly onClose?: () => void;
  readonly onSelectPatternRange?: (range: SourceRange) => void;
  readonly createSupervisor?: () => AnalysisWorkerClient;
}

function formatMilliseconds(value: number | undefined): string {
  if (value === undefined) return "—";
  if (value > 0 && value < 0.01) return "<0.01 ms";
  return `${value.toFixed(2)} ms`;
}

function formatThroughput(value: number | undefined): string {
  if (value === undefined) return "—";
  return `${(value / (1024 * 1024)).toFixed(2)} MiB/s`;
}

function metricValue(
  statistic: MetricStatistics | undefined,
  key: "minimum" | "median" | "p95" | "maximum",
  throughput: boolean,
): string {
  const value = statistic?.[key];
  return throughput ? formatThroughput(value) : formatMilliseconds(value);
}

interface MetricRow {
  readonly label: string;
  readonly cold?: number;
  readonly warm?: MetricStatistics;
  readonly throughput?: boolean;
  readonly unavailableReason?: string;
}

function BenchmarkTable({ result }: { readonly result: RegexBenchmarkResult }) {
  const cold = result.coldSample;
  const rows: readonly MetricRow[] = [
    {
      label: "Worker cold start",
      cold: result.coldStartMs,
    },
    {
      label: "Compile",
      cold: cold?.compileMs,
      warm: result.warmStatistics.compileMs,
    },
    {
      label: "First match",
      cold: cold?.firstMatchMs,
      warm: result.warmStatistics.firstMatchMs,
    },
    {
      label: "All matches",
      cold: cold?.allMatchesMs,
      warm: result.warmStatistics.allMatchesMs,
    },
    {
      label: "Replacement",
      cold: cold?.replacementMs,
      warm: result.warmStatistics.replacementMs,
      unavailableReason: cold?.replacementSkippedReason,
    },
    {
      label: "All-match throughput",
      cold: cold?.throughputBytesPerSecond,
      warm: result.warmStatistics.throughputBytesPerSecond,
      throughput: true,
    },
  ];

  return (
    <div className="analysis-table-scroll">
      <table aria-label="Cold and warm benchmark metrics">
        <thead>
          <tr>
            <th scope="col">Metric</th>
            <th scope="col">Cold / first use</th>
            <th scope="col">Warm minimum</th>
            <th scope="col">Warm median</th>
            <th scope="col">Warm p95</th>
            <th scope="col">Warm maximum</th>
            <th scope="col">Samples</th>
          </tr>
        </thead>
        <tbody>
          {rows.map((row) => (
            <tr key={row.label}>
              <th scope="row">{row.label}</th>
              <td title={row.unavailableReason}>
                {row.throughput
                  ? formatThroughput(row.cold)
                  : formatMilliseconds(row.cold)}
              </td>
              <td>
                {metricValue(row.warm, "minimum", row.throughput === true)}
              </td>
              <td>
                {metricValue(row.warm, "median", row.throughput === true)}
              </td>
              <td>{metricValue(row.warm, "p95", row.throughput === true)}</td>
              <td>
                {metricValue(row.warm, "maximum", row.throughput === true)}
              </td>
              <td>{row.warm?.count.toLocaleString() ?? "—"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function BenchmarkSummary({
  result,
}: {
  readonly result: RegexBenchmarkResult;
}) {
  return (
    <section
      className="analysis-result-block"
      aria-labelledby="benchmark-result"
    >
      <header>
        <div>
          <p className="eyebrow">Actual native engine · bounded worker</p>
          <h3 id="benchmark-result">Benchmark result</h3>
        </div>
        <span className={`analysis-run-badge is-${result.status}`}>
          {result.status.replaceAll("-", " ")}
        </span>
      </header>
      <dl className="analysis-result-meta">
        <div>
          <dt>Engine</dt>
          <dd>
            {result.identity
              ? `${result.identity.engineName} · ${result.identity.engineVersion}`
              : "Worker identity unavailable"}
          </dd>
        </div>
        <div>
          <dt>Subject</dt>
          <dd>
            {result.coldSample
              ? `${result.coldSample.subjectBytes.toLocaleString()} bytes · ${result.coldSample.subjectUtf16.toLocaleString()} UTF-16 units`
              : "No completed cold sample"}
          </dd>
        </div>
        <div>
          <dt>Output count</dt>
          <dd>
            {result.coldSample
              ? `${result.coldSample.matchCount.toLocaleString()} matches${
                  result.coldSample.matchCollectionTruncated
                    ? " · bounded prefix"
                    : ""
                }`
              : "Unavailable"}
          </dd>
        </div>
        <div>
          <dt>Sample count</dt>
          <dd>
            {result.completedMeasuredIterations.toLocaleString()} measured ·{" "}
            {result.completedWarmups.toLocaleString()} warm-up
          </dd>
        </div>
        <div>
          <dt>Wall time</dt>
          <dd>{formatMilliseconds(result.wallTimeMs)}</dd>
        </div>
        <div>
          <dt>Effective flags</dt>
          <dd>
            <code>{result.coldSample?.effectiveFlags || "(none)"}</code>
          </dd>
        </div>
      </dl>
      {result.stoppedReason ? (
        <p className="analysis-stop-reason">{result.stoppedReason}</p>
      ) : null}
      <BenchmarkTable result={result} />
      <ul className="analysis-limitations" aria-label="Benchmark limitations">
        {result.warnings.map((warning) => (
          <li key={warning}>{warning}</li>
        ))}
      </ul>
    </section>
  );
}

function RiskFindingCard({
  finding,
  onSelectPatternRange,
}: {
  readonly finding: RegexRiskFinding;
  readonly onSelectPatternRange?: (range: SourceRange) => void;
}) {
  const content = (
    <>
      <span className={`analysis-risk-level is-${finding.severity}`}>
        {finding.severity}
      </span>
      <span>
        <strong>{finding.title}</strong>
        <small>
          {finding.evidence.replaceAll("-", " ")} · {finding.confidence}{" "}
          confidence · UTF-16 {finding.range.startUtf16}…
          {finding.range.endUtf16}
        </small>
      </span>
    </>
  );
  return (
    <article className={`analysis-finding is-${finding.severity}`}>
      {onSelectPatternRange ? (
        <button
          type="button"
          className="analysis-finding-title"
          onClick={() => onSelectPatternRange(finding.range)}
        >
          {content}
        </button>
      ) : (
        <div className="analysis-finding-title">{content}</div>
      )}
      <p>{finding.explanation}</p>
      <dl>
        <div>
          <dt>Example risk</dt>
          <dd>{finding.exampleRisk}</dd>
        </div>
        <div>
          <dt>Suggested investigation</dt>
          <dd>{finding.suggestedInvestigation}</dd>
        </div>
        <div>
          <dt>Limitations</dt>
          <dd>{finding.limitations}</dd>
        </div>
      </dl>
    </article>
  );
}

function TimeGrowthPlot({ result }: { readonly result: GrowthAnalysisResult }) {
  const measured = result.samples.filter(
    (
      sample,
    ): sample is (typeof result.samples)[number] & {
      readonly executionMs: number;
    } => sample.executionMs !== undefined,
  );
  const maximumTime = Math.max(
    0.01,
    ...measured.map((sample) => sample.executionMs),
  );
  const points = measured
    .map((sample, index) => {
      const x =
        measured.length === 1 ? 50 : (index / (measured.length - 1)) * 100;
      const y = 100 - (sample.executionMs / maximumTime) * 92;
      return `${x.toFixed(2)},${y.toFixed(2)}`;
    })
    .join(" ");

  return (
    <figure className="analysis-chart">
      <figcaption>
        <strong>Execution time by generated input</strong>
        <span>
          Maximum observed {formatMilliseconds(maximumTime)} · separate from
          outcome status
        </span>
      </figcaption>
      {measured.length > 0 ? (
        <svg
          viewBox="0 0 100 104"
          preserveAspectRatio="none"
          role="img"
          aria-label={`Execution-time growth plot with ${measured.length} completed samples`}
        >
          <path d="M0 100H100" className="analysis-chart-axis" />
          <polyline points={points} className="analysis-chart-line" />
          {points.split(" ").map((point) => {
            const [x, y] = point.split(",");
            return (
              <circle
                key={point}
                cx={x}
                cy={y}
                r="1.8"
                vectorEffect="non-scaling-stroke"
              />
            );
          })}
        </svg>
      ) : (
        <p className="analysis-empty-chart">No completed timing sample.</p>
      )}
    </figure>
  );
}

function OutcomeGrowthPlot({
  result,
}: {
  readonly result: GrowthAnalysisResult;
}) {
  return (
    <figure className="analysis-chart">
      <figcaption>
        <strong>Match and worker outcome by generated input</strong>
        <span>Timeout is distinct from non-match and crash</span>
      </figcaption>
      <div
        className="analysis-outcome-plot"
        role="img"
        aria-label={`Outcome plot with ${result.samples.length} samples`}
      >
        {result.samples.map((sample) => (
          <span
            key={`${sample.repetitions}-${sample.status}`}
            className={`is-${sample.status} ${
              sample.matched ? "is-match" : "is-no-match"
            }`}
            title={`${sample.repetitions.toLocaleString()} repetitions: ${
              sample.status
            }${sample.matched === undefined ? "" : sample.matched ? ", match" : ", no match"}`}
          />
        ))}
      </div>
      <div className="analysis-outcome-key" aria-hidden="true">
        <span className="is-match">match</span>
        <span className="is-no-match">no match</span>
        <span className="is-timeout">timeout</span>
        <span className="is-crash">crash</span>
      </div>
    </figure>
  );
}

function GrowthSummary({
  result,
  onSelectPatternRange,
}: {
  readonly result: GrowthAnalysisResult;
  readonly onSelectPatternRange?: (range: SourceRange) => void;
}) {
  return (
    <section className="analysis-result-block" aria-labelledby="growth-result">
      <header>
        <div>
          <p className="eyebrow">
            Generated subject family · bounded observation
          </p>
          <h3 id="growth-result">Dynamic growth result</h3>
        </div>
        <span className={`analysis-run-badge is-${result.status}`}>
          {result.status.replaceAll("-", " ")}
        </span>
      </header>
      <p className="analysis-stop-reason">{result.stoppedReason}</p>
      <div className="analysis-chart-grid">
        <TimeGrowthPlot result={result} />
        <OutcomeGrowthPlot result={result} />
      </div>
      <div className="analysis-table-scroll">
        <table aria-label="Dynamic growth samples">
          <thead>
            <tr>
              <th scope="col">Repetitions</th>
              <th scope="col">Input bytes</th>
              <th scope="col">Time</th>
              <th scope="col">Match result</th>
              <th scope="col">Status</th>
              <th scope="col">Normalized growth</th>
            </tr>
          </thead>
          <tbody>
            {result.samples.map((sample) => (
              <tr key={`${sample.repetitions}-${sample.status}`}>
                <td>{sample.repetitions.toLocaleString()}</td>
                <td>{sample.inputBytes.toLocaleString()}</td>
                <td>{formatMilliseconds(sample.executionMs)}</td>
                <td>
                  {sample.matched === undefined
                    ? "—"
                    : sample.matched
                      ? `${sample.matchCount?.toLocaleString() ?? "≥1"} match(es)`
                      : "no match"}
                </td>
                <td>{sample.status.replaceAll("-", " ")}</td>
                <td>
                  {sample.normalizedGrowth === undefined
                    ? "—"
                    : `${sample.normalizedGrowth.toFixed(2)}×`}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      {result.dynamicFindings.map((finding) => (
        <RiskFindingCard
          key={finding.id}
          finding={finding}
          onSelectPatternRange={onSelectPatternRange}
        />
      ))}
      <p className="analysis-advisory">
        These observations apply only to this generated input family, browser
        runtime and selected limits. They are not proof of general complexity or
        safety.
      </p>
    </section>
  );
}

function numericValue(
  value: string,
  label: string,
  minimum: number,
  maximum: number,
): number {
  const parsed = Number(value);
  if (
    value.trim() === "" ||
    !Number.isFinite(parsed) ||
    parsed < minimum ||
    parsed > maximum
  ) {
    throw new RangeError(`${label} must be from ${minimum} to ${maximum}.`);
  }
  return parsed;
}

export function AnalysisPanel({
  active,
  flavour,
  pattern,
  flags,
  subject,
  replacement,
  scanAll,
  syntax,
  onClose,
  onSelectPatternRange,
  createSupervisor = () => new AnalysisSupervisor(),
}: AnalysisPanelProps) {
  const [view, setView] = useState<AnalysisView>("risk");
  const [activeRun, setActiveRun] = useState<ActiveRun>();
  const [progressState, setProgressState] = useState<AnalysisProgress>();
  const [message, setMessage] = useState(
    "Analysis has not run for this configuration.",
  );
  const [error, setError] = useState<string>();
  const [benchmarkRecord, setBenchmarkRecord] = useState<{
    readonly configurationKey: symbol;
    readonly result: RegexBenchmarkResult;
  }>();
  const [growthRecord, setGrowthRecord] = useState<{
    readonly configurationKey: symbol;
    readonly result: GrowthAnalysisResult;
  }>();
  const [warmupIterations, setWarmupIterations] = useState(
    String(DEFAULT_BENCHMARK_SETTINGS.warmupIterations),
  );
  const [measuredIterations, setMeasuredIterations] = useState(
    String(DEFAULT_BENCHMARK_SETTINGS.measuredIterations),
  );
  const [benchmarkTimeoutMs, setBenchmarkTimeoutMs] = useState(
    String(DEFAULT_BENCHMARK_SETTINGS.sampleTimeoutMs),
  );
  const [growthPrefix, setGrowthPrefix] = useState(
    DEFAULT_GROWTH_SETTINGS.prefix,
  );
  const [growthFragment, setGrowthFragment] = useState(
    DEFAULT_GROWTH_SETTINGS.repeatedFragment,
  );
  const [growthSuffix, setGrowthSuffix] = useState(
    DEFAULT_GROWTH_SETTINGS.suffix,
  );
  const [growthStart, setGrowthStart] = useState(
    String(DEFAULT_GROWTH_SETTINGS.startingRepetitions),
  );
  const [growthMaximum, setGrowthMaximum] = useState(
    String(DEFAULT_GROWTH_SETTINGS.maximumRepetitions),
  );
  const [growthMultiplier, setGrowthMultiplier] = useState(
    String(DEFAULT_GROWTH_SETTINGS.multiplier),
  );
  const [growthSteps, setGrowthSteps] = useState(
    String(DEFAULT_GROWTH_SETTINGS.maximumSteps),
  );
  const [growthTimeoutMs, setGrowthTimeoutMs] = useState(
    String(DEFAULT_GROWTH_SETTINGS.sampleTimeoutMs),
  );
  const [growthMaximumBytes, setGrowthMaximumBytes] = useState(
    String(DEFAULT_GROWTH_SETTINGS.maximumSubjectBytes),
  );
  const [growthThreshold, setGrowthThreshold] = useState(
    String(DEFAULT_GROWTH_SETTINGS.normalizedGrowthThreshold),
  );
  const supervisor = useRef<AnalysisWorkerClient | undefined>(undefined);
  const abortController = useRef<AbortController | undefined>(undefined);
  const generation = useRef(0);

  const syntaxCurrent =
    syntax?.accepted === true &&
    syntax.root.raw === pattern &&
    syntax.root.support.flavour === flavour;
  const analysisAvailable = flavour === "ecmascript" && syntaxCurrent;
  const staticReport = useMemo(
    () =>
      syntaxCurrent && syntax
        ? analyseStaticRisk({
            flavour,
            root: syntax.root,
            flags,
            scanAll,
            replacement,
          })
        : undefined,
    [flavour, flags, replacement, scanAll, syntax, syntaxCurrent],
  );
  const configurationKey = useMemo(
    () =>
      Symbol(
        `analysis-${flavour}-${flags.join("")}-${pattern.length}-${subject.length}-${replacement.length}-${String(scanAll)}`,
      ),
    [flavour, flags, pattern, replacement, scanAll, subject],
  );
  const previousConfiguration = useRef(configurationKey);
  const benchmarkResult =
    benchmarkRecord?.configurationKey === configurationKey
      ? benchmarkRecord.result
      : undefined;
  const growthResult =
    growthRecord?.configurationKey === configurationKey
      ? growthRecord.result
      : undefined;

  const disposeActive = useCallback((abort = true) => {
    if (abort) abortController.current?.abort();
    supervisor.current?.cancel();
    supervisor.current?.dispose();
    supervisor.current = undefined;
    abortController.current = undefined;
  }, []);

  useEffect(() => {
    if (active) return;
    const stoppedGeneration = generation.current + 1;
    generation.current = stoppedGeneration;
    disposeActive();
    globalThis.queueMicrotask(() => {
      if (generation.current === stoppedGeneration) setActiveRun(undefined);
    });
  }, [active, disposeActive]);

  useEffect(() => {
    if (previousConfiguration.current === configurationKey) return;
    previousConfiguration.current = configurationKey;
    const stoppedGeneration = generation.current + 1;
    generation.current = stoppedGeneration;
    disposeActive();
    globalThis.queueMicrotask(() => {
      if (generation.current !== stoppedGeneration) return;
      setActiveRun(undefined);
      setBenchmarkRecord(undefined);
      setGrowthRecord(undefined);
      setError(undefined);
      setMessage(
        "Configuration changed; previous timing results were cleared.",
      );
    });
  }, [configurationKey, disposeActive]);

  useEffect(
    () => () => {
      generation.current += 1;
      disposeActive();
    },
    [disposeActive],
  );

  const beginRun = useCallback(
    (kind: ActiveRun) => {
      disposeActive();
      const currentSupervisor = createSupervisor();
      const controller = new AbortController();
      supervisor.current = currentSupervisor;
      abortController.current = controller;
      generation.current += 1;
      setActiveRun(kind);
      setError(undefined);
      setProgressState(undefined);
      setMessage(
        kind === "benchmark"
          ? "Starting bounded benchmark…"
          : "Starting bounded dynamic growth analysis…",
      );
      return {
        currentSupervisor,
        controller,
        runGeneration: generation.current,
      };
    },
    [createSupervisor, disposeActive],
  );

  const finishRun = useCallback(
    (runGeneration: number, currentSupervisor: AnalysisWorkerClient) => {
      currentSupervisor.dispose();
      if (supervisor.current === currentSupervisor) {
        supervisor.current = undefined;
        abortController.current = undefined;
      }
      if (generation.current === runGeneration) {
        setActiveRun(undefined);
        setProgressState(undefined);
      }
    },
    [],
  );

  const runBenchmark = async () => {
    if (!analysisAvailable) return;
    const run = beginRun("benchmark");
    try {
      const result = await runRegexBenchmark(
        {
          flavour: "ecmascript",
          pattern,
          flags,
          subject,
          replacement,
          scanAll,
          maximumMatches: DEFAULT_REGEX_LIMITS.maximumMatches,
          settings: {
            warmupIterations: numericValue(
              warmupIterations,
              "Warm-up iterations",
              0,
              100,
            ),
            measuredIterations: numericValue(
              measuredIterations,
              "Measured iterations",
              1,
              1_000,
            ),
            sampleTimeoutMs: numericValue(
              benchmarkTimeoutMs,
              "Sample timeout",
              25,
              DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs,
            ),
            maximumWallTimeMs: DEFAULT_REGEX_LIMITS.maximumBenchmarkWallTimeMs,
          },
        },
        run.currentSupervisor,
        {
          signal: run.controller.signal,
          onProgress: (next) => {
            if (generation.current !== run.runGeneration) return;
            setProgressState(next);
            setMessage(next.message);
          },
        },
      );
      if (generation.current !== run.runGeneration) return;
      setBenchmarkRecord({ configurationKey, result });
      setMessage(
        result.status === "complete"
          ? `Benchmark complete with ${result.completedMeasuredIterations.toLocaleString()} measured warm samples.`
          : (result.stoppedReason ??
              `Benchmark stopped with status ${result.status}.`),
      );
    } catch (cause) {
      if (generation.current !== run.runGeneration) return;
      const next =
        cause instanceof Error ? cause.message : "Benchmark could not start.";
      setError(next);
      setMessage("Benchmark settings require review.");
    } finally {
      finishRun(run.runGeneration, run.currentSupervisor);
    }
  };

  const growthSettings = (): GrowthSettings => ({
    prefix: growthPrefix,
    repeatedFragment: growthFragment,
    suffix: growthSuffix,
    startingRepetitions: numericValue(
      growthStart,
      "Starting repetitions",
      1,
      10_000_000,
    ),
    maximumRepetitions: numericValue(
      growthMaximum,
      "Maximum repetitions",
      1,
      10_000_000,
    ),
    multiplier: numericValue(growthMultiplier, "Growth multiplier", 1.1, 10),
    maximumSteps: numericValue(growthSteps, "Maximum steps", 1, 24),
    sampleTimeoutMs: numericValue(
      growthTimeoutMs,
      "Sample timeout",
      25,
      DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs,
    ),
    maximumWallTimeMs: DEFAULT_REGEX_LIMITS.maximumBenchmarkWallTimeMs,
    maximumSubjectBytes: numericValue(
      growthMaximumBytes,
      "Maximum generated bytes",
      1,
      DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes,
    ),
    normalizedGrowthThreshold: numericValue(
      growthThreshold,
      "Growth threshold",
      1.1,
      100,
    ),
  });

  const runGrowth = async () => {
    if (!analysisAvailable) return;
    const run = beginRun("growth");
    try {
      const result = await runGrowthAnalysis(
        {
          flavour: "ecmascript",
          pattern,
          flags,
          scanAll,
          maximumMatches: DEFAULT_REGEX_LIMITS.maximumMatches,
          settings: growthSettings(),
        },
        run.currentSupervisor,
        {
          signal: run.controller.signal,
          onProgress: (next) => {
            if (generation.current !== run.runGeneration) return;
            setProgressState(next);
            setMessage(next.message);
          },
        },
      );
      if (generation.current !== run.runGeneration) return;
      setGrowthRecord({ configurationKey, result });
      setMessage(result.stoppedReason);
    } catch (cause) {
      if (generation.current !== run.runGeneration) return;
      const next =
        cause instanceof Error
          ? cause.message
          : "Growth analysis could not start.";
      setError(next);
      setMessage("Growth settings require review.");
    } finally {
      finishRun(run.runGeneration, run.currentSupervisor);
    }
  };

  const cancel = () => {
    abortController.current?.abort();
    supervisor.current?.cancel();
    setMessage(
      activeRun === "benchmark"
        ? "Cancelling benchmark and terminating its worker…"
        : "Cancelling growth analysis and terminating its worker…",
    );
  };

  const progressMaximum = Math.max(1, progressState?.total ?? 1);
  const progressValue = Math.min(
    progressMaximum,
    progressState?.completed ?? 0,
  );

  return (
    <section
      className="panel analysis-panel"
      aria-labelledby="analysis-heading"
    >
      <header className="panel-heading">
        <div>
          <p className="eyebrow">Advisory structure + bounded observations</p>
          <h2 id="analysis-heading">Performance &amp; risk analysis</h2>
        </div>
        <div className="panel-heading-actions">
          <span className="provenance-badge">ECMAScript-specific</span>
          {onClose ? (
            <button
              type="button"
              className="icon-button"
              aria-label="Close analysis"
              data-dialog-initial-focus
              onClick={onClose}
            >
              ×
            </button>
          ) : null}
        </div>
      </header>

      <div
        className="analysis-view-tabs"
        role="tablist"
        aria-label="Analysis view"
      >
        <button
          id="analysis-risk-tab"
          type="button"
          role="tab"
          aria-selected={view === "risk"}
          aria-controls="analysis-risk-panel"
          onClick={() => setView("risk")}
        >
          Risk &amp; growth
        </button>
        <button
          id="analysis-benchmark-tab"
          type="button"
          role="tab"
          aria-selected={view === "benchmark"}
          aria-controls="analysis-benchmark-panel"
          onClick={() => setView("benchmark")}
        >
          Benchmark
        </button>
      </div>

      {flavour !== "ecmascript" ? (
        <p className="analysis-unavailable" role="alert">
          The current analyser supports ECMAScript 2025 only. It will not apply
          ECMAScript heuristics or native-browser timing claims to {flavour}.
        </p>
      ) : !syntaxCurrent ? (
        <p className="analysis-unavailable" role="alert">
          Analysis requires a current pattern accepted by the ECMAScript syntax
          provider.
        </p>
      ) : null}

      <div className="analysis-status" role="status" aria-live="polite">
        <span>{activeRun ? "running" : "ready"}</span>
        <strong>{message}</strong>
      </div>
      {progressState ? (
        <div className="analysis-progress">
          <progress max={progressMaximum} value={progressValue} />
          <span>
            {progressState.completed.toLocaleString()} /{" "}
            {progressState.total.toLocaleString()}
          </span>
        </div>
      ) : null}
      {error ? (
        <p className="analysis-error" role="alert">
          {error}
        </p>
      ) : null}

      {view === "risk" ? (
        <div
          id="analysis-risk-panel"
          className="analysis-content"
          role="tabpanel"
          aria-labelledby="analysis-risk-tab"
        >
          <section className="analysis-section" aria-labelledby="static-risk">
            <header>
              <div>
                <p className="eyebrow">Static · normalized ECMAScript AST</p>
                <h3 id="static-risk">Potential risk findings</h3>
              </div>
              {staticReport ? (
                <span>
                  {staticReport.analysedNodes.toLocaleString()} nodes reviewed
                </span>
              ) : null}
            </header>
            {staticReport ? (
              <>
                <p className="analysis-summary">{staticReport.summary}</p>
                <div className="analysis-findings">
                  {staticReport.findings.map((finding) => (
                    <RiskFindingCard
                      key={finding.id}
                      finding={finding}
                      onSelectPatternRange={onSelectPatternRange}
                    />
                  ))}
                </div>
                <details className="analysis-details">
                  <summary>Static analyser scope and limitations</summary>
                  <ul>
                    {staticReport.limitations.map((limitation) => (
                      <li key={limitation}>{limitation}</li>
                    ))}
                  </ul>
                </details>
              </>
            ) : (
              <p className="analysis-summary">
                Static findings are unavailable until the active pattern has a
                current supported syntax tree.
              </p>
            )}
          </section>

          <section
            className="analysis-section"
            aria-labelledby="dynamic-growth"
          >
            <header>
              <div>
                <p className="eyebrow">
                  Dynamic · killable native-engine worker
                </p>
                <h3 id="dynamic-growth">Bounded growth probe</h3>
              </div>
              <span>30 s aggregate hard stop</span>
            </header>
            <p className="analysis-advisory">
              Subjects are built as prefix + repeated fragment + suffix. Use a
              representative near-miss; generation stops on timeout, selected
              growth threshold, byte bound, repetition bound, step bound or
              cancellation.
            </p>
            <div className="analysis-growth-text-controls">
              <label>
                <span>Prefix</span>
                <input
                  type="text"
                  value={growthPrefix}
                  maxLength={16_384}
                  onChange={(event) => setGrowthPrefix(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Repeated fragment</span>
                <input
                  type="text"
                  value={growthFragment}
                  maxLength={4_096}
                  onChange={(event) => setGrowthFragment(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Suffix</span>
                <input
                  type="text"
                  value={growthSuffix}
                  maxLength={16_384}
                  onChange={(event) => setGrowthSuffix(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
            </div>
            <div className="analysis-number-controls">
              <label>
                <span>Start repetitions</span>
                <input
                  type="number"
                  min={1}
                  max={10_000_000}
                  value={growthStart}
                  onChange={(event) => setGrowthStart(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Maximum repetitions</span>
                <input
                  type="number"
                  min={1}
                  max={10_000_000}
                  value={growthMaximum}
                  onChange={(event) => setGrowthMaximum(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Multiplier</span>
                <input
                  type="number"
                  min={1.1}
                  max={10}
                  step={0.1}
                  value={growthMultiplier}
                  onChange={(event) => setGrowthMultiplier(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Maximum steps</span>
                <input
                  type="number"
                  min={1}
                  max={24}
                  value={growthSteps}
                  onChange={(event) => setGrowthSteps(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Per-sample timeout</span>
                <select
                  value={growthTimeoutMs}
                  onChange={(event) => setGrowthTimeoutMs(event.target.value)}
                  disabled={activeRun !== undefined}
                >
                  <option value="100">100 ms</option>
                  <option value="250">250 ms</option>
                  <option value="1000">1 s</option>
                  <option value="2000">2 s</option>
                  <option value="5000">5 s</option>
                </select>
              </label>
              <label>
                <span>Maximum generated input</span>
                <select
                  value={growthMaximumBytes}
                  onChange={(event) =>
                    setGrowthMaximumBytes(event.target.value)
                  }
                  disabled={activeRun !== undefined}
                >
                  <option value={64 * 1024}>64 KiB</option>
                  <option value={256 * 1024}>256 KiB</option>
                  <option value={1024 * 1024}>1 MiB</option>
                  <option value={4 * 1024 * 1024}>4 MiB</option>
                </select>
              </label>
              <label>
                <span>Normalized-growth stop</span>
                <input
                  type="number"
                  min={1.1}
                  max={100}
                  step={0.5}
                  value={growthThreshold}
                  onChange={(event) => setGrowthThreshold(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
            </div>
            <div className="analysis-run-actions">
              <button
                type="button"
                className="primary-button"
                disabled={!analysisAvailable || activeRun !== undefined}
                onClick={() => void runGrowth()}
              >
                Run bounded growth probe
              </button>
              {activeRun === "growth" ? (
                <button
                  type="button"
                  className="secondary-button"
                  onClick={cancel}
                >
                  Cancel growth probe
                </button>
              ) : null}
            </div>
            {growthResult ? (
              <GrowthSummary
                result={growthResult}
                onSelectPatternRange={onSelectPatternRange}
              />
            ) : null}
          </section>
        </div>
      ) : (
        <div
          id="analysis-benchmark-panel"
          className="analysis-content"
          role="tabpanel"
          aria-labelledby="analysis-benchmark-tab"
        >
          <section
            className="analysis-section"
            aria-labelledby="benchmark-runner"
          >
            <header>
              <div>
                <p className="eyebrow">
                  Cold first use + warm measured samples
                </p>
                <h3 id="benchmark-runner">Bounded benchmark</h3>
              </div>
              <span>
                {utf8ByteLength(subject).toLocaleString()} subject bytes
              </span>
            </header>
            <p className="analysis-advisory">
              Each timing sample measures compile, first match, all matches and
              replacement separately in the native ECMAScript worker. Worker
              startup is reported only as cold start. Trace mode is never used.
            </p>
            <div className="analysis-number-controls">
              <label>
                <span>Warm-up samples</span>
                <input
                  type="number"
                  min={0}
                  max={100}
                  value={warmupIterations}
                  onChange={(event) => setWarmupIterations(event.target.value)}
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Measured samples</span>
                <input
                  type="number"
                  min={1}
                  max={1_000}
                  value={measuredIterations}
                  onChange={(event) =>
                    setMeasuredIterations(event.target.value)
                  }
                  disabled={activeRun !== undefined}
                />
              </label>
              <label>
                <span>Per-sample timeout</span>
                <select
                  value={benchmarkTimeoutMs}
                  onChange={(event) =>
                    setBenchmarkTimeoutMs(event.target.value)
                  }
                  disabled={activeRun !== undefined}
                >
                  <option value="100">100 ms</option>
                  <option value="250">250 ms</option>
                  <option value="1000">1 s</option>
                  <option value="2000">2 s</option>
                  <option value="5000">5 s</option>
                  <option value="10000">10 s</option>
                </select>
              </label>
            </div>
            <dl className="analysis-input-summary">
              <div>
                <dt>Pattern</dt>
                <dd>
                  {pattern.length.toLocaleString()} UTF-16 units · flags{" "}
                  <code>{flags.join("") || "(none)"}</code>
                </dd>
              </div>
              <div>
                <dt>Subject</dt>
                <dd>
                  {subject.length.toLocaleString()} UTF-16 units ·{" "}
                  {utf8ByteLength(subject).toLocaleString()} bytes
                </dd>
              </div>
              <div>
                <dt>Replacement</dt>
                <dd>
                  {replacement.length.toLocaleString()} UTF-16 units · output
                  estimate is bounded before timing
                </dd>
              </div>
            </dl>
            <div className="analysis-run-actions">
              <button
                type="button"
                className="primary-button"
                disabled={!analysisAvailable || activeRun !== undefined}
                onClick={() => void runBenchmark()}
              >
                Run bounded benchmark
              </button>
              {activeRun === "benchmark" ? (
                <button
                  type="button"
                  className="secondary-button"
                  onClick={cancel}
                >
                  Cancel benchmark
                </button>
              ) : null}
            </div>
            {benchmarkResult ? (
              <BenchmarkSummary result={benchmarkResult} />
            ) : null}
          </section>
        </div>
      )}
    </section>
  );
}
