<script lang="ts">
  /**
   * Connection screen (FR-9.1–9.3).
   *
   * The one thing this screen must get right: **connected is not
   * controllable**. A trainer can be attached at the BLE level and still refuse
   * the FTMS control point, in which case nothing you do moves the resistance.
   * So every trainer row carries two independent states, side by side, and the
   * ride screen stays gated until control is real.
   */
  import { app } from '../lib/app.svelte';
  import { api } from '../lib/bridge';
  import { connectionText, rssiBars } from '../lib/format';
  import type { DeviceInfo, DeviceKind } from '../lib/types';
  import ClickPanel from './ClickPanel.svelte';

  const devices = $derived(app.devices.devices);
  const scanning = $derived(app.devices.scanning);
  const trainerReady = $derived(app.trainerReady);

  const KIND_LABEL: Record<DeviceKind, string> = {
    trainer: 'Smart trainer · FTMS',
    // Named for the shift paddle, which is printed on the pod — unlike left and
    // right, which nothing in the advertisement actually tells us (§2.3.1).
    clickMinus: 'Zwift Click · − pod',
    clickPlus: 'Zwift Click · + pod',
    heartRate: 'Heart rate monitor',
    unknown: 'Unidentified',
  };

  function isConnected(d: DeviceInfo): boolean {
    return d.state === 'Connected' || d.state === 'Controlling';
  }

  function busy(d: DeviceInfo): boolean {
    return d.state === 'Connecting' || d.state === 'Reconnecting';
  }
</script>

<div class="screen">
  <header>
    <div>
      <h1>Devices</h1>
      <p>
        {#if scanning}
          Scanning for Bluetooth peripherals…
        {:else}
          Scan stopped.
        {/if}
      </p>
    </div>
    <div class="actions">
      {#if scanning}
        <button class="btn ghost" onclick={() => app.run(() => api.stopScan())}>Stop scan</button>
      {:else}
        <button class="btn ghost" onclick={() => app.run(() => api.startScan())}>Scan</button>
      {/if}
      <!-- Disabled until control is real: there is no ride without a trainer,
           and offering one would be offering a session that records nothing. -->
      <button
        class="btn primary"
        disabled={!trainerReady}
        title={trainerReady ? '' : 'Connect a trainer and acquire FTMS control first'}
        onclick={() => app.goToRide()}
      >
        Go to ride
      </button>
    </div>
  </header>

  <!--
    The Click gets a panel of its own above the device list rather than two more
    rows in it. It is a different kind of thing with a different failure mode —
    two pods that sleep in seconds and must be woken by hand — and the one thing
    the old single line could not do was say *which* pod was missing (FR-1.4).
  -->
  <ClickPanel />

  {#if !trainerReady}
    <div class="gate">
      <span class="dot tone-warn"></span>
      <span
        >No trainer under control yet. The ride screen stays <strong>locked</strong> until an FTMS
        trainer accepts the control point.</span
      >
    </div>
  {/if}

  <div class="list">
    {#each devices as device (device.id)}
      {@const conn = connectionText(device.state)}
      {@const bars = rssiBars(device.rssi)}
      <article class="row" class:live={isConnected(device)}>
        <div class="identity">
          <span class="name">{device.name}</span>
          <span class="meta">{KIND_LABEL[device.kind]} · {device.address}</span>
          {#if device.error}
            <span class="error">{device.error}</span>
          {/if}
        </div>

        <div class="signal" title="{device.rssi} dBm">
          <span class="bars">
            {#each [1, 2, 3, 4] as bar}
              <i class:on={bar <= bars} style:height="{bar * 25}%"></i>
            {/each}
          </span>
          <span class="dbm">{device.rssi}</span>
        </div>

        <!-- Two states, never conflated (FR-9.3). -->
        <div class="states">
          <span class="state">
            <span class="label">Bluetooth</span>
            <span class="value {conn.tone === 'ok' ? 'tone-ok' : `tone-${conn.tone}`}">
              <span class="dot"></span>{busy(device) ? conn.label + '…' : conn.label}
            </span>
          </span>

          {#if device.kind === 'trainer'}
            <span class="state">
              <span class="label">FTMS control</span>
              <span class="value" class:tone-ok={device.controlAcquired} class:tone-warn={!device.controlAcquired}>
                <span class="dot"></span>{device.controlAcquired ? 'Acquired' : 'Not acquired'}
              </span>
            </span>
          {:else if device.kind === 'clickMinus' || device.kind === 'clickPlus'}
            <!--
              Which pod, and what the panel above says about its link. The
              unlock countdown that used to sit here was fiction: nothing
              reports how much of the ~24 h unlock is left, so it rendered
              "expired" against a pod that was working perfectly.
            -->
            {@const pod = device.kind === 'clickPlus' ? app.controller?.plus : app.controller?.minus}
            <span class="state">
              <span class="label">Click pod</span>
              <span class="value" class:tone-ok={pod?.state === 'connected'} class:tone-idle={pod?.state !== 'connected'}>
                <span class="dot"></span>{pod?.symbol ?? '?'} pod{pod?.state === 'connected'
                  ? ' · linked'
                  : ''}
              </span>
            </span>
          {:else if device.kind === 'heartRate'}
            <!-- The live reading is the row's proof that data is flowing: a
                 monitor that is connected but silent looks exactly like a
                 working one otherwise. -->
            <span class="state">
              <span class="label">Heart rate</span>
              <span
                class="value"
                class:tone-ok={device.heartRateBpm != null}
                class:tone-idle={device.heartRateBpm == null}
              >
                <span class="dot"></span>{device.heartRateBpm != null
                  ? `${device.heartRateBpm} bpm`
                  : '—'}
              </span>
            </span>
            {#if device.batteryPct != null}
              <span class="state">
                <span class="label">Battery</span>
                <span class="value tone-idle"><span class="dot"></span>{device.batteryPct}%</span>
              </span>
            {/if}
          {:else if device.batteryPct != null}
            <span class="state">
              <span class="label">Battery</span>
              <span class="value tone-idle"><span class="dot"></span>{device.batteryPct}%</span>
            </span>
          {/if}
        </div>

        <div class="controls">
          {#if isConnected(device)}
            <button class="btn ghost" onclick={() => app.run(() => api.disconnect(device.id))}>
              Disconnect
            </button>
          {:else}
            <button class="btn" disabled={busy(device)} onclick={() => app.run(() => api.connect(device.id))}>
              {busy(device) ? 'Connecting…' : 'Connect'}
            </button>
          {/if}
          <button class="btn ghost danger" onclick={() => app.run(() => api.forget(device.id))}>
            Forget
          </button>
        </div>
      </article>
    {/each}

    {#if devices.length === 0}
      <!-- A-4 / FR-1.8: absent is not the same as missing. -->
      <div class="empty">
        <h2>Nothing found yet</h2>
        <p>Most devices sleep until you touch them. To wake them:</p>
        <ul>
          <li><strong>Trainer</strong> — turn the pedals for a few seconds.</li>
          <li><strong>Zwift Click</strong> — press any button on the pod.</li>
          <li>
            <strong>Heart rate</strong> — wet a strap's contacts and put it on; on a sports watch,
            switch on <em>Broadcast Heart Rate</em>.
          </li>
        </ul>
        <p class="quiet">
          They will appear here as soon as they advertise. Scanning continues in the background.
        </p>
      </div>
    {/if}
  </div>
</div>

<style>
  /*
   * A column, not a fixed grid — the same lesson as `.ride`.
   *
   * This was `grid-template-rows: auto auto 1fr` with *four* children: header,
   * ClickPanel, the trainer gate, and the list. The gate is conditional, so
   * when it rendered it took the 1fr track and the device list fell into an
   * implicit `auto` row past the bottom of the screen — with `overflow-y` set
   * on the list rather than on the thing that was actually overflowing, so
   * nothing scrolled. On a desktop window everything fit and it never showed.
   * On a phone it meant the trainer could not be reached at all: the Click
   * panel alone is taller than the viewport, and the D100's Connect button was
   * below the fold with no way to get to it.
   *
   * Flex has no fixed track count, so a conditional child cannot displace
   * anything.
   */
  .screen {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
  }

  .screen > :global(*) {
    flex: none;
  }

  header {
    display: flex;
    align-items: flex-end;
    gap: var(--gap);
    padding: clamp(1.4rem, 3vw, 2.6rem) var(--edge) 1rem;
  }

  h1 {
    margin: 0;
    font-size: clamp(1.6rem, 2.6vw, 2.4rem);
    font-weight: 300;
    letter-spacing: -0.03em;
  }

  header p {
    margin: 0.2rem 0 0;
    color: var(--ink-dim);
    font-size: 0.92rem;
  }

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

  .gate {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0 var(--edge) 0.6rem;
    padding: 0.7rem 0.9rem;
    border-radius: 0.55rem;
    background: rgba(255, 207, 74, 0.06);
    color: var(--ink-soft);
    font-size: 0.9rem;
  }

  .gate strong {
    color: var(--warn);
    font-weight: 600;
  }

  .list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0.4rem var(--edge) 2rem;
  }

  /*
   * On a phone the whole screen scrolls as one document instead of pinning a
   * header over a scrolling list. Giving the list its own scroll region there
   * would leave it a few pixels tall — the Click panel is taller than the
   * viewport on its own — so the list would technically scroll and still be
   * unusable.
   */
  :global([data-size='compact']) .screen {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }

  :global([data-size='compact']) .list {
    flex: none;
    overflow-y: visible;
  }

  .row {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) auto minmax(0, 1.4fr) auto;
    align-items: center;
    gap: var(--gap);
    padding: 1rem 0.25rem;
    border-bottom: 1px solid var(--hairline);
  }

  .row.live .name {
    color: var(--ink);
  }

  .identity {
    display: flex;
    flex-direction: column;
    gap: 0.18rem;
    min-width: 0;
  }

  .name {
    font-size: 1.12rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .meta {
    font-size: 0.8rem;
    color: var(--ink-dim);
  }

  .error {
    font-size: 0.82rem;
    color: var(--bad);
  }

  .signal {
    display: flex;
    align-items: center;
    gap: 0.45rem;
  }

  .bars {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 1.15rem;
  }

  .bars i {
    width: 3px;
    background: var(--ink-faint);
    border-radius: 1px;
  }

  .bars i.on {
    background: var(--ok);
  }

  .dbm {
    font-size: 0.78rem;
    color: var(--ink-dim);
    min-width: 2.3em;
  }

  .states {
    display: flex;
    gap: var(--gap);
    flex-wrap: wrap;
  }

  .state {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
  }

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

  .controls {
    display: flex;
    gap: 0.4rem;
    justify-content: flex-end;
  }

  .empty {
    max-width: 42rem;
    margin: clamp(2rem, 8vh, 6rem) auto;
    text-align: left;
    color: var(--ink-soft);
  }

  .empty h2 {
    margin: 0 0 0.5rem;
    font-size: 1.6rem;
    font-weight: 300;
    letter-spacing: -0.02em;
    color: var(--ink);
  }

  .empty ul {
    margin: 0.6rem 0;
    padding-left: 1.1rem;
    line-height: 1.9;
  }

  .empty strong {
    color: var(--ink);
    font-weight: 600;
  }

  .quiet {
    color: var(--ink-faint);
    font-size: 0.88rem;
  }

  /*
   * Narrow: a device row becomes three stacked bands instead of four columns.
   * Driven by the measured size class rather than a `max-width` of its own —
   * see ui/src/lib/viewport.ts for why there is exactly one definition of
   * "narrow" in this codebase.
   */
  :global([data-size='compact']) .row,
  :global([data-size='medium']) .row {
    grid-template-columns: 1fr auto;
    grid-template-areas:
      'identity signal'
      'states states'
      'controls controls';
  }

  :global([data-size='compact']) .identity,
  :global([data-size='medium']) .identity {
    grid-area: identity;
  }

  :global([data-size='compact']) .signal,
  :global([data-size='medium']) .signal {
    grid-area: signal;
  }

  :global([data-size='compact']) .states,
  :global([data-size='medium']) .states {
    grid-area: states;
  }

  :global([data-size='compact']) .controls,
  :global([data-size='medium']) .controls {
    grid-area: controls;
    justify-content: flex-start;
    flex-wrap: wrap;
  }
</style>
