// Data layer — auth, API calls, and transformations from API shape to the
// design's SERIES contract (raw / avg / std / target / color / unit / invert).

// ── Auth ────────────────────────────────────────────────────────────
const TOKEN_KEY = 'hc_token';

// iOS PWA gotcha: "Add to Home Screen" creates a standalone webview with a
// separate localStorage from Safari. If we strip the token from the URL on
// first load, the user's add-to-home-screen capture loses the token and the
// installed icon launches token-less. Keep the token in the URL — single-user
// personal tool, the URL is private. Worst case (paste a screenshot of the
// URL bar publicly) is no worse than leaking the token anyway.
function loadToken() {
  const params = new URLSearchParams(window.location.search);
  const fromUrl = params.get('token');
  if (fromUrl) {
    try { window.localStorage.setItem(TOKEN_KEY, fromUrl); } catch (e) {}
    return fromUrl;
  }
  try { return window.localStorage.getItem(TOKEN_KEY) || null; }
  catch (e) { return null; }
}

function saveToken(token) {
  if (!token) return;
  try { window.localStorage.setItem(TOKEN_KEY, token); } catch (e) {}
  window.location.reload();
}

let TOKEN = loadToken();

async function api(path, opts = {}) {
  const headers = {
    'Content-Type': 'application/json',
    ...(opts.headers || {}),
  };
  if (TOKEN) headers['Authorization'] = `Bearer ${TOKEN}`;
  const res = await fetch(path, { ...opts, headers });
  if (!res.ok) {
    let detail;
    try { detail = await res.json(); } catch (_) { detail = await res.text(); }
    const err = new Error(`api ${path} -> ${res.status}`);
    err.status = res.status;
    err.detail = detail;
    throw err;
  }
  return res.json();
}

// ── Math helpers ────────────────────────────────────────────────────

// Forward-fill nulls so the spark stays continuous. If a leading value is
// null, replace it with the first non-null. If the whole array is null,
// returns zeros — caller is responsible for treating that as no-data.
function fillNulls(arr) {
  const out = arr.slice();
  let prev = null;
  for (let i = 0; i < out.length; i++) {
    if (out[i] == null) out[i] = prev;
    else prev = out[i];
  }
  // Back-fill leading nulls with the first known.
  let first = out.find((v) => v != null);
  if (first == null) return out.map(() => 0);
  for (let i = 0; i < out.length; i++) if (out[i] == null) out[i] = first;
  return out;
}

function rollingMean(arr, win = 10) {
  return arr.map((_, i) => {
    const start = Math.max(0, i - win + 1);
    const slice = arr.slice(start, i + 1);
    return slice.reduce((s, v) => s + v, 0) / slice.length;
  });
}

function rollingStd(arr, win = 10) {
  return arr.map((_, i) => {
    const start = Math.max(0, i - win + 1);
    const slice = arr.slice(start, i + 1);
    const mean = slice.reduce((s, v) => s + v, 0) / slice.length;
    const variance = slice.reduce((s, v) => s + (v - mean) ** 2, 0) / slice.length;
    return Math.sqrt(variance);
  });
}

// 5-day rolling sum, capped at 5, gives a "sessions / wk" proxy similar to
// the design's synthetic training series.
function trainingSeries(days) {
  const flags = days.map((d) => (d.has_run ? 1 : 0) + (d.has_strength ? 1 : 0));
  return flags.map((_, i) => {
    const start = Math.max(0, i - 6);
    const slice = flags.slice(start, i + 1);
    return slice.reduce((a, b) => a + b, 0);
  });
}

// ── SERIES builder ──────────────────────────────────────────────────
// Bedtime is stored on the API as decimal hours, +24 if after midnight
// (so 22:45 = 22.75, 00:15 = 24.25). We render the chart in that scale,
// then format ticks back to HH:MM via fmtBedtime.

const PALETTE = window.HC_THEME.PALETTE;

function buildSeries(days) {
  if (!days || !days.length) return null;

  const sleep_raw = fillNulls(days.map((d) => d.sleep_h));
  const hrv_raw = fillNulls(days.map((d) => d.hrv));
  const hr_raw = fillNulls(days.map((d) => d.avg_hr));
  const ready_raw = fillNulls(days.map((d) => d.readiness));
  const bedtime_raw = fillNulls(days.map((d) => d.bedtime_decimal));
  const train_raw = trainingSeries(days);

  return {
    sleep: {
      raw: sleep_raw, avg: rollingMean(sleep_raw), std: rollingStd(sleep_raw),
      target: 6.5, color: PALETTE.sleep, unit: 'h',
    },
    hrv: {
      raw: hrv_raw, avg: rollingMean(hrv_raw), std: rollingStd(hrv_raw),
      target: 25, color: PALETTE.hrv, unit: 'ms',
    },
    hr: {
      raw: hr_raw, avg: rollingMean(hr_raw), std: rollingStd(hr_raw),
      target: null, color: PALETTE.hr, unit: 'bpm', invert: true,
    },
    ready: {
      raw: ready_raw, avg: rollingMean(ready_raw), std: rollingStd(ready_raw),
      target: 70, color: PALETTE.ready, unit: '',
    },
    bedtime: {
      raw: bedtime_raw, avg: rollingMean(bedtime_raw), std: rollingStd(bedtime_raw),
      target: 22.75, color: PALETTE.sleep, unit: 'h',
    },
    training: {
      raw: train_raw, avg: rollingMean(train_raw, 7), std: rollingStd(train_raw, 7),
      target: null, color: PALETTE.training, unit: '/wk',
    },
    // Drinks per day (units). 0 means a true zero (no bar should render),
    // not null/missing — backend already collapses both into 0.
    drinks: (() => {
      const raw = days.map((d) => d.drinks == null ? 0 : Number(d.drinks));
      return {
        raw, avg: rollingMean(raw, 7), std: rollingStd(raw, 7),
        target: 3, color: PALETTE.warning, unit: 'u',
      };
    })(),
  };
}

// Format decimal hour (with +24 if after midnight) as HH:MM.
function fmtBedtime(decHour) {
  if (decHour == null) return '--:--';
  const totalMin = Math.round(decHour * 60);
  const h = Math.floor(totalMin / 60) % 24;
  const m = ((totalMin % 60) + 60) % 60;
  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}

// Split a days array into three equal phases for the Peak/Decline/Recovery
// summary cards. Real data doesn't carry phase labels — we approximate by
// thirds so the visual contract is preserved without needing user input.
function thirdsPhases(days) {
  const n = days ? days.length : 0;
  const a = Math.floor(n / 3);
  const b = Math.floor((2 * n) / 3);
  return [
    { name: 'Early',    range: [0, a],   color: 'rgba(58,125,92,0.05)' },
    { name: 'Mid',      range: [a, b],   color: 'rgba(192,57,43,0.05)' },
    { name: 'Recent',   range: [b, n],   color: 'rgba(46,111,173,0.05)' },
  ];
}

function phaseAvg(series, phase) {
  const [a, b] = phase.range;
  if (!series || a >= b) return NaN;
  const slice = series.raw.slice(a, b);
  if (!slice.length) return NaN;
  return slice.reduce((s, v) => s + v, 0) / slice.length;
}

// Format a delta value with arrow + sign. positiveGood=true means up is good.
function fmtDelta(v, decimals = 1, positiveGood = true) {
  if (v == null || isNaN(v)) return '';
  const abs = Math.abs(v).toFixed(decimals);
  const arrow = v >= 0 ? '↑' : '↓';
  return `${arrow} ${abs}`;
}

function fmtHoursMinutes(hours) {
  if (hours == null) return '--';
  const totalMin = Math.round(hours * 60);
  const h = Math.floor(totalMin / 60);
  const m = totalMin % 60;
  return `${h}h${String(m).padStart(2, '0')}m`;
}

// Map ISO date -> integer index in the days array, for event annotations.
function eventsToAnnotations(events, days) {
  if (!events || !days) return [];
  const idx = new Map();
  days.forEach((d, i) => idx.set(d.day, i));
  return events
    .map((e) => ({ idx: idx.get(e.date), label: e.label, category: e.category }))
    .filter((e) => e.idx != null);
}

window.HC_DATA = {
  TOKEN, api, saveToken,
  buildSeries, fmtBedtime, fmtDelta, fmtHoursMinutes,
  thirdsPhases, phaseAvg, eventsToAnnotations,
  rollingMean, rollingStd,
};
