<script lang="ts">
  /**
   * The baseline is generated once per prompt and then held fixed. It mirrors
   * whatever stage the watermark panel is on, so every claim on the right has
   * an unwatermarked control on the left.
   */
  import {
    baseline,
    wizards,
    activeMethod,
    algoParams,
    baselineCollapsed,
    biasStepIndex,
    editorDetect,
    requestDetect,
    METHOD_META,
  } from '../lib/stores/app-state';
  import { kirchenbauerSeed, greenUniform, keyFromSeed } from '../lib/utils/hashing';
  import { isGreen } from '../lib/watermark/kirchenbauer';
  import ProbBars from './shared/ProbBars.svelte';
  import type { BarRow } from './shared/ProbBars.svelte';

  const method = $derived($activeMethod);
  const wizard = $derived($wizards[method]);
  const stage = $derived(wizard.stage);
  const dist = $derived($baseline.distribution);
  const result = $derived($baseline.result);
  const stageLabel = $derived(METHOD_META[method].stages[stage - 1]);

  const BKEY = 'baseline';
  const bundle = $derived($editorDetect[BKEY]?.detections ?? null);

  // Score the baseline text with the same secrets whenever we reach a stage
  // that compares classifications.
  $effect(() => {
    const text = result?.text;
    if (!text) return;
    if (stage >= 3) requestDetect(BKEY, text, false, 0);
  });

  const kirchDet = $derived(bundle?.kirchenbauer ?? null);
  const ksemDet = $derived(bundle?.ksemstamp ?? null);
  const sealDet = $derived(bundle?.textseal ?? null);

  /** Mirror the exact step the watermark panel is illustrating. */
  const steps = $derived(result?.trace.steps ?? []);
  const stepIdx = $derived(Math.min($biasStepIndex, Math.max(0, steps.length - 1)));
  const mirroredStep = $derived(steps[stepIdx]);

  const barRows = $derived.by((): BarRow[] => {
    if (mirroredStep) {
      return mirroredStep.topCandidates.map((c) => ({
        tokenId: c.tokenId,
        text: c.tokenText,
        before: c.prob,
        kind: 'neutral' as const,
      }));
    }
    if (!dist) return [];
    return dist.candidates.slice(0, 12).map((c) => ({
      tokenId: c.tokenId,
      text: c.text,
      before: c.prob,
      kind: 'neutral' as const,
    }));
  });

  const greenSummary = $derived.by(() => {
    if (method !== 'kirchenbauer' || !kirchDet) return null;
    const units = kirchDet.perUnit ?? [];
    const scored = units.filter((u) => !u.skipped);
    const green = scored.filter((u) => u.green).length;
    return { green, total: scored.length };
  });
</script>

<section class="panel base" class:collapsed={$baselineCollapsed}>
  <header>
    <div>
      <strong>Baseline</strong>
      <span class="badge">no watermark</span>
    </div>
    <button class="toggle" onclick={() => baselineCollapsed.update((v) => !v)}>
      {$baselineCollapsed ? 'show' : 'hide'}
    </button>
  </header>

  {#if !$baselineCollapsed}
    <div class="mirror small dim">Mirroring stage {stage}: {stageLabel}</div>

    {#if $baseline.status === 'idle'}
      <p class="dim">Enter a prompt above and generate the baseline to begin.</p>
    {:else if $baseline.status === 'running'}
      <p class="text-box stream">{$baseline.streamingText || 'generating…'}</p>
    {:else if $baseline.status === 'error'}
      <p class="err small mono">{$baseline.error}</p>
    {:else if result}
      {#if stage === 1}
        <!--
          Nothing to mirror here: there is no secret on this side, so there is
          no split, no cluster rule and no R values. Showing a greyed-out
          version of the watermark's artefacts would invent a comparison that
          does not exist. The text itself is the control.
        -->
        <p class="dim small">
          Written once for this prompt and then left alone — the control the watermarked text on the
          right is measured against.
        </p>
        <p class="text-box">{result.text}</p>
      {:else if stage === 2}
        {#if method === 'ksemstamp'}
          <p class="dim small">
            The baseline wrote every sentence in one pass. Nothing was embedded, judged or thrown
            away — the cost the watermark on the right is paying does not exist here.
          </p>
          <ol class="text-box list sentences">
            {#each result.text.split(/(?<=[.!?])\s+/).filter(Boolean) as s}
              <li><span class="badge">kept</span><span>{s}</span></li>
            {/each}
          </ol>
        {:else}
          <p class="dim small">
            The unmodified distribution at step {stepIdx} — this is what the watermark on the right
            is transforming. Baseline sampling picked
            <span class="mono">"{mirroredStep?.chosenTokenText ?? ''}"</span> here.
          </p>
          <ProbBars
            rows={barRows}
            chosenTokenId={mirroredStep?.chosenTokenId ?? null}
            showAfter={false}
          />
        {/if}
      {:else if stage === 3 || stage === 4}
        {#if method === 'kirchenbauer'}
          <p class="dim small">
            The same secret applied to unwatermarked text. Green appears only at the rate chance
            allows.
          </p>
          {#if kirchDet}
            <div class="text-box">
              {#each kirchDet.perUnit ?? [] as u}
                <span class={u.skipped ? 'tok-skip' : u.green ? 'tok-green' : 'tok-red'}
                  >{u.label}</span
                >
              {/each}
            </div>
            {#if greenSummary}
              <div class="small dim">
                {greenSummary.green} of {greenSummary.total} scored tokens green
                (<span class="mono"
                  >{((greenSummary.green / Math.max(1, greenSummary.total)) * 100).toFixed(0)}%</span
                >, expected {Math.round($algoParams.kirchenbauer.gamma * 100)}%)
              </div>
            {/if}
          {:else}
            <p class="dim small">scoring…</p>
          {/if}
        {:else if method === 'ksemstamp'}
          <p class="dim small">Baseline sentences judged against the same valid regions.</p>
          {#if ksemDet}
            <ol class="text-box list sentences">
              {#each ksemDet.perUnit ?? [] as u}
                <li class={u.skipped ? '' : u.green ? 'valid' : 'invalid'}>
                  <span class="badge {u.skipped ? '' : u.green ? 'ok' : 'bad'}">
                    {u.skipped ? 'seed' : u.green ? 'valid' : 'invalid'}
                  </span>
                  <span>{u.label}</span>
                </li>
              {/each}
            </ol>
          {:else}
            <p class="dim small">needs at least two sentences…</p>
          {/if}
        {:else if sealDet}
          <p class="dim small">The same key over unwatermarked text: evidence stays flat.</p>
          <div class="text-box plain mono heat">
            {#each sealDet.perUnit ?? [] as u}
              {@const s = u.score ?? 0}
              <span
                style="background: rgba(29,95,208,{Math.min(0.55, 0.05 + 0.14 * s).toFixed(2)})"
                title={`s=${s.toFixed(2)}`}>{u.label}</span
              >
            {/each}
          </div>
        {:else}
          <p class="dim small">scoring…</p>
        {/if}

        {#if stage === 4}
          <div class="scores">
            {#each [['Kirchenbauer', kirchDet], ['k-SemStamp', ksemDet], ['TextSeal', sealDet]] as const as [label, d]}
              <div class="score">
                <div class="small dim">{label}</div>
                {#if d}
                  <div class="mono stat">{d.statistic.toFixed(2)}</div>
                  <span class="badge {d.inconclusive ? 'warn' : d.watermarked ? 'ok' : 'bad'}">
                    {d.inconclusive ? 'inconclusive' : d.watermarked ? 'detected' : 'not detected'}
                  </span>
                {:else}
                  <div class="dim small">n/a</div>
                {/if}
              </div>
            {/each}
          </div>
        {/if}
      {/if}

      {#if stage === 2}
        <details class="raw">
          <summary class="small dim">baseline text</summary>
          <p class="text-box">{result.text}</p>
        </details>
      {/if}

      <div class="meta small dim mono">
        {result.tokenIds.length} tok · {(result.timings.totalMs / 1000).toFixed(1)}s ·
        {result.timings.tokensPerSec.toFixed(1)} tok/s
      </div>
    {/if}
  {/if}
</section>

<style>
  .base {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
  }
  header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .toggle {
    font-size: 12px;
    padding: 2px 8px;
  }
  .mirror {
    border-bottom: 1px solid var(--border);
    padding-bottom: 6px;
  }
  .heat {
    font-size: 13px;
    line-height: 2;
  }
  .stream {
    opacity: 0.75;
  }
  .sentences {
    font-size: 13.5px;
  }
  .sentences li {
    display: flex;
    gap: 8px;
    align-items: baseline;
    padding: 5px 9px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--panel-2);
  }
  .sentences li.valid {
    background: var(--green-bg);
    border-color: var(--green-border);
  }
  .sentences li.invalid {
    background: var(--red-bg);
    border-color: var(--red-border);
  }
  .scores {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    border-top: 1px solid var(--border);
    padding-top: 8px;
  }
  .score {
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 6px 8px;
  }
  .stat {
    font-size: 17px;
    font-weight: 650;
  }
  .meta {
    border-top: 1px solid var(--border);
    padding-top: 6px;
  }
  .err {
    color: var(--red);
  }
</style>
