<script lang="ts">
  /**
   * Rider setup (FR-7.4).
   *
   * This screen did not exist, and its absence was the app's largest single
   * lie: `RiderConfig::default()` is a 105 kg rider on an 8 kg bike, and with
   * no way to change it every rider was shown a stranger's speed, a stranger's
   * ETA and a stranger's calorie count. Mass is not a preference, it is half
   * the physics.
   *
   * Three rules hold here:
   *
   *   1. **Rust owns validity.** Every commit round-trips through the command,
   *      which validates, persists and hands back what it accepted — and that
   *      is what lands in the form. A field can never end up showing a value
   *      the engine is not using.
   *   2. **Commit on change, not on a Save button.** There is no "unsaved"
   *      state to lose, and no button to forget to press. Fields commit when
   *      they are left, so a half-typed "7" on the way to "72" is never sent.
   *   3. **What matters is on top.** Weight, FTP and units are the three a
   *      rider actually sets. Everything below them is aerodynamic and
   *      drivetrain detail that has a defensible default, so it is folded away
   *      rather than made to look equally important.
   */
  import { app } from '../lib/app.svelte';
  import { api } from '../lib/bridge';
  import { fromMass, massUnit, toMass } from '../lib/format';
  import type { Preferences, RiderConfig, SafetyLimits, Units } from '../lib/types';

  let rider = $state<RiderConfig | null>(null);
  let limits = $state<SafetyLimits | null>(null);
  const prefs = $derived(app.prefs);
  const units = $derived(app.units);

  /** Bumped after every commit so the inputs re-read the accepted values —
   *  an input the rider has typed into keeps its own DOM value otherwise, and
   *  a rejected 900 kg would sit there looking accepted. */
  let revision = $state(0);

  $effect(() => {
    void (async () => {
      rider = await api.riderConfig();
      limits = await api.safetyLimits();
    })();
  });

  /**
   * Send the whole config, spread over what Rust last gave us.
   *
   * Spread rather than rebuilt: `RiderConfig` carries fields this form does not
   * show (the load channel, the fixed drivetrain loss), and a form that
   * reconstructs the struct would quietly reset them to serde defaults every
   * time somebody changed their weight.
   */
  async function commitRider(patch: Partial<RiderConfig>) {
    if (!rider) return;
    const next = { ...rider, ...patch };
    const accepted = await app.run(() => api.setRiderConfig(next));
    if (accepted) rider = accepted;
    revision++;
  }

  async function commitLimits(patch: Partial<SafetyLimits>) {
    const base = limits;
    if (!base) return;
    const accepted = await app.run(() => api.setSafetyLimits({ ...base, ...patch }));
    if (accepted) limits = accepted;
    revision++;
  }

  async function commitPrefs(patch: Partial<Preferences>) {
    await app.savePrefs({ ...prefs, ...patch });
    revision++;
  }

  /** Total mass is what the physics actually uses, so it is worth seeing. */
  const totalMass = $derived(rider ? rider.rider_kg + rider.bike_kg : 0);
</script>

<!--
  One field. `value` is read through `key` so that a commit — accepted or
  rejected — reseats the input from the truth Rust returned.
-->
{#snippet field(
  label: string,
  value: number,
  step: number,
  unit: string,
  hint: string | null,
  commit: (v: number) => void,
)}
  <label class="field">
    <span class="name">{label}</span>
    <span class="input">
      {#key revision}
        <input
          type="number"
          {step}
          value={Number.isFinite(value) ? Number(value.toFixed(4)) : 0}
          onchange={(e) => commit(e.currentTarget.valueAsNumber)}
        />
      {/key}
      <span class="unit">{unit}</span>
    </span>
    {#if hint}<span class="hint">{hint}</span>{/if}
  </label>
{/snippet}

<div class="screen">
  <header>
    <h1>Rider setup</h1>
    <button class="btn ghost" onclick={() => app.closeSettings()}>
      Done <span class="kbd">Esc</span>
    </button>
  </header>

  <div class="body">
    {#if !rider || !limits}
      <p class="waiting">Reading your setup…</p>
    {:else}
      <section class="card">
        <h2>You</h2>
        <div class="grid">
          {@render field(
            'Weight',
            toMass(rider.rider_kg, units),
            0.5,
            massUnit(units),
            'Sets speed, ETA and calories',
            (v) => commitRider({ rider_kg: fromMass(v, units) }),
          )}
          {@render field(
            'Bike',
            toMass(rider.bike_kg, units),
            0.1,
            massUnit(units),
            `${toMass(totalMass, units).toFixed(1)} ${massUnit(units)} on the road`,
            (v) => commitRider({ bike_kg: fromMass(v, units) }),
          )}
          {@render field(
            'FTP',
            prefs.ftpW,
            5,
            'W',
            // Zero is a real answer, not an empty box: it is how a rider says
            // "I do not know mine", and the screen then shows plain watts
            // rather than a zone measured against a guess.
            prefs.ftpW ? 'Colours power by zone' : '0 — no power zones',
            (v) => commitPrefs({ ftpW: Math.round(v) }),
          )}
          {@render field(
            'Max heart rate',
            prefs.maxHrBpm,
            1,
            'bpm',
            prefs.maxHrBpm ? 'Colours heart rate by zone' : '0 — no heart-rate zones',
            (v) => commitPrefs({ maxHrBpm: Math.round(v) }),
          )}

          <div class="field">
            <span class="name">Units</span>
            <div class="segmented">
              {#each [['metric', 'km · kg'], ['imperial', 'mi · lb']] as [value, label]}
                <button
                  class:on={units === value}
                  onclick={() => commitPrefs({ units: value as Units })}
                >
                  {label}
                </button>
              {/each}
            </div>
            <span class="hint">Display only — rides record in SI</span>
          </div>
        </div>
      </section>

      <!--
        Everything below has a defensible default and changes the ride subtly
        rather than obviously. Folded, so the three settings that matter are not
        buried among nine that do not.
      -->
      <details class="card">
        <summary><h2>Bike and physics</h2></summary>
        <div class="grid">
          {@render field('CdA', rider.cda, 0.005, 'm²', 'Road position ≈ 0.32', (v) =>
            commitRider({ cda: v }),
          )}
          {@render field('Rolling resistance', rider.crr, 0.0005, 'Crr', 'Road tyre ≈ 0.004', (v) =>
            commitRider({ crr: v }),
          )}
          {@render field(
            'Drivetrain',
            rider.drivetrain_efficiency * 100,
            0.5,
            '%',
            'Clean chain ≈ 97%',
            (v) => commitRider({ drivetrain_efficiency: v / 100 }),
          )}
          {@render field('Air density', rider.air_density, 0.005, 'kg/m³', 'Sea level 1.225', (v) =>
            commitRider({ air_density: v }),
          )}
          {@render field(
            'Wheel',
            rider.wheel_circumference_m * 1000,
            5,
            'mm',
            '700×25 ≈ 2100 mm',
            (v) => commitRider({ wheel_circumference_m: v / 1000 }),
          )}
          {@render field(
            'Crank',
            rider.crank_length_m * 1000,
            2.5,
            'mm',
            'Road 170–175 mm',
            (v) => commitRider({ crank_length_m: v / 1000 }),
          )}
          {@render field(
            'Real gear',
            rider.physical_development_m,
            0.1,
            'm/rev',
            'Through the Zwift Cog — 34×14 ≈ 5.1',
            (v) => commitRider({ physical_development_m: v }),
          )}
          {@render field(
            'Descent floor',
            rider.descent_load_floor_pct,
            0.5,
            '%',
            'Keeps load under the pedals downhill',
            (v) => commitRider({ descent_load_floor_pct: v }),
          )}
        </div>
      </details>

      <!-- SAF-3. These bound everything sent to the trainer, whatever asked
           for it, so they are shown last and phrased as limits, not targets. -->
      <details class="card">
        <summary><h2>Safety limits</h2></summary>
        <div class="grid">
          {@render field('Gradient floor', limits.min_gradient_pct, 0.5, '%', null, (v) =>
            commitLimits({ min_gradient_pct: v }),
          )}
          {@render field('Gradient ceiling', limits.max_gradient_pct, 0.5, '%', null, (v) =>
            commitLimits({ max_gradient_pct: v }),
          )}
          {@render field('Resistance floor', limits.min_resistance, 1, 'L', null, (v) =>
            commitLimits({ min_resistance: Math.round(v) }),
          )}
          {@render field('Resistance ceiling', limits.max_resistance, 1, 'L', null, (v) =>
            commitLimits({ max_resistance: Math.round(v) }),
          )}
          {@render field('Power floor', limits.min_power_w, 5, 'W', null, (v) =>
            commitLimits({ min_power_w: Math.round(v) }),
          )}
          {@render field('Power ceiling', limits.max_power_w, 5, 'W', 'Clamped at transmission', (v) =>
            commitLimits({ max_power_w: Math.round(v) }),
          )}
        </div>
      </details>
    {/if}
  </div>
</div>

<style>
  .screen {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
  }

  header {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: clamp(1rem, 2.4vw, 1.8rem) var(--edge) 0.7rem;
  }

  h1 {
    margin: 0;
    font-size: clamp(1.4rem, 2.2vw, 2rem);
    font-weight: 300;
    letter-spacing: -0.03em;
  }

  header .btn {
    margin-left: auto;
  }

  .body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0 var(--edge) 3rem;
    -webkit-overflow-scrolling: touch;
  }

  .card {
    max-width: 54rem;
    margin: 0 0 0.6rem;
    padding: 0.9rem 1rem 1rem;
    border-radius: 0.7rem;
    background: var(--bg-lift);
  }

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

  summary {
    cursor: pointer;
    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);
  }

  summary h2 {
    margin: 0;
    color: var(--ink-soft);
  }

  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 0.7rem 1rem;
    margin-top: 0.7rem;
  }

  .field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-width: 0;
  }

  .name {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--ink-soft);
  }

  .input {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.45rem 0.6rem;
    border-radius: 0.45rem;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--hairline);
  }

  .input:focus-within {
    border-color: color-mix(in srgb, var(--route) 45%, transparent);
  }

  input {
    flex: 1 1 auto;
    min-width: 0;
    background: none;
    border: none;
    color: var(--ink);
    font: inherit;
    font-size: 1.05rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    /* The spinners are 12 px of target on a screen where nothing else is under
       48, and they steal the width the number needs. */
    -moz-appearance: textfield;
    appearance: textfield;
    min-height: var(--touch-min);
  }

  input:focus {
    outline: none;
  }

  input::-webkit-outer-spin-button,
  input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

  .unit {
    flex: none;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--ink-dim);
  }

  .hint {
    font-size: 0.75rem;
    color: var(--ink-faint);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .segmented {
    display: flex;
    gap: 2px;
    padding: 2px;
    border-radius: 0.45rem;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--hairline);
  }

  .segmented button {
    flex: 1 1 0;
    padding: 0.4rem 0.5rem;
    border-radius: 0.35rem;
    color: var(--ink-dim);
    font-size: 0.85rem;
    font-weight: 600;
    min-height: var(--touch-min);
  }

  .segmented button.on {
    background: var(--route);
    color: #04121a;
  }

  .waiting {
    color: var(--ink-dim);
  }
</style>
