// ═══ HELIDECK · Availability Study · NORMAM-223/DPC ═══════════════════════
// Portado do IMT "Helideck Module v4". O estudo-base (134 dias) vive no banco
// (source='study'); cada Excel diário do HMS é agregado no client e gravado por
// cima (source='loaded', upsert por data). Reset apaga só os carregados.
// A agregação é um port 1:1 do IMT — NÃO alterar sem reler a referência.

// Limites NORMAM-223/DPC (heave só informativo — não dispara a luz nesta instalação)
const HELI_LIMITS = { roll: 3.0, pitch: 3.0, inclination: 3.5, shr: 1.0, heave: 3.0 };

const _heliEsc = (s) => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

// ── SheetJS vendored (mesmo padrão do manifesto-docs/npt — NUNCA CDN) ──
const _heliLoadScript = (src) => new Promise((resolve, reject) => {
  const s = document.createElement('script');
  s.src = src; s.onload = () => resolve(true); s.onerror = () => reject(new Error('load fail: ' + src));
  document.head.appendChild(s);
});
const _heliEnsureXLSX = async () => {
  if (window.XLSX) return;
  await _heliLoadScript('vendor/xlsx.full.min.js'); // vendored local — sem CDN (a internet de bordo derruba CDN)
};

/* ---------- Parse do Excel bruto do HMS ----------
   Lê SEM cellDates. Serial Excel -> UTC manual ((serial-25569)*86400*1000),
   para o agrupamento por dia ser idêntico em qualquer fuso (HMS loga em UTC). */
const heliNormHdr = (h) => String(h == null ? '' : h).trim().toUpperCase();
const heliFindCol = (map, names) => { for (let i = 0; i < names.length; i++) { if (Object.prototype.hasOwnProperty.call(map, names[i])) return map[names[i]]; } return -1; };

const heliParseWorkbook = (wb) => {
  const records = [];
  let dayNightMissing = false;
  const names = wb.SheetNames || [];
  for (let s = 0; s < names.length; s++) {
    const ws = wb.Sheets[names[s]];
    const rows = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true, defval: null, blankrows: false });
    if (!rows || rows.length < 2) continue;
    const hdr = rows[0], map = {};
    for (let c = 0; c < hdr.length; c++) map[heliNormHdr(hdr[c])] = c;
    const cT = heliFindCol(map, ['DATE/TIME', 'DATE / TIME', 'DATETIME']);
    const cG = heliFindCol(map, ['GREENLIGHT']);
    const cR = heliFindCol(map, ['REDLIGHT']);
    const cDN = heliFindCol(map, ['DAYNIGHTSTATUSNO']); // a tag texto DAYNIGHTSTATUS sempre exporta 0 neste HMS — nunca usar
    const cRoll = heliFindCol(map, ['MAXROLL20MIN', 'MAXROLLPORT', 'MAXROLLSTB']);
    const cPitch = heliFindCol(map, ['MAXPITCH20MIN', 'MAXPITCHFWD', 'MAXPITCHAFT']);
    const cIncl = heliFindCol(map, ['MAXHELIINCL']);
    const cShr = heliFindCol(map, ['SHRROUNDED']);
    const cHeave = heliFindCol(map, ['MAXHEAVE']);
    const cWS = heliFindCol(map, ['WINDSPEED2']);
    const cWG = heliFindCol(map, ['WINDGUSTKN3S']);
    if (cT < 0 || cG < 0 || cR < 0) continue;
    if (cDN < 0) dayNightMissing = true;
    for (let r = 1; r < rows.length; r++) {
      const row = rows[r]; if (!row || row[cT] == null) continue;
      const raw = row[cT];
      let ts;
      if (typeof raw === 'number') { ts = new Date(Math.round((raw - 25569) * 86400 * 1000)); }
      else if (raw instanceof Date) { ts = raw; }
      else { ts = new Date(raw); }
      if (isNaN(ts.getTime())) continue;
      const green = parseFloat(row[cG]), red = parseFloat(row[cR]);
      let isDay = null;
      if (cDN >= 0) { const dv = parseFloat(row[cDN]); if (dv === 1) isDay = true; else if (dv === 2) isDay = false; }
      records.push({ ts, green: green === 1 ? 1 : 0, red: red === 1 ? 1 : 0, isDay,
        roll: cRoll >= 0 && row[cRoll] != null ? parseFloat(row[cRoll]) : null,
        pitch: cPitch >= 0 && row[cPitch] != null ? parseFloat(row[cPitch]) : null,
        incl: cIncl >= 0 && row[cIncl] != null ? parseFloat(row[cIncl]) : null,
        shr: cShr >= 0 && row[cShr] != null ? parseFloat(row[cShr]) : null,
        heave: cHeave >= 0 && row[cHeave] != null ? parseFloat(row[cHeave]) : null,
        windSpeed: cWS >= 0 && row[cWS] != null ? parseFloat(row[cWS]) : null,
        windGust: cWG >= 0 && row[cWG] != null ? parseFloat(row[cWG]) : null });
    }
  }
  records.sort((a, b) => a.ts - b.ts);
  return { records, dayNightMissing };
};

// Duração de cada amostra = intervalo até a próxima; gaps >6h ignorados;
// a última recebe a mediana dos intervalos conhecidos (default 60s).
const heliDeltas = (records) => {
  const d = new Array(records.length).fill(60), known = [];
  for (let i = 0; i < records.length - 1; i++) { const dt = (records[i + 1].ts - records[i].ts) / 1000; if (dt > 0 && dt < 21600) { d[i] = dt; known.push(dt); } }
  if (records.length > 0) { known.sort((a, b) => a - b); d[records.length - 1] = known.length ? known[Math.floor(known.length / 2)] : 60; }
  return d;
};
const heliClassify = (r) => { if (r.green === 1 && r.red === 1) return 'trans'; if (r.green === 1) return 'green'; if (r.red === 1) return 'red'; return 'unknown'; };

const heliAggregate = (records, deltas, filterFn) => {
  let total = 0, green = 0, red = 0, trans = 0, unknown = 0;
  const cs = { roll: 0, pitch: 0, inclination: 0, shr: 0 };
  let heaveInfo = 0, wSum = 0, wN = 0, wGust = 0;
  for (let i = 0; i < records.length; i++) {
    const r = records[i]; if (filterFn && !filterFn(r)) continue;
    const d = deltas[i]; total += d;
    const st = heliClassify(r);
    if (st === 'green') green += d; else if (st === 'red') red += d; else if (st === 'trans') trans += d; else unknown += d;
    if (st === 'red' || st === 'trans') {
      if (r.roll != null && r.roll > HELI_LIMITS.roll) cs.roll += d;
      if (r.pitch != null && r.pitch > HELI_LIMITS.pitch) cs.pitch += d;
      if (r.incl != null && r.incl > HELI_LIMITS.inclination) cs.inclination += d;
      if (r.shr != null && r.shr > HELI_LIMITS.shr) cs.shr += d;
      if (r.heave != null && r.heave > HELI_LIMITS.heave) heaveInfo += d; // só informativo — não dispara a luz nesta instalação
    }
    if (r.windSpeed != null) { wSum += r.windSpeed; wN++; }
    if (r.windGust != null && r.windGust > wGust) wGust = r.windGust;
  }
  return { totalSec: total, greenSec: green, redSec: red, transSec: trans, unknownSec: unknown,
    causeMinutes: { roll: cs.roll / 60, pitch: cs.pitch / 60, inclination: cs.inclination / 60, shr: cs.shr / 60 }, heaveInfoMin: heaveInfo / 60,
    windAvg: wN ? wSum / wN : null, windGustMax: wGust || null };
};
const heliLongestDayGreen = (records, deltas) => { let best = 0, cur = 0; for (let i = 0; i < records.length; i++) { const r = records[i]; if (r.isDay === true && heliClassify(r) === 'green') { cur += deltas[i]; if (cur > best) best = cur; } else cur = 0; } return best; };
const heliDateKey = (ts) => ts.getUTCFullYear() + '-' + String(ts.getUTCMonth() + 1).padStart(2, '0') + '-' + String(ts.getUTCDate()).padStart(2, '0');
const heliGroupByDay = (records) => { const g = {}; for (let i = 0; i < records.length; i++) { const k = heliDateKey(records[i].ts); (g[k] || (g[k] = [])).push(records[i]); } return g; };

/* Day summary. Dias com <10 min de amostras diurnas viram 'nodata' (ex.: arquivo
   que espia uns minutos no dia seguinte) e NÃO contaminam o KPI consolidado. */
const heliBuildDays = (records) => {
  const groups = heliGroupByDay(records), out = [];
  for (const k in groups) {
    const dr = groups[k], dd = heliDeltas(dr);
    const aDay = heliAggregate(dr, dd, (r) => r.isDay === true);
    const a24 = heliAggregate(dr, dd, null);
    const win = heliLongestDayGreen(dr, dd);
    const gpd = aDay.totalSec > 0 ? aDay.greenSec / aDay.totalSec * 100 : 0;
    const g24 = a24.totalSec > 0 ? a24.greenSec / a24.totalSec * 100 : 0;
    let status;
    if (aDay.totalSec < 600) status = 'nodata';
    else if (win >= 1800) status = 'go';
    else if (gpd > 0) status = 'partial';
    else status = 'nogo';
    out.push({ date: k, status, greenPctDay: +gpd.toFixed(1), greenPct24: +g24.toFixed(1),
      windowSec: Math.round(win),
      causeMinutes: { roll: +aDay.causeMinutes.roll.toFixed(1), pitch: +aDay.causeMinutes.pitch.toFixed(1), inclination: +aDay.causeMinutes.inclination.toFixed(1), shr: +aDay.causeMinutes.shr.toFixed(1) }, heaveInfoMin: +aDay.heaveInfoMin.toFixed(1),
      windAvg: aDay.windAvg != null ? +aDay.windAvg.toFixed(1) : null,
      windGustMax: aDay.windGustMax != null ? +aDay.windGustMax.toFixed(1) : null,
      samples: dr.length, loaded: true });
  }
  out.sort((a, b) => a.date < b.date ? -1 : 1);
  return out;
};

/* ---------- Helpers ---------- */
const _heliMeses = {
  pt: ['jan','fev','mar','abr','mai','jun','jul','ago','set','out','nov','dez'],
  en: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
};
const heliFmtD = (s, lang) => { const p = s.split('-'); const m = _heliMeses[lang === 'pt' ? 'pt' : 'en'][parseInt(p[1]) - 1]; return lang === 'pt' ? `${parseInt(p[2])} ${m} ${p[0]}` : `${parseInt(p[2])} ${m} ${p[0]}`; };
const heliMoName = (k, lang) => { const p = k.split('-'); return _heliMeses[lang === 'pt' ? 'pt' : 'en'][parseInt(p[1]) - 1] + (lang === 'pt' ? '/' + p[0] : ' ' + p[0]); };
const heliStatusOf = (d) => { if (d.status) return d.status; const g = d.greenPctDay != null ? d.greenPctDay : 0; if (d.windowSec >= 1800) return 'go'; if (g > 0) return 'partial'; return 'nogo'; };
const heliAvg = (a) => a.length ? a.reduce((s, v) => s + v, 0) / a.length : null;
const heliN = (v, d) => { d = d == null ? 1 : d; return (v == null || isNaN(+v)) ? '—' : (+v).toFixed(d); };
const heliSeq = (days, val) => { let m = 0, c = 0; for (let i = 0; i < days.length; i++) { if (heliStatusOf(days[i]) === val) { c++; if (c > m) m = c; } else c = 0; } return m; };

/* KPI consolidado sobre um conjunto de dias */
const heliKPIs = (days) => {
  const real = days.filter((d) => heliStatusOf(d) !== 'nodata');
  const go = real.filter((d) => heliStatusOf(d) === 'go');
  const partial = real.filter((d) => heliStatusOf(d) === 'partial');
  const nogo = real.filter((d) => heliStatusOf(d) === 'nogo');
  const total = real.length;
  return {
    real, go, partial, nogo, total,
    goRate: total ? Math.round(go.length / total * 100) : 0,
    avgGP: heliAvg(real.filter((d) => d.greenPctDay != null).map((d) => +d.greenPctDay)),
    avg24: heliAvg(real.filter((d) => d.greenPct24 != null).map((d) => +d.greenPct24)),
    avgWin: heliAvg(go.filter((d) => d.windowSec > 0).map((d) => d.windowSec / 3600)),
    seqGo: heliSeq(days, 'go'), seqNg: heliSeq(days, 'nogo'),
    wGo: heliAvg(go.filter((d) => d.windAvg != null).map((d) => +d.windAvg)),
    wNg: heliAvg(nogo.filter((d) => d.windAvg != null).map((d) => +d.windAvg)),
  };
};

// disponibilidade por mês: { 'YYYY-MM': {go, t} }
const heliByMonth = (real) => {
  const mo = {};
  real.forEach((d) => { const k = d.date.slice(0, 7); (mo[k] || (mo[k] = { go: 0, t: 0 })); mo[k].t++; if (heliStatusOf(d) === 'go') mo[k].go++; });
  return mo;
};

/* ---------- EXPORT: Excel (Daily KPI + Consolidated KPI) ---------- */
const _heliExportExcel = async (days, K, toast, lang) => {
  try { await _heliEnsureXLSX(); }
  catch (e) { toast.push({ icon: 'x', title: (lang === 'pt' ? 'SheetJS não carregou' : 'SheetJS failed to load') }); return; }
  const dRows = [['Date','Status','Green Day %','Green 24h %','Window (h)','Pitch >3° (min)','SHR >1m/s (min)','Roll >3° (min)','Incl >3.5° (min)','Heave >3m INFO (min)','Wind avg (kn)','Gust max (kn)','Source']];
  days.forEach((d) => {
    const c = d.causeMinutes || {};
    const hv = (d.heaveInfoMin != null) ? d.heaveInfoMin : (c.heave != null ? c.heave : '');
    dRows.push([d.date, heliStatusOf(d).toUpperCase(), d.greenPctDay != null ? +d.greenPctDay : '', d.greenPct24 != null ? +d.greenPct24 : '',
      d.windowSec ? +(d.windowSec / 3600).toFixed(2) : 0,
      c.pitch != null ? +c.pitch : '', c.shr != null ? +c.shr : '', c.roll != null ? +c.roll : '', c.inclination != null ? +c.inclination : '', hv !== '' ? +hv : '',
      d.windAvg != null ? +d.windAvg : '', d.windGustMax != null ? +d.windGustMax : '', d.loaded ? 'Loaded' : 'Study']);
  });
  const ws1 = XLSX.utils.aoa_to_sheet(dRows);
  ws1['!cols'] = [{wch:11},{wch:8},{wch:11},{wch:11},{wch:10},{wch:13},{wch:13},{wch:14},{wch:14},{wch:14},{wch:12},{wch:12},{wch:8}];

  const loadedN = days.filter((d) => d.loaded).length;
  const kRows = [['CONSOLIDATED KPI — MPSV GENESIS I HELIDECK'],
    ['Range', heliFmtD(days[0].date, 'en') + ' → ' + heliFmtD(days[days.length - 1].date, 'en')],
    ['Days monitored (with data)', K.total],
    ['Loaded days on top of study', loadedN], [],
    ['Operable days (GO)', K.go.length, K.goRate + '%'],
    ['Partial days', K.partial.length],
    ['No-go days', K.nogo.length, (K.total ? Math.round(K.nogo.length / K.total * 100) : 0) + '%'],
    ['Avg daytime green %', K.avgGP != null ? +K.avgGP.toFixed(1) : ''],
    ['Avg 24h green %', K.avg24 != null ? +K.avg24.toFixed(1) : ''],
    ['Avg landing window (h, GO days)', K.avgWin != null ? +K.avgWin.toFixed(1) : ''],
    ['Longest GO streak (days)', K.seqGo],
    ['Longest NO-GO streak (days)', K.seqNg],
    ['Wind avg on GO days (kn)', K.wGo != null ? +K.wGo.toFixed(1) : ''],
    ['Wind avg on NO-GO days (kn)', K.wNg != null ? +K.wNg.toFixed(1) : ''],
    [], ['Availability by month'], ['Month', 'GO days', 'Days', 'GO %']];
  const mo = heliByMonth(K.real);
  Object.keys(mo).sort().forEach((k) => { kRows.push([heliMoName(k, 'en'), mo[k].go, mo[k].t, mo[k].t ? Math.round(mo[k].go / mo[k].t * 100) : 0]); });
  const ws2 = XLSX.utils.aoa_to_sheet(kRows);
  ws2['!cols'] = [{wch:32},{wch:22},{wch:8},{wch:8}];

  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws1, 'Daily KPI');
  XLSX.utils.book_append_sheet(wb, ws2, 'Consolidated KPI');
  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
  XLSX.writeFile(wb, 'helideck_kpi_genesis_' + stamp + '.xlsx');
  toast.push({ icon: 'check', title: (lang === 'pt' ? 'Excel do Helideck exportado' : 'Helideck Excel exported') });
};

/* ---------- EXPORT: PDF (A4 landscape, página única, print window) ---------- */
const _heliExportPDF = (days, K, toast, lang) => {
  const last = days.slice().reverse().find((d) => heliStatusOf(d) !== 'nodata');
  if (!last) { toast.push({ icon: 'x', title: (lang === 'pt' ? 'Sem dia com dados para exportar' : 'No day with data to export') }); return; }
  const lst = heliStatusOf(last);
  const stClr = { go: '#1e9e55', partial: '#d9821a', nogo: '#c0392b' };
  const stLbl = { go: 'GO — OPERABLE DAY', partial: 'PARTIAL — LIMITED WINDOW', nogo: 'NO-GO — NO WINDOW' };
  const dt = new Date().toLocaleString('en-GB');
  const cs = last.causeMinutes || {};
  const hv = (last.heaveInfoMin != null) ? last.heaveInfoMin : (cs.heave != null ? cs.heave : 0);
  const cDefs = [['Pitch > 3°', cs.pitch], ['SHR (SVArf) > 1 m/s', cs.shr], ['Roll > 3°', cs.roll], ['Inclination > 3.5°', cs.inclination]];
  const cMax = Math.max.apply(null, cDefs.map((x) => x[1] || 0).concat([1]));
  const causeRows = cDefs.map((x) => { const v = x[1] || 0; return '<div class="crow"><div class="clbl">' + x[0] + '</div><div class="ctrk"><div class="cfill" style="width:' + (v / cMax * 100).toFixed(1) + '%"></div></div><div class="cval">' + Math.round(v) + ' min</div></div>'; }).join('')
    + '<div style="font-size:8px;color:#8593a8;margin-top:4px;padding-top:3px;border-top:1px dashed #e0e8f0">Heave &gt; 3 m: ' + Math.round(hv) + ' min — informative only (does not trip the light).</div>';

  const mo = heliByMonth(K.real);
  const moKeys = Object.keys(mo).sort();
  const moRows = moKeys.map((k) => { const o = mo[k], p = o.t ? Math.round(o.go / o.t * 100) : 0; const c = p >= 50 ? '#1e9e55' : p >= 33 ? '#d9821a' : '#c0392b';
    return '<div class="crow"><div class="clbl" style="width:60px">' + heliMoName(k, 'en') + '</div><div class="ctrk"><div class="cfill" style="width:' + Math.max(2, p) + '%;background:' + c + '"></div></div><div class="cval" style="color:' + c + ';font-weight:700">' + p + '%</div><div class="cval" style="width:44px">' + o.go + '/' + o.t + '</div></div>'; }).join('');

  const kpiBox = (label, val, unit, color) => '<div class="kbox"><div class="klbl">' + label + '</div><div class="kval" style="color:' + color + '">' + val + '<span class="kunit">' + unit + '</span></div></div>';
  const kpis =
    kpiBox('Operable Days', K.go.length + '/' + K.total, '', '#1e9e55')
    + kpiBox('% Operable', K.goRate, '%', '#1e9e55')
    + kpiBox('Daytime Avail.', heliN(K.avgGP), '%', '#b07c10')
    + kpiBox('24h Avail.', heliN(K.avg24), '%', '#0a8aa0')
    + kpiBox('Avg Window', heliN(K.avgWin), 'h', '#d9821a')
    + kpiBox('No-go Days', K.nogo.length, '', '#c0392b')
    + kpiBox('Max GO Streak', K.seqGo, ' d', '#1e9e55')
    + kpiBox('Max NO-GO Streak', K.seqNg, ' d', '#c0392b');

  const windLine = (K.wGo != null && K.wNg != null) ? ('Mean wind: <b style="color:#1e9e55">' + K.wGo.toFixed(1) + ' kn</b> on GO days × <b style="color:#c0392b">' + K.wNg.toFixed(1) + ' kn</b> on NO-GO' + (K.wGo > K.wNg ? ' — likely bottleneck: DP motion (pitch/roll/SHR), not wind.' : '.')) : '';

  const win = window.open('', '_blank');
  if (!win) { toast.push({ icon: 'x', title: (lang === 'pt' ? 'Libere os pop-ups' : 'Allow pop-ups'), body: (lang === 'pt' ? 'O relatório abre numa aba nova' : 'The report opens in a new tab') }); return; }
  win.document.write('<!DOCTYPE html><html><head><meta charset="utf-8"><title>Helideck KPI — Genesis I</title><style>'
    + '@page{size:A4 landscape;margin:8mm 10mm;}'
    + '*{box-sizing:border-box;margin:0;padding:0;}'
    + 'body{font-family:Arial,Helvetica,sans-serif;color:#1a2a3a;font-size:10px;}'
    + '.hdr{display:flex;align-items:center;justify-content:space-between;border-bottom:2.5px solid #1b2a4a;padding-bottom:6px;margin-bottom:8px;}'
    + '.hdr .t1{font-size:15px;font-weight:800;color:#1b2a4a;letter-spacing:.5px;}'
    + '.hdr .t2{font-size:9px;color:#6a8090;margin-top:1px;}'
    + '.hdr .meta{text-align:right;font-size:8.5px;color:#6a8090;line-height:1.5;}'
    + '.cols{display:grid;grid-template-columns:1fr 1.35fr;gap:10px;}'
    + '.panel{border:1px solid #c8d8e8;border-radius:6px;padding:8px 10px;}'
    + '.ptitle{font-size:9px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#6a8090;border-bottom:1px solid #e0e8f0;padding-bottom:4px;margin-bottom:7px;}'
    + '.lamp{display:inline-block;width:14px;height:14px;border-radius:50%;vertical-align:-2px;margin-right:6px;}'
    + '.bigdate{font-size:17px;font-weight:800;color:#1b2a4a;}'
    + '.stline{font-size:12px;font-weight:800;margin:4px 0 8px;}'
    + '.mini{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px;}'
    + '.mbox{border:1px solid #e0e8f0;border-radius:5px;padding:5px 7px;}'
    + '.mlbl{font-size:7.5px;text-transform:uppercase;letter-spacing:1px;color:#8593a8;}'
    + '.mval{font-size:15px;font-weight:800;color:#1a2a4a;}'
    + '.mval small{font-size:9px;color:#8593a8;font-weight:600;}'
    + '.crow{display:flex;align-items:center;gap:6px;margin-bottom:4px;}'
    + '.clbl{width:112px;font-size:8.5px;color:#5a6a7a;flex:none;}'
    + '.ctrk{flex:1;height:6px;background:#eef1f6;border-radius:3px;overflow:hidden;}'
    + '.cfill{height:100%;background:#c0392b;border-radius:3px;}'
    + '.cval{width:46px;text-align:right;font-size:8.5px;color:#1a2a3a;flex:none;}'
    + '.kgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:8px;}'
    + '.kbox{border:1px solid #e0e8f0;border-radius:5px;padding:6px 8px;}'
    + '.klbl{font-size:7.5px;text-transform:uppercase;letter-spacing:1px;color:#8593a8;margin-bottom:2px;}'
    + '.kval{font-size:17px;font-weight:800;line-height:1;}'
    + '.kunit{font-size:9px;color:#8593a8;font-weight:600;}'
    + '.wind{font-size:9px;color:#3a5060;padding:5px 0 0;border-top:1px dashed #e0e8f0;margin-top:4px;line-height:1.5;}'
    + '.crit{font-size:7.8px;color:#5a6a7a;line-height:1.5;border:1px solid #e0e8f0;background:#f7fafc;border-radius:5px;padding:5px 8px;margin-top:8px;}'
    + '.foot{display:flex;justify-content:space-between;font-size:8px;color:#8593a8;border-top:1px solid #c8d8e8;padding-top:4px;margin-top:6px;}'
    + '</style></head><body>'
    + '<div class="hdr">'
    + '<div><div class="t1">MPSV GENESIS I — HELIDECK</div><div class="t2">Availability Study · NORMAM-223/DPC · Bow Helideck CAT 3 · B412-CatB</div></div>'
    + '<div class="meta">IMO 9246114 · Marshall Islands · GT 5,886<br>' + heliFmtD(days[0].date, 'en') + ' → ' + heliFmtD(days[days.length - 1].date, 'en') + ' · ' + K.total + ' days<br>Generated: ' + _heliEsc(dt) + '</div>'
    + '</div>'
    + '<div class="cols">'
    + '<div class="panel">'
    + '<div class="ptitle">Last logged day</div>'
    + '<div class="bigdate">' + heliFmtD(last.date, 'en') + '</div>'
    + '<div class="stline" style="color:' + stClr[lst] + '"><span class="lamp" style="background:' + stClr[lst] + '"></span>' + stLbl[lst] + '</div>'
    + '<div class="mini">'
    + '<div class="mbox"><div class="mlbl">Daytime green</div><div class="mval">' + heliN(last.greenPctDay) + '<small> %</small></div></div>'
    + '<div class="mbox"><div class="mlbl">24h green</div><div class="mval">' + heliN(last.greenPct24) + '<small> %</small></div></div>'
    + '<div class="mbox"><div class="mlbl">Longest green window</div><div class="mval">' + (last.windowSec ? (last.windowSec / 60).toFixed(0) : '0') + '<small> min</small></div></div>'
    + '<div class="mbox"><div class="mlbl">Wind avg / gust</div><div class="mval">' + heliN(last.windAvg) + '<small> / ' + heliN(last.windGustMax) + ' kn</small></div></div>'
    + '</div>'
    + '<div class="ptitle" style="margin-top:2px">Cause of unavailability — daytime minutes above limit</div>'
    + causeRows
    + '</div>'
    + '<div class="panel">'
    + '<div class="ptitle">Consolidated KPI — ' + K.total + ' day(s) monitored</div>'
    + '<div class="kgrid">' + kpis + '</div>'
    + '<div class="ptitle">Availability by month</div>'
    + moRows
    + (windLine ? '<div class="wind">' + windLine + '</div>' : '')
    + '</div>'
    + '</div>'
    + '<div class="crit"><b>How to read this:</b> '
    + '<span style="color:#1e9e55;font-weight:700">GO</span> = at least 30 continuous minutes of green light in daylight (a flight can be scheduled) · '
    + '<span style="color:#d9821a;font-weight:700">PARTIAL</span> = some green in daylight but never 30 continuous minutes · '
    + '<span style="color:#c0392b;font-weight:700">NO-GO</span> = no green light in daylight. '
    + 'Daytime availability uses the HMS day/night tag (not clock hours) and is the operationally meaningful figure, since transfers are flown in daylight. '
    + 'Days with under 10 min of daytime samples are excluded from the KPIs.</div>'
    + '<div class="foot"><span>MPSV Genesis I · Helideck Availability Report</span><span>Limits (NORMAM-223/DPC): Pitch 3° · Roll 3° · Incl 3.5° · SHR 1 m/s · Wind per art. 9.4 · Heave 3 m informative</span></div>'
    + '<scr' + 'ipt>window.onload=function(){setTimeout(function(){window.print();},350);}</scr' + 'ipt>'
    + '</body></html>');
  win.document.close();
};

/* ════════════════════════════════════════════════════════════════════════
   COMPONENTE REACT (padrão Tidal)
   ════════════════════════════════════════════════════════════════════════ */

// Card de KPI no estilo do módulo
const HeliKpi = ({label, val, unit, sub, col}) => {
  const clr = { green:'#22c55e', amber:'#f59e0b', red:'#ef4444', teal:'#06b6d4', yellow:'#f0a500' }[col] || 'var(--muted)';
  return (
    <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:14,padding:'14px 12px',position:'relative',overflow:'hidden',boxShadow:'var(--shadow-sm)'}}>
      <div style={{position:'absolute',top:0,left:0,right:0,height:3,background:clr}}/>
      <div className="t-eyebrow" style={{marginBottom:6}}>{label}</div>
      <div style={{fontFamily:'var(--fs)',fontSize:28,lineHeight:1,marginBottom:3,color:clr}}>{val}<span style={{fontSize:13,color:'var(--muted)'}}>{unit}</span></div>
      <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',lineHeight:1.4}}>{sub}</div>
    </div>
  );
};

const Helideck = ({state, setState, setPage}) => {
  const _tt = useT();
  const lang = _tt.lang;
  const toast = useToast();
  const isMobile = useMobile();
  const token = () => localStorage.getItem('genesis_token');
  const me = state.currentUser || {};
  const canManage = me.role === 'admin' || me.is_staff === true;

  const [days, setDays] = React.useState(null);        // null = carregando
  const [limits, setLimits] = React.useState(HELI_LIMITS);
  const [status, setStatus] = React.useState(null);    // {msg, kind: 'ok'|'err'|null}
  const [busy, setBusy] = React.useState(false);
  const fileRef = React.useRef(null);

  const H = () => ({'Authorization':`Bearer ${token()}`});
  const load = () => fetch('/api/helideck/days',{headers:H()}).then(r=>r.ok?r.json():[]).then(setDays).catch(()=>setDays([]));
  React.useEffect(()=>{ load(); }, []);
  React.useEffect(()=>{
    fetch('/api/helideck/limits',{headers:H()}).then(r=>r.ok?r.json():null)
      .then(d=>{ if (d && d.roll != null) setLimits(d); }).catch(()=>{});
  }, []);

  /* ---------- upload de arquivos HMS (.xls/.xlsx, múltiplos) ---------- */
  const processFiles = async (list) => {
    const files = Array.prototype.slice.call(list || []).filter(Boolean);
    if (!files.length || busy) return;
    setBusy(true);
    try { await _heliEnsureXLSX(); }
    catch (e) {
      setStatus({ msg: (lang==='pt'?'Leitor de Excel indisponível (SheetJS não carregou).':'Excel reader unavailable (SheetJS did not load).'), kind: 'err' });
      setBusy(false); return;
    }
    const acc = { readings: 0, days: 0, skipped: 0, names: [], errors: [], dnMissing: false, allDays: [] };
    const readOne = (file) => new Promise((resolve) => {
      const reader = new FileReader();
      reader.onerror = () => { acc.errors.push(file.name); resolve(); };
      reader.onload = (e) => {
        try {
          const wb = XLSX.read(new Uint8Array(e.target.result), { type: 'array' });
          const parsed = heliParseWorkbook(wb);
          if (!parsed.records.length) { acc.errors.push(file.name + (lang==='pt'?' (sem linhas válidas)':' (no valid rows)')); resolve(); return; }
          const dayDefs = heliBuildDays(parsed.records);
          const valid = dayDefs.filter((d) => d.status !== 'nodata');
          acc.readings += parsed.records.length;
          acc.days += valid.length;
          acc.skipped += (dayDefs.length - valid.length);
          if (parsed.dayNightMissing) acc.dnMissing = true;
          acc.names.push(file.name);
          acc.allDays = acc.allDays.concat(dayDefs);
        } catch (err) { console.error(err); acc.errors.push(file.name + ' (' + err.message + ')'); }
        resolve();
      };
      reader.readAsArrayBuffer(file);
    });
    for (let i = 0; i < files.length; i++) {
      setStatus({ msg: (lang==='pt'?'Lendo ':'Reading ') + files[i].name + '…', kind: null });
      await readOne(files[i]);
    }
    // grava os daySummaries agregados (upsert por data, source='loaded')
    if (acc.allDays.length) {
      try {
        const r = await fetch('/api/helideck/days', { method: 'POST', headers: { ...H(), 'Content-Type': 'application/json' }, body: JSON.stringify({ days: acc.allDays }) });
        const d = await r.json();
        if (!r.ok) acc.errors.push((lang==='pt'?'falha ao gravar: ':'save failed: ') + (d.error || r.status));
      } catch (e) { acc.errors.push(lang==='pt'?'falha de conexão ao gravar':'connection error while saving'); }
    }
    load();
    let msg = acc.readings + (lang==='pt'?' leituras · ':' readings · ') + acc.days + (lang==='pt'?' dia(s) no consolidado':' day(s) added to consolidated')
      + (acc.skipped ? (lang==='pt'?' (+':' (+') + acc.skipped + (lang==='pt'?' fragmento(s) ignorados)':' fragment(s) skipped)') : '')
      + (acc.names.length ? ' · ' + acc.names.join(', ') : '');
    if (acc.dnMissing) msg += lang==='pt'
      ? ' ⚠ Um arquivo não tinha a coluna DAYNIGHTSTATUSNO — exporte do HMS (a tag texto DAYNIGHTSTATUS sempre lê 0 e não deve ser usada).'
      : ' ⚠ A file had no DAYNIGHTSTATUSNO column — export it from the HMS (the text DAYNIGHTSTATUS tag always reads 0 and must not be used).';
    if (acc.errors.length) msg += (lang==='pt'?' ✖ Falhou: ':' ✖ Failed: ') + acc.errors.join('; ');
    setStatus({ msg, kind: acc.errors.length ? 'err' : (acc.dnMissing ? null : 'ok') });
    if (acc.days) toast.push({ icon: 'check', title: (lang==='pt'?`HMS: ${acc.days} dia(s) incorporados`:`HMS: ${acc.days} day(s) merged`) });
    setBusy(false);
  };

  const reset = async () => {
    if (!window.confirm(lang==='pt'
      ? 'Isso apaga todos os dias carregados e volta ao estudo-base (134 dias). Continuar?'
      : 'This clears every loaded day and returns to the baseline study (134 days). Continue?')) return;
    const r = await fetch('/api/helideck/reset', { method: 'POST', headers: H() });
    if (r.ok) { setStatus({ msg: (lang==='pt'?'Voltou ao estudo-base.':'Reset to baseline study.'), kind: 'ok' }); load(); }
    else toast.push({ icon: 'x', title: (lang==='pt'?'Erro no reset':'Reset error') });
  };

  /* ---------- render ---------- */
  if (days === null) return <main style={{maxWidth:1080,margin:'0 auto',padding:isMobile?'16px 14px 60px':'26px 30px 80px'}}><div style={{fontFamily:'var(--fm)',fontSize:13,color:'var(--muted)',padding:20}}>{lang==='pt'?'Carregando…':'Loading…'}</div></main>;

  const K = heliKPIs(days);
  const studyN = days.filter((d) => !d.loaded).length;
  const loadedN = days.filter((d) => d.loaded).length;
  const last = days.slice().reverse().find((d) => heliStatusOf(d) !== 'nodata');
  const lst = last ? heliStatusOf(last) : 'nodata';
  const stClr = { go: '#22c55e', partial: '#f59e0b', nogo: '#ef4444' };
  const stLbl = {
    go:     lang==='pt' ? 'GO — DIA OPERÁVEL' : 'GO — Operable Day',
    partial:lang==='pt' ? 'PARCIAL — JANELA LIMITADA' : 'PARTIAL — Limited Window',
    nogo:   lang==='pt' ? 'NO-GO — SEM JANELA VERDE' : 'NO-GO — No Green Window',
  };
  const srcLine = (lang==='pt' ? `Estudo-base (${studyN} dias) + ${loadedN} dia(s) carregado(s)` : `Baseline study (${studyN} days) + ${loadedN} loaded day(s)`);

  // semáforo do último dia
  const lamps = ['r','a','g'].map((l) => {
    const on = (lst === 'nogo' && l === 'r') || (lst === 'partial' && l === 'a') || (lst === 'go' && l === 'g');
    const bc = { r: '#ef4444', a: '#f59e0b', g: '#22c55e' }[l];
    return (
      <div key={l} style={{display:'flex',flexDirection:'column',alignItems:'center',gap:4}}>
        <div style={{width:20,height:20,borderRadius:'50%',background:bc,opacity:on?1:0.14,boxShadow:on?`0 0 12px 3px ${bc}44`:'none'}}/>
        <div style={{fontFamily:'var(--fm)',fontSize:8,color:'var(--muted)',textTransform:'uppercase',letterSpacing:0.5}}>{{r:'Red',a:'Amb',g:'Green'}[l]}</div>
      </div>
    );
  });

  // gráfico dia a dia
  const bC = { go: '#22c55e', partial: '#f59e0b', nogo: '#ef4444', nodata: '#b8c4d0' };
  const hasNd = days.some((d) => heliStatusOf(d) === 'nodata');
  const a90 = days.filter((d) => +(d.greenPctDay || 0) > 90).length;
  const b10 = days.filter((d) => +(d.greenPctDay != null ? d.greenPctDay : 100) < 10 && heliStatusOf(d) !== 'nodata').length;

  // disponibilidade por mês
  const mo = heliByMonth(K.real);

  // causas de indisponibilidade (minutos diurnos acima do limite)
  const tC = { pitch: 0, shr: 0, roll: 0, inclination: 0 };
  let tHeave = 0;
  K.real.forEach((d) => {
    if (d.causeMinutes) for (const k in tC) { if (d.causeMinutes[k]) tC[k] += +d.causeMinutes[k]; }
    const hv = (d.heaveInfoMin != null) ? d.heaveInfoMin : (d.causeMinutes && d.causeMinutes.heave != null ? d.causeMinutes.heave : 0);
    if (hv) tHeave += +hv;
  });
  const mxC = Math.max(tC.pitch, tC.shr, tC.roll, tC.inclination, 1);
  const cLbl = { pitch: 'Pitch > 3°', shr: 'SHR (SVArf) > 1 m/s', roll: 'Roll > 3°', inclination: 'Inclination > 3.5°' };
  const anyC = tC.pitch + tC.shr + tC.roll + tC.inclination > 0;

  // conclusões (portadas do IMT, interpolando goRate/avgWin)
  const aW = K.avgWin != null ? K.avgWin.toFixed(1) + 'h' : '—';
  const ccs = lang === 'pt' ? [
    { n: 1, t: `Operações viáveis em ~${K.goRate}% dos dias — janela média de ${aW}. Uso real, não marginal.`, c: '#22c55e' },
    { n: 2, t: 'Padrão bimodal: o dia tende a ser ou operável ou totalmente fechado. Favorece voos agendados por previsão, não disponibilidade 24/7.', c: '#06b6d4' },
    { n: 3, t: 'Forte sazonalidade. No estudo, novembro foi o melhor mês. Um ano completo daria a curva sazonal inteira.', c: '#f59e0b' },
    { n: 4, t: 'Provável gargalo: movimento do navio em DP — pitch e SHR dominam os minutos vermelhos. Heave é só informativo nesta instalação (não dispara a luz).', c: '#f0a500' },
  ] : [
    { n: 1, t: `Operations viable on ~${K.goRate}% of days — average window of ${aW}. Real use, not marginal.`, c: '#22c55e' },
    { n: 2, t: 'Bimodal pattern: a day tends to be either operable or fully closed. Favors forecast-scheduled flights, not 24/7 availability.', c: '#06b6d4' },
    { n: 3, t: 'Strong seasonality. In the study, November was the best month. A full year would give the entire seasonal curve.', c: '#f59e0b' },
    { n: 4, t: 'Likely bottleneck: vessel motion in DP — pitch and SHR dominate the red minutes. Heave is informative only on this installation (does not trip the light).', c: '#f0a500' },
  ];

  const limRow = (tag, param, lim, note) => (
    <tr key={tag}>
      <td style={{padding:'4px 8px',borderBottom:'1px solid var(--line-soft)',fontFamily:'var(--fm)',fontSize:10,color:'var(--ink)',whiteSpace:'nowrap'}}>{tag}</td>
      <td style={{padding:'4px 8px',borderBottom:'1px solid var(--line-soft)',fontFamily:'var(--fm)',fontSize:10,color:'var(--ink)'}}>{param}</td>
      <td style={{padding:'4px 8px',borderBottom:'1px solid var(--line-soft)',fontFamily:'var(--fm)',fontSize:10,fontWeight:700,color:note?'var(--muted)':'#ef4444',whiteSpace:'nowrap'}}>{lim}</td>
      <td style={{padding:'4px 8px',borderBottom:'1px solid var(--line-soft)',fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',lineHeight:1.4}}>{note || (lang==='pt'?'Dispara a luz vermelha quando excedido':'Trips the red light when exceeded')}</td>
    </tr>
  );

  const card = (children, extra) => <div style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:18,padding:'18px 22px',marginBottom:14,boxShadow:'var(--shadow-sm)',...(extra||{})}}>{children}</div>;
  const cardTitle = (txt) => <div className="t-eyebrow" style={{marginBottom:12,paddingBottom:8,borderBottom:'1px solid var(--line-soft)'}}>{txt}</div>;

  return (
    <main style={{maxWidth:1080,margin:'0 auto',padding:isMobile?'16px 14px 60px':'26px 30px 80px'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:12,flexWrap:'wrap',marginBottom:16}}>
        <div style={{fontFamily:'var(--fs)',fontSize:isMobile?24:30,letterSpacing:'-0.5px'}}>🚁 Helideck · <em style={{fontStyle:'italic',color:'var(--accent)'}}>{lang==='pt'?'estudo de disponibilidade':'availability study'}</em></div>
        <div style={{display:'flex',gap:8,alignItems:'center',flexWrap:'wrap'}}>
          <button className="btn-icone" onClick={()=>_heliExportExcel(days, K, toast, lang)} title="Exportar Excel (KPIs)">📊</button>
          <button className="btn-icone" onClick={()=>_heliExportPDF(days, K, toast, lang)} title="KPI Report (PDF A4)">📄</button>
          {canManage && loadedN > 0 && <button className="btn btn-sm" onClick={reset}>↺ Reset</button>}
        </div>
      </div>

      {/* drop zone — só para quem pode gerir */}
      {canManage && (
        <>
          <div onClick={()=>fileRef.current && fileRef.current.click()}
            onDragOver={(e)=>{e.preventDefault();e.currentTarget.style.borderColor='var(--teal)';}}
            onDragLeave={(e)=>{e.preventDefault();e.currentTarget.style.borderColor='var(--line)';}}
            onDrop={(e)=>{e.preventDefault();e.currentTarget.style.borderColor='var(--line)';processFiles(e.dataTransfer.files);}}
            style={{border:'1.5px dashed var(--line)',borderRadius:14,background:'var(--surface)',padding:22,textAlign:'center',cursor:'pointer',transition:'all .15s',marginBottom:6}}>
            <div style={{fontSize:26,marginBottom:6}}>📄</div>
            <div style={{fontWeight:600,fontSize:14,color:'var(--ink)',marginBottom:4}}>{lang==='pt'?'Arraste o(s) Excel(s) diário(s) do HMS aqui, ou clique para escolher — múltiplos arquivos':'Drop the daily HMS Excel export(s) here, or click to choose — multiple files allowed'}</div>
            <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',lineHeight:1.5}}>.xls / .xlsx · Date/Time, GREENLIGHT, REDLIGHT, DAYNIGHTSTATUSNO, MAXROLL20MIN, MAXPITCH20MIN, MAXHELIINCL, SHRROUNDED, MAXHEAVE, WINDSPEED2, WINDGUSTKN3S</div>
            <input type="file" ref={fileRef} accept=".xls,.xlsx" multiple style={{display:'none'}} onChange={(e)=>{processFiles(e.target.files);e.target.value='';}}/>
          </div>
          <div style={{fontFamily:'var(--fm)',fontSize:11,minHeight:15,marginBottom:14,color:status?(status.kind==='err'?'var(--rose)':status.kind==='ok'?'var(--leaf)':'var(--muted)'):'var(--muted)'}}>{status ? status.msg : ''}</div>
        </>
      )}

      <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginBottom:12,display:'flex',justifyContent:'space-between',flexWrap:'wrap',gap:6}}>
        <span>{lang==='pt'?'Consolidado':'Consolidated'}: <strong style={{color:'var(--ink)'}}>{srcLine}</strong></span>
        {days.length > 0 && <span>{heliFmtD(days[0].date, lang)} → {heliFmtD(days[days.length-1].date, lang)}</span>}
      </div>

      {/* painel de critérios & métricas (colapsável) */}
      <details style={{background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:14,marginBottom:16,overflow:'hidden',boxShadow:'var(--shadow-sm)'}}>
        <summary style={{cursor:'pointer',padding:'11px 16px',fontFamily:'var(--fm)',fontSize:11,fontWeight:700,letterSpacing:1.5,textTransform:'uppercase',color:'var(--muted)',listStyle:'none',display:'flex',alignItems:'center',gap:8}}>
          <span style={{fontSize:13}}>ℹ️</span> {lang==='pt'?'Critérios & Métricas — como ler este relatório':'Criteria & Metrics — how to read this report'}
          <span style={{marginLeft:'auto',fontSize:9,fontWeight:600,letterSpacing:1}}>{lang==='pt'?'clique para expandir':'click to expand'}</span>
        </summary>
        <div style={{padding:'0 16px 14px'}}>
          <div style={{fontFamily:'var(--f)',fontSize:11.5,color:'var(--ink)',lineHeight:1.65,padding:'6px 0 12px',borderBottom:'1px solid var(--line-soft)',marginBottom:12}}>
            <strong>{lang==='pt'?'Propósito.':'Purpose.'}</strong> {lang==='pt'
              ? <>Este estudo mede com que frequência o Helideck de proa (CAT 3, B412-CatB) seria de fato utilizável, para o armador decidir se vale ativá-lo. A decisão depende de duas coisas: <strong>quantos dias oferecem luz verde</strong> e <strong>quanto dura cada janela verde</strong> — uma janela curta não é operacionalmente útil.</>
              : <>This study measures how often the Bow Helideck (CAT 3, B412-CatB) would actually be usable, so the owner can decide whether activating it is worth the investment. The decision hinges on two things: <strong>how many days offer a green light</strong>, and <strong>how long each green window lasts</strong> — a short window is not operationally useful.</>}
          </div>
          <div style={{display:'grid',gridTemplateColumns:isMobile?'1fr':'1fr 1fr',gap:'10px 18px',marginBottom:14}}>
            <div><div style={{fontFamily:'var(--fm)',fontSize:10,fontWeight:700,color:'#22c55e',letterSpacing:1,marginBottom:3}}>GO — {lang==='pt'?'DIA OPERÁVEL':'OPERABLE DAY'}</div><div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',lineHeight:1.5}}>{lang==='pt'?<>Pelo menos <strong>30 minutos contínuos</strong> de luz verde em horário diurno (dá para agendar um voo).</>:<>At least <strong>30 continuous minutes</strong> of green light in daylight (a flight can be scheduled).</>}</div></div>
            <div><div style={{fontFamily:'var(--fm)',fontSize:10,fontWeight:700,color:'#f59e0b',letterSpacing:1,marginBottom:3}}>PARTIAL — {lang==='pt'?'JANELA LIMITADA':'LIMITED WINDOW'}</div><div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',lineHeight:1.5}}>{lang==='pt'?'Alguma luz verde de dia, mas nunca 30 minutos contínuos.':'Some green light in daylight, but never 30 continuous minutes.'}</div></div>
            <div><div style={{fontFamily:'var(--fm)',fontSize:10,fontWeight:700,color:'#ef4444',letterSpacing:1,marginBottom:3}}>NO-GO — {lang==='pt'?'SEM JANELA':'NO WINDOW'}</div><div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',lineHeight:1.5}}>{lang==='pt'?'Zero luz verde durante o dia. O convés está inutilizável para voos.':'Zero green light during daylight hours. The deck is unusable for flights.'}</div></div>
            <div><div style={{fontFamily:'var(--fm)',fontSize:10,fontWeight:700,color:'#94a3b8',letterSpacing:1,marginBottom:3}}>{lang==='pt'?'SEM DADOS DIURNOS':'NO DAYTIME DATA'}</div><div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',lineHeight:1.5}}>{lang==='pt'?'Menos de 10 min de amostras diurnas (arquivo parcial). Excluído dos KPIs.':'Fewer than 10 min of daytime samples (partial file). Excluded from KPIs.'}</div></div>
          </div>
          <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',lineHeight:1.7,marginBottom:14,padding:'9px 11px',background:'var(--cream-2)',borderRadius:8}}>
            <strong style={{color:'var(--ink)'}}>{lang==='pt'?'Disponibilidade Diurna':'Daytime Availability'}</strong> — {lang==='pt'?'parcela dos minutos de luz do dia com luz verde. É o número operacionalmente relevante, pois voos são feitos de dia.':'share of daylight minutes with a green light. The operationally meaningful figure, since transfers are flown in daylight.'}<br/>
            <strong style={{color:'var(--ink)'}}>{lang==='pt'?'Disponibilidade 24h':'24h Availability'}</strong> — {lang==='pt'?'a mesma medida no dia inteiro. Sempre exibida junto, porque um convés pode parecer disponível às 03:00 e ser inútil na prática.':'same measure across the full day. Always shown alongside, because a deck can look available at 03:00 and be useless in practice.'}<br/>
            <strong style={{color:'var(--ink)'}}>{lang==='pt'?'Janela Média':'Avg Window'}</strong> — {lang==='pt'?'duração média da maior sequência verde contínua nos dias operáveis. É o número que decide se um voo cabe.':'mean duration of the longest continuous green stretch on operable days. The number that decides whether a flight fits.'}<br/>
            <strong style={{color:'var(--ink)'}}>{lang==='pt'?'Separação dia/noite':'Day/night split'}</strong> — {lang==='pt'?<>vem da tag HMS <span style={{fontFamily:'var(--fm)'}}>DAYNIGHTSTATUSNO</span> (1 = dia, 2 = noite), não do relógio.</>:<>taken from the HMS tag <span style={{fontFamily:'var(--fm)'}}>DAYNIGHTSTATUSNO</span> (1 = day, 2 = night), not from clock hours.</>}
          </div>
          <div className="t-eyebrow" style={{marginBottom:6}}>{lang==='pt'?'Limites monitorados — tags HMS (Database OS101143) · NORMAM-223/DPC':'Monitored limits — HMS tags (Database OS101143) · NORMAM-223/DPC'}</div>
          <div style={{overflowX:'auto'}}>
            <table style={{width:'100%',borderCollapse:'collapse'}}>
              <thead>
                <tr style={{background:'var(--cream-2)'}}>
                  <th style={{padding:'5px 8px',textAlign:'left',fontFamily:'var(--fm)',fontSize:9,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)'}}>Tag</th>
                  <th style={{padding:'5px 8px',textAlign:'left',fontFamily:'var(--fm)',fontSize:9,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)'}}>{lang==='pt'?'Parâmetro':'Parameter'}</th>
                  <th style={{padding:'5px 8px',textAlign:'left',fontFamily:'var(--fm)',fontSize:9,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)'}}>{lang==='pt'?'Limite':'Limit'}</th>
                  <th style={{padding:'5px 8px',textAlign:'left',fontFamily:'var(--fm)',fontSize:9,letterSpacing:1,textTransform:'uppercase',color:'var(--muted)'}}>{lang==='pt'?'Nota':'Note'}</th>
                </tr>
              </thead>
              <tbody>
                {limRow('MAXPITCH20MIN', lang==='pt'?'Pitch — máx dos últimos 20 min':'Pitch — max of last 20 min', limits.pitch.toFixed(1)+'°', '')}
                {limRow('MAXROLL20MIN', lang==='pt'?'Roll — máx dos últimos 20 min':'Roll — max of last 20 min', limits.roll.toFixed(1)+'°', '')}
                {limRow('MAXHELIINCL', lang==='pt'?'Inclinação — máx dos últimos 20 min':'Inclination — max of last 20 min', limits.inclination.toFixed(1)+'°', '')}
                {limRow('SHRROUNDED', 'SVArf — significant heave rate', limits.shr.toFixed(1)+' m/s', '')}
                {limRow('MAXHEAVE', lang==='pt'?'Amplitude de heave — máx dos últimos 20 min':'Heave amplitude — max of last 20 min', limits.heave.toFixed(1)+' m', lang==='pt'?'Só informativo — não dispara a luz nesta instalação':'Informative only — does not trip the light on this installation')}
                {limRow('WINDSPEED2', lang==='pt'?'Vento — média de 2 min':'Wind speed — 2 min mean', '—', lang==='pt'?'Janela exigida pela NORMAM-223 art. 9.4':'Window required by NORMAM-223 art. 9.4')}
                {limRow('WINDGUSTKN3S', lang==='pt'?'Rajada — últimos 3 s':'Wind gust — last 3 s', '—', lang==='pt'?'Única variante de rajada com variação real nos testes':'Only gust variant showing real variation in testing')}
                {limRow('GREENLIGHT / REDLIGHT', lang==='pt'?'Estado da lâmpada HMS (0/1)':'HMS lamp state (0/1)', '—', lang==='pt'?'Base do cálculo de disponibilidade':'Basis of the availability calculation')}
                {limRow('DAYNIGHTSTATUSNO', lang==='pt'?'Dia / noite (1 = dia, 2 = noite)':'Day / night (1 = day, 2 = night)', '—', lang==='pt'?'A tag texto DAYNIGHTSTATUS sempre exporta 0 e não deve ser usada':'Text tag DAYNIGHTSTATUS always exports 0 and must not be used')}
              </tbody>
            </table>
          </div>
          <div style={{fontFamily:'var(--fm)',fontSize:9.5,color:'var(--muted)',lineHeight:1.6,marginTop:10,paddingTop:8,borderTop:'1px dashed var(--line-soft)'}}>
            <strong>{lang==='pt'?'Fonte dos dados.':'Data source.'}</strong> {lang==='pt'
              ? <>O estado verde/vermelho vem direto das tags de lâmpada do HMS; os limites acima são os thresholds configurados no HMS e servem aqui para atribuir <em>por que</em> um período vermelho ocorreu. Os timestamps são lidos em UTC, então o agrupamento por dia é idêntico em qualquer computador. O estudo-base cobre 134 dias monitorados (01 ago – 21 dez 2025); cada export diário do HMS solto aqui é incorporado por data.</>
              : <>Green/red state comes straight from the HMS lamp tags; the limits above are the thresholds configured in the HMS and are used here to attribute <em>why</em> a red period occurred. Timestamps are read in UTC, so day grouping is identical on any computer. The baseline study covers 134 monitored days (01 Aug – 21 Dec 2025); every daily HMS export dropped here is merged on top of it by date.</>}
          </div>
        </div>
      </details>

      {/* semáforo do último dia */}
      <div style={{display:'flex',alignItems:'center',gap:14,padding:'13px 18px',background:'var(--surface)',border:'1px solid var(--line-soft)',borderRadius:14,marginBottom:16,boxShadow:'var(--shadow-sm)'}}>
        {lamps}
        <div style={{width:1,height:40,background:'var(--line-soft)'}}/>
        <div style={{flex:1}}>
          <div style={{fontFamily:'var(--fs)',fontSize:20,color:stClr[lst]||'var(--muted)'}}>{last ? stLbl[lst] : (lang==='pt'?'SEM DADOS':'NO DATA')}</div>
          {last && <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',marginTop:2}}>{lang==='pt'?'Verde diurno':'Daytime green'}: {heliN(last.greenPctDay)}% · {lang==='pt'?'Vento':'Wind'}: {heliN(last.windAvg,1)} kn{last.windGustMax != null ? ` · ${lang==='pt'?'Rajada':'Gust'}: ${heliN(last.windGustMax,1)} kn` : ''}</div>}
        </div>
        {last && <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',textAlign:'right'}}>{lang==='pt'?'Último registro':'Last entry'}<br/><strong style={{color:'var(--ink)'}}>{heliFmtD(last.date, lang)}</strong></div>}
      </div>

      {/* 8 KPI cards */}
      <div style={{display:'grid',gridTemplateColumns:isMobile?'repeat(2,1fr)':'repeat(4,1fr)',gap:10,marginBottom:10}}>
        <HeliKpi label={lang==='pt'?'Dias Operáveis':'Operable Days'} val={`${K.go.length}/${K.total}`} unit="" sub={`${K.goRate}% ${lang==='pt'?'dos dias monitorados':'of monitored days'}`} col="green"/>
        <HeliKpi label={lang==='pt'?'Disponib. Diurna':'Daytime Avail.'} val={K.avgGP!=null?K.avgGP.toFixed(1):'—'} unit="%" sub={lang==='pt'?'Verde em horário diurno':'Green in daylight'} col="yellow"/>
        <HeliKpi label={lang==='pt'?'Disponib. 24h':'24h Avail.'} val={K.avg24!=null?K.avg24.toFixed(1):'—'} unit="%" sub={lang==='pt'?'Verde no dia inteiro':'Green over the full day'} col="teal"/>
        <HeliKpi label={lang==='pt'?'Janela Média':'Avg Window'} val={K.avgWin!=null?K.avgWin.toFixed(1):'—'} unit="h" sub={lang==='pt'?'Duração típica (dias operáveis)':'Typical duration (operable days)'} col="amber"/>
      </div>
      <div style={{display:'grid',gridTemplateColumns:isMobile?'repeat(2,1fr)':'repeat(4,1fr)',gap:10,marginBottom:18}}>
        <HeliKpi label={lang==='pt'?'Dias sem Janela':'Days w/o Window'} val={K.nogo.length} unit="" sub={`${K.total?Math.round(K.nogo.length/K.total*100):0}% — ${lang==='pt'?'zero verde contínuo':'zero continuous green'}`} col="red"/>
        <HeliKpi label={lang==='pt'?'Dias Parciais':'Partial Days'} val={K.partial.length} unit="" sub={lang==='pt'?'Verde, mas <30 min contínuos':'Green but <30 min continuous'} col="amber"/>
        <HeliKpi label={lang==='pt'?'Maior Seq. GO':'Longest GO Streak'} val={K.seqGo} unit=" d" sub={lang==='pt'?'Operáveis consecutivos':'Consecutive operable'} col="green"/>
        <HeliKpi label={lang==='pt'?'Maior Seq. NO-GO':'Longest NO-GO Streak'} val={K.seqNg} unit=" d" sub={lang==='pt'?'Sem janela consecutivos':'Consecutive no-window'} col="red"/>
      </div>

      {/* gráfico dia a dia */}
      {card(
        <>
          {cardTitle(lang==='pt'?'O período, dia a dia':'The period, day by day')}
          <div style={{display:'flex',gap:12,marginBottom:8,flexWrap:'wrap'}}>
            <div style={{display:'flex',alignItems:'center',gap:5,fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}><div style={{width:10,height:10,borderRadius:2,background:'#22c55e'}}/>{lang==='pt'?'Operável (GO)':'Operable (GO)'}</div>
            <div style={{display:'flex',alignItems:'center',gap:5,fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}><div style={{width:10,height:10,borderRadius:2,background:'#f59e0b'}}/>{lang==='pt'?'Parcial':'Partial'}</div>
            <div style={{display:'flex',alignItems:'center',gap:5,fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}><div style={{width:10,height:10,borderRadius:2,background:'#ef4444'}}/>No-go</div>
            {hasNd && <div style={{display:'flex',alignItems:'center',gap:5,fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}><div style={{width:10,height:10,borderRadius:2,background:'#b8c4d0'}}/>{lang==='pt'?'Sem dados diurnos':'No daytime data'}</div>}
          </div>
          <div style={{display:'flex',gap:2,alignItems:'flex-end',height:90}}>
            {days.map((d) => {
              const st = heliStatusOf(d);
              const g = +(d.greenPctDay != null ? d.greenPctDay : 0);
              const h = st === 'go' ? Math.max(8, g) : st === 'partial' ? Math.max(5, g) : 5;
              return <div key={d.date} title={`${heliFmtD(d.date, lang)} — ${st.toUpperCase()} — Green: ${heliN(d.greenPctDay)}%`} style={{flex:1,minWidth:3,height:`${h}px`,background:bC[st],borderRadius:'2px 2px 0 0',opacity:0.9,cursor:'default'}}/>;
            })}
          </div>
          {days.length >= 2 && (
            <div style={{display:'flex',justifyContent:'space-between',fontFamily:'var(--fm)',fontSize:9,color:'var(--muted)',marginTop:4}}>
              <span>{heliFmtD(days[0].date, lang)}</span><span>{heliFmtD(days[Math.floor(days.length/2)].date, lang)}</span><span>{heliFmtD(days[days.length-1].date, lang)}</span>
            </div>
          )}
          <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',marginTop:8,lineHeight:1.5}}>{lang==='pt'?'Padrão bimodal':'Bimodal pattern'}: {a90} {lang==='pt'?'dias acima de 90%':'days above 90%'} · {b10} {lang==='pt'?'dias abaixo de 10%':'days below 10%'}.</div>
        </>
      )}

      {/* disponibilidade por mês + causas */}
      <div style={{display:'grid',gridTemplateColumns:isMobile?'1fr':'1fr 1fr',gap:14,marginBottom:14}}>
        {card(
          <>
            {cardTitle(lang==='pt'?'Disponibilidade por mês':'Availability by month')}
            {Object.keys(mo).sort().map((k) => {
              const o = mo[k], pct = o.t ? Math.round(o.go / o.t * 100) : 0;
              const c = pct >= 50 ? '#22c55e' : pct >= 33 ? '#f59e0b' : '#ef4444';
              return (
                <div key={k} style={{display:'flex',alignItems:'center',gap:8,marginBottom:6}}>
                  <div style={{width:64,fontFamily:'var(--fm)',fontSize:10,fontWeight:600,color:'var(--ink)',flexShrink:0}}>{heliMoName(k, lang)}</div>
                  <div style={{flex:1,height:7,background:'var(--cream-3)',borderRadius:4,overflow:'hidden'}}><div style={{width:`${Math.max(2,pct)}%`,height:'100%',background:c,borderRadius:4}}/></div>
                  <div style={{fontFamily:'var(--fm)',fontSize:13,fontWeight:700,color:c,minWidth:36,textAlign:'right'}}>{pct}%</div>
                  <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',minWidth:44}}>{o.go}/{o.t} d</div>
                </div>
              );
            })}
          </>
        )}
        {card(
          <>
            {cardTitle(lang==='pt'?'Causas de indisponibilidade (min diurnos acima do limite)':'Cause of unavailability (daytime min above limit)')}
            {anyC ? ['pitch','shr','roll','inclination'].map((k) => (
              <div key={k} style={{display:'flex',alignItems:'center',gap:8,marginBottom:6}}>
                <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',minWidth:130}}>{cLbl[k]}</div>
                <div style={{flex:1,height:6,background:'var(--cream-3)',borderRadius:3,overflow:'hidden'}}><div style={{width:`${(tC[k]/mxC*100).toFixed(1)}%`,height:'100%',background:'#ef4444',borderRadius:3}}/></div>
                <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--ink)',minWidth:52,textAlign:'right'}}>{Math.round(tC[k])} min</div>
              </div>
            )) : <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',lineHeight:1.5}}>{lang==='pt'?'Nenhum parâmetro bloqueante excedeu o limite nos minutos vermelhos diurnos.':'No blocking parameter exceeded its limit during daytime red minutes.'}</div>}
            <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)',marginTop:8,paddingTop:6,borderTop:'1px dashed var(--line-soft)',lineHeight:1.5}}>Heave &gt; 3 m: {Math.round(tHeave)} min — <strong>{lang==='pt'?'só informativo':'informative only'}</strong>; {lang==='pt'?'não dispara a luz nesta instalação.':'does not trip the light on this installation.'}</div>
          </>
        )}
      </div>

      {/* vento GO × NO-GO + conclusões */}
      <div style={{display:'grid',gridTemplateColumns:isMobile?'1fr':'1fr 1fr',gap:14}}>
        {card(
          <>
            {cardTitle(lang==='pt'?'Vento médio — GO × NO-GO':'Mean wind — GO × NO-GO')}
            {(K.wGo != null && K.wNg != null) ? (
              <>
                <div style={{display:'flex',alignItems:'center',gap:20,padding:'12px 0'}}>
                  <div style={{flex:1,textAlign:'center'}}>
                    <div style={{fontFamily:'var(--fs)',fontSize:30,color:'#22c55e'}}>{K.wGo.toFixed(1)}<span style={{fontSize:12,color:'var(--muted)'}}> kn</span></div>
                    <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>{lang==='pt'?'dias GO':'GO days'}</div>
                  </div>
                  <div style={{width:1,height:40,background:'var(--line-soft)'}}/>
                  <div style={{flex:1,textAlign:'center'}}>
                    <div style={{fontFamily:'var(--fs)',fontSize:30,color:'#ef4444'}}>{K.wNg.toFixed(1)}<span style={{fontSize:12,color:'var(--muted)'}}> kn</span></div>
                    <div style={{fontFamily:'var(--fm)',fontSize:10,color:'var(--muted)'}}>{lang==='pt'?'dias NO-GO':'NO-GO days'}</div>
                  </div>
                </div>
                {K.wGo > K.wNg && <div style={{fontFamily:'var(--fm)',fontSize:10,color:'#f59e0b',lineHeight:1.4}}>{lang==='pt'?<>O vento é <strong>maior</strong> nos dias GO — vento não é o gargalo. Causa provável: movimento do navio em DP (pitch/roll/SHR).</>:<>Wind is <strong>higher</strong> on GO days — wind is not the bottleneck. Likely cause: vessel motion in DP (pitch/roll/SHR).</>}</div>}
              </>
            ) : <div style={{fontFamily:'var(--fm)',fontSize:11,color:'var(--muted)',padding:'10px 0'}}>{lang==='pt'?'Dados de vento insuficientes.':'Not enough wind data.'}</div>}
          </>
        )}
        {card(
          <>
            {cardTitle(lang==='pt'?'Conclusões':'Conclusions')}
            <div style={{display:'grid',gap:8}}>
              {ccs.map((c) => (
                <div key={c.n} style={{display:'flex',gap:10,alignItems:'flex-start',padding:10,background:'var(--cream-2)',borderRadius:8}}>
                  <div style={{fontFamily:'var(--fs)',fontSize:22,color:c.c,flexShrink:0,lineHeight:1}}>{c.n}</div>
                  <div style={{fontFamily:'var(--f)',fontSize:11.5,color:'var(--ink)',lineHeight:1.6}}>{c.t}</div>
                </div>
              ))}
            </div>
          </>
        )}
      </div>
    </main>
  );
};
