<script lang="ts">
  import { onMount } from 'svelte';
  import type { ActivitySummary, BASIndex, Sport } from '../lib/types';
  import { formatDistance, formatDuration, formatElevation, formatDate, formatSpeed, formatPace, isPaceSport, isUnlisted, sportIcon, sportColor, sportLabel } from '../lib/format';
  import { loadIndexPaged, loadShardActivities, loadCombinedFeed } from '../lib/dataloader';
  import FeedMapView from './FeedMapView.svelte';

  /** Render preview_coords as an SVG polyline path string. */
  function trackPath(coords: [number, number][] | null, w: number, h: number): string {
    if (!coords || coords.length < 2) return '';
    const lats = coords.map(c => c[0]);
    const lons = coords.map(c => c[1]);
    const minLat = Math.min(...lats), maxLat = Math.max(...lats);
    const minLon = Math.min(...lons), maxLon = Math.max(...lons);
    const latR = maxLat - minLat || 0.001;
    const lonR = maxLon - minLon || 0.001;
    const pad = 4;
    const scaleX = (w - pad * 2) / lonR;
    const scaleY = (h - pad * 2) / latR;
    const scale = Math.min(scaleX, scaleY);
    const offX = pad + (w - pad * 2 - lonR * scale) / 2;
    const offY = pad + (h - pad * 2 - latR * scale) / 2;
    return coords
      .map(([lat, lon], i) => {
        const x = (lon - minLon) * scale + offX;
        const y = h - ((lat - minLat) * scale + offY); // flip: SVG y↓, lat↑
        return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
      })
      .join(' ');
  }

  /** Base URL of the site (passed from Astro). */
  export let base: string = '/';
  /** When set, load this index URL instead of the root (for per-user profile pages). */
  export let profileIndexUrl: string = '';
  /** When set, only show activities from this handle. */
  export let filterHandle: string = '';

  const PAGE_SIZE = 60;

  let all: ActivitySummary[] = [];
  let sport: Sport | 'all' = 'all';
  let datePre = 'all';
  let dateFrom = '';
  let dateTo = '';
  let customFrom = '';
  let customTo   = '';
  const today = new Date().toISOString().slice(0, 10);
  let query = '';
  let shown = PAGE_SIZE;
  let loading = true;
  let loadingMore = false;
  let error = '';
  let viewMode: 'list' | 'map' = 'list';
  let mounted = false;
  let pendingShards: string[] = [];
  /** Grand total from feed.json — shows instance-wide count even before all pages are loaded. */
  let totalActivities = 0;
  /** Logged-in handle — resolved async via bincio:me event. */
  let me: string = '';

  // ── Bulk / select mode ────────────────────────────────────────────────────
  let selectMode = false;
  let selected: Set<string> = new Set();
  let hiddenIds: Set<string> = new Set();
  let bulkWorking = false;

  async function _loadMerges() {
    try {
      const r = await fetch('/api/merges', { credentials: 'include' });
      if (r.ok) {
        const data = await r.json();
        hiddenIds = new Set(data.hidden ?? []);
      }
    } catch { /* non-critical */ }
  }

  function _toggleSelect(id: string) {
    const s = new Set(selected);
    s.has(id) ? s.delete(id) : s.add(id);
    selected = s;
  }

  function _exitSelect() {
    selectMode = false;
    selected = new Set();
  }

  async function _deleteSelected() {
    const ids = [...selected];
    if (!confirm(`Delete ${ids.length} activit${ids.length === 1 ? 'y' : 'ies'}? This cannot be undone.`)) return;
    bulkWorking = true;
    for (const id of ids) {
      try {
        const r = await fetch(`/api/activity/${id}`, { method: 'DELETE', credentials: 'include' });
        if (r.ok) all = all.filter(a => a.id !== id);
      } catch { /* continue */ }
    }
    bulkWorking = false;
    _exitSelect();
  }

  async function _mergeSelected() {
    const ids = [...selected];
    if (ids.length < 2) return;
    bulkWorking = true;
    try {
      const r = await fetch('/api/merge', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ activity_ids: ids }),
      });
      const data = await r.json();
      if (r.ok) {
        for (const hid of data.hidden ?? []) hiddenIds.add(hid);
        hiddenIds = new Set(hiddenIds);
        _exitSelect();
      } else {
        alert(data.detail ?? 'Merge failed');
      }
    } catch {
      alert('Merge failed');
    } finally {
      bulkWorking = false;
    }
  }

  function computeDateRange(preset: string): { dateFrom: string; dateTo: string } {
    if (preset === 'all') return { dateFrom: '', dateTo: '' };
    if (/^\d{4}$/.test(preset)) {
      const y = parseInt(preset, 10);
      return { dateFrom: `${y}-01-01T`, dateTo: `${y + 1}-01-01T` };
    }
    const pad = (n: number) => String(n).padStart(2, '0');
    const now = new Date();
    let d: Date;
    if      (preset === '7d')  d = new Date(now.getTime() - 7  * 86_400_000);
    else if (preset === '30d') d = new Date(now.getTime() - 30 * 86_400_000);
    else                       { d = new Date(now); d.setMonth(d.getMonth() - 6); }
    return { dateFrom: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T`, dateTo: '' };
  }

  // Filter out secondary activities hidden by a merge.
  $: withMerge = all.filter(a => !hiddenIds.has(a.id));

  // Show private activities only to their owner.
  // On a profile page (filterHandle set): show unlisted if me === filterHandle.
  // On the global feed: show unlisted only for the logged-in user's own activities.
  $: isOwner = filterHandle !== '' && me === filterHandle;
  $: withPrivacy = withMerge.filter(a => {
    if (isUnlisted(a.privacy)) {
      return filterHandle ? isOwner : (me !== '' && (a as any).handle === me);
    }
    return true;
  });
  $: allYears = [...new Set(all.map(a => a.started_at?.slice(0, 4)).filter(Boolean) as string[])].sort().reverse();
  $: dateFrom = (customFrom || customTo) ? customFrom : computeDateRange(datePre).dateFrom;
  $: dateTo   = (customFrom || customTo) ? (customTo ? customTo + '' : '') : computeDateRange(datePre).dateTo;
  $: withDate = !dateFrom && !dateTo ? withPrivacy : withPrivacy.filter(a =>
    (!dateFrom || a.started_at >= dateFrom) && (!dateTo || a.started_at < dateTo)
  );
  $: filtered = sport === 'all' ? withDate : withDate.filter(a => a.sport === sport);
  $: withSearch = query.trim()
    ? filtered.filter(a => a.title?.toLowerCase().includes(query.trim().toLowerCase()))
    : filtered;
  $: visible = withSearch.slice(0, shown);
  $: canShowMore = shown < withSearch.length;
  $: hasMore = canShowMore || pendingShards.length > 0;

  async function loadMore() {
    if (canShowMore) {
      shown += PAGE_SIZE;
      return;
    }
    if (!pendingShards.length) return;
    loadingMore = true;
    try {
      const url = pendingShards[0];
      pendingShards = pendingShards.slice(1);
      const fresh = await loadShardActivities(url);
      const existing = new Map(all.map(a => [a.id, a]));
      for (const a of fresh) if (!existing.has(a.id)) existing.set(a.id, a);
      all = [...existing.values()].sort((a, b) =>
        (b.started_at ?? '').localeCompare(a.started_at ?? ''),
      );
      shown += PAGE_SIZE;
    } catch {
      // load failed — don't block the user
    } finally {
      loadingMore = false;
    }
  }

  $: if (sport || datePre || query || customFrom || customTo) shown = PAGE_SIZE; // reset pagination on filter change

  // Eager-load shards when a filter needs data not yet in memory.
  let loadingAllShards = false;

  function _yearFromShard(url: string): number | null {
    const m = url.match(/(?:index|feed)-(\d{4})(?:-\d{2})?\.json$/);
    return m ? parseInt(m[1], 10) : null;
  }

  // Returns [minYear, maxYear] for year-specific filters (year preset or custom
  // date range).  Returns null for open-ended filters (all, 7d, 30d, 6mo).
  function _neededYearRange(pre: string, from: string, to: string): [number, number] | null {
    if (from || to) {
      const fy = from ? parseInt(from.slice(0, 4), 10) : 0;
      const ty = to   ? parseInt(to.slice(0, 4),   10) : 9999;
      return [fy, ty];
    }
    if (/^\d{4}$/.test(pre)) { const y = parseInt(pre, 10); return [y, y]; }
    return null;
  }

  $: {
    const yr = _neededYearRange(datePre, customFrom, customTo);
    const needEager = !!query.trim() || yr !== null;
    if (needEager && pendingShards.length > 0 && !loadingAllShards) {
      // Compute toLoad first — if empty (needed years already loaded, others remain),
      // skip the async entirely to avoid an infinite reactive loop.
      const toLoad = (yr && !query.trim())
        ? pendingShards.filter(url => { const y = _yearFromShard(url); return y !== null && y >= yr[0] && y <= yr[1]; })
        : [...pendingShards];
      if (toLoad.length > 0) {
        loadingAllShards = true;
        (async () => {
          for (const url of toLoad) {
            pendingShards = pendingShards.filter(u => u !== url);
            try {
              const fresh = await loadShardActivities(url);
              const existing = new Map(all.map(a => [a.id, a]));
              for (const a of fresh) if (!existing.has(a.id)) existing.set(a.id, a);
              all = [...existing.values()].sort((a, b) =>
                (b.started_at ?? '').localeCompare(a.started_at ?? ''),
              );
            } catch { /* ignore — partial results still useful */ }
          }
          loadingAllShards = false;
        })();
      }
    }
  }

  $: if (mounted) {
    const params = new URLSearchParams(window.location.search);
    if (sport === 'all') params.delete('sport'); else params.set('sport', sport);
    if (datePre === 'all' || customFrom || customTo) params.delete('date'); else params.set('date', datePre);
    if (!query.trim()) params.delete('q'); else params.set('q', query.trim());
    if (!customFrom) params.delete('from'); else params.set('from', customFrom);
    if (!customTo)   params.delete('to');   else params.set('to',   customTo);
    const qs = params.toString();
    history.replaceState(null, '', qs ? `?${qs}` : window.location.pathname);
  }

  onMount(async () => {
    const params = new URLSearchParams(window.location.search);
    sport      = (params.get('sport') as Sport | 'all') ?? 'all';
    datePre    = params.get('date') ?? 'all';
    query      = params.get('q')    ?? '';
    customFrom = params.get('from') ?? '';
    customTo   = params.get('to')   ?? '';
    mounted    = true;

    // Resolve the logged-in handle so we can show the owner their private activities.
    if ((window as any).__bincioMe !== undefined) {
      me = (window as any).__bincioMe;
      _loadMerges();
    } else {
      window.addEventListener('bincio:me', (e: Event) => {
        me = (e as CustomEvent).detail;
        _loadMerges();
      }, { once: true });
    }

    try {
      const isGlobalFeed = !profileIndexUrl && !filterHandle;
      if (isGlobalFeed) {
        const combined = await loadCombinedFeed(base);
        if (combined) {
          all = combined.activities;
          totalActivities = combined.totalActivities;
          pendingShards = combined.pendingShards;
          loading = false;
          return;
        }
      }
      const indexUrl = profileIndexUrl
        ? `${base}data/${profileIndexUrl}`
        : `${base}data/index.json`;
      const { index, pendingShards: pending } = await loadIndexPaged(base, indexUrl);
      pendingShards = pending;
      let activities = index.activities;
      if (filterHandle && !profileIndexUrl) {
        activities = activities.filter(a => (a as any).handle === filterHandle);
      }
      all = activities;
    } catch (e: any) {
      error = e.message;
    } finally {
      loading = false;
    }
  });

  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>

<!-- Search + date range -->
<div class="flex flex-col md:flex-row gap-2 mb-4">
  <input
    type="search"
    placeholder="Search activities…"
    bind:value={query}
    class="flex-1 px-4 py-2 rounded-lg bg-zinc-900 border border-zinc-700 text-white placeholder-zinc-500 text-sm focus:outline-none focus:border-zinc-500 transition-colors"
  />
  <div class="flex gap-2 items-center">
    <div class="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-700 focus-within:border-zinc-500 transition-colors">
      <span class="text-xs text-zinc-500 whitespace-nowrap select-none">From</span>
      <input
        type="date"
        bind:value={customFrom}
        max={customTo || today}
        on:change={() => { datePre = 'all'; }}
        class="bg-transparent text-white text-sm focus:outline-none [color-scheme:dark]"
      />
    </div>
    <div class="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-700 focus-within:border-zinc-500 transition-colors">
      <span class="text-xs text-zinc-500 whitespace-nowrap select-none">To</span>
      <input
        type="date"
        bind:value={customTo}
        min={customFrom}
        max={today}
        on:change={() => { datePre = 'all'; }}
        class="bg-transparent text-white text-sm focus:outline-none [color-scheme:dark]"
      />
    </div>
    <div class="flex rounded-lg border border-zinc-700 overflow-hidden text-sm shrink-0">
      <button
        class="px-3 py-2 transition-colors"
        class:bg-zinc-800={viewMode === 'list'}
        class:text-white={viewMode === 'list'}
        class:text-zinc-400={viewMode !== 'list'}
        on:click={() => viewMode = 'list'}
      >List</button>
      <button
        class="px-3 py-2 border-l border-zinc-700 transition-colors"
        class:bg-zinc-800={viewMode === 'map'}
        class:text-white={viewMode === 'map'}
        class:text-zinc-400={viewMode !== 'map'}
        on:click={() => viewMode = 'map'}
      >Map</button>
    </div>
    {#if isOwner}
      <button
        class="px-3 py-2 rounded-lg border text-sm transition-colors shrink-0"
        class:border-zinc-700={!selectMode}
        class:text-zinc-400={!selectMode}
        class:border-[--accent]={selectMode}
        class:text-white={selectMode}
        style={selectMode ? 'background:var(--accent-dim)' : ''}
        on:click={() => { if (selectMode) _exitSelect(); else selectMode = true; }}
      >{selectMode ? 'Cancel' : 'Select'}</button>
    {/if}
  </div>
</div>

<!-- Sport filter bar -->
<div class="flex gap-2 mb-3 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">
      {#if totalActivities > withSearch.length}
        {withSearch.length} of {totalActivities} activities
      {:else}
        {withSearch.length} {withSearch.length === 1 ? 'activity' : 'activities'}
      {/if}
    </span>
  {/if}
</div>

<!-- Date filter bar -->
<div class="flex gap-2 mb-6 flex-wrap">
  {#each [{ value: 'all', label: 'All time' }, { value: '7d', label: '7 days' }, { value: '30d', label: '30 days' }, { value: '6mo', label: '6 months' }, ...allYears.map(y => ({ value: y, label: y }))] as d}
    {@const isActive = datePre === d.value && !customFrom && !customTo}
    <button
      class="px-3 py-1 rounded-full text-sm font-medium border transition-colors"
      class:border-zinc-700={!isActive}
      class:text-zinc-400={!isActive}
      class:border-[--accent]={isActive}
      class:text-white={isActive}
      style={isActive ? 'background:var(--accent-dim)' : ''}
      on:click={() => { datePre = d.value; customFrom = ''; customTo = ''; }}
    >
      {d.label}
    </button>
  {/each}
</div>

{#if loading}
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
    {#each Array(12) as _}
      <div class="h-36 rounded-xl bg-zinc-800 animate-pulse"></div>
    {/each}
  </div>
{:else if error}
  <p class="text-red-400 text-center py-12">Could not load activities: {error}</p>
{:else if viewMode === 'map'}
  <FeedMapView activities={withSearch} base={base} />
  {#if hasMore}
    <div class="text-center mt-4">
      <button
        class="px-6 py-2 rounded-full border border-zinc-700 text-zinc-300 hover:border-zinc-500 hover:text-white disabled:opacity-40 transition-colors text-sm"
        disabled={loadingMore}
        on:click={loadMore}
      >
        {#if loadingMore}
          Loading…
        {:else if canShowMore}
          Load more ({filtered.length - shown} remaining)
        {:else}
          Load older activities ({pendingShards.length} more {pendingShards.length === 1 ? 'period' : 'periods'})
        {/if}
      </button>
    </div>
  {/if}
{:else if withSearch.length === 0}
  <p class="text-zinc-500 text-center py-12">
    {#if loadingAllShards}Loading…{:else if query.trim()}No activities match "{query.trim()}".{:else}No activities found.{/if}
  </p>
{:else}
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
    {#each visible as a (a.id)}
      {@const isSelected = selected.has(a.id)}
      <!-- relative + isolate so the stretched activity link stays below the handle link -->
      <!-- svelte-ignore a11y-click-events-have-key-events -->
      <!-- svelte-ignore a11y-no-static-element-interactions -->
      <div
        class="relative rounded-xl bg-zinc-900 border p-4 transition-all group {isSelected ? 'border-[--accent]' : 'border-zinc-800'} {selectMode ? 'cursor-pointer' : 'hover:border-zinc-600 hover:bg-zinc-800/80'}"
        style={isSelected ? 'background:var(--accent-dim)' : ''}
        on:click={selectMode ? () => _toggleSelect(a.id) : undefined}
      >
        <!-- header -->
        <div class="flex items-start justify-between gap-2 mb-3">
          <div class="flex-1 min-w-0">
            <p class="text-xs text-zinc-500 mb-0.5">
              {formatDate(a.started_at)}{#if a.handle} · <a
                href="{import.meta.env.BASE_URL}u/{a.handle}/"
                class="relative z-10 hover:text-zinc-300 transition-colors"
              >@{a.handle}</a>{/if}
            </p>
            <!-- stretched link covers the whole card; sits below the handle link -->
            <h3 class="font-semibold text-white truncate group-hover:text-[--accent] transition-colors flex items-center gap-1.5">
              {#if isUnlisted(a.privacy)}
                <span class="text-zinc-500 shrink-0" title="Unlisted">🔒</span>
              {/if}
              {#if selectMode}
                <span class="truncate">{a.title}</span>
              {:else}
                <a
                  href={a.detail_url ? `${import.meta.env.BASE_URL}activity/${a.id}/` : `${import.meta.env.BASE_URL}activity/local/?id=${a.id}`}
                  class="before:absolute before:inset-0 before:content-[''] truncate"
                  on:click={() => { try { sessionStorage.setItem(`bincio:activity:${a.id}`, JSON.stringify(filterHandle && !a.handle ? { ...a, handle: filterHandle } : a)); } catch {} }}
                >{a.title}</a>
              {/if}
            </h3>
          </div>
          {#if selectMode}
            <div
              class="shrink-0 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors"
              class:border-zinc-600={!isSelected}
              class:border-[--accent]={isSelected}
              style={isSelected ? 'background:var(--accent)' : ''}
            >
              {#if isSelected}<span class="text-[10px] text-white leading-none">✓</span>{/if}
            </div>
          {:else}
            <span
              class="text-xs font-medium px-2 py-0.5 rounded-full flex-shrink-0"
              style="background:{sportColor(a.sport)}22; color:{sportColor(a.sport)}"
            >
              {sportIcon(a.sport)} {sportLabel(a.sport, a.sub_sport)}
            </span>
          {/if}
        </div>

        <!-- track thumbnail -->
        {#if a.preview_coords}
          <svg viewBox="0 0 120 70" class="w-full mt-2 mb-3 rounded overflow-hidden bg-zinc-800/60" style="height:70px">
            <path
              d={trackPath(a.preview_coords, 120, 70)}
              fill="none"
              stroke={sportColor(a.sport)}
              stroke-width="1.5"
              stroke-linecap="round"
              stroke-linejoin="round"
              opacity="0.9"
            />
          </svg>
        {/if}

        <!-- stats row -->
        <div class="grid grid-cols-3 gap-2 text-center">
          <div>
            <p class="text-lg font-bold text-white">{formatDistance(a.distance_m)}</p>
            <p class="text-xs text-zinc-500">Distance</p>
          </div>
          <div>
            <p class="text-lg font-bold text-white">{formatDuration(a.moving_time_s ?? a.duration_s)}</p>
            <p class="text-xs text-zinc-500">Moving time</p>
          </div>
          <div>
            <p class="text-lg font-bold text-white">{formatElevation(a.elevation_gain_m)}</p>
            <p class="text-xs text-zinc-500">Elevation</p>
          </div>
        </div>

        <!-- secondary stats -->
        {#if a.avg_speed_kmh || a.avg_hr_bpm}
          <div class="flex gap-4 mt-3 pt-3 border-t border-zinc-800 text-xs text-zinc-400">
            {#if a.avg_speed_kmh}
              <span>⚡ {isPaceSport(a.sport) ? formatPace(a.avg_speed_kmh) : formatSpeed(a.avg_speed_kmh)}</span>
            {/if}
            {#if a.avg_hr_bpm}
              <span>♥ {a.avg_hr_bpm} bpm</span>
            {/if}
            {#if a.avg_cadence_rpm}
              <span>↻ {a.avg_cadence_rpm} rpm</span>
            {/if}
          </div>
        {/if}
      </div>
    {/each}
  </div>

  {#if hasMore}
    <div class="text-center mt-8">
      <button
        class="px-6 py-2 rounded-full border border-zinc-700 text-zinc-300 hover:border-zinc-500 hover:text-white disabled:opacity-40 transition-colors text-sm"
        disabled={loadingMore}
        on:click={loadMore}
      >
        {#if loadingMore}
          Loading…
        {:else if canShowMore}
          Load more ({filtered.length - shown} remaining)
        {:else}
          Load older activities ({pendingShards.length} more {pendingShards.length === 1 ? 'period' : 'periods'})
        {/if}
      </button>
    </div>
  {/if}
{/if}

<!-- Bulk action bar — fixed at bottom when select mode is active -->
{#if selectMode}
  <div class="fixed bottom-0 left-0 right-0 z-50 flex items-center justify-center gap-3 px-4 py-4 bg-zinc-950/95 backdrop-blur border-t border-zinc-800">
    <span class="text-sm text-zinc-400 mr-2">
      {selected.size} selected
    </span>
    <button
      class="px-4 py-2 rounded-lg border border-red-700 text-red-400 hover:bg-red-900/30 disabled:opacity-40 transition-colors text-sm"
      disabled={selected.size === 0 || bulkWorking}
      on:click={_deleteSelected}
    >
      Delete ({selected.size})
    </button>
    <button
      class="px-4 py-2 rounded-lg border border-zinc-600 text-zinc-300 hover:bg-zinc-800 disabled:opacity-40 transition-colors text-sm"
      disabled={selected.size < 2 || bulkWorking}
      on:click={_mergeSelected}
    >
      Merge ({selected.size})
    </button>
    <button
      class="px-4 py-2 rounded-lg border border-zinc-700 text-zinc-500 hover:text-zinc-300 transition-colors text-sm"
      on:click={_exitSelect}
    >
      Cancel
    </button>
    {#if bulkWorking}
      <span class="text-xs text-zinc-500">Working…</span>
    {/if}
  </div>
{/if}
