<script lang="ts">
  import { pageTitle } from '$lib/branding';
  import { useQueryClient } from '@tanstack/svelte-query';
  import type { CreateQueryResult, CreateMutationResult } from '@tanstack/svelte-query';
  import {
    createLBStatusQuery,
    createTokenMutation,
    createEnabledMutation,
    type LBStatus
  } from '$lib/api/listenbrainz';
  import {
    createRecommendationMetricsQuery,
    type RecommendationMetrics,
    type SurfaceIntent,
    type MetricDelta
  } from '$lib/api/metrics';
  import { theme, setTheme, type ThemePreference } from '$lib/stores/theme.svelte';
  import { player, setCrossfade } from '$lib/player/store.svelte';
  import {
    updateProfile,
    changePassword,
    getAPIToken,
    regenerateAPIToken
  } from '$lib/api/me';
  import { errCode } from '$lib/api/errors';
  import { pushToast } from '$lib/stores/toast.svelte';
  import MobileAppDownload from '$lib/components/MobileAppDownload.svelte';
  import ServerVersion from '$lib/components/ServerVersion.svelte';
  import ActiveSessions from '$lib/components/ActiveSessions.svelte';

  const queryClient = useQueryClient();

  const status = createLBStatusQuery() as CreateQueryResult<LBStatus>;
  const metrics = createRecommendationMetricsQuery() as CreateQueryResult<RecommendationMetrics>;

  // Per-intent expectation copy: each surface band is judged against its
  // job — discovery mixes are supposed to run hotter skip rates.
  const intentHints: Record<SurfaceIntent, string> = {
    go_to: 'Everyday surfaces — these should beat your own picking.',
    discovery: 'Exploration surfaces — higher skip rates here are expected and healthy.',
    direct: 'Music you chose yourself — naturally close to the baseline.'
  };

  function pct(v: number): string {
    return `${(v * 100).toFixed(0)}%`;
  }

  // Deltas come from the server with their margin of error (#2495). The client
  // no longer subtracts rates itself: the margin needs the sample sizes and
  // variances, and having both clients re-derive it invites them to disagree.
  //
  // A delta that isn't distinguishable from zero is prefixed "≈" and dimmed.
  // That is the point of this whole change — the card used to render a −12 on
  // 59 plays exactly as boldly as a −6 on 400, and the first of those is noise.
  function deltaText(d: MetricDelta | undefined): string {
    if (!d) return '';
    const pts = Math.round(d.delta_pp);
    const signed = pts > 0 ? `+${pts}` : `${pts}`;
    return d.distinguishable ? signed : `≈${signed}`;
  }

  function deltaTitle(d: MetricDelta | undefined): string | undefined {
    if (!d) return undefined;
    const range = `${d.delta_pp.toFixed(1)} ± ${d.margin_pp.toFixed(1)} points vs baseline`;
    return d.distinguishable
      ? `${range} (95% confidence)`
      : `${range} — not distinguishable from zero at 95% confidence, so read this as no measured difference.`;
  }

  // A skip delta is "worse" above the baseline; a completion delta is "worse"
  // below it. Neither gets a colour unless it's distinguishable — colouring
  // noise red is what made the old card misleading.
  function skipDeltaClass(d: MetricDelta | undefined): string {
    if (!d?.distinguishable) return 'text-text-secondary opacity-60';
    return d.delta_pp > 0 ? 'text-danger' : 'text-text-secondary';
  }

  function completionDeltaClass(d: MetricDelta | undefined): string {
    if (!d?.distinguishable) return 'text-text-secondary opacity-60';
    return d.delta_pp < 0 ? 'text-danger' : 'text-text-secondary';
  }

  // Pick-kind breakdowns are collapsed by default (#1270): with every
  // system mix stamping provenance, always-open sub-rows would triple
  // the table height and bury the surface-level comparison.
  let expandedBreakdowns = $state(new Set<string>());

  function toggleBreakdown(key: string) {
    const next = new Set(expandedBreakdowns);
    if (next.has(key)) next.delete(key);
    else next.add(key);
    expandedBreakdowns = next;
  }
  const tokenMutation = createTokenMutation(queryClient);
  const enabledMutation = createEnabledMutation(queryClient);

  let tokenInput = $state('');

  function saveToken() {
    const t = tokenInput.trim();
    if (!t) return;
    $tokenMutation.mutate(t, { onSuccess: () => { tokenInput = ''; } });
  }

  function clearToken() {
    $tokenMutation.mutate('');
  }

  function toggleEnabled() {
    if (!$status.data) return;
    $enabledMutation.mutate(!$status.data.enabled);
  }

  // Profile card ------------------------------------------------------------

  let profileForm = $state<{ displayName: string; email: string }>({
    displayName: '',
    email: '',
  });
  let profileSaving = $state(false);

  async function onSaveProfile() {
    profileSaving = true;
    try {
      await updateProfile({
        display_name: profileForm.displayName,
        email: profileForm.email,
      });
      pushToast('Profile saved.');
    } catch (e: unknown) {
      const code = errCode(e);
      if (code === 'email_taken') pushToast('That email is already in use.', 'error');
      else if (code === 'email_invalid') pushToast('Email format is invalid.', 'error');
      else pushToast(`Save failed: ${code}`, 'error');
    } finally {
      profileSaving = false;
    }
  }

  // Password card -----------------------------------------------------------

  let passwordForm = $state<{ current: string; new: string; confirm: string }>({
    current: '', new: '', confirm: '',
  });
  let passwordSaving = $state(false);

  async function onSavePassword(e: SubmitEvent) {
    e.preventDefault();
    if (passwordForm.new !== passwordForm.confirm) {
      pushToast('New passwords do not match.', 'error');
      return;
    }
    passwordSaving = true;
    try {
      await changePassword(passwordForm.current, passwordForm.new);
      pushToast('Password changed.');
      passwordForm = { current: '', new: '', confirm: '' };
    } catch (e: unknown) {
      const code = errCode(e);
      if (code === 'wrong_password') pushToast('Current password is incorrect.', 'error');
      else if (code === 'password_too_short') pushToast('Password must be at least 8 characters.', 'error');
      else pushToast(`Change failed: ${code}`, 'error');
    } finally {
      passwordSaving = false;
    }
  }

  // API Token card ----------------------------------------------------------

  let apiToken = $state<string | null>(null);
  let tokenSaving = $state(false);
  let confirmRegen = $state(false);
  let regenTimer: ReturnType<typeof setTimeout> | undefined;

  $effect(() => {
    getAPIToken().then(r => { apiToken = r.api_token; }).catch(() => {});
  });

  async function copyToken() {
    if (!apiToken) return;
    try {
      await navigator.clipboard.writeText(apiToken);
      pushToast('Token copied to clipboard.');
    } catch {
      pushToast('Copy failed.', 'error');
    }
  }

  async function onRegenerateToken() {
    if (!confirmRegen) {
      confirmRegen = true;
      if (regenTimer) clearTimeout(regenTimer);
      regenTimer = setTimeout(() => { confirmRegen = false; }, 5000);
      return;
    }
    if (regenTimer) clearTimeout(regenTimer);
    confirmRegen = false;
    tokenSaving = true;
    try {
      const r = await regenerateAPIToken();
      apiToken = r.api_token;
      pushToast('API token regenerated.');
    } catch (e: unknown) {
      pushToast(`Regenerate failed: ${errCode(e)}`, 'error');
    } finally {
      tokenSaving = false;
    }
  }
</script>

<svelte:head><title>{pageTitle('Settings')}</title></svelte:head>

<div class="space-y-6">
  <h1 class="text-2xl font-semibold">Settings</h1>

  <section class="space-y-3 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">Appearance</h2>

    <fieldset class="space-y-2">
      <legend class="block text-sm text-text-secondary">Theme</legend>
      <div class="flex gap-2">
        {#each ['dark', 'light', 'system'] as opt (opt)}
          <label
            class="flex cursor-pointer items-center gap-2 rounded border border-border px-3 py-1.5 text-sm
                   {theme.value === opt ? 'bg-action-secondary text-action-fg' : 'text-text-primary hover:bg-surface-hover'}"
          >
            <input
              type="radio"
              name="theme"
              value={opt}
              checked={theme.value === opt}
              onchange={() => setTheme(opt as ThemePreference)}
              class="sr-only"
            />
            <span class="capitalize">{opt}</span>
          </label>
        {/each}
      </div>
    </fieldset>

    <p class="text-xs text-text-secondary">
      System follows your device's light/dark preference.
    </p>
  </section>

  <section class="space-y-3 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">Playback</h2>

    <div class="space-y-2">
      <div class="flex items-center justify-between">
        <label for="crossfade-slider" class="text-sm text-text-secondary">Crossfade</label>
        <span class="text-sm tabular-nums text-text-primary">
          {player.crossfadeSec === 0 ? 'Off' : `${player.crossfadeSec}s`}
        </span>
      </div>
      <input
        id="crossfade-slider"
        type="range"
        min="0"
        max="12"
        step="1"
        value={player.crossfadeSec}
        oninput={(e) => setCrossfade(Number((e.currentTarget as HTMLInputElement).value))}
        aria-label="Crossfade duration in seconds"
        class="w-full accent-accent"
      />
      <p class="text-xs text-text-secondary">
        Fades the end of one track into the start of the next.
        0 = off · most albums sound best at 0.
      </p>
    </div>
  </section>

  <section class="space-y-4 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">ListenBrainz</h2>

    {#if $status.isPending}
      <p class="text-text-secondary">Loading…</p>
    {:else if $status.isError}
      <p class="text-text-secondary">Couldn't load status.</p>
    {:else if $status.data}
      <div class="space-y-3">
        <label class="block text-sm">
          <span class="block text-text-secondary mb-1">User token</span>
          {#if $status.data.token_set}
            <div class="flex items-center gap-2">
              <span class="text-text-primary">••••••••••• (set)</span>
              <button
                type="button"
                onclick={clearToken}
                class="text-sm text-accent hover:underline"
                disabled={$tokenMutation.isPending}
              >clear</button>
            </div>
          {:else}
            <div class="flex gap-2">
              <input
                type="password"
                bind:value={tokenInput}
                placeholder="paste your LB token"
                class="flex-1 rounded border border-border bg-background px-2 py-1 text-sm"
              />
              <button
                type="button"
                onclick={saveToken}
                disabled={!tokenInput.trim() || $tokenMutation.isPending}
                class="rounded bg-accent px-3 py-1 text-sm text-background disabled:opacity-50"
              >Save</button>
            </div>
          {/if}
        </label>

        <label class="flex items-center gap-2 text-sm">
          <input
            type="checkbox"
            checked={$status.data.enabled}
            disabled={!$status.data.token_set || $enabledMutation.isPending}
            onchange={toggleEnabled}
          />
          <span>Send my plays to ListenBrainz</span>
        </label>

        {#if $status.data.last_scrobbled_at}
          <p class="text-sm text-text-secondary">
            Last scrobbled: {new Date($status.data.last_scrobbled_at).toLocaleString()}
          </p>
        {/if}

        <p class="text-xs text-text-secondary">
          Get a token at <a href="https://listenbrainz.org/profile/" target="_blank" rel="noopener" class="text-accent hover:underline">listenbrainz.org/profile</a>.
          Tokens are stored unencrypted in this server's database — treat as sensitive.
        </p>
      </div>
    {/if}
  </section>

  <!-- Recommendation metrics card -->
  <section class="space-y-3 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">Recommendation metrics</h2>
    <p class="text-sm text-text-secondary">
      How plays launched from each recommendation surface land, over the last
      {$metrics.data?.window_days ?? 30} days — compared against the baseline of
      music you picked yourself. Deltas are percentage points vs that baseline.
    </p>
    {#if $metrics.isPending}
      <p class="text-sm text-text-secondary">Loading…</p>
    {:else if $metrics.isError}
      <p class="text-sm text-text-secondary">Couldn't load metrics.</p>
    {:else if $metrics.data && ($metrics.data.groups.length > 0 || $metrics.data.baseline)}
      {@const baseline = $metrics.data.baseline}
      {#if baseline}
        <p class="rounded bg-background px-3 py-2 text-sm">
          <span class="font-medium">Baseline — manual plays:</span>
          <span class="text-text-secondary">
            {baseline.plays} plays · {pct(baseline.skip_rate)} skip ·
            {pct(baseline.avg_completion)} completion
          </span>
        </p>
      {:else}
        <p class="text-sm text-text-secondary">
          No manual plays in this window yet, so deltas are hidden — raw rates only.
        </p>
      {/if}
      {#each $metrics.data.groups as group (group.intent)}
        <div class="space-y-1">
          <h3 class="text-sm font-medium">{group.label}</h3>
          <p class="text-xs text-text-secondary">{intentHints[group.intent]}</p>
          <table class="w-full text-sm">
            <thead>
              <tr class="text-left text-text-secondary">
                <th class="py-1 font-medium">Surface</th>
                <th class="py-1 text-right font-medium">Plays</th>
                <th class="py-1 text-right font-medium">Skip rate</th>
                <th class="py-1 text-right font-medium">Avg completion</th>
              </tr>
            </thead>
            <tbody>
              {#each group.surfaces as m (m.key)}
                <tr
                  class="border-t border-border"
                  class:opacity-60={m.low_confidence}
                  title={m.low_confidence
                    ? 'Fewer than 20 plays — treat these rates as anecdote, not signal.'
                    : undefined}
                >
                  <td class="py-1">
                    {#if (m.breakdown ?? []).length > 0}
                      <button
                        type="button"
                        class="inline-flex items-center gap-1 rounded outline-none focus-visible:ring-1 focus-visible:ring-accent"
                        aria-expanded={expandedBreakdowns.has(m.key)}
                        onclick={() => toggleBreakdown(m.key)}
                      >
                        <span class="text-xs text-text-secondary" aria-hidden="true">
                          {expandedBreakdowns.has(m.key) ? '▾' : '▸'}
                        </span>
                        {m.label}
                      </button>
                    {:else}
                      {m.label}
                    {/if}{#if m.low_confidence}<span class="ml-1 text-xs text-text-secondary">· low data</span>{/if}
                  </td>
                  <td class="py-1 text-right tabular-nums">{m.plays}</td>
                  <td class="py-1 text-right tabular-nums">
                    {pct(m.skip_rate)}
                    {#if m.skip_delta}
                      <span class="ml-1 text-xs {skipDeltaClass(m.skip_delta)}"
                            data-testid="skip-delta-{m.key}"
                            title={deltaTitle(m.skip_delta)}>{deltaText(m.skip_delta)}</span>
                    {/if}
                  </td>
                  <td class="py-1 text-right tabular-nums">
                    {pct(m.avg_completion)}
                    {#if m.completion_delta}
                      <span class="ml-1 text-xs {completionDeltaClass(m.completion_delta)}"
                            data-testid="completion-delta-{m.key}"
                            title={deltaTitle(m.completion_delta)}>{deltaText(m.completion_delta)}</span>
                    {/if}
                  </td>
                </tr>
                <!-- Pick-kind provenance sub-rows (#1249/#1270): each
                     surface splits into the populations its builder
                     stamped — For You's taste vs freshness injection,
                     Discover's buckets, the tiered mixes' tiers — the
                     numbers that say WHERE a surface's skips come from.
                     Toggled per surface to keep the default view flat. -->
                {#if expandedBreakdowns.has(m.key)}
                  {#each m.breakdown ?? [] as b (b.key)}
                    <tr
                      class="border-t border-border/50 text-text-secondary"
                      class:opacity-60={b.low_confidence}
                      title={b.low_confidence
                        ? 'Fewer than 20 plays — treat these rates as anecdote, not signal.'
                        : undefined}
                    >
                      <td class="py-1 pl-4 text-xs">
                        ↳ {b.label}{#if b.low_confidence}<span class="ml-1">· low data</span>{/if}
                      </td>
                      <td class="py-1 text-right text-xs tabular-nums">{b.plays}</td>
                      <td class="py-1 text-right text-xs tabular-nums">
                        {pct(b.skip_rate)}
                        {#if b.skip_delta}
                          <span class="ml-1 {skipDeltaClass(b.skip_delta)}"
                                data-testid="skip-delta-{b.key}"
                                title={deltaTitle(b.skip_delta)}>{deltaText(b.skip_delta)}</span>
                        {/if}
                      </td>
                      <td class="py-1 text-right text-xs tabular-nums">
                        {pct(b.avg_completion)}
                        {#if b.completion_delta}
                          <span class="ml-1 {completionDeltaClass(b.completion_delta)}"
                                data-testid="completion-delta-{b.key}"
                                title={deltaTitle(b.completion_delta)}>{deltaText(b.completion_delta)}</span>
                        {/if}
                      </td>
                    </tr>
                  {/each}
                {/if}
              {/each}
            </tbody>
          </table>
        </div>
      {/each}
      <p class="text-xs text-text-secondary">
        Deltas compare each surface with your manual plays. A delta marked
        <span class="opacity-60">≈</span> is smaller than its own margin of error at this
        sample size — it can't be told apart from no difference, however big it looks.
        Hover any delta for its range.
      </p>
    {:else}
      <p class="text-sm text-text-secondary">
        No plays recorded yet. Play something from For You, Discover, or a mix.
      </p>
    {/if}
  </section>

  <!-- Profile card -->
  <section class="space-y-3 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">Profile</h2>
    <p class="text-sm text-text-secondary">
      Your display name and recovery email.
    </p>
    <div class="grid gap-3 sm:grid-cols-[auto_1fr] items-center">
      <label class="text-sm text-text-secondary" for="profile-displayname">Display name</label>
      <input id="profile-displayname" type="text" maxlength="64"
             bind:value={profileForm.displayName}
             class="w-full rounded border border-border bg-background px-3 py-2 outline-none focus:border-accent" />

      <label class="text-sm text-text-secondary" for="profile-email">Email</label>
      <input id="profile-email" type="email" autocomplete="email"
             bind:value={profileForm.email}
             class="w-full rounded border border-border bg-background px-3 py-2 outline-none focus:border-accent" />
    </div>
    <button type="button" disabled={profileSaving}
            onclick={onSaveProfile}
            class="rounded bg-accent px-3 py-2 text-sm font-medium text-background disabled:opacity-60">
      {profileSaving ? 'Saving…' : 'Save profile'}
    </button>
  </section>

  <!-- Password card -->
  <section class="space-y-3 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">Password</h2>
    <form onsubmit={onSavePassword} class="space-y-3">
      <div class="grid gap-3 sm:grid-cols-[auto_1fr] items-center">
        <label class="text-sm text-text-secondary" for="password-current">Current password</label>
        <input id="password-current" type="password" required minlength="8"
               autocomplete="current-password"
               bind:value={passwordForm.current}
               class="w-full rounded border border-border bg-background px-3 py-2 outline-none focus:border-accent" />

        <label class="text-sm text-text-secondary" for="password-new">New password</label>
        <input id="password-new" type="password" required minlength="8"
               autocomplete="new-password"
               bind:value={passwordForm.new}
               class="w-full rounded border border-border bg-background px-3 py-2 outline-none focus:border-accent" />

        <label class="text-sm text-text-secondary" for="password-confirm">Confirm</label>
        <input id="password-confirm" type="password" required minlength="8"
               bind:value={passwordForm.confirm}
               class="w-full rounded border border-border bg-background px-3 py-2 outline-none focus:border-accent" />
      </div>
      <button type="submit" disabled={passwordSaving}
              class="rounded bg-accent px-3 py-2 text-sm font-medium text-background disabled:opacity-60">
        {passwordSaving ? 'Saving…' : 'Change password'}
      </button>
    </form>
  </section>

  <!-- API Token card -->
  <section class="space-y-3 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">API Token</h2>
    <p class="text-sm text-text-secondary">
      Used by Subsonic clients (DSub, Symfonium, etc.) to authenticate to your library.
      Regenerating invalidates clients that have the old token cached.
    </p>
    {#if apiToken}
      <code class="block break-all rounded bg-background p-2 text-xs">
        {apiToken}
      </code>
    {/if}
    <div class="flex gap-2">
      <button type="button" onclick={copyToken}
              class="inline-flex items-center rounded-md border border-border bg-transparent px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary disabled:opacity-50">
        Copy
      </button>
      <button type="button" disabled={tokenSaving}
              onclick={onRegenerateToken}
              class="inline-flex items-center rounded-md border border-border bg-transparent px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary disabled:opacity-50">
        {confirmRegen ? 'Click again to confirm' : (tokenSaving ? 'Regenerating…' : 'Regenerate')}
      </button>
    </div>
  </section>

  <!-- Sits with Password and API Token rather than near the bottom: these
       three are the account-security group, and this is the one that tells
       you the other two need attention. -->
  <ActiveSessions />

  <section class="space-y-3 rounded border border-border bg-surface p-4">
    <h2 class="text-lg font-semibold">Library</h2>
    <ul class="space-y-2 text-sm">
      <li>
        <a href="/library/hidden" class="text-accent hover:underline">Hidden tracks</a>
        <span class="text-text-secondary"> — tracks you've flagged as bad rips, wrong tags, or otherwise unplayable.</span>
      </li>
    </ul>
  </section>

  <MobileAppDownload />

  <div class="pt-2 text-center">
    <ServerVersion />
  </div>
</div>
