<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import maplibregl from 'maplibre-gl';
  import 'maplibre-gl/dist/maplibre-gl.css';

  export let handle: string;
  export let base: string;
  export let embedded: boolean = false;

  interface Track { id: string; date: string; type: string; name: string; dist: number; coords: [number,number][]; }

  let mapEl: HTMLDivElement;
  let map: any;
  let mapReady = false;

  let tracks: Track[] = [];
  let loading = true;
  let loadingYears = false;   // background years still fetching
  let loadedCount = 0;        // years fetched so far
  let totalYears = 0;         // total years in manifest
  let error = '';

  // Filters
  let selectedTypes: Set<string> = new Set();
  let dateFrom = '';
  let dateTo = '';
  let selectedYear: string | null = null;
  let selectedMonth: number | null = null;

  // View
  let viewMode: 'lines' | 'heatmap' = 'heatmap';
  let heatmapMode: 'global' | 'bytype' = 'bytype';

  // Heatmap appearance controls
  let heatWidth = 4;
  let heatOpacityPct = 10; // per-line opacity %, stacks with overlapping routes

  // Tile layers — same as planner
  const TILES: Record<string, { tiles: string[]; attribution: string; label: string }> = {
    grey:      { label: 'Grey',      attribution: '© CARTO | © OpenStreetMap contributors',     tiles: ['https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png','https://b.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png','https://c.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png'] },
    cyclosm:   { label: 'Cycle',     attribution: '© CyclOSM | © OpenStreetMap contributors',   tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png','https://b.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png','https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png'] },
    osm:       { label: 'OSM',       attribution: '© OpenStreetMap contributors',               tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'] },
    topo:      { label: 'Topo',      attribution: '© OpenTopoMap | © OpenStreetMap contributors', tiles: ['https://a.tile.opentopomap.org/{z}/{x}/{y}.png','https://b.tile.opentopomap.org/{z}/{x}/{y}.png','https://c.tile.opentopomap.org/{z}/{x}/{y}.png'] },
    satellite: { label: 'Sat',       attribution: '© Esri World Imagery',                       tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'] },
  };
  const TILE_ORDER = ['grey', 'cyclosm', 'osm', 'topo', 'satellite'];
  let tileKey = 'grey';

  // 5 points sampled from matplotlib's plasma colormap (t=0.12, 0.32, 0.52, 0.72, 0.88)
  const TYPE_COLORS: Record<string, string> = {
    cycling: '#f89540',  // plasma t≈0.75 — orange
    running: '#cc4778',  // plasma t≈0.50 — crimson
    hiking:  '#7e03a8',  // plasma t≈0.25 — purple
    skiing:  '#f5d126',  // plasma t≈0.87 — amber-yellow
    other:   '#3b0f70',  // plasma t≈0.12 — deep violet
  };
  const HEAT_TYPES = Object.keys(TYPE_COLORS);

  function typeColor(t: string): string { return TYPE_COLORS[t] ?? TYPE_COLORS.other; }
  function typeTextOn(t: string): string {
    const hex = typeColor(t).replace('#', '');
    const r = parseInt(hex.slice(0,2), 16);
    const g = parseInt(hex.slice(2,4), 16);
    const b = parseInt(hex.slice(4,6), 16);
    return (0.299*r + 0.587*g + 0.114*b) > 128 ? '#000000' : '#ffffff';
  }

  const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

  // ── Derived ────────────────────────────────────────────────────────────────

  $: allTypes = [...new Set(tracks.map(t => t.type))].sort();
  $: availableYears = [...new Set(tracks.map(t => t.date.slice(0,4)).filter(Boolean))].sort().reverse();

  // Auto-select any type that hasn't been seen before (handles progressive loading)
  $: { const s = new Set(selectedTypes); let changed = false; for (const t of allTypes) { if (!s.has(t)) { s.add(t); changed = true; } } if (changed) selectedTypes = s; }

  $: filteredTracks = tracks.filter(t => {
    if (!selectedTypes.has(t.type)) return false;
    if (dateFrom && t.date < dateFrom) return false;
    if (dateTo && t.date > dateTo) return false;
    return true;
  });

  $: if (mapReady) _updateMap(filteredTracks, viewMode, heatmapMode);

  // ── Filters ────────────────────────────────────────────────────────────────

  function setTile(key: string) {
    tileKey = key;
    map?.getSource('base')?.setTiles(TILES[key].tiles);
  }

  function toggleType(t: string) {
    const s = new Set(selectedTypes);
    s.has(t) ? s.delete(t) : s.add(t);
    selectedTypes = s;
  }

  function selectAllTypes() { selectedTypes = new Set(allTypes); }
  function clearAllTypes()  { selectedTypes = new Set(); }

  function setYear(y: string) {
    if (selectedYear === y) { selectedYear = null; selectedMonth = null; dateFrom = ''; dateTo = ''; return; }
    selectedYear = y;
    selectedMonth = null;
    dateFrom = `${y}-01-01`;
    dateTo   = `${y}-12-31`;
  }

  function setMonth(m: number) { // m: 1-12
    if (!selectedYear) return;
    selectedMonth = m;
    const mm  = String(m).padStart(2, '0');
    const last = new Date(+selectedYear, m, 0).getDate();
    dateFrom = `${selectedYear}-${mm}-01`;
    dateTo   = `${selectedYear}-${mm}-${String(last).padStart(2,'0')}`;
  }

  function clearMonth() {
    if (!selectedYear) return;
    selectedMonth = null;
    dateFrom = `${selectedYear}-01-01`;
    dateTo   = `${selectedYear}-12-31`;
  }

  function clearDates() { dateFrom = ''; dateTo = ''; selectedYear = null; selectedMonth = null; }

  // ── Map ────────────────────────────────────────────────────────────────────

  let _bbox: { w: number; e: number; s: number; n: number } | null = null;

  function _getBbox() {
    if (!map) return null;
    const b = map.getBounds();
    return { w: b.getWest(), e: b.getEast(), s: b.getSouth(), n: b.getNorth() };
  }

  function _inBbox(lng: number, lat: number): boolean {
    if (!_bbox) return true;
    return lng >= _bbox.w && lng <= _bbox.e && lat >= _bbox.s && lat <= _bbox.n;
  }

  function _linesGeoJSON(ts: Track[]) {
    const visible = _bbox ? ts.filter(t => t.coords.some(([lng, lat]) => _inBbox(lng, lat))) : ts;
    return { type: 'FeatureCollection', features: visible.map(t => ({
      type: 'Feature',
      geometry: { type: 'LineString', coordinates: t.coords },
      properties: { type: t.type },
    }))};
  }

  function _empty() { return { type: 'FeatureCollection', features: [] }; }

  function _heatPaint(width: number, opacityPct: number) {
    const o = opacityPct / 100;
    return {
      'line-width':   ['interpolate', ['linear'], ['zoom'],  5, width*0.5, 10, width,     14, width*1.8,   18, width*2.5],
      'line-opacity': ['interpolate', ['linear'], ['zoom'],  5, o,         10, o*1.2,     14, o*1.5,       18, o*1.8],
    };
  }

  function _updateHeatPaint(width: number, opacityPct: number) {
    if (!map) return;
    const p = _heatPaint(width, opacityPct);
    // Lines layer: width from slider, opacity always full
    if (map.getLayer('explore-lines')) {
      map.setPaintProperty('explore-lines', 'line-width',   p['line-width']);
      map.setPaintProperty('explore-lines', 'line-opacity', 1);
    }
    // Heat layers: both width and opacity from sliders
    for (const id of ['explore-heat-global', ...HEAT_TYPES.map(t => `explore-heat-${t}`)]) {
      if (!map.getLayer(id)) continue;
      map.setPaintProperty(id, 'line-width',   p['line-width']);
      map.setPaintProperty(id, 'line-opacity', p['line-opacity']);
    }
  }

  $: if (mapReady) _updateHeatPaint(heatWidth, heatOpacityPct);

  function _updateMap(filtered: Track[], view: string, heatMode: string) {
    const linesSrc = map.getSource('explore-lines');
    if (!linesSrc) return;

    linesSrc.setData(_linesGeoJSON(filtered));

    map.setLayoutProperty('explore-lines',       'visibility', view === 'lines'   ? 'visible' : 'none');
    map.setLayoutProperty('explore-heat-global', 'visibility', view === 'heatmap' && heatMode === 'global' ? 'visible' : 'none');
    for (const t of HEAT_TYPES)
      map.setLayoutProperty(`explore-heat-${t}`, 'visibility',
        view === 'heatmap' && heatMode === 'bytype' && selectedTypes.has(t) ? 'visible' : 'none');
  }

  function _fitBounds(ts: Track[]) {
    if (!ts.length) return;
    let w = Infinity, e = -Infinity, s = Infinity, n = -Infinity;
    for (const t of ts) for (const [lng, lat] of t.coords) {
      if (lng < w) w = lng; if (lng > e) e = lng;
      if (lat < s) s = lat; if (lat > n) n = lat;
    }
    if (w === Infinity) return;
    map.fitBounds([[w,s],[e,n]], { padding: 40, maxZoom: 14 });
  }

  async function _loadYears(years: string[]) {
    for (const year of years) {
      try {
        const r = await fetch(`/api/me/tracks/${year}`, { credentials: 'include' });
        if (!r.ok) continue;
        const d = await r.json();
        const newTracks: Track[] = d.tracks ?? [];
        if (newTracks.length > 0) {
          tracks = [...tracks, ...newTracks];
          // reactive $: statement picks up the tracks change and updates the map
        }
      } catch { /* non-fatal — skip year */ }
      loadedCount += 1;
    }
    loadingYears = false;
  }

  onMount(async () => {
    // Fetch manifest to discover available years
    let years: string[] = [];
    try {
      const r = await fetch('/api/me/tracks', { credentials: 'include' });
      if (!r.ok) { error = r.status === 404 ? 'No tracks baked yet — upload activities first.' : `Error ${r.status}`; loading = false; return; }
      const d = await r.json();
      years = d.years ?? [];
    } catch (e: any) { error = e.message ?? 'Failed to load'; loading = false; return; }

    if (years.length === 0) { loading = false; return; }

    totalYears = years.length;

    // Load the two most recent years before showing the map
    const eager = years.slice(0, 2);
    const lazy  = years.slice(2);

    for (const year of eager) {
      try {
        const r = await fetch(`/api/me/tracks/${year}`, { credentials: 'include' });
        if (r.ok) {
          const d = await r.json();
          tracks = [...tracks, ...(d.tracks ?? [])];
        }
      } catch { /* skip */ }
      loadedCount += 1;
    }
    loading = false;

    // Stream remaining years in the background
    if (lazy.length > 0) {
      loadingYears = true;
      _loadYears(lazy);
    }

    map = new maplibregl.Map({
      container: mapEl,
      style: { version: 8,
        sources: { base: { type: 'raster', tiles: TILES.grey.tiles, tileSize: 256, attribution: TILES.grey.attribution } },
        layers: [{ id: 'base', type: 'raster', source: 'base' }],
      },
      center: [12, 42], zoom: 5,
    });
    map.addControl(new maplibregl.NavigationControl(), 'top-right');

    map.on('load', () => {
      map.addSource('explore-lines', { type: 'geojson', data: _empty() });

      // Normal lines — color by type, readable opacity
      map.addLayer({ id: 'explore-lines', type: 'line', source: 'explore-lines', layout: { visibility: 'none' },
        paint: { 'line-width': 2, 'line-opacity': 0.5,
          'line-color': ['match', ['get', 'type'],
            'cycling', TYPE_COLORS.cycling, 'running', TYPE_COLORS.running,
            'hiking',  TYPE_COLORS.hiking,  'skiing',  TYPE_COLORS.skiing,
            TYPE_COLORS.other],
        },
      });

      const hp = _heatPaint(heatWidth, heatOpacityPct);

      // Global heatmap — all lines in warm amber, very low opacity so overlapping routes stack up
      map.addLayer({ id: 'explore-heat-global', type: 'line', source: 'explore-lines', layout: { visibility: 'none' },
        paint: { ...hp, 'line-blur': 1, 'line-color': '#f97316' },
      });

      // Per-type heatmap layers — same accumulation trick, type-specific colour
      for (const [type, hex] of Object.entries(TYPE_COLORS)) {
        map.addLayer({ id: `explore-heat-${type}`, type: 'line', source: 'explore-lines',
          filter: ['==', ['get', 'type'], type], layout: { visibility: 'none' },
          paint: { ...hp, 'line-blur': 1, 'line-color': hex },
        });
      }

      mapReady = true;
      _bbox = _getBbox();
      _updateMap(tracks, viewMode, heatmapMode);
      _fitBounds(tracks);
    });

    map.on('moveend', () => {
      _bbox = _getBbox();
      if (mapReady) _updateMap(filteredTracks, viewMode, heatmapMode);
    });
  });

  onDestroy(() => { if (map) map.remove(); });
</script>

<div class="explore-layout">
  <!-- Sidebar -->
  <aside class="sidebar">
    {#if !embedded}
    <header class="sidebar-header">
      <span class="sidebar-title">Explore</span>
      <a href="{base}u/{handle}/athlete/" class="back-link">← Athlete</a>
    </header>
    {/if}

    <!-- Tile layer -->
    <section class="section">
      <p class="label">Map</p>
      <div class="pills">
        {#each TILE_ORDER as key}
          <button class="pill" class:active={tileKey === key} onclick={() => setTile(key)}>{TILES[key].label}</button>
        {/each}
      </div>
    </section>

    <!-- Type filter -->
    <section class="section">
      <div class="label-row">
        <p class="label">Type</p>
        <div class="label-actions">
          <button class="mini-btn" onclick={selectAllTypes}>all</button>
          <button class="mini-btn" onclick={clearAllTypes}>none</button>
        </div>
      </div>
      <div class="pills">
        {#each allTypes as t}
          <button class="pill type-pill" class:active={selectedTypes.has(t)} onclick={() => toggleType(t)}
            style:--type-color={typeColor(t)} style:--pill-text-on={typeTextOn(t)}>
            <span class="type-dot"></span>{t}
          </button>
        {/each}
      </div>
    </section>

    <!-- Date filter -->
    <section class="section">
      <p class="label">Date</p>
      <div class="pills year-pills">
        <button class="pill" class:active={!dateFrom && !dateTo} onclick={clearDates}>All</button>
        {#each availableYears as y}
          <button class="pill" class:active={selectedYear === y} onclick={() => setYear(y)}>{y}</button>
        {/each}
      </div>
      {#if selectedYear}
        <div class="pills month-pills">
          <button class="pill small" class:active={selectedMonth === null} onclick={clearMonth}>All</button>
          {#each MONTHS as m, i}
            <button class="pill small"
              class:active={selectedMonth === i + 1}
              onclick={() => setMonth(i + 1)}>{m}</button>
          {/each}
        </div>
      {/if}
      <div class="date-inputs">
        <input type="date" class="date-input" bind:value={dateFrom} placeholder="From" />
        <input type="date" class="date-input" bind:value={dateTo}   placeholder="To" />
      </div>
    </section>

    <!-- View toggle -->
    <section class="section">
      <p class="label">View</p>
      <div class="pills">
        <button class="pill" class:active={viewMode === 'lines'}   onclick={() => viewMode = 'lines'}>Lines</button>
        <button class="pill" class:active={viewMode === 'heatmap'} onclick={() => viewMode = 'heatmap'}>Heatmap</button>
      </div>
      {#if viewMode === 'heatmap'}
        <div class="pills" style="margin-top:0.375rem">
          <button class="pill small" class:active={heatmapMode === 'global'} onclick={() => heatmapMode = 'global'}>Global</button>
          <button class="pill small" class:active={heatmapMode === 'bytype'} onclick={() => heatmapMode = 'bytype'}>By type</button>
        </div>
        <div class="slider-row">
          <span class="slider-label">Opacity</span>
          <input type="range" min="3" max="30" step="1" class="slider" bind:value={heatOpacityPct} />
          <span class="slider-val">{heatOpacityPct}%</span>
        </div>
      {/if}
      <div class="slider-row">
        <span class="slider-label">Width</span>
        <input type="range" min="2" max="12" step="1" class="slider" bind:value={heatWidth} />
        <span class="slider-val">{heatWidth}</span>
      </div>
    </section>

    <!-- Stats -->
    <section class="section">
      <p class="stat-line">
        <span class="stat-val">{filteredTracks.length}</span>
        <span class="muted"> / {tracks.length} tracks</span>
      </p>
      {#if filteredTracks.length > 0}
        <p class="stat-line muted small">
          {(filteredTracks.reduce((s,t) => s + t.dist, 0) / 1000).toFixed(0)} km total
        </p>
      {/if}
    </section>

    {#if loading}<p class="status">Loading tracks…</p>{/if}
    {#if !loading && loadingYears}<p class="status">Loading history… {loadedCount}/{totalYears}</p>{/if}
    {#if error}<p class="status error">{error}</p>{/if}
  </aside>

  <!-- Map -->
  <div class="map-wrap" bind:this={mapEl}></div>
</div>

<style>
  .explore-layout {
    display: flex;
    height: 100%;
    overflow: hidden;
  }

  .sidebar {
    width: 240px;
    flex-shrink: 0;
    background: var(--bg-card);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
  }

  .sidebar-header {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-shrink: 0;
  }
  .sidebar-title { font-size: 0.9rem; font-weight: 700; color: var(--text-primary); }
  .back-link { font-size: 0.72rem; color: var(--text-5); text-decoration: none; }
  .back-link:hover { color: var(--text-4); }

  .section {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--border-sub, var(--border));
  }

  .label {
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-5);
    margin: 0 0 0.45rem;
  }

  .label-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.45rem; }
  .label-row .label { margin-bottom: 0; }
  .label-actions { display: flex; gap: 0.25rem; }
  .mini-btn { background: none; border: none; font-size: 0.65rem; color: var(--text-5); cursor: pointer; padding: 0 0.2rem; }
  .mini-btn:hover { color: var(--text-4); }

  .pills { display: flex; gap: 0.3rem; flex-wrap: wrap; }
  .pill {
    padding: 0.2rem 0.55rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text-4);
    font-size: 0.72rem;
    cursor: pointer;
    transition: all 0.12s;
    display: flex;
    align-items: center;
    gap: 0.3rem;
  }
  .pill:hover { border-color: var(--accent); color: var(--accent); }
  .pill.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }
  .pill.small { padding: 0.15rem 0.45rem; font-size: 0.67rem; }

  .type-pill             { border-color: color-mix(in srgb, var(--type-color) 45%, transparent); color: color-mix(in srgb, var(--type-color) 65%, var(--text-5)); background: rgba(128,128,128,0.70); }
  .type-pill.active      { background: var(--type-color); border-color: var(--type-color); color: var(--pill-text-on); }
  .type-pill:hover       { border-color: var(--type-color); color: var(--type-color); }
  .type-pill.active:hover { background: var(--type-color); border-color: var(--type-color); color: var(--pill-text-on); }
  :global([data-theme="light"]) .type-pill:not(.active) { background: transparent; }
  :global([data-theme="light"]) .type-pill.active       { background: color-mix(in srgb, var(--type-color) 18%, transparent); color: var(--type-color); }
  :global([data-theme="light"]) .type-pill.active:hover { background: color-mix(in srgb, var(--type-color) 18%, transparent); border-color: var(--type-color); color: var(--type-color); }
  .type-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--type-color); flex-shrink: 0; }

  .year-pills { margin-bottom: 0.4rem; }
  .month-pills { margin-bottom: 0.4rem; }

  .date-inputs { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.4rem; }
  .date-input {
    width: 100%;
    padding: 0.25rem 0.4rem;
    border-radius: 0.3rem;
    border: 1px solid var(--border);
    background: var(--bg-elevated, var(--bg));
    color: var(--text-primary);
    font-size: 0.72rem;
    box-sizing: border-box;
  }
  .date-input:focus { outline: none; border-color: var(--accent); }

  .stat-line { margin: 0 0 0.2rem; font-size: 0.8rem; }
  .stat-val { font-weight: 600; color: var(--text-primary); }

  .status { font-size: 0.72rem; color: var(--text-5); padding: 0.5rem 1rem 0; margin: 0; }
  .status.error { color: #f87171; }

  .muted { color: var(--text-5); }
  .small { font-size: 0.72rem; margin: 0; }

  .slider-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-top: 0.5rem;
  }
  .slider-label { font-size: 0.68rem; color: var(--text-5); width: 2.8rem; flex-shrink: 0; }
  .slider { flex: 1; accent-color: var(--accent); cursor: pointer; }
  .slider-val { font-size: 0.68rem; color: var(--text-4); width: 2rem; text-align: right; flex-shrink: 0; }

  .map-wrap { flex: 1; height: 100%; }
</style>
