<script lang="ts">
  /**
   * The Zwift Click (FR-1.4, FR-9.1–9.2).
   *
   * A Click v2 is two peripherals, but it is **one controller**: connected on
   * its own, the `−` pod delivers all ten buttons — its own paddle and D-pad,
   * *and* the `+` paddle and face buttons relayed from its twin (§2.3.1,
   * confirmed on this hardware 2026-08-21). So one link is the whole thing, and
   * this panel says so rather than presenting two halves that both look
   * required. Pairing both is not merely redundant: it is the configuration in
   * which the `−` pod stops reporting its own paddle.
   *
   * The `+` pod keeps a row of its own, because it is the fallback that matters
   * when the `−` pod is flat or left in the garage.
   *
   * They are named for the shift paddle each carries, not for the side of the
   * bar. Nothing a pod advertises says which end of the handlebar it is clamped
   * to, so left and right would be a guess; the paddle is printed on the pod,
   * and pressing it settles the question on screen (`confirmed`).
   *
   * The second job of this panel is to say what to *do* when nothing is
   * connected — a Click sleeps within seconds and only advertises while awake
   * (A-4), which no rider can guess from the words "not connected". But that is
   * the only thing worth a sentence, and only while it is true: connected, this
   * panel is two lines and a badge. The five-step drill it used to open with
   * lives behind a summary now, and the pods' MAC addresses are gone — they
   * identified nothing a rider could act on.
   */
  import { app } from '../lib/app.svelte';
  import { api, type Pod, type PodState } from '../lib/bridge';

  const controller = $derived(app.controller);
  const pods = $derived(controller ? [controller.minus, controller.plus] : []);
  /** Auto-connect rides on the device scan, so a stopped scan is a reason a
   *  pod stays missing — and the fix belongs next to the symptom. */
  const scanning = $derived(app.devices.scanning);
  const anyConnected = $derived(pods.some((p) => p.state === 'connected'));
  /** The pod that speaks for the pair. Connected, this is the whole controller. */
  const minusLive = $derived(controller?.minus.state === 'connected');
  /** Running on the fallback: the `+` pod alone, with no `−` paddle to shift
   *  down with beyond its `Y` button. Worth saying out loud. */
  const plusOnly = $derived(!minusLive && controller?.plus.state === 'connected');
  const busy = $derived(pods.some((p) => p.state === 'searching'));
  /** A pod reporting the other's paddle: the pair may be filed the wrong way
   *  round, and the rider is the only one who can say. */
  const mixedUp = $derived(pods.some((p) => p.contradicted));

  const STATE_TEXT: Record<PodState, string> = {
    idle: 'Not connected',
    searching: 'Searching…',
    connected: 'Connected',
    reconnecting: 'Reconnecting…',
    gaveUp: 'Gave up',
  };

  const STATE_TONE: Record<PodState, string> = {
    idle: 'tone-idle',
    searching: 'tone-warn',
    connected: 'tone-ok',
    reconnecting: 'tone-warn',
    gaveUp: 'tone-bad',
  };

  /** What each pod is for, so a rider who has lost one knows what they lost. */
  const PURPOSE: Record<Pod, string> = {
    minus: 'All ten buttons',
    plus: 'Fallback · shift up, A B Y Z',
  };

  function connect(pod: Pod) {
    app.run(() => api.connectController(pod));
  }

  function disconnect(pod: Pod) {
    app.run(() => api.disconnectController(pod));
  }
</script>

<section class="click">
  <header>
    <h2>Zwift Click</h2>
    <div class="actions">
      {#if !scanning}
        <!-- Nothing can be picked up automatically while the scan is off, so
             the way to fix that sits here rather than only in the header. -->
        <button class="btn" onclick={() => app.run(() => api.startScan())}>Start scan</button>
      {:else if !minusLive}
        <!-- The − pod, not both: it is the one that carries the whole
             controller. The + pod has its own button on its own row. -->
        <button class="btn" disabled={busy} onclick={() => app.run(() => api.connectController())}>
          {busy ? 'Searching…' : 'Find − pod'}
        </button>
      {/if}
      {#if anyConnected}
        <button class="btn ghost" onclick={() => app.run(() => api.disconnectController())}>
          Disconnect
        </button>
      {/if}
    </div>
  </header>

  <!-- Only while it is telling the rider something they cannot see. Connected,
       the badge above has already said it. -->
  {#if !scanning}
    <p class="lede"><strong>The scan is off</strong> — pods will not be picked up.</p>
  {:else if plusOnly}
    <p class="lede">
      Running on the <strong>+ pod alone</strong>: shift down with <span class="kbd">Y</span>.
      Press a button on the − pod for the D-pad.
    </p>
  {:else if !minusLive}
    <p class="lede">
      <strong>Press any button on the − pod.</strong> It only advertises while awake; the running
      scan connects it as soon as it does.
    </p>
  {/if}

  <div class="pods">
    {#each pods as pod (pod.pod)}
      {@const dormant = pod.pod === 'plus' && minusLive && pod.state !== 'connected'}
      <article class="pod" class:live={pod.state === 'connected'}>
        <!-- The paddle glyph is the pod's identity — big enough to match
             against the one printed on the hardware at arm's length. -->
        <span class="paddle" class:on={pod.state === 'connected'}>{pod.symbol}</span>

        <div class="what">
          <span class="pod-name">{pod.symbol} pod</span>
          <span class="purpose">
            {#if dormant}
              <!-- Not a fault, and the panel must not let it read as one: this
                   pod is idle because the − pod is already sending its buttons. -->
              Relayed by the − pod
            {:else if pod.confirmed}
              Confirmed — sent its own {pod.symbol} paddle
            {:else if pod.state === 'connected'}
              Press its {pod.symbol} paddle to confirm
            {:else}
              {PURPOSE[pod.pod]}
            {/if}
          </span>
        </div>

        <span class="facts">
          {#if pod.batteryPercent != null}<span>{pod.batteryPercent}%</span>{/if}
          <!-- Connected and silent looks exactly like working until you press
               something, so the count is the honest test of the link. -->
          {#if pod.state === 'connected'}
            <span>{pod.buttonsSeen === 0 ? 'no presses yet' : `${pod.buttonsSeen} presses`}</span>
          {/if}
        </span>

        <span class="state {dormant ? 'tone-idle' : STATE_TONE[pod.state]}">
          <span class="dot"></span>{dormant ? 'Standing by' : STATE_TEXT[pod.state]}
        </span>

        <div class="controls">
          {#if pod.state === 'connected' || pod.state === 'reconnecting'}
            <button class="btn ghost" onclick={() => disconnect(pod.pod)}>Disconnect</button>
          {:else if pod.state === 'searching'}
            <button class="btn ghost" onclick={() => disconnect(pod.pod)}>Stop</button>
          {:else}
            <button class="btn ghost" onclick={() => connect(pod.pod)}>
              {dormant ? 'Connect anyway' : 'Find it'}
            </button>
          {/if}
        </div>

        <!-- Verbatim (FR-9.2). Rust writes these as instructions, not codes. -->
        {#if pod.error}<p class="note tone-bad">{pod.error}</p>{/if}
      </article>
    {/each}

    {#if pods.length === 0}
      <p class="note">Waiting for the controller supervisor…</p>
    {/if}
  </div>

  {#if mixedUp || controller?.swapped}
    <div class="swap">
      <span>
        {mixedUp
          ? 'A pod is sending the other pod’s paddle — the pair may be filed the wrong way round.'
          : 'The pods are swapped from what their advertisement claims.'}
      </span>
      <button class="btn ghost" onclick={() => app.run(() => api.swapControllerPods())}>
        Swap + / −
      </button>
    </div>
  {/if}

  {#if !minusLive}
    <!--
      FR-1.8. "Not connected" on its own reads as a broken app, and the real
      cause — a pod that is simply asleep — is something only the rider can fix.
      The lede above says that much; the rest is for the rider it did not work
      for, and stays folded until they ask, because it is five paragraphs of
      things that are usually already true.
    -->
    <details class="help">
      <summary>Still not connecting?</summary>
      <ul>
        <li><strong>Press a button.</strong> A pod that is asleep does not advertise at all.</li>
        <li><strong>Keep the scan on.</strong> Auto-connect runs off it.</li>
        <li><strong>Close Zwift.</strong> One app at a time holds a pod.</li>
        <li><strong>Charge it.</strong> A flat pod stops advertising, and the app stops chasing.</li>
        <li>
          <strong>Or use the + pod</strong> — you lose the D-pad, <span class="kbd">Y</span> still
          shifts down.
        </li>
        <li>
          The keyboard mirrors every Click action; <span class="kbd">?</span> lists them. A ride
          never depends on a pod.
        </li>
      </ul>
    </details>
  {/if}
</section>

<style>
  .click {
    margin: 0 var(--edge) 0.7rem;
    padding: 0.75rem 0.9rem 0.85rem;
    border-radius: 0.7rem;
    background: var(--bg-lift);
  }

  header {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
  }

  h2 {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -0.01em;
  }

  .lede {
    margin: 0.45rem 0 0;
    font-size: 0.84rem;
    line-height: 1.45;
    color: var(--ink-dim);
  }

  .lede strong {
    color: var(--ink-soft);
  }

  .actions {
    display: flex;
    gap: 0.4rem;
    margin-left: auto;
  }

  .actions .btn {
    padding: 0.45em 0.8em;
    font-size: 0.85rem;
  }

  .pods {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin-top: 0.6rem;
  }

  /* A row, not a card: two pods stacked read as one controller with a fallback,
     which is what they are. Two equal cards read as two things to pair. */
  .pod {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.4rem 0.7rem;
    padding: 0.45rem 0.55rem;
    border-radius: 0.5rem;
    border: 1px solid transparent;
    background: rgba(255, 255, 255, 0.015);
  }

  .pod.live {
    border-color: color-mix(in srgb, var(--ok) 30%, transparent);
  }

  .paddle {
    display: grid;
    place-items: center;
    width: 1.9rem;
    height: 1.9rem;
    border-radius: 0.45rem;
    background: var(--hairline);
    color: var(--ink-dim);
    font-size: 1.15rem;
    font-weight: 300;
    line-height: 1;
    flex: none;
  }

  .paddle.on {
    background: color-mix(in srgb, var(--ok) 18%, transparent);
    color: var(--ok);
  }

  .what {
    display: flex;
    flex-direction: column;
    flex: 1 1 10rem;
    min-width: 0;
  }

  /* Not `.label`: that is app.css's uppercase, letter-spaced section label,
     and a pod's name is a name, not a heading. */
  .pod-name {
    font-size: 0.95rem;
    font-weight: 600;
  }

  .purpose {
    font-size: 0.76rem;
    color: var(--ink-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .facts {
    display: flex;
    gap: 0.5rem;
    font-size: 0.8rem;
    color: var(--ink-dim);
    white-space: nowrap;
  }

  .state {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    font-size: 0.84rem;
    font-weight: 600;
    white-space: nowrap;
  }

  .controls {
    display: flex;
    gap: 0.4rem;
    margin-left: auto;
  }

  .controls .btn {
    padding: 0.4em 0.7em;
    font-size: 0.82rem;
  }

  .note {
    flex: 1 1 100%;
    margin: 0;
    font-size: 0.82rem;
    color: var(--ink-soft);
    line-height: 1.4;
  }

  .swap {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-top: 0.5rem;
    padding: 0.45rem 0.7rem;
    border-radius: 0.5rem;
    background: rgba(255, 207, 74, 0.07);
    color: var(--ink-soft);
    font-size: 0.83rem;
  }

  .swap button {
    margin-left: auto;
  }

  .help {
    margin-top: 0.55rem;
    font-size: 0.83rem;
    color: var(--ink-dim);
  }

  summary {
    cursor: pointer;
    color: var(--ink-dim);
    font-weight: 600;
    min-height: var(--touch-min);
    display: flex;
    align-items: center;
  }

  /* `display: flex` on a summary drops the browser's own marker, and a
     disclosure with nothing to disclose-looking about it does not read as one. */
  summary::before {
    content: '';
    width: 0;
    height: 0;
    margin-right: 0.5rem;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    opacity: 0.6;
    transition: transform 120ms ease;
  }

  details[open] > summary::before {
    transform: rotate(90deg);
  }

  .help ul {
    margin: 0.2rem 0 0;
    padding-left: 1.1rem;
    line-height: 1.6;
  }

  .help strong {
    color: var(--ink-soft);
  }

  /* Narrow: the state and the buttons drop below the pod's name rather than
     crushing it. */
  :global([data-size='compact']) .facts,
  :global([data-size='compact']) .state {
    order: 3;
  }

  :global([data-size='compact']) .controls {
    order: 4;
    flex: 1 1 100%;
    margin-left: 2.3rem;
  }
</style>
