<script lang="ts">
  import { onMount } from 'svelte';
  import type { ActivitySummary, BASIndex, Sport } from '../lib/types';
  import { formatDistance, formatDuration, sportIcon, sportColor, sportLabel } from '../lib/format';

  let all: ActivitySummary[] = [];
  let sport: Sport | 'all' = 'all';
  let loading = true;

  onMount(async () => {
    const res = await fetch(`${import.meta.env.BASE_URL}data/index.json`);
    const index: BASIndex = await res.json();
    all = index.activities.filter(a => a.privacy !== 'private' && a.distance_m);
    loading = false;
  });

  $: activities = sport === 'all' ? all : all.filter(a => a.sport === sport);

  // ── Tooltip ───────────────────────────────────────────────────────────────
  $: activitiesByDate = (() => {
    const m = new Map<string, ActivitySummary[]>();
    for (const a of activities) {
      const d = a.started_at.slice(0, 10);
      if (!m.has(d)) m.set(d, []);
      m.get(d)!.push(a);
    }
    return m;
  })();

  let hoveredDate: string | null = null;
  let pinnedDate: string | null = null;
  let tooltipEl: HTMLElement | null = null;
  let tooltipPos = { x: 0, y: 0 };
  let hideTimer: ReturnType<typeof setTimeout> | null = null;

  $: tooltipDate = pinnedDate ?? hoveredDate;
  $: tooltipActivities = tooltipDate ? (activitiesByDate.get(tooltipDate) ?? []) : [];

  function updatePos(e: MouseEvent) {
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    tooltipPos = {
      x: e.clientX > vw - 310 ? e.clientX - 305 : e.clientX + 14,
      y: Math.min(e.clientY - 8, vh - 260),
    };
  }

  function onCellEnter(date: string, e: MouseEvent) {
    if (!date || !activitiesByDate.has(date)) return;
    if (pinnedDate) return;
    if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
    hoveredDate = date;
    updatePos(e);
  }

  function onCellLeave() {
    if (pinnedDate) return;
    hideTimer = setTimeout(() => { hoveredDate = null; }, 120);
  }

  function onCellClick(date: string, e: MouseEvent) {
    if (!date || !activitiesByDate.has(date)) return;
    e.stopPropagation();
    if (pinnedDate === date) {
      pinnedDate = null;
    } else {
      pinnedDate = date;
      updatePos(e);
    }
  }

  function onTooltipEnter() {
    if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
  }

  function onTooltipLeave() {
    if (pinnedDate) return;
    hoveredDate = null;
  }

  function onWindowClick(e: MouseEvent) {
    if (!pinnedDate) return;
    if (tooltipEl && tooltipEl.contains(e.target as Node)) return;
    pinnedDate = null;
  }

  function onKeydown(e: KeyboardEvent) {
    if (e.key === 'Escape') pinnedDate = null;
  }

  // ── Heatmap data ─────────────────────────────────────────────────────────
  // byDateBySport: date → sport → total distance (m)
  $: byDateBySport = (() => {
    const m = new Map<string, Map<string, number>>();
    for (const a of activities) {
      const d = a.started_at.slice(0, 10);
      if (!m.has(d)) m.set(d, new Map());
      const sm = m.get(d)!;
      sm.set(a.sport, (sm.get(a.sport) ?? 0) + (a.distance_m ?? 0));
    }
    return m;
  })();

  $: byDate = new Map(
    [...byDateBySport.entries()].map(([d, sm]) => [
      d,
      [...sm.values()].reduce((s, v) => s + v, 0),
    ])
  );

  // Sorted daily distances for percentile-based intensity scaling
  $: sortedDaily = [...byDate.values()].sort((a, b) => a - b);
  $: maxDailyKm = (sortedDaily[sortedDaily.length - 1] ?? 0) / 1000 || 1;

  function pctRank(value: number, sorted: number[]): number {
    if (!sorted.length) return 0;
    let lo = 0, hi = sorted.length;
    while (lo < hi) { const mid = (lo + hi) >> 1; if (sorted[mid] <= value) lo = mid + 1; else hi = mid; }
    return lo / sorted.length;
  }

  // ── Totals ────────────────────────────────────────────────────────────────
  $: totalsByYear = (() => {
    const m = new Map<number, { dist: number; count: number }>();
    for (const a of activities) {
      const y = new Date(a.started_at).getFullYear();
      const cur = m.get(y) ?? { dist: 0, count: 0 };
      cur.dist += a.distance_m ?? 0;
      cur.count += 1;
      m.set(y, cur);
    }
    return m;
  })();

  $: allYears = [...totalsByYear.keys()].sort((a, b) => b - a);

  // ── Color helpers ─────────────────────────────────────────────────────────
  function hexToRgb(hex: string): [number, number, number] {
    return [
      parseInt(hex.slice(1, 3), 16),
      parseInt(hex.slice(3, 5), 16),
      parseInt(hex.slice(5, 7), 16),
    ];
  }

  // Lerp from zinc-800 bg (#27272a = 39,39,42) toward target color
  function applyIntensity(hex: string, intensity: number): string {
    const [tr, tg, tb] = hexToRgb(hex);
    return `rgb(${Math.round(39 + (tr - 39) * intensity)},${Math.round(39 + (tg - 39) * intensity)},${Math.round(42 + (tb - 42) * intensity)})`;
  }

  // Precompute date→color as a reactive Map so Svelte tracks it directly in
  // the template. (Calling a plain function with a static string arg won't
  // re-trigger when byDate/maxDailyKm change — the Map reference does.)
  $: cellColors = (() => {
    const m = new Map<string, string>();
    for (const [date, sportMap] of byDateBySport) {
      const total = byDate.get(date) ?? 0;
      if (total === 0) { m.set(date, '#27272a'); continue; }
      const intensity = 0.12 + pctRank(total, sortedDaily) * 0.88;
      let tr = 0, tg = 0, tb = 0;
      for (const [sp, dist] of sportMap) {
        const w = dist / total;
        const [cr, cg, cb] = hexToRgb(sportColor(sp as Sport));
        tr += cr * w; tg += cg * w; tb += cb * w;
      }
      const blended = `#${Math.round(tr).toString(16).padStart(2,'0')}${Math.round(tg).toString(16).padStart(2,'0')}${Math.round(tb).toString(16).padStart(2,'0')}`;
      m.set(date, applyIntensity(blended, intensity));
    }
    return m;
  })();

  // Legend: 6 swatches from dark to full sport color (or neutral for 'all')
  $: legendColor = sport !== 'all' ? sportColor(sport) : '#00c8ff';
  $: legendSwatches = [0, 0.18, 0.38, 0.58, 0.78, 1.0].map(t =>
    t === 0 ? '#27272a' : applyIntensity(legendColor, t)
  );

  // Sport chips present in filtered data (for 'all' color key)
  $: sportsInData = sport === 'all'
    ? ([...new Set(activities.map(a => a.sport))] as Sport[]).sort()
    : ([] as Sport[]);

  // ── Calendar helpers ──────────────────────────────────────────────────────
  const now = new Date();
  const years = [now.getFullYear(), now.getFullYear()-1, now.getFullYear()-2, now.getFullYear()-3];

  function localISO(d: Date): string {
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  }

  function getWeeks(year: number): string[][] {
    const jan1 = new Date(year, 0, 1);
    const dec31 = new Date(year, 11, 31);
    const start = new Date(jan1);
    start.setDate(jan1.getDate() - ((jan1.getDay() + 6) % 7));
    const end = new Date(dec31);
    end.setDate(dec31.getDate() + (6 - (dec31.getDay() + 6) % 7));
    const weeks: string[][] = [];
    let cur = new Date(start);
    while (cur <= end) {
      const week: string[] = [];
      for (let d = 0; d < 7; d++) {
        week.push(cur.getFullYear() === year ? localISO(cur) : '');
        cur.setDate(cur.getDate() + 1);
      }
      weeks.push(week);
    }
    return weeks;
  }

  const DOW = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
  const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

  const sports: Array<{ value: Sport | 'all'; label: string }> = [
    { value: 'all',      label: 'All' },
    { value: 'cycling',  label: '🚴 Cycling' },
    { value: 'running',  label: '🏃 Running' },
    { value: 'hiking',   label: '🥾 Hiking' },
    { value: 'walking',  label: '🚶 Walking' },
    { value: 'swimming', label: '🏊 Swimming' },
    { value: 'skiing',   label: '⛷️ Skiing' },
    { value: 'other',    label: '⚡ Other' },
  ];
</script>

<!-- Filter bar -->
<div class="flex gap-2 mb-6 flex-wrap">
  {#each sports as s}
    <button
      class="px-3 py-1 rounded-full text-sm font-medium border transition-colors"
      class:border-zinc-700={sport !== s.value}
      class:text-zinc-400={sport !== s.value}
      class:border-[--accent]={sport === s.value}
      class:text-white={sport === s.value}
      style={sport === s.value ? 'background:var(--accent-dim)' : ''}
      on:click={() => sport = s.value}
    >
      {s.label}
    </button>
  {/each}
  {#if all.length > 0}
    <span class="ml-auto text-sm text-zinc-500 self-center">
      {activities.length} {activities.length === 1 ? 'activity' : 'activities'}
    </span>
  {/if}
</div>

{#if loading}
  <div class="h-64 rounded-xl bg-zinc-800 animate-pulse mb-6" />
{:else}

<!-- Year totals -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-8">
  {#each allYears.slice(0, 4) as year}
    {@const t = totalsByYear.get(year)}
    <div class="bg-zinc-900 rounded-xl border border-zinc-800 p-4">
      <p class="text-xs text-zinc-500 mb-1">{year}</p>
      <p class="text-2xl font-bold text-white">{formatDistance(t?.dist ?? 0)}</p>
      <p class="text-sm text-zinc-400">{t?.count ?? 0} activities</p>
    </div>
  {/each}
</div>

<!-- Heatmaps per year -->
{#each years as year}
  {@const weeks = getWeeks(year)}
  {@const yt = totalsByYear.get(year)}
  {#if yt}
    <div class="mb-8" role="presentation">
      <div class="flex items-baseline gap-3 mb-2">
        <h2 class="text-lg font-semibold text-white">{year}</h2>
        <span class="text-sm text-zinc-400">
          {formatDistance(yt.dist)} · {yt.count} activities
        </span>
      </div>

      <div class="overflow-x-auto">
        <div class="inline-flex gap-[3px]">
          <!-- Day-of-week labels: blank slot at top to align with month row -->
          <div class="flex flex-col gap-[3px] mr-1">
            <div class="h-4" />
            {#each DOW as d, i}
              <span class="text-[9px] text-zinc-600 h-[10px] leading-[10px] w-3 text-right">
                {i % 2 === 1 ? d : ''}
              </span>
            {/each}
          </div>
          <!-- Week columns: month label at top, day cells below -->
          {#each weeks as week, wi}
            {@const firstDay = week.find(d => d)}
            {@const prevFirstDay = wi > 0 ? weeks[wi - 1].find(d => d) : null}
            {@const showMonth = firstDay && (!prevFirstDay || prevFirstDay.slice(5, 7) !== firstDay.slice(5, 7))}
            <div class="flex flex-col gap-[3px]">
              <div class="h-4 relative">
                {#if showMonth}
                  <span class="text-[10px] text-zinc-500 absolute left-0 top-0 whitespace-nowrap">
                    {MONTHS[parseInt(firstDay.slice(5, 7)) - 1]}
                  </span>
                {/if}
              </div>
              {#each week as date}
                <div
                  class="w-[10px] h-[10px] rounded-[2px] {date && activitiesByDate.has(date) ? 'cursor-pointer' : ''} {date && date === pinnedDate ? 'ring-1 ring-white ring-offset-[1px] ring-offset-zinc-950' : ''}"
                  style="background:{cellColors.get(date) ?? '#27272a'}"
                  on:mouseenter={e => onCellEnter(date, e)}
                  on:mouseleave={onCellLeave}
                  on:click={e => onCellClick(date, e)}
                />
              {/each}
            </div>
          {/each}
        </div>
      </div>

      <!-- Legend -->
      <div class="flex items-center gap-3 mt-2 flex-wrap">
        <div class="flex items-center gap-1">
          <span class="text-xs text-zinc-500 mr-1">Less</span>
          {#each legendSwatches as c}
            <div class="w-[10px] h-[10px] rounded-[2px]" style="background:{c}" />
          {/each}
          <span class="text-xs text-zinc-500 ml-1">More (percentile · max {Math.round(maxDailyKm)} km)</span>
        </div>
        {#if sportsInData.length > 1}
          <div class="flex items-center gap-2 ml-2">
            {#each sportsInData as sp}
              <span class="text-xs flex items-center gap-1" style="color:{sportColor(sp)}">
                <span class="w-[10px] h-[10px] rounded-[2px] inline-block" style="background:{sportColor(sp)}" />
                {sportIcon(sp)}
              </span>
            {/each}
          </div>
        {/if}
      </div>
    </div>
  {/if}
{/each}

{/if}

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

<!-- Day tooltip -->
{#if tooltipDate && tooltipActivities.length > 0}
  <div
    bind:this={tooltipEl}
    class="fixed z-50 bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl p-3 w-[280px]"
    style="left:{tooltipPos.x}px; top:{tooltipPos.y}px"
    on:mouseenter={onTooltipEnter}
    on:mouseleave={onTooltipLeave}
  >
    <div class="flex items-center justify-between mb-2">
      <p class="text-xs font-medium text-zinc-400">
        {new Date(tooltipDate + 'T12:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
      </p>
      {#if pinnedDate}
        <button
          class="text-zinc-500 hover:text-zinc-300 transition-colors text-sm leading-none ml-2"
          on:click|stopPropagation={() => pinnedDate = null}
          aria-label="Close"
        >✕</button>
      {/if}
    </div>
    <div class="flex flex-col gap-1">
      {#each tooltipActivities as a}
        <a
          href="{import.meta.env.BASE_URL}activity/{a.id}/"
          class="flex flex-col gap-0.5 rounded-lg px-2 py-1.5 hover:bg-zinc-800 transition-colors"
        >
          <span class="text-sm font-medium text-white truncate">
            {sportIcon(a.sport)} {a.title}
          </span>
          <span class="text-xs text-zinc-400">
            {formatDistance(a.distance_m)}
            {#if a.moving_time_s ?? a.duration_s}
              · {formatDuration(a.moving_time_s ?? a.duration_s)}
            {/if}
            <span class="ml-1" style="color:{sportColor(a.sport)}">{sportLabel(a.sport, a.sub_sport)}</span>
          </span>
        </a>
      {/each}
    </div>
  </div>
{/if}
