// Weekly (map) page. Imports replace the old script-tag ordering contract.
import { loadLastLocation, saveLastLocation, locHash } from "./nav.js";
import { fmtTemp, onUnitChange, toWind, windUnit, precipUnit } from "./units.js";
import { uv } from "./account.js";   // header sign-in entry + notification bell, and the API-version resolver
import { loadView, TTL, prefetchViews } from "./cache.js";
import { initFindButton, setFindLabel } from "./mappicker.js";
import { W, PW, H, PL, PR, plotTop, plotBot, xLabY, setChartWidth, chartPalette,
         tempChart, precipChart, dryChart, attachChartHover } from "./chart.js";
import { track } from "./digest.js";
import { TIER_COLORS, SCALE_TEMP, drynessColor, pctOrd, esc, todayISO, placeLabel,
         tierKeySegs, GUIDE_LINK, fmtPrecip, fmtPrecipTier, fmtWind, fmtHumid } from "./shared.js";

let selected = null; // {lat, lon}

const dateInput = document.getElementById("date-input");
const todayBtn = document.getElementById("today-btn");
dateInput.value = todayISO();
dateInput.max = dateInput.value;

// The "Today" chip snaps the target date back to today; it stays lit while the
// target already is today, so it doubles as a "you're viewing now" indicator.
function syncTodayBtn() {
  todayBtn.classList.toggle("active", dateInput.value === todayISO());
}
syncTodayBtn();
todayBtn.addEventListener("click", () => {
  const t = todayISO();
  if (dateInput.value === t) return;
  dateInput.value = t;
  dateInput.max = t;
  syncTodayBtn();
  if (selected) runGrade();
});

function selectLocation(lat, lon) {
  selected = { lat, lon };
  // Show the raw coordinates immediately; render() replaces them with the resolved
  // neighborhood + city name once the grade fetch returns.
  locLabel.textContent = `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
  locLabel.hidden = false;
  runGrade();
}

// ---- location picker ----
// The Find button opens the shared modal map picker; choosing a spot loads it.
const findBtn = document.getElementById("find-btn");
const locLabel = document.getElementById("loc-label");
initFindButton(findBtn, "Find a location",
  () => selected, (lat, lon) => selectLocation(lat, lon));

// ---- hero ----
// The hero's grade card is server-rendered showing the most unusual city we're
// tracking; once the visitor has a place of their own it re-points at that. The
// frame and its text slots already exist in the HTML, so swapping the numbers in
// shifts nothing on the page.
const hero = document.getElementById("hero");

const heroLocate = document.getElementById("hero-locate");
const heroPick = document.getElementById("hero-pick");
const heroMsg = document.getElementById("hero-locate-msg");

function locateMsg(text) {
  if (!heroMsg) return;
  heroMsg.textContent = text || "";
  heroMsg.hidden = !text;
}

// Browsers expose geolocation ONLY on secure origins, but `navigator.geolocation`
// still exists on plain http:// — getCurrentPosition just fails with a permission
// error. So feature-testing the object is not enough: without the isSecureContext
// check the button is a dead control on the plain-HTTP LAN dev server (and on any
// phone reaching it by IP), which is exactly how it shipped broken.
// Rather than offer a button that cannot work, promote the map picker instead.
const canLocate = !!navigator.geolocation && window.isSecureContext;
if (heroLocate && !canLocate) {
  heroLocate.hidden = true;
  heroPick?.classList.replace("btn-ghost", "btn-primary");
}

heroLocate?.addEventListener("click", () => {
  track("home.locate");
  const label = heroLocate.textContent;
  const restore = () => {
    heroLocate.disabled = false;
    heroLocate.textContent = label;
  };
  // Geolocation can take seconds (or hang until the timeout), so say so —
  // a button that looks inert is indistinguishable from a broken one.
  heroLocate.disabled = true;
  heroLocate.textContent = "Locating…";
  locateMsg("");

  navigator.geolocation.getCurrentPosition(
    (pos) => {
      restore();
      selectLocation(pos.coords.latitude, pos.coords.longitude);
    },
    (err) => {
      restore();
      // Always say something: a declined prompt or a timeout must not look
      // like the button did nothing.
      locateMsg(
        err.code === err.PERMISSION_DENIED
          ? "Location permission was declined. Pick a spot on the map instead."
          : err.code === err.TIMEOUT
            ? "That took too long. Try again, or pick a spot on the map."
            : "Your location isn't available right now. Pick a spot on the map instead.",
      );
    },
    { enableHighAccuracy: false, timeout: 10000, maximumAge: 600000 },
  );
});

heroPick?.addEventListener("click", () => { locateMsg(""); findBtn.click(); });

/** Re-point the hero card at the place the visitor actually chose. */
function updateHero(data) {
  if (!hero) return;
  const card = document.getElementById("hero-grade");
  const targetDay = data.recent.find((d) => d.date === data.target_date);
  const g = targetDay && (targetDay.tmax || targetDay.tmin);
  if (!card || !g) return;

  document.getElementById("hero-where").textContent = `Today in ${placeLabel(data)}`;
  document.getElementById("hero-band").textContent = g.grade;
  document.getElementById("hero-detail").textContent =
    `${targetDay.tmax === g ? "High temp" : "Low temp"} in the ${pctOrd(g.percentile)} percentile`;
  // Band colour rides the same token set as everything else; the band word is
  // always present too, so colour is never the only signal.
  card.style.setProperty("--cat", TIER_COLORS[g.class] || "");
  hero.querySelector(".grade-why")?.remove();
}

dateInput.addEventListener("change", () => { syncTodayBtn(); selected && runGrade(); });

// ---- grade request + render ----
const placeholder = document.getElementById("placeholder");
const results = document.getElementById("results");
let lastGraded = null; // most recent /api/grade response (for the PNG export)

// Clicking a graded day (any cell or its date header) opens its full breakdown.
results.addEventListener("click", (e) => {
  const row = e.target.closest(".rd-grid [data-date]");
  if (!row || !selected) return;
  location.href = `day${locHash(selected.lat, selected.lon, row.dataset.date)}`;
});

let gradeSeq = 0;   // supersedes an in-flight load when the user picks a new spot/date

async function runGrade() {
  if (!selected) return;
  updateHash();
  placeholder.hidden = true;
  results.hidden = false;
  // Always send the target date, even when it's "today" — the backend only
  // falls back to its own UTC today (api_grade's `date` param default; see
  // backend/web/app.py) when the param is omitted or empty, a day behind local
  // midnight for any viewer east of UTC (reported from Kaunas, UTC+3: asking
  // for 7/26 rendered 7/25). The comment this replaces claimed omitting the
  // date kept this URL matching the cross-view prefetch — but that match was
  // the other half of the bug: cache.js's viewFetches() seeds the grade view's
  // cache entry under this exact dateless URL too, tagged fresh for the
  // viewer's local day no matter which day the server actually resolved, so a
  // same-tab nav that had already prefetched this cell (e.g. via the Day page)
  // could serve that stale, server-resolved slice straight out of IndexedDB
  // with no live request ever going out. Sending the real date makes every
  // load self-consistent with the viewer's own clock instead of aliasing onto
  // whatever "today" the server or an earlier prefetch resolved.
  // dateInput.value can also come back empty (a cleared native date field —
  // day.js guards the same input for the same reason), so fall back to the
  // viewer's local today rather than ever send `date=` empty, which hits that
  // same server fallback.
  const date = dateInput.value || todayISO();
  const q = `lat=${selected.lat}&lon=${selected.lon}&date=${date}`;
  const url = uv(`grade?${q}`);
  await loadView({
    url, ttl: TTL.grade, seq: ++gradeSeq, current: () => gradeSeq,
    spinner: () => { results.innerHTML = `<p class="spinner">Fetching decades of climate history…</p>`; },
    onData: render,
    onError: (err) => { results.innerHTML = `<p class="error">${err.message}</p>`; },
  });
}

// Compact cell formatters for the dense recent/forecast table (units live in the
// column headers, so values stay short). Dry days label the running dry streak
// (see precipCell) rather than the rain amount.
const cTemp = fmtTemp;   // active unit, bare degree
const cHum = (v) => (v == null ? "—" : `${Math.round(v)}%`);   // relative %, unit-invariant
const cWind = (v) => (v == null ? "—" : `${Math.round(toWind(v))}`);
// fmtPrecip carries the precision: whole millimetres, two decimals for inches.
const cRain = (v) => (v == null ? "—" : v > 0 ? fmtPrecip(v, false) : "·");

// Monochrome inline icons (currentColor) so the UI chrome matches the dark theme
// instead of using colorful emoji. Feather-style line paths.
const IC = {
  link: `<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
  download: `<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
  calendar: `<svg class="ic ic-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`,
};

const MONTH_NAMES = ["January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December"];

// Friendly, plain-language "what to expect" sentences for the selected date's
// season, built from the ±7-day climatology frequencies (rain_freq / sun_freq)
// the backend now returns. Rain drives a bit of cute packing advice. Any piece
// with no data (e.g. sunshine on a NASA/MET-sourced cell) is simply omitted.
function insightsHTML(data) {
  const c = data.climatology || {};
  const parts = data.target_date ? data.target_date.split("-") : [];
  const monthName = parts.length === 3 ? MONTH_NAMES[+parts[1] - 1] : null;
  const around = monthName ? `around mid-${monthName}` : "this time of year";
  const lines = [];

  const hi = c.tmax && c.tmax.p50 != null ? c.tmax.p50 : null;
  const lo = c.tmin && c.tmin.p50 != null ? c.tmin.p50 : null;
  if (hi != null && lo != null) {
    lines.push(`<li><span class="ins-emoji" aria-hidden="true">🌡️</span> A typical day ${esc(around)} peaks near <b>${fmtTemp(hi, true)}</b> and dips to <b>${fmtTemp(lo, true)}</b>.</li>`);
  }

  if (c.sun_freq != null) {
    const pct = Math.round(c.sun_freq * 100);
    const emoji = c.sun_freq >= 0.6 ? "☀️" : c.sun_freq >= 0.3 ? "⛅" : "☁️";
    const tone = c.sun_freq >= 0.6 ? "plenty of sunshine — " : c.sun_freq < 0.3 ? "often cloudy — " : "";
    lines.push(`<li><span class="ins-emoji" aria-hidden="true">${emoji}</span> ${tone}sunny about <b>${pct}%</b> of days ${esc(around)}.</li>`);
  }

  if (c.rain_freq != null) {
    const pct = Math.round(c.rain_freq * 100);
    let emoji, advice;
    if (c.rain_freq < 0.15) {
      emoji = "🌤️"; advice = "you can probably leave the umbrella at home.";
    } else if (c.rain_freq < 0.4) {
      emoji = "🌦️"; advice = "maybe tuck a compact umbrella in your bag just in case.";
    } else {
      emoji = "🌧️"; advice = "don't forget your favorite umbrella or raincoat!";
    }
    lines.push(`<li class="ins-rain"><span class="ins-emoji" aria-hidden="true">${emoji}</span> Rain on about <b>${pct}%</b> of days ${esc(around)} — ${advice}</li>`);
  }

  if (!lines.length) return "";
  return `<div class="insights">
    <p class="insight-lead">What to expect ${esc(around)}</p>
    <ul class="insight-list">${lines.join("")}</ul>
  </div>`;
}

function render(data) {
  recentData = data;
  updateHero(data);
  // The place name appears once — as the results heading (<h2>) below, which also
  // carries the coordinates + climatology context. The top label only holds the
  // transient coordinates written on selection, so hide it now that the named
  // results have rendered (otherwise the name would show twice).
  locLabel.hidden = true;
  locLabel.textContent = "";
  setFindLabel(findBtn, "Change location");
  const c = data.climatology;
  const cell = data.cell;
  const yrs = c.year_range ? `${c.year_range[0]}–${c.year_range[1]}` : "—";
  const coords = `${cell.center_lat.toFixed(3)}, ${cell.center_lon.toFixed(3)}`;

  // Each card compares the target day's value to the normal (median) for that
  // metric, tinted by its grade, and links to the full single-day breakdown. The
  // window now runs past the target into the forecast, so the target is looked up
  // by date rather than taken as the newest row.
  const targetDay = data.recent.find((d) => d.date === data.target_date) || null;
  const dayHref = `day${locHash(selected.lat, selected.lon, data.target_date)}`;
  const cmpCard = (label, key, clim, fmt) => {
    const g = targetDay ? targetDay[key] : null;
    const normalVal = clim ? fmt(clim.p50) : "—";
    const cat = g ? (TIER_COLORS[g.class] || "") : "";
    const big = g ? fmt(g.value) : normalVal;
    const grade = g
      ? `<span class="nc-grade" style="--cat:${cat}">${g.grade}</span>`
      : "";
    return `<a class="normal-card" href="${dayHref}">
      <div class="nc-head"><h3>${label}</h3>${grade}</div>
      <div class="big"${g ? ` style="--cat:${cat}"` : ""}>${big}</div>
      <div class="range">normal: <b>${normalVal}</b></div>
    </a>`;
  };

  results.innerHTML = `
    <div class="loc-head">
      <div class="loc-title">
        <h2>${placeLabel(data)}</h2>
        <ul class="loc-meta">
          ${data.place ? `<li><b>${coords}</b></li>` : ""}
          <li>Climatology from <b>${yrs}</b></li>
          <li><b>${c.n_samples.toLocaleString()}</b> days sample size (day ±7)</li>
        </ul>
      </div>
      <div class="share">
        <button id="btn-link" title="Copy a link back to this exact view">${IC.link} Copy link</button>
        <button id="btn-png" title="Download the trend chart as an image">${IC.download} Chart</button>
      </div>
    </div>
    <a class="cta-cal" href="calendar${locHash(selected.lat, selected.lon)}">
      <span class="cta-cal-icon" aria-hidden="true">${IC.calendar}</span>
      <span>Check historical data</span>
      <span class="cta-cal-arrow" aria-hidden="true">→</span>
    </a>
    ${insightsHTML(data)}
    <div class="section-title">${data.target_date} vs a typical day. Tap a metric for the full breakdown</div>
    <div class="normals">
      ${cmpCard("High", "tmax", c.tmax, fmtTemp)}
      ${cmpCard("Low", "tmin", c.tmin, fmtTemp)}
      ${cmpCard("Feels", "feels", c.feels, fmtTemp)}
      ${cmpCard("Humidity", "humid", c.humid, fmtHumid)}
      ${cmpCard("Wind", "wind", c.wind, fmtWind)}
      ${cmpCard("Gust", "gust", c.gust, fmtWind)}
      ${cmpCard("Precip", "precip", c.precip, fmtPrecip)}
    </div>
    <div id="series"></div>
  `;

  renderSeries();          // fills #series (chart + graded-days timeline) from the window
  prefetchViews(selected.lat, selected.lon, ["grade", "forecast"]);  // warm calendar/day
  document.getElementById("btn-link").onclick = copyShareLink;
  document.getElementById("btn-png").onclick = downloadChartPng;
}

// One tinted value cell: background + value colored by the day's grade tier for
// that metric (the same diverging scale as everywhere else). Carries the date so
// tapping any cell opens that day's full breakdown.
function rdCell(g, fmt, date, isFc, isToday) {
  const dd = date ? ` data-date="${date}"` : "";
  const cls = "rd-c" + (isFc ? " rd-fc" : isToday ? " rd-today" : "");
  if (!g) return `<span class="${cls}"${dd}>—</span>`;
  const col = TIER_COLORS[g.class] || "";
  return `<span class="${cls}"${dd} style="--c:${col}">${fmt(g.value)}</span>`;
}

// Precip cell: rain days show the amount (tinted by intensity tier); dry days
// label the running dry streak — "3d" = 3 days since measurable rain, warmed by
// the same dryness ramp as the chart — instead of a bare dot.
function precipCell(d, isFc, isToday) {
  const g = d.precip;
  const dd = ` data-date="${d.date}"`;
  const cls = "rd-c" + (isFc ? " rd-fc" : isToday ? " rd-today" : "");
  if (!g) return `<span class="${cls}"${dd}>—</span>`;
  // Only a genuinely dry day (no rain at all) labels the streak; any rain day —
  // down to sub-0.01" trace that rounds to 0 — shows its depth, tinted by tier.
  if (g.class === "dry" && d.dsr != null && d.dsr > 0) {
    return `<span class="${cls}"${dd} style="--c:${drynessColor(d.dsr)}" title="${d.dsr} days since rain">${d.dsr}d</span>`;
  }
  const col = TIER_COLORS[g.class] || "";
  const amt = g.class && g.class !== "dry" ? fmtPrecipTier(g.value, g.class, false) : cRain(g.value);
  return `<span class="${cls}"${dd} style="--c:${col}">${amt}</span>`;
}

// Compact "graded days" table — a ROW per metric, a COLUMN per day (newest first).
// Wide runs (e.g. 2 weeks) scroll horizontally inside their own container; the
// metric-label column stays pinned. Tapping any cell/day opens that day's detail.
// Wind and rain cells are bare numbers to keep the grid narrow, so their row
// label carries the unit — and has to be built per render, since the unit can
// change under us (onUnitChange re-renders).
const RD_METRICS = () => [
  ["tmax", "Hi", cTemp], ["tmin", "Lo", cTemp], ["feels", "Feel", cTemp],
  ["humid", "Hum", cHum],
  ["wind", `Wind <span class="rd-unit">${windUnit()}</span>`, cWind],
  ["gust", `Gust <span class="rd-unit">${windUnit()}</span>`, cWind],
  ["precip", `Rain <span class="rd-unit">${precipUnit()}</span>`, cRain],
];
function daysTable(rows, targetDate) {
  if (!rows.length) return "";
  const todayDate = todayISO();
  const isFc = (d) => d.date > todayDate;             // future = forecast columns
  const isTarget = (d) => d.date === targetDate;      // the graded target day (orange marker)
  const cols = `grid-template-columns: 46px repeat(${rows.length}, minmax(42px, 1fr));`;
  const header = `<div class="rd-corner"></div>` + rows.map((d) => {
    const dt = new Date(d.date + "T00:00:00");
    const dow = dt.toLocaleDateString(undefined, { weekday: "short" });
    const md = dt.toLocaleDateString(undefined, { month: "numeric", day: "numeric" });
    const cls = "rd-colh" + (isTarget(d) ? " rd-today" : isFc(d) ? " rd-fc" : "");
    return `<div class="${cls}" data-date="${d.date}" title="See the full breakdown for this day"><b>${dow}</b><span>${md}</span></div>`;
  }).join("");
  const body = RD_METRICS().map(([key, label, fmt]) =>
    `<div class="rd-mlabel">${label}</div>` +
    rows.map((d) => key === "precip"
      ? precipCell(d, isFc(d), isTarget(d))
      : rdCell(d[key], fmt, d.date, isFc(d), isTarget(d))).join("")
  ).join("");
  return `<div class="rd-scroll"><div class="rd-grid" style="${cols}">${header}${body}</div></div>`;
}

// ---- graded-days timeline ----
// The header normals stay fixed on the target day; the chart + graded-days table
// below show one continuous window built server-side: two weeks of history before
// the target, the target itself (orange marker), then the days after it — real
// observations when they're in the past, the forward forecast when they run into
// the future (see backend _build_grade).
let recentData = null;     // /grade response — the whole window, newest-first

// Repaint everything in the newly-picked unit.
onUnitChange(() => {
  if (recentData) render(recentData);
});

// Rebuild the chart when the window is resized so its drawing width keeps
// tracking the container (debounced — resize fires continuously while dragging).
let resizeTimer = null;
window.addEventListener("resize", () => {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(() => { if (recentData) renderSeries(); }, 160);
});

function renderSeries() {
  const host = document.getElementById("series");
  if (!host || !recentData) return;
  const days = recentData.recent;                  // newest-first: history · target · after/forecast
  const target = recentData.target_date;
  const hasFc = days.some((d) => d.date > todayISO());   // window runs into the forecast
  host.innerHTML = `
    <div class="section-title">Trend vs normal${hasFc ? " · recent → forecast" : ""}</div>
    <div id="chart-wrap"></div>
    ${tierKeyHtml()}
    <div class="section-title">Daily, graded${hasFc ? ": recent &amp; 7-day forecast" : ""}</div>
    <div class="rd-orient"><span>← Older</span><span>Newer →</span></div>
    ${daysTable([...days].reverse(), target) || '<p class="muted">Nothing to grade.</p>'}
  `;
  buildChart(recentData);
  scrollTableToTarget(host, target);
}

// Open the timeline centered on the target day's column (the orange marker), so
// the two weeks of history and the days after it are both in view.
function scrollTableToTarget(host, target) {
  const scroll = host.querySelector(".rd-scroll");
  if (!scroll) return;
  requestAnimationFrame(() => {
    const col = scroll.querySelector(`.rd-colh[data-date="${target}"]`);
    if (!col) { scroll.scrollLeft = scroll.scrollWidth; return; }
    scroll.scrollLeft = Math.max(0, col.offsetLeft - (scroll.clientWidth - col.offsetWidth) / 2);
  });
}

// ---- trend chart (self-contained inline SVG) ----

function tierKeyHtml() {
  // Highest tier first (Near Record hot → Near Record cold).
  const segs = tierKeySegs([...SCALE_TEMP].reverse().map((t) =>
    ({ color: TIER_COLORS[t.c], label: t.label })));
  return `<div class="section-title">Grade scale · low → high vs local normal</div>
    <div class="tier-key">${segs}</div>${GUIDE_LINK}`;
}

let chartDays = [];
// Which chart category is shown. Persisted so a refresh keeps your selection.
let chartMetric = localStorage.getItem("thermograph:chartMetric") || "tmax";

function buildChart(data) {
  const wrap = document.getElementById("chart-wrap");
  const C = chartPalette();
  // Drawing width tracks the on-screen box (see chart.js) so wide monitors gain
  // plot room while phones keep the 720-unit floor (the SVG just scales down).
  setChartWidth(wrap.clientWidth);
  lastGraded = data;                       // the series currently drawn (for PNG export)
  chartDays = [...data.recent].reverse(); // oldest -> newest
  const days = chartDays, n = days.length;
  if (!n) { wrap.innerHTML = ""; return; }
  const xFor = (i) => (n === 1 ? PL + PW / 2 : PL + (i * PW) / (n - 1));

  // shared: x date labels
  const step = n > 9 ? 2 : 1;
  let xlab = "";
  days.forEach((d, i) => {
    if (i % step && i !== n - 1) return;
    const dt = new Date(d.date + "T00:00:00");
    xlab += `<text x="${xFor(i).toFixed(1)}" y="${xLabY}" text-anchor="middle" font-size="10.5" fill="${C.muted}">${dt.getMonth() + 1}/${dt.getDate()}</text>`;
  });
  // shared: hover hit targets over the plot
  let hits = "";
  days.forEach((_, i) => {
    const w = n === 1 ? PW : PW / (n - 1);
    hits += `<rect class="hit" data-i="${i}" x="${(xFor(i) - w / 2).toFixed(1)}" y="${plotTop}" width="${w.toFixed(1)}" height="${plotBot - plotTop}" fill="transparent"/>`;
  });

  const built = chartMetric === "precip" ? precipChart(days, n, xFor, C)
    : chartMetric === "dry" ? dryChart(days, n, xFor, C)
    : tempChart(chartMetric, days, n, xFor, C);

  // Solid orange marker line ON the target day, plus a dashed "forecast →" divider
  // where the window crosses from the past into the future — drawn only when it's
  // clearly separated from the target marker (i.e. the target is in the past; when
  // the target is today the marker itself already sits at that boundary).
  let marks = "";
  const tIdx = days.findIndex((d) => d.date === data.target_date);
  if (tIdx >= 0) {
    const xt = xFor(tIdx).toFixed(1);
    const td = new Date(data.target_date + "T00:00:00");
    marks += `<line x1="${xt}" y1="${plotTop}" x2="${xt}" y2="${plotBot}" stroke="${C.accent}" stroke-width="1.8" opacity="0.9"/>`
      + `<text x="${xt}" y="${plotTop - 3}" text-anchor="middle" font-size="9" font-weight="700" fill="${C.accent}">${td.getMonth() + 1}/${td.getDate()}</text>`;
  }
  const fIdx = days.findIndex((d) => d.date > todayISO());   // first forecast (future) day
  if (fIdx > tIdx + 1) {
    const xd = ((xFor(fIdx - 1) + xFor(fIdx)) / 2).toFixed(1);
    marks += `<line x1="${xd}" y1="${plotTop}" x2="${xd}" y2="${plotBot}" stroke="${C.accent}" stroke-width="1.1" stroke-dasharray="4 3" opacity="0.55"/>`
      + `<text x="${xd}" y="${plotTop - 3}" text-anchor="middle" font-size="8.5" font-weight="700" fill="${C.accent}" opacity="0.8">forecast →</text>`;
  }

  const title = `${placeLabel(data)} · ${data.target_date}`;
  const svg = `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" font-family="system-ui, -apple-system, Segoe UI, sans-serif" role="img" aria-label="${built.aria}">
    <rect x="0" y="0" width="${W}" height="${H}" fill="${C.surface}"/>
    <text x="${PL - 8}" y="24" font-size="12.5" font-weight="700" fill="${built.color}">${built.subtitle}</text>
    <text x="${W - PR}" y="24" text-anchor="end" font-size="11" fill="${C.muted}">${title}</text>
    ${built.content}
    ${marks}
    ${xlab}
    <line id="crosshair" x1="0" y1="${plotTop}" x2="0" y2="${plotBot}" stroke="${C.muted}" stroke-width="1" stroke-dasharray="3 3" opacity="0"/>
    ${hits}
  </svg>`;

  const btn = (m, label) => `<button type="button" data-metric="${m}"${chartMetric === m ? ' class="active"' : ""}>${label}</button>`;
  wrap.innerHTML = `
    <div class="chart-toggle" id="chart-toggle" role="group" aria-label="Chart metric">
      ${btn("tmax", "High")}${btn("tmin", "Low")}${btn("feels", "Feels")}${btn("humid", "Humid")}${btn("wind", "Wind")}${btn("gust", "Gust")}${btn("precip", "Precip")}${btn("dry", "Dry")}
    </div>
    <div class="chart-legend">${built.legend}</div>
    ${svg}
    <div id="chart-tip" class="chart-tip" hidden></div>`;

  document.getElementById("chart-toggle").addEventListener("click", (e) => {
    const b = e.target.closest("button[data-metric]");
    if (!b || b.dataset.metric === chartMetric) return;
    chartMetric = b.dataset.metric;
    try { localStorage.setItem("thermograph:chartMetric", chartMetric); } catch (e) {}
    buildChart(data);
  });
  attachChartHover(wrap, chartDays);
}

// ---- share ----
function copyShareLink() {
  if (!selected) return;
  const url = `${location.origin}${location.pathname}${locHash(selected.lat, selected.lon, dateInput.value)}`;
  navigator.clipboard.writeText(url).then(
    () => flashBtn("btn-link", "✓ Copied!"),
    () => { prompt("Copy this link:", url); }
  );
}

// Build a table of graded days as SVG markup, stacked below the chart.
function exportTableSvg(C) {
  const rows = lastGraded.recent; // newest first (matches the on-screen table)
  const rowH = 27, headH = 42, padB = 16;
  const top = H + 10;
  const cDate = PL, cPcp = PL + 150, cHigh = PL + 322, cLow = PL + 494;
  const totalH = top + headH + rows.length * rowH + padB;

  const cell = (x, g, isPcp) => {
    if (!g) return `<text x="${x}" y="0" font-size="12" fill="${C.muted}">—</text>`;
    const color = TIER_COLORS[g.class] || C.ink;
    const v = isPcp ? `${g.value.toFixed(2)}"` : fmtTemp(g.value);
    return `<text x="${x}" y="0"><tspan font-size="13" font-weight="700" fill="${C.ink}">${v}</tspan>` +
           `<tspan dx="7" font-size="11" font-weight="600" fill="${color}">${esc(g.grade)}</tspan></text>`;
  };

  const th = (x, t) =>
    `<text x="${x}" y="${top + 30}" font-size="10.5" font-weight="700" letter-spacing="0.05em" fill="${C.muted}">${t}</text>`;

  let body = "";
  rows.forEach((d, i) => {
    const y = top + headH + i * rowH;
    const dt = new Date(d.date + "T00:00:00");
    const dstr = dt.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
    body += `<g transform="translate(0 ${y})">` +
      (i ? `<line x1="${PL}" y1="-7" x2="${W - PR}" y2="-7" stroke="${C.grid}" stroke-width="1"/>` : "") +
      `<text x="${cDate}" y="0" font-size="12.5" fill="${C.ink}">${esc(dstr)}</text>` +
      cell(cPcp, d.precip, true) + cell(cHigh, d.tmax, false) + cell(cLow, d.tmin, false) +
      `</g>`;
  });

  const markup = `
    <text x="${PL}" y="${top + 8}" font-size="13" font-weight="700" fill="${C.ink}">Recent days, graded: ${esc(placeLabel(lastGraded))}</text>
    ${th(cDate, "DATE")}${th(cPcp, "PRECIP")}${th(cHigh, "HIGH")}${th(cLow, "LOW")}
    <line x1="${PL}" y1="${top + headH - 9}" x2="${W - PR}" y2="${top + headH - 9}" stroke="${C.grid}" stroke-width="1.5"/>
    ${body}`;
  return { markup, totalH };
}

function downloadChartPng() {
  const chart = document.querySelector("#chart-wrap svg");
  if (!chart || !lastGraded) return;
  const C = chartPalette();

  // Chart body minus interactive-only layers.
  const clone = chart.cloneNode(true);
  const cross = clone.querySelector("#crosshair"); if (cross) cross.remove();
  clone.querySelectorAll("rect.hit").forEach((r) => r.remove());
  const chartInner = clone.innerHTML;

  const { markup, totalH } = exportTableSvg(C);
  const xml = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${totalH}" width="${W * 2}" height="${totalH * 2}" font-family="system-ui, -apple-system, Segoe UI, sans-serif">` +
    `<rect x="0" y="0" width="${W}" height="${totalH}" fill="${C.surface}"/>` +
    chartInner + markup + `</svg>`;

  const url = URL.createObjectURL(new Blob([xml], { type: "image/svg+xml;charset=utf-8" }));
  const img = new Image();
  img.onload = () => {
    const canvas = document.createElement("canvas");
    canvas.width = W * 2; canvas.height = totalH * 2;
    canvas.getContext("2d").drawImage(img, 0, 0);
    URL.revokeObjectURL(url);
    canvas.toBlob((blob) => {
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = `thermograph_${dateInput.value}.png`;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 1000);
      flashBtn("btn-png", "✓ Saved");
    }, "image/png");
  };
  img.src = url;
}

function flashBtn(id, text) {
  const b = document.getElementById(id);
  if (!b) return;
  const orig = b.innerHTML;          // preserve the inline icon markup
  b.textContent = text;
  setTimeout(() => (b.innerHTML = orig), 1600);
}

function updateHash() {
  if (!selected) return;
  history.replaceState(null, "", locHash(selected.lat, selected.lon, dateInput.value));
  saveLastLocation(selected.lat, selected.lon, dateInput.value);
}

// Start where you left off: an explicit link (hash) wins, otherwise the last
// place you looked at (remembered across views), otherwise the default world map.
function restoreFromHash() {
  const loc = loadLastLocation();
  if (!loc) return;
  if (loc.date) dateInput.value = loc.date;
  syncTodayBtn();
  selectLocation(loc.lat, loc.lon);
}
restoreFromHash();
