Source: js/gap-utils.js

// js/gap-utils.js
export function median(arr) {
  const a = arr.slice().sort((x, y) => x - y);
  const n = a.length;
  if (!n) return null;
  const m = n >> 1;
  return n % 2 ? a[m] : (a[m - 1] + a[m]) / 2;
}

/** Hybrid-Gap-Erkennung: erwartet 1-Min-Takt, erlaubt Jitter & fasst nahe Gaps zusammen. */
export function detectGapsHybrid(
  series,
  {
    expectedMs = 60_000,
    tolerancePct = 0.35, // ±35% Jitter tolerieren
    factor = 1.6, // zusätzlicher Median-Faktor
    minDurationMs = 0, // alles markieren (wir zeigen die Schwelle im Badge)
    mergeWithinMs = 90_000, // nahe Gaps zusammenfassen
  } = {}
) {
  if (!series || series.length < 3) return [];

  const diffs = [];
  for (let i = 1; i < series.length; i++) {
    const d = series[i].t - series[i - 1].t;
    if (d > 0) diffs.push(d);
  }
  const med = median(diffs) || expectedMs;
  const thresh = Math.max(expectedMs * (1 + tolerancePct), med * factor);

  const raw = [];
  for (let i = 1; i < series.length; i++) {
    const a = series[i - 1].t,
      b = series[i].t;
    const dt = b - a;
    if (dt >= thresh) raw.push({ from: a, to: b, dt });
  }
  if (!raw.length) return [];

  // mergen
  const merged = [];
  let cur = raw[0];
  for (let i = 1; i < raw.length; i++) {
    const g = raw[i];
    if (g.from - cur.to <= mergeWithinMs) {
      cur.to = Math.max(cur.to, g.to);
      cur.dt = cur.to - cur.from;
    } else {
      merged.push(cur);
      cur = g;
    }
  }
  merged.push(cur);

  return merged
    .filter((g) => g.dt >= minDurationMs)
    .map((g) => ({ ...g, missed: Math.max(1, Math.round(g.dt / expectedMs) - 1) }));
}

/** Metriken über den sichtbaren Zeitraum (Qualität etc.). */
export function summarizeQuality(series, { start, end, expectedMs = 60_000, gapOpts = {} } = {}) {
  const s = Array.isArray(series) ? series.filter((p) => p.t >= start && p.t <= end) : [];
  const gaps = detectGapsHybrid(s, gapOpts);

  // „vorhandene Minuten“: wir gehen davon aus, dass mindestens ein Wert (innen/außen) pro Minute reicht
  const minutes = new Set();
  for (const p of s) minutes.add(Math.floor(p.t / expectedMs));

  const expectedMinutes = Math.max(0, Math.round((end - start) / expectedMs));
  const presentMinutes = Math.min(expectedMinutes, minutes.size);
  const missingMinutes = Math.max(0, expectedMinutes - presentMinutes);

  const longestGapMs = gaps.reduce((m, g) => Math.max(m, g.dt), 0);
  const lastGapEnd = gaps.length ? Math.max(...gaps.map((g) => g.to)) : null;

  const quality = expectedMinutes ? presentMinutes / expectedMinutes : 1;

  return {
    gaps,
    expectedMinutes,
    presentMinutes,
    missingMinutes,
    quality, // 0…1
    longestGapMs,
    lastGapEnd,
  };
}

/** hübsche Farben (Bootstrap) je nach Schwellwerten */
export function colorByQuality(pct) {
  // Qualität (in Prozent, 0..100):
  // ≥99 → grün, ≥97 → gelb, sonst rot
  const p = Number(pct);
  if (p >= 99) return 'success';
  if (p >= 97) return 'warning';
  return 'danger';
}
export function colorByMissingRatio(ratio) {
  // Fehlende Minuten relativ (0..1):
  // ≤1% → grün, ≤3% → gelb, sonst rot
  const r = Number(ratio);
  if (r <= 0.01) return 'success';
  if (r <= 0.03) return 'warning';
  return 'danger';
}
export function colorByMinutes(mins, { ok = 5, warn = 15 } = {}) {
  if (mins <= ok) return 'success';
  if (mins <= warn) return 'warning';
  return 'danger';
}
export function colorSince(mins, { good = 60, warn = 15 } = {}) {
  // Gegenteil-Logik: je MEHR Minuten, desto besser
  if (mins >= good) return 'success';
  if (mins >= warn) return 'warning';
  return 'danger';
}
export const fmtHM = (ms) =>
  new Date(ms).toLocaleString('de-AT', { hour: '2-digit', minute: '2-digit' });
export function fmtDateHM(ms) {
  // DD.MM., HH:MM (ohne "Uhr")
  const d = new Date(ms);
  const dd = String(d.getDate()).padStart(2, '0');
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  const hh = String(d.getHours()).padStart(2, '0');
  const mi = String(d.getMinutes()).padStart(2, '0');
  return `${dd}.${mm}., ${hh}:${mi}`;
}
export function fmtDateHMSms(ms) {
  // DD.MM., HH:MM:SS[.mmm] – Millisekunden nur, wenn ≠ 0
  const d = new Date(ms);
  const dd = String(d.getDate()).padStart(2, '0');
  const mo = String(d.getMonth() + 1).padStart(2, '0');
  const hh = String(d.getHours()).padStart(2, '0');
  const mi = String(d.getMinutes()).padStart(2, '0');
  const ss = String(d.getSeconds()).padStart(2, '0');
  const ms3 = d.getMilliseconds();
  const tail = ms3 ? `.${String(ms3).padStart(3, '0')}` : '';
  return `${dd}.${mo}., ${hh}:${mi}:${ss}${tail}`;
}

// --- Präsenz-Lader & Bucket-Helper (für Charts, die Gaps anzeigen wollen) ---

/** Bucket je Range (day/week/month/auto) */
export const bucketForRange = (range) =>
  range === 'week' ? 'hour' : range === 'month' ? 'day' : 'minute';

/** erwartetes Intervall (ms) je Bucket */
export const expectedMsForBucket = (b) => (b === 'day' ? 86400000 : b === 'hour' ? 3600000 : 60000);

/** Lädt reine Präsenzzeiten aus /api/series.php, um Gaps zuverlässig zu zeichnen */
export async function loadPresence(win, bucket = 'minute') {
  const data = await getSeries({ bucket: bucket, start: win.start, end: win.end });
  const rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
  return rows.map((r) => (r.time > 2_000_000_000 ? r.time : r.time * 1000)).sort((a, b) => a - b);
  /*
  const url = `/api/series.php?bucket=${bucket}&start=${win.start}&end=${win.end}`;
  const res = await fetch(url, { cache: 'no-store' });
  if (!res.ok) throw new Error(`series.php HTTP ${res.status}`);
  const { rows = [] } = await res.json();
  return rows.map((r) => (r.time > 2_000_000_000 ? r.time : r.time * 1000)).sort((a, b) => a - b);
*/
}

export function gapOptsFor(mode) {
  switch (mode) {
    case 'auto': // alle zeigen
      return {
        mode: 'auto',
        minDurationMs: 0,
        mergeWithinMs: 90_000,
        tolerancePct: 0.35,
        factor: 1.6,
      };
    case 'day': // ≥ 5 min
      return {
        mode: 'day',
        minDurationMs: 5 * 60_000,
        mergeWithinMs: 90_000,
        tolerancePct: 0.35,
        factor: 1.6,
      };
    case 'week': // ≥ 15 min
      return {
        mode: 'week',
        minDurationMs: 15 * 60_000,
        mergeWithinMs: 90_000,
        tolerancePct: 0.35,
        factor: 1.6,
      };
    case 'month': // ≥ 60 min
      return {
        mode: 'month',
        minDurationMs: 60 * 60_000,
        mergeWithinMs: 90_000,
        tolerancePct: 0.35,
        factor: 1.6,
      };
    default:
      return {
        mode: 'auto',
        minDurationMs: 0,
        mergeWithinMs: 90_000,
        tolerancePct: 0.35,
        factor: 1.6,
      };
  }
}