<script lang="ts">
  import { onMount } from 'svelte';
  import { marked } from 'marked';
  import DOMPurify from 'dompurify';
  import type { ActivitySummary, ActivityDetail, AthleteZones, Timeseries } from '../lib/types';
  import { formatDistance, formatDuration, formatElevation, formatSpeed, formatPace, isPaceSport, formatDate, formatTime, formatElapsed, sportIcon, sportLabel, sportColor } from '../lib/format';
  import ActivityMap from './ActivityMap.svelte';
  import ActivityCharts from './ActivityCharts.svelte';
  import ActivityPowerCurve from './ActivityPowerCurve.svelte';
  import EditDrawer from './EditDrawer.svelte';
  import { loadActivity, loadTimeseries } from '../lib/dataloader';

  export let activity: ActivitySummary;
  export let base: string = '/';
  export let athlete: AthleteZones | null = null;

  const editUrl = import.meta.env.PUBLIC_EDIT_URL ?? '';
  const editGloballyEnabled = editUrl !== '' || import.meta.env.PUBLIC_EDIT_ENABLED === 'true';

  let currentHandle: string | null = null;
  $: editEnabled = editGloballyEnabled && currentHandle !== null && currentHandle === activity.handle;

  let detail: ActivityDetail | null = null;
  let timeseries: Timeseries | null = null;
  let timeseriesLoading = false;
  let error = '';
  let hoveredIdx: number | null = null;
  let editOpen = false;
  let lightboxIndex: number | null = null;

  interface SegmentEffortHit {
    segment_id: string;
    segment_name: string;
    segment_distance_m: number;
    elapsed_s: number;
    pr_elapsed_s: number;
  }
  let segmentEfforts: SegmentEffortHit[] = [];
  let unmergeWorking = false;

  async function _unmerge() {
    if (!confirm('Unmerge this activity? The original tracks will be restored and the secondary activities will reappear in your feed.')) return;
    unmergeWorking = true;
    try {
      const r = await fetch(`/api/unmerge/${activity.id}`, { method: 'POST', credentials: 'include' });
      if (r.ok) {
        window.location.reload();
      } else {
        const d = await r.json();
        alert(d.detail ?? 'Unmerge failed');
      }
    } catch {
      alert('Unmerge failed');
    } finally {
      unmergeWorking = false;
    }
  }

  // Local overrides applied immediately after a save (no re-fetch needed)
  let localTitle = '';
  let localDescription = '';
  let localSubSport: string | null | undefined = undefined;
  $: displayTitle = localTitle || activity.title;
  $: displaySubSport = localSubSport !== undefined ? localSubSport : activity.sub_sport;

  onMount(async () => {
    fetch('/api/me', { credentials: 'include' })
      .then(r => r.ok ? r.json() : null)
      .then(u => { if (u?.handle) currentHandle = u.handle; })
      .catch(() => {});

    fetch(`/api/activities/${activity.id}/segment_efforts`, { credentials: 'include' })
      .then(r => r.ok ? r.json() : [])
      .then(d => { segmentEfforts = d; })
      .catch(() => {});

    // Start timeseries fetch immediately in parallel with detail — the URL is
    // predictable from detail_url, saving a full sequential round-trip.
    const detailUrl = activity.detail_url ?? '';
    const earlyTsPromise: Promise<Timeseries | null> = detailUrl && !detailUrl.startsWith('idb:')
      ? loadTimeseries(
          detailUrl.replace(/\.json$/, '.timeseries.json').replace(/^.*activities\//, 'activities/'),
          detailUrl,
          base,
        ).catch(() => null)
      : Promise.resolve(null);

    try {
      detail = await loadActivity(activity.id, detailUrl, base);
      if (!detail) throw new Error('Activity not found');
      // Use embedded timeseries (IDB activities) or the already-in-flight fetch
      if (detail.timeseries) {
        timeseries = detail.timeseries;
      } else if (detail.timeseries_url) {
        timeseriesLoading = true;
        timeseries = await earlyTsPromise ??
          await loadTimeseries(detail.timeseries_url, detailUrl, base);
        timeseriesLoading = false;
      }
    } catch (e: any) {
      error = e.message;
      timeseriesLoading = false;
    }
  });

  function onSaved(e: CustomEvent<{ title: string; description: string; sub_sport: string | null }>) {
    editOpen = false;
    localTitle = e.detail.title;
    localDescription = e.detail.description;
    localSubSport = e.detail.sub_sport;
  }

  $: trackUrl = activity.track_url
    ? (activity.track_url.startsWith('http') || activity.track_url.startsWith('/') ? activity.track_url : `${base}data/${activity.track_url}`)
    : null;
  $: color = sportColor(activity.sport);

  function lightboxPrev() { if (lightboxIndex !== null) lightboxIndex = (lightboxIndex - 1 + galleryImages.length) % galleryImages.length; }
  function lightboxNext() { if (lightboxIndex !== null) lightboxIndex = (lightboxIndex + 1) % galleryImages.length; }
  function onKeydown(e: KeyboardEvent) {
    if (lightboxIndex === null) return;
    if (e.key === 'ArrowLeft')  { e.preventDefault(); lightboxPrev(); }
    if (e.key === 'ArrowRight') { e.preventDefault(); lightboxNext(); }
    if (e.key === 'Escape')     { lightboxIndex = null; }
  }

  $: rawDescription = localDescription || detail?.description || '';
  $: descriptionHtml = (() => {
    if (!rawDescription) return '';
    // Strip local image refs before marked sees them. marked only parses ![alt](url) as an
    // image when the URL has no spaces — filenames like "WhatsApp Image 2026.jpg" are left
    // as literal text instead. The lazy .*? anchored to the image extension handles filenames
    // with spaces and nested parens (e.g. "file(2).jpg") correctly.
    const stripped = rawDescription
      .replace(/!\[[^\]]*\]\((?!https?:\/\/|\/|data:).*?\.(?:jpe?g|png|gif|webp|bmp|avif|heic)\)/gi, '')
      .trim();
    if (!stripped) return '';
    const renderer = new marked.Renderer();
    // Any remaining remote images render inline; local ones (shouldn't exist after strip) are suppressed
    renderer.image = ({ href, title, text }) => {
      const isLocal = href && !href.startsWith('http') && !href.startsWith('/') && !href.startsWith('data:');
      if (isLocal) return '';
      const titleAttr = title ? ` title="${title}"` : '';
      return `<img src="${href ?? ''}" alt="${text}"${titleAttr} class="rounded-lg max-w-full my-2">`;
    };
    return DOMPurify.sanitize(marked(stripped, { renderer }) as string);
  })();

  // Derive image dir from detail_url so multi-user paths resolve correctly.
  // Relative: "dave/_merged/activities/foo.json" → "/data/dave/_merged/activities/images/{id}/"
  // Absolute: "/data/dave/_merged/activities/foo.json" → "/data/dave/_merged/activities/images/{id}/"
  $: imageBase = (() => {
    const du = activity.detail_url ?? '';
    const dir = du.startsWith('http') || du.startsWith('/')
      ? du.substring(0, du.lastIndexOf('/') + 1)
      : du.includes('/')
        ? `${base}data/${du.substring(0, du.lastIndexOf('/') + 1)}`
        : `${base}data/activities/`;
    return `${dir}images/${activity.id}/`;
  })();
  $: galleryImages = (detail?.custom as any)?.images as string[] ?? [];


  // TODO(cleanup): fallback NP from timeseries — remove once all activities have been
  // re-extracted with np_power_w baked into the JSON. Mirrors metrics.py → _np_power().
  function computeNpFromTimeseries(ts: Timeseries): number | null {
    const { t, power_w } = ts;
    if (!power_w || !t || t.length < 30) return null;
    if (!power_w.some(v => v != null)) return null;

    const sparse = new Map<number, number>();
    for (let i = 0; i < t.length; i++) {
      if (power_w[i] != null) sparse.set(t[i], power_w[i]!);
    }
    if (sparse.size < 2) return null;

    const tMin = Math.min(...sparse.keys());
    const tMax = Math.max(...sparse.keys());
    if (tMax - tMin > 7 * 24 * 3600) return null;

    const dense: number[] = [];
    for (let i = 0; i <= tMax - tMin; i++) dense.push(sparse.get(tMin + i) ?? 0);

    const win = 30;
    if (dense.length < win) return null;

    const half = Math.floor(win / 2);
    let windowSum = dense.slice(0, win).reduce((a, b) => a + b, 0);
    const fourthPowers: number[] = [];
    for (let i = half; i < dense.length - half; i++) {
      fourthPowers.push((windowSum / win) ** 4);
      if (i + half + 1 < dense.length) windowSum += dense[i + half + 1] - dense[i - half];
    }
    if (!fourthPowers.length) return null;
    return Math.round((fourthPowers.reduce((a, b) => a + b, 0) / fourthPowers.length) ** 0.25);
  }

  $: npPower = detail?.np_power_w ?? (timeseries ? computeNpFromTimeseries(timeseries) : null);

  type ColorMode = 'default' | 'speed' | 'hr' | 'power' | 'elevation' | 'cadence';
  let colorMode: ColorMode = 'default';
  let stickyMode: ColorMode | null = null;

  function handleStatClick(cm: ColorMode | null) {
    if (!cm) return;
    if (stickyMode === cm) {
      stickyMode = null;
      colorMode = 'default';
    } else {
      stickyMode = cm;
      colorMode = cm;
    }
  }

  // Per-metric: is there data AND a GPS track to colour?
  $: hasSpeedTrack    = !!trackUrl && !!timeseries?.speed_kmh?.some(v => v != null);
  $: hasHrTrack       = !!trackUrl && !!timeseries?.hr_bpm?.some(v => v != null);
  $: hasPowerTrack    = !!trackUrl && !!timeseries?.power_w?.some(v => v != null);
  $: hasElevTrack     = !!trackUrl && !!timeseries?.elevation_m?.some(v => v != null);
  $: hasCadenceTrack  = !!trackUrl && !!timeseries?.cadence_rpm?.some(v => v != null);

  function statColorMode(key: string | undefined): ColorMode | null {
    if (key === 'speed'      && hasSpeedTrack)   return 'speed';
    if (key === 'heart_rate' && hasHrTrack)      return 'hr';
    if (key === 'power'      && hasPowerTrack)   return 'power';
    if (key === 'elevation'  && hasElevTrack)    return 'elevation';
    if (key === 'cadence'    && hasCadenceTrack) return 'cadence';
    return null;
  }

  // Legend shown on the map when a metric is active.
  const MODE_LABEL: Record<string, string> = {
    speed: 'Speed', hr: 'Heart rate', power: 'Power',
    elevation: 'Elevation', cadence: 'Cadence',
  };
  $: legendInfo = (() => {
    if (colorMode === 'default' || !timeseries) return null;
    const values: (number | null)[] | null =
      colorMode === 'speed'     ? timeseries.speed_kmh :
      colorMode === 'hr'        ? timeseries.hr_bpm :
      colorMode === 'power'     ? timeseries.power_w :
      colorMode === 'elevation' ? timeseries.elevation_m :
      colorMode === 'cadence'   ? timeseries.cadence_rpm : null;
    if (!values) return null;
    let minV = Infinity, maxV = -Infinity;
    for (const v of values) { if (v != null) { if (v < minV) minV = v; if (v > maxV) maxV = v; } }
    if (!isFinite(minV)) return null;
    const fmt =
      colorMode === 'speed'     ? (v: number) => formatSpeed(v) :
      colorMode === 'hr'        ? (v: number) => `${Math.round(v)} bpm` :
      colorMode === 'power'     ? (v: number) => `${Math.round(v)} W` :
      colorMode === 'elevation' ? (v: number) => formatElevation(v) :
                                  (v: number) => `${Math.round(v)} rpm`;
    return { label: MODE_LABEL[colorMode], min: fmt(minV), max: fmt(maxV) };
  })();

  type Stat = { label: string; value: string; key?: string };
  const stat = (label: string, value: string, key?: string): Stat => ({ label, value, key });
  $: hiddenStats = new Set<string>((detail?.custom as any)?.hide_stats ?? []);

  // Fixed-position pairs: null = empty slot. Pairing is always preserved regardless
  // of which optional values (VAM, power) are available.
  $: statRows = (() => {
    const h = hiddenStats;
    const vis = (s: Stat | null): Stat | null => s && (!s.key || !h.has(s.key)) ? s : null;
    const rows: [Stat | null, Stat | null][] = [
      [
        stat('Distance',    formatDistance(activity.distance_m)),
        stat('Moving time', formatDuration(activity.moving_time_s ?? activity.duration_s)),
      ],
      [
        stat('Elevation ↑', formatElevation(activity.elevation_gain_m), 'elevation'),
        detail?.climbing_vam_mh != null
          ? stat('Climbing VAM', `${detail.climbing_vam_mh.toLocaleString()} m/h`, 'elevation')
          : null,
      ],
      [
        stat(isPaceSport(activity.sport) ? 'Avg pace' : 'Avg speed', isPaceSport(activity.sport) ? formatPace(activity.avg_speed_kmh) : formatSpeed(activity.avg_speed_kmh), 'speed'),
        stat(isPaceSport(activity.sport) ? 'Max pace' : 'Max speed', isPaceSport(activity.sport) ? formatPace(activity.max_speed_kmh) : formatSpeed(activity.max_speed_kmh), 'speed'),
      ],
      [
        stat('Avg HR', activity.avg_hr_bpm ? `${activity.avg_hr_bpm} bpm` : '—', 'heart_rate'),
        stat('Max HR', activity.max_hr_bpm ? `${activity.max_hr_bpm} bpm` : '—', 'heart_rate'),
      ],
      ...(activity.avg_power_w != null ? [[
        stat('Avg power', `${activity.avg_power_w} W`,                   'power'),
        stat('NP',        npPower != null ? `${npPower} W` : '—',        'power'),
      ] as [Stat, Stat]] : []),
      [
        stat('Cadence', activity.avg_cadence_rpm ? `${activity.avg_cadence_rpm} rpm` : '—', 'cadence'),
        null,
      ],
    ];
    return rows
      .map(([a, b]) => [vis(a), vis(b)] as [Stat | null, Stat | null])
      .filter(([a, b]) => a !== null || b !== null);
  })();
</script>

<svelte:window on:keydown={onKeydown} />

{#if editOpen && editEnabled}
  <EditDrawer activityId={activity.id} {editUrl} on:saved={onSaved} on:close={() => editOpen = false} on:deleted={() => { window.location.href = base; }} />
{/if}

<!-- Lightbox -->
{#if lightboxIndex !== null}
  <!-- svelte-ignore a11y-no-static-element-interactions -->
  <div
    class="fixed inset-0 z-50 bg-black/95 flex items-center justify-center"
    on:click={() => lightboxIndex = null}
    on:keydown={onKeydown}
  >
    <!-- Prev -->
    {#if galleryImages.length > 1}
      <button
        class="absolute left-4 top-1/2 -translate-y-1/2 text-white/60 hover:text-white text-3xl px-3 py-6 transition-colors z-10"
        on:click|stopPropagation={lightboxPrev}
        aria-label="Previous"
      >‹</button>
    {/if}

    <button
      type="button"
      class="contents"
      on:click|stopPropagation
      aria-label="Image {lightboxIndex + 1} of {galleryImages.length}"
    >
      <img
        src={imageBase + galleryImages[lightboxIndex]}
        alt={galleryImages[lightboxIndex]}
        class="max-h-[90vh] max-w-[90vw] rounded-lg shadow-2xl object-contain"
      />
    </button>

    <!-- Next -->
    {#if galleryImages.length > 1}
      <button
        class="absolute right-4 top-1/2 -translate-y-1/2 text-white/60 hover:text-white text-3xl px-3 py-6 transition-colors z-10"
        on:click|stopPropagation={lightboxNext}
        aria-label="Next"
      >›</button>
    {/if}

    <!-- Counter + filename -->
    <div class="absolute bottom-6 left-1/2 -translate-x-1/2 text-white/50 text-xs text-center">
      <p>{galleryImages[lightboxIndex]}</p>
      {#if galleryImages.length > 1}
        <p class="mt-0.5">{lightboxIndex + 1} / {galleryImages.length}</p>
      {/if}
    </div>

    <!-- Close -->
    <button
      class="absolute top-4 right-5 text-white/50 hover:text-white text-2xl transition-colors"
      on:click={() => lightboxIndex = null}
      aria-label="Close"
    >×</button>
  </div>
{/if}

<!-- Header -->
<div class="flex items-start gap-4 mb-6">
  <button on:click={() => history.back()} class="text-zinc-500 hover:text-white transition-colors mt-1 shrink-0 cursor-pointer">
    ← Back
  </button>
  <div class="flex-1 min-w-0">
    <div class="flex items-center gap-2 mb-1">
      <span
        class="text-xs font-medium px-2 py-0.5 rounded-full"
        style="background:{color}22;color:{color}"
      >
        {sportIcon(activity.sport)} {sportLabel(activity.sport)}
      </span>
      {#if displaySubSport && displaySubSport !== 'generic'}
        <span
          class="text-xs font-medium px-2 py-0.5 rounded-full"
          style="background:{color}11;color:{color}cc"
        >
          {sportLabel(activity.sport, displaySubSport).split(' ')[0]}
        </span>
      {/if}
      <span class="text-xs text-zinc-500">
        {formatDate(activity.started_at)} · {formatTime(activity.started_at)}{#if activity.handle} · <a href="{base}u/{activity.handle}/" class="hover:text-zinc-300 transition-colors">@{activity.handle}</a>{/if}
      </span>
    </div>
    <div class="flex items-center gap-3">
      <h1 class="text-2xl font-bold text-white">{displayTitle}</h1>
      {#if editEnabled}
        <button
          class="text-xs px-2 py-0.5 rounded border border-zinc-700 text-zinc-400 hover:border-zinc-500 hover:text-white transition-colors shrink-0"
          on:click={() => editOpen = true}
        >
          Edit
        </button>
      {/if}
      {#if trackUrl}
        <a
          href="{base}segments/new/?activity={activity.id}"
          class="text-xs px-2 py-0.5 rounded border border-zinc-700 text-zinc-400 hover:border-zinc-500 hover:text-white transition-colors shrink-0"
        >+ segment</a>
      {/if}
      {#if detail?.merged_ids?.length}
        <span class="text-xs px-2 py-0.5 rounded border border-zinc-600 text-zinc-400 shrink-0">
          Merged ({detail.merged_ids.length})
        </span>
        {#if editEnabled}
          <button
            class="text-xs px-2 py-0.5 rounded border border-zinc-700 text-zinc-400 hover:border-zinc-500 hover:text-white transition-colors shrink-0 disabled:opacity-40"
            disabled={unmergeWorking}
            on:click={_unmerge}
          >
            {unmergeWorking ? 'Working…' : 'Unmerge'}
          </button>
        {/if}
      {/if}
    </div>
    {#if descriptionHtml}
      <div class="text-zinc-400 mt-2 text-sm leading-relaxed [&_img]:rounded-lg [&_img]:my-2 [&_p]:my-1 [&_a]:text-blue-400">
        {@html descriptionHtml}
      </div>
    {/if}
  </div>
</div>

<!-- Photo gallery -->
{#if galleryImages.length}
  <div class="mb-4 grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-2">
    {#each galleryImages as img, i}
      <button
        class="relative overflow-hidden rounded-lg bg-zinc-800 aspect-square hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-blue-500"
        on:click={() => lightboxIndex = i}
        aria-label="Open photo {i + 1}"
      >
        <img
          src={imageBase + img}
          alt={img}
          class="w-full h-full object-cover"
          loading="lazy"
        />
      </button>
    {/each}
  </div>
{/if}

<!-- Map + Charts (left) / Stats (right) -->
<div class="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-4 mb-4 items-start">

  <!-- Left column: map stacked above charts -->
  <div class="flex flex-col gap-4 min-w-0">
    <div class="relative h-[360px] rounded-xl overflow-hidden bg-zinc-800">
      {#if trackUrl}
        <ActivityMap
          {trackUrl}
          {timeseries}
          bbox={detail?.bbox ?? null}
          initialCoords={activity.preview_coords}
          accentColor={color}
          {colorMode}
          bind:hoveredIdx
        />
        {#if legendInfo}
          <div class="absolute bottom-3 left-3 flex items-center gap-2 bg-zinc-900/80 backdrop-blur-sm rounded-lg px-2.5 py-1.5 text-xs pointer-events-none select-none">
            <span class="text-blue-400 tabular-nums">{legendInfo.min}</span>
            <div class="w-14 h-1.5 rounded-full flex-shrink-0" style="background:linear-gradient(to right,#3b82f6,#4ade80,#facc15,#ef4444)"></div>
            <span class="text-red-400 tabular-nums">{legendInfo.max}</span>
            <span class="text-zinc-500 ml-0.5">{legendInfo.label}</span>
          </div>
        {/if}
      {:else}
        <div class="w-full h-full flex items-center justify-center text-zinc-600 text-sm">
          No GPS track
        </div>
      {/if}
    </div>

    {#if error}
      <p class="text-red-400 text-sm">{error}</p>
    {:else if timeseries && timeseries.t.length > 0}
      <div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
        <ActivityCharts {timeseries} sport={activity.sport} bind:hoveredIdx {athlete} />
      </div>
    {:else if !detail || timeseriesLoading}
      <div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4 h-32 animate-pulse"></div>
    {/if}
  </div>

  <!-- Right column: stats summary -->
  <div class="grid grid-cols-2 gap-px bg-zinc-800 rounded-xl overflow-hidden">
    {#each statRows as [left, right]}
      {#each [left, right] as s}
        {@const cm =
          s?.key === 'speed'      && hasSpeedTrack   ? 'speed'     :
          s?.key === 'heart_rate' && hasHrTrack      ? 'hr'        :
          s?.key === 'power'      && hasPowerTrack   ? 'power'     :
          s?.key === 'elevation'  && hasElevTrack    ? 'elevation' :
          s?.key === 'cadence'    && hasCadenceTrack ? 'cadence'   : null}
        {#if s}
          <div
            class="bg-zinc-900 px-4 py-3 transition-colors {cm ? 'hover:bg-zinc-800 cursor-pointer' : ''} {cm && stickyMode === cm ? 'ring-1 ring-inset ring-white/25' : ''}"
            role={cm ? 'button' : 'none'}
            tabindex={cm ? 0 : -1}
            on:mouseenter={() => { if (cm) colorMode = cm; }}
            on:mouseleave={() => { colorMode = stickyMode ?? 'default'; }}
            on:click={() => handleStatClick(cm)}
            on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleStatClick(cm); } }}
          >
            <p class="text-2xl font-bold text-white">{s.value}</p>
            <p class="text-xs text-zinc-500">{s.label}</p>
          </div>
        {:else}
          <div class="bg-zinc-900 px-4 py-3"></div>
        {/if}
      {/each}
    {/each}
    {#if detail?.gear}
      <div class="bg-zinc-900 px-4 py-3 col-span-2">
        <p class="text-sm font-medium text-zinc-300">{detail.gear}</p>
        <p class="text-xs text-zinc-500">Gear</p>
      </div>
    {/if}
  </div>

</div>

<!-- Power curve -->
{#if detail?.mmp?.length}
  <div class="mt-4 bg-zinc-900 rounded-xl border border-zinc-800 p-4">
    <p class="text-xs font-semibold text-zinc-500 uppercase tracking-wide mb-3">Power curve</p>
    <ActivityPowerCurve mmp={detail.mmp} ftp_w={athlete?.ftp_w ?? null} />
  </div>
{/if}

<!-- Laps -->
{#if detail?.laps?.length}
  <div class="mt-4 bg-zinc-900 rounded-xl border border-zinc-800 overflow-hidden">
    <table class="w-full text-sm">
      <thead class="border-b border-zinc-800">
        <tr class="text-left text-zinc-500 text-xs">
          <th class="px-4 py-2">Lap</th>
          <th class="px-4 py-2">Distance</th>
          <th class="px-4 py-2">Time</th>
          <th class="px-4 py-2">Avg speed</th>
          <th class="px-4 py-2">Avg HR</th>
        </tr>
      </thead>
      <tbody>
        {#each detail.laps as lap}
          <tr class="border-b border-zinc-800/50 hover:bg-zinc-800/50">
            <td class="px-4 py-2 text-zinc-400">#{lap.index + 1}</td>
            <td class="px-4 py-2 text-white">{formatDistance(lap.distance_m)}</td>
            <td class="px-4 py-2 text-white">{formatDuration(lap.duration_s)}</td>
            <td class="px-4 py-2 text-white">{isPaceSport(activity.sport) ? formatPace(lap.avg_speed_kmh) : formatSpeed(lap.avg_speed_kmh)}</td>
            <td class="px-4 py-2 text-white">{lap.avg_hr_bpm ? `${lap.avg_hr_bpm} bpm` : '—'}</td>
          </tr>
        {/each}
      </tbody>
    </table>
  </div>
{/if}

<!-- Download -->
{#if detail && (!detail.download_disabled || editEnabled)}
  <div class="mt-4 bg-zinc-900 rounded-xl border border-zinc-800 px-4 py-3">
    <p class="text-xs font-semibold text-zinc-500 uppercase tracking-wide mb-2">Download</p>
    <div class="flex flex-wrap gap-2">
      <a
        href="/api/activity/{activity.id}/download/bas"
        download
        class="px-3 py-1.5 rounded-lg bg-zinc-800 border border-zinc-700 text-xs text-zinc-300 hover:border-zinc-500 hover:text-white transition-colors"
      >⬇ BAS JSON</a>
      {#if detail.timeseries_url}
        <a
          href="/api/activity/{activity.id}/download/gpx"
          download
          class="px-3 py-1.5 rounded-lg bg-zinc-800 border border-zinc-700 text-xs text-zinc-300 hover:border-zinc-500 hover:text-white transition-colors"
        >⬇ GPX</a>
      {/if}
      {#if detail.source === 'fit_file' || detail.source === 'gpx_file'}
        <a
          href="/api/activity/{activity.id}/download/original"
          download
          class="px-3 py-1.5 rounded-lg bg-zinc-800 border border-zinc-700 text-xs text-zinc-300 hover:border-zinc-500 hover:text-white transition-colors"
        >⬇ Original {detail.source === 'fit_file' ? 'FIT' : 'GPX'}</a>
      {/if}
    </div>
  </div>
{/if}

<!-- Segment efforts -->
{#if segmentEfforts.length > 0}
  <div class="mt-4 bg-zinc-900 rounded-xl border border-zinc-800 overflow-hidden">
    <div class="px-4 py-3 border-b border-zinc-800">
      <h3 class="text-sm font-semibold text-white">Segments</h3>
    </div>
    <table class="w-full text-sm">
      <tbody>
        {#each segmentEfforts as hit}
          {@const isPR = hit.elapsed_s === hit.pr_elapsed_s}
          {@const delta = hit.elapsed_s - hit.pr_elapsed_s}
          <tr class="border-b border-zinc-800/50 last:border-0 hover:bg-zinc-800/50 transition-colors">
            <td class="px-4 py-2.5">
              <a href="{base}segments/{hit.segment_id}/"
                 class="text-white hover:text-blue-400 transition-colors font-medium">
                {hit.segment_name}
              </a>
              <span class="text-zinc-500 text-xs ml-2">{formatDistance(hit.segment_distance_m)}</span>
            </td>
            <td class="px-4 py-2.5 font-mono text-white text-right">{formatElapsed(hit.elapsed_s)}</td>
            <td class="px-4 py-2.5 text-right text-xs font-medium w-16"
                class:text-green-400={isPR}
                class:text-zinc-500={!isPR}>
              {#if isPR}PR{:else}+{Math.floor(delta/60) > 0 ? `${Math.floor(delta/60)}m${(delta%60).toString().padStart(2,'0')}s` : `${delta}s`}{/if}
            </td>
          </tr>
        {/each}
      </tbody>
    </table>
  </div>
{/if}
