/* global React */
// =====================================================================
// Colo Ritmo — modelo de dados com datas reais (ISO) + recorrências
// =====================================================================

// ---- Hospitais ------------------------------------------------------
// Famílias do design system Colo: sand, blue, coral, sage, lavender, aqua.
window.FAMILIAS_HOSPITAL = [
  { family: 'sand',     cor: '#E8C79A', corDeep: '#B98A3F', corWash: '#FBF1E1' },
  { family: 'blue',     cor: '#9BC2E7', corDeep: '#3F6E9C', corWash: '#EAF2F9' },
  { family: 'coral',    cor: '#E7A59C', corDeep: '#B25A4D', corWash: '#FBE9E5' },
  { family: 'sage',     cor: '#A4D498', corDeep: '#5A6E50', corWash: '#ECF6E7' },
  { family: 'lavender', cor: '#A299CB', corDeep: '#5A4E8C', corWash: '#ECEAF4' },
  { family: 'aqua',     cor: '#9AD8E1', corDeep: '#3D7884', corWash: '#E8F6F8' },
  { family: 'pink',     cor: '#E79BC4', corDeep: '#A85C82', corWash: '#FAEAF2' },
  { family: 'olive',    cor: '#C5BE99', corDeep: '#7A7350', corWash: '#F1EFE0' },
];

const HOSPITAIS_DEFAULT = [
  {
    id: 1, nome: "Hospital Santa Lúcia", abrev: "HSL",
    endereco: "Asa Sul, Brasília", family: "sand",
    cor: '#E8C79A', corDeep: '#B98A3F', corWash: '#FBF1E1',
    deslocCasa: 25, telefone: "(61) 3445-0000", notas: "",
    lat: -15.821, lng: -47.916,
    turnos: {
      // HSL UTIP: convenção da escala definitiva (maio/2026 em diante)
      // Manhã virou 6h (7–13). Antes era 12h (7–19). Tarde 1 e Tarde 2 cobrem o vespertino.
      manha:    { inicio: 7,  duracao: 6  },
      tarde:    { inicio: 13, duracao: 6  },
      noitinha: { inicio: 19, duracao: 5  },
      noite:    { inicio: 19, duracao: 12 },
    },
  },
  {
    id: 2, nome: "Hospital de Base do DF", abrev: "HBDF",
    endereco: "SMHS Q.101 Área Especial · Asa Sul", family: "blue",
    cor: '#9BC2E7', corDeep: '#3F6E9C', corWash: '#EAF2F9',
    deslocCasa: 35, telefone: "(61) 3315-1200", notas: "",
    lat: -15.770, lng: -47.876,
    turnos: {},
  },
  {
    id: 3, nome: "Hospital DF Star", abrev: "HDS",
    endereco: "Avenida das Castanheiras · Águas Claras", family: "coral",
    cor: '#E7A59C', corDeep: '#B25A4D', corWash: '#FBE9E5',
    deslocCasa: 40, telefone: "(61) 3037-9000", notas: "",
    lat: -15.834, lng: -48.025,
    turnos: {},
  },
  {
    id: 4, nome: "Hospital da Criança de Brasília", abrev: "HCB",
    endereco: "SAIN Lote A · Asa Norte", family: "pink",
    cor: '#E79BC4', corDeep: '#A85C82', corWash: '#FAEAF2',
    deslocCasa: 30, telefone: "(61) 3025-8200", notas: "UTI Pediátrica José de Alencar.",
    lat: -15.788, lng: -47.880,
    // Turnos chutados — convenção pediátrica padrão. Confirme com a Mariana antes de
    // confiar 100%. A coluna "Noitinha" do PDF de maio/2026 estava sempre vazia, então
    // não cadastrei.
    turnos: {
      manha: { inicio: 7,  duracao: 6 },
      tarde: { inicio: 13, duracao: 6 },
      noite: { inicio: 19, duracao: 12 },
    },
  },
];

window.HOSPITAIS_DEFAULT = HOSPITAIS_DEFAULT;

// ---- Regras de escala (feature "Fazer escala") -----------------------
// Cada hospital pode opcionalmente ter um campo `regrasEscala` que o solver
// usa pra montar propostas de escala mensal. Se ausente, hospital não
// participa do solver (só recebe plantões manualmente).
//
// Shape:
//   regrasEscala: {
//     ativo:             boolean,   // se inclui esse hospital na simulação
//     plantoesMin/Max:   number,    // total de plantões/mês alvo
//     fdsMin/Max:        number,    // qtos plantões em sáb/dom
//     horasMin/Max:      number,    // horas totais alvo
//     turnosPermitidos:  string[],  // subset de Object.keys(hospital.turnos) — quais turnos pega
//     diasIndisponiveis: number[],  // 0=Seg..6=Dom — dias da semana que NUNCA pega
//     intervaloMinH:     number,    // gap mínimo entre 2 plantões consecutivos (horas)
//   }
//
// Sugestão de defaults razoáveis quando Mariana ativar pela primeira vez:
window.regrasEscalaPadrao = (hospital) => ({
  ativo: true,
  plantoesMin: 6,
  plantoesMax: 10,
  fdsMin: 1,
  fdsMax: 3,
  horasMin: 36,
  horasMax: 72,
  turnosPermitidos: Object.keys(hospital?.turnos || {}),
  diasIndisponiveis: [],
  intervaloMinH: 12,
});

// Helpers de leitura segura — usados no solver e UI.
window.temRegrasAtivas = (hospital) => !!hospital?.regrasEscala?.ativo;
window.hospitaisComRegras = (hospitais) => (hospitais || []).filter(window.temRegrasAtivas);

// =====================================================================
// Calculadora de remuneração de plantão
// =====================================================================
// Schema de hospital.remuneracao:
//   {
//     ativo: boolean,
//     valorHoraBase: number,                              // R$/hora
//     adicionalNoturno: { ativo, desde, ate, pct },       // janela horária + % adicional
//     adicionalFDS:     { ativo, diasSemana, pct },       // [5,6]=sáb+dom (0=Seg, 6=Dom convenção BR)
//     adicionalFeriado: { ativo, datas, pct },            // datas ISO específicas
//     descontoPct:      number,                            // 0-100, retido na fonte
//     observacao:       string
//   }
// Adicionais ACUMULAM (noturno + FDS + feriado podem somar). Plantão cedido não conta.

window.remuneracaoPadrao = (tipo = 'particular') => {
  if (tipo === 'publico') {
    return {
      ativo: true,
      modelo: 'fixoMensal',                          // ← marca explícita
      valorFixoMensal: 8000,                          // R$ fixo/mês independente de horas
      adicionalNoturnoRS: 30,                         // R$ por hora noturna (em cima do fixo)
      adicionalNoturno: { ativo: true, desde: 22, ate: 5 },  // janela horária
      descontoPct: 11,                                // INSS típico no público
      observacao: '',
    };
  }
  // Particular — modelo % por hora (como antes)
  return {
    ativo: true,
    modelo: 'horaBase',
    valorHoraBase: 100,
    adicionalNoturno: { ativo: true, desde: 22, ate: 5, pct: 20 },
    adicionalFDS:     { ativo: true, diasSemana: [5, 6], pct: 50 },
    adicionalFeriado: { ativo: false, datas: [], pct: 100 },
    descontoPct: 0,
    observacao: '',
  };
};

// Calcula bruto/líquido de UM plantão. Retorna { bruto, liquido, breakdown, ... }
// Suporta 2 modelos:
//  - 'horaBase' (particular): valor/h × adicionais % (acumulam)
//  - 'fixoMensal' (público): só calcula adicional noturno em R$. Fixo mensal entra no agregado.
window.calcRemuneracao = (bloco, hospital) => {
  const r = hospital?.remuneracao;
  const fallback = { bruto: 0, liquido: 0, breakdown: [], descontoValor: 0, descontoPct: 0 };
  if (!r?.ativo) return { ...fallback, inativo: true };
  if (bloco.tipo === 'cedido') return { ...fallback, cedido: true };
  if (bloco.tipo !== 'plantao' && !bloco.viaTroca) return fallback;

  const modelo = r.modelo || (r.valorHoraBase ? 'horaBase' : 'fixoMensal');

  // Modelo PÚBLICO (fixo mensal) — calcula apenas adicional noturno por plantão
  if (modelo === 'fixoMensal') {
    const adicRS = parseFloat(r.adicionalNoturnoRS) || 0;
    if (adicRS === 0 || !r.adicionalNoturno?.ativo) {
      return { ...fallback, modelo, fixoMensal: parseFloat(r.valorFixoMensal) || 0 };
    }
    const { desde = 22, ate = 5 } = r.adicionalNoturno;
    let horasNoturnas = 0;
    for (let i = 0; i < bloco.duracao; i++) {
      const horaAbs = bloco.horaInicio + i;
      const horaAtual = ((horaAbs % 24) + 24) % 24;
      const dentro = desde < ate
        ? horaAtual >= desde && horaAtual < ate
        : horaAtual >= desde || horaAtual < ate;
      if (dentro) horasNoturnas++;
    }
    const adicTotal = horasNoturnas * adicRS;
    const descontoPct = parseFloat(r.descontoPct) || 0;
    const descontoValor = adicTotal * (descontoPct / 100);
    return {
      bruto: Math.round(adicTotal * 100) / 100,
      liquido: Math.round((adicTotal - descontoValor) * 100) / 100,
      descontoValor: Math.round(descontoValor * 100) / 100,
      descontoPct,
      modelo: 'fixoMensal',
      fixoMensal: parseFloat(r.valorFixoMensal) || 0,
      breakdown: horasNoturnas > 0
        ? [{ tipo: 'adicional noturno', horas: horasNoturnas, valorRS: adicRS, valor: adicTotal }]
        : [],
    };
  }

  const valorBase = parseFloat(r.valorHoraBase) || 0;
  // Quebra em segmentos de 1h (suficiente pra precisão prática)
  const segmentos = [];
  for (let i = 0; i < bloco.duracao; i++) {
    const horaAbs = bloco.horaInicio + i;
    const horaAtual = ((horaAbs % 24) + 24) % 24;
    const dataAtual = horaAbs >= 24
      ? window.addDias(bloco.data, Math.floor(horaAbs / 24))
      : bloco.data;
    const dow = window.diaSemanaBR(dataAtual);

    let pctAdic = 0;
    const labels = [];

    // Noturno — janela pode atravessar meia-noite (desde > ate)
    if (r.adicionalNoturno?.ativo) {
      const { desde, ate, pct } = r.adicionalNoturno;
      const dentro = desde < ate
        ? horaAtual >= desde && horaAtual < ate
        : horaAtual >= desde || horaAtual < ate;
      if (dentro) { pctAdic += pct; labels.push('noturno'); }
    }
    // FDS
    if (r.adicionalFDS?.ativo && (r.adicionalFDS.diasSemana || []).includes(dow)) {
      pctAdic += r.adicionalFDS.pct;
      labels.push('fds');
    }
    // Feriado
    if (r.adicionalFeriado?.ativo && (r.adicionalFeriado.datas || []).includes(dataAtual)) {
      pctAdic += r.adicionalFeriado.pct;
      labels.push('feriado');
    }

    const valorSegmento = valorBase * (1 + pctAdic / 100);
    segmentos.push({ horaAtual, dataAtual, valor: valorSegmento, pctAdic, labels });
  }

  const bruto = segmentos.reduce((a, s) => a + s.valor, 0);
  const descontoPct = parseFloat(r.descontoPct) || 0;
  const descontoValor = bruto * (descontoPct / 100);
  const liquido = bruto - descontoValor;

  // Breakdown agrupado por combinação de adicionais
  const grupos = {};
  segmentos.forEach(s => {
    const key = s.labels.length === 0 ? 'base' : s.labels.sort().join('+');
    if (!grupos[key]) grupos[key] = { horas: 0, valor: 0, pctAdic: s.pctAdic };
    grupos[key].horas += 1;
    grupos[key].valor += s.valor;
  });

  return {
    bruto: Math.round(bruto * 100) / 100,
    liquido: Math.round(liquido * 100) / 100,
    descontoValor: Math.round(descontoValor * 100) / 100,
    descontoPct,
    breakdown: Object.entries(grupos).map(([tipo, g]) => ({
      tipo,
      horas: g.horas,
      pctAdic: g.pctAdic,
      valor: Math.round(g.valor * 100) / 100,
    })),
    valorBase,
  };
};

// Soma de bruto/líquido pra um conjunto de blocos, agrupado por hospital.
// Hospitais com modelo 'fixoMensal' somam o valor fixo uma vez (independente
// da quantidade de plantões), mais os adicionais noturnos por plantão.
window.calcRemuneracaoPeriodo = (blocos, hospitais) => {
  const porHospital = {};
  let totalBruto = 0, totalLiquido = 0;
  let temHospitalSemRemuneracao = false;
  for (const b of blocos || []) {
    if (b.tipo !== 'plantao' || b.tipo === 'cedido') continue;
    const h = (hospitais || []).find(x => x.id === b.hospitalId);
    if (!h) continue;
    const r2 = h.remuneracao;
    const ativo = r2?.ativo && (r2.valorHoraBase || r2.valorFixoMensal);
    if (!ativo) { temHospitalSemRemuneracao = true; continue; }
    const r = window.calcRemuneracao(b, h);
    if (r.inativo || r.cedido) continue;
    const key = h.abrev || `H${h.id}`;
    if (!porHospital[key]) {
      porHospital[key] = {
        hospitalNome: h.nome, hospitalAbrev: h.abrev,
        cor: h.cor, corDeep: h.corDeep, corWash: h.corWash,
        modelo: r.modelo || 'horaBase',
        valorFixoMensal: r.fixoMensal || 0,
        bruto: 0, liquido: 0, plantoes: 0, horas: 0,
        adicionaisBruto: 0,
      };
    }
    porHospital[key].adicionaisBruto += r.bruto;
    porHospital[key].bruto += r.bruto;
    porHospital[key].liquido += r.liquido;
    porHospital[key].plantoes++;
    porHospital[key].horas += b.duracao;
  }
  // Soma fixo mensal pra hospitais públicos com pelo menos 1 plantão
  const descontoPctFor = (h) => parseFloat(h.remuneracao?.descontoPct) || 0;
  for (const key in porHospital) {
    const g = porHospital[key];
    if (g.modelo === 'fixoMensal' && g.valorFixoMensal > 0 && g.plantoes > 0) {
      const h = (hospitais || []).find(x => (x.abrev || `H${x.id}`) === key);
      const desc = h ? descontoPctFor(h) : 0;
      const fixoLiquido = g.valorFixoMensal * (1 - desc / 100);
      g.bruto += g.valorFixoMensal;
      g.liquido += fixoLiquido;
    }
    g.bruto = Math.round(g.bruto * 100) / 100;
    g.liquido = Math.round(g.liquido * 100) / 100;
    g.adicionaisBruto = Math.round(g.adicionaisBruto * 100) / 100;
    totalBruto += g.bruto;
    totalLiquido += g.liquido;
  }
  return {
    totalBruto: Math.round(totalBruto * 100) / 100,
    totalLiquido: Math.round(totalLiquido * 100) / 100,
    porHospital,
    temHospitalSemRemuneracao,
  };
};

// Formata BRL
window.fmtBRL = (v) => {
  if (v == null || isNaN(v)) return '—';
  return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
};

// =====================================================================
// Extrato financeiro — print/PDF detalhado pra cruzar com folha de pagamento
// =====================================================================
window.imprimirExtrato = (mesAlvoISO, blocos, hospitais) => {
  const w = window.open('', '_blank', 'width=900,height=1100');
  if (!w) { alert('Pop-up bloqueado. Libere pop-ups pra exportar.'); return; }

  const [y, m] = mesAlvoISO.split('-').map(Number);
  const totalDias = new Date(y, m, 0).getDate();
  const ini = window.toISO(new Date(y, m-1, 1));
  const fim = window.toISO(new Date(y, m-1, totalDias));
  const blocosMes = (blocos || [])
    .filter(b => b.tipo === 'plantao' && b.data >= ini && b.data <= fim)
    .sort((a, b) => a.data.localeCompare(b.data) || a.horaInicio - b.horaInicio);

  // Calcula linha por linha
  const linhas = blocosMes.map(b => {
    const h = (hospitais || []).find(x => x.id === b.hospitalId);
    const r = h?.remuneracao?.ativo ? window.calcRemuneracao(b, h) : null;
    return { bloco: b, hospital: h, remu: r };
  });

  // Totais
  const totais = window.calcRemuneracaoPeriodo(blocosMes, hospitais);

  const fmt = window.fmtBRL;
  const dt = (iso) => {
    const d = window.fromISO(iso);
    return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}`;
  };
  const dow = (iso) => window.DIAS_SEMANA[window.diaSemanaBR(iso)];

  const html = `<!doctype html><html lang="pt-BR"><head>
<meta charset="utf-8"><title>Extrato — ${window.MESES[m-1]} ${y}</title>
<style>
  @page { size: A4 portrait; margin: 12mm; }
  * { box-sizing: border-box; }
  body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #3A2E2A; margin: 0; padding: 16px; line-height: 1.4; }
  h1 { font-size: 20pt; margin: 0 0 4pt; font-weight: 600; letter-spacing: -0.02em; }
  .subt { color: #7A6A60; font-size: 11pt; margin: 0 0 14pt; }
  .resumo { display: grid; grid-template-columns: repeat(auto-fit, minmax(120pt, 1fr)); gap: 10pt; margin: 10pt 0 16pt; }
  .resumo-card { padding: 8pt 12pt; border: 1px solid #DDD; border-radius: 4pt; }
  .resumo-num { font-size: 16pt; font-weight: 700; }
  .resumo-lbl { font-size: 9pt; text-transform: uppercase; letter-spacing: 0.04em; color: #7A6A60; }
  table { width: 100%; border-collapse: collapse; font-size: 9pt; margin-top: 8pt; }
  th, td { padding: 4pt 6pt; text-align: left; border-bottom: 1px solid #EEE; }
  th { font-size: 8pt; text-transform: uppercase; letter-spacing: 0.04em; color: #7A6A60; background: #FAF7F2; }
  .num { font-family: ui-monospace, 'SF Mono', Menlo, monospace; text-align: right; font-variant-numeric: tabular-nums; }
  .small { font-size: 8pt; color: #7A6A60; }
  .swatch { display: inline-block; width: 8pt; height: 8pt; border-radius: 2pt; vertical-align: middle; margin-right: 4pt; }
  .por-hospital { margin-top: 16pt; padding-top: 8pt; border-top: 1px solid #DDD; }
  .por-hosp-item { display: flex; justify-content: space-between; padding: 3pt 0; }
  .footer { margin-top: 18pt; padding-top: 8pt; border-top: 1px solid #DDD; font-size: 8pt; color: #7A6A60; }
  @media print { body { padding: 0; } .no-print { display: none !important; } }
</style></head><body>

<h1>Extrato de plantões — ${window.MESES[m-1]} ${y}</h1>
<p class="subt">Pra conferir com a folha de pagamento dos hospitais.</p>

<div class="resumo">
  <div class="resumo-card">
    <div class="resumo-lbl">Plantões</div>
    <div class="resumo-num">${linhas.length}</div>
  </div>
  <div class="resumo-card">
    <div class="resumo-lbl">Horas</div>
    <div class="resumo-num">${linhas.reduce((a, l) => a + l.bloco.duracao, 0)}h</div>
  </div>
  <div class="resumo-card">
    <div class="resumo-lbl">Bruto</div>
    <div class="resumo-num">${fmt(totais.totalBruto)}</div>
  </div>
  <div class="resumo-card">
    <div class="resumo-lbl">Líquido</div>
    <div class="resumo-num" style="color:#5A6E50">${fmt(totais.totalLiquido)}</div>
  </div>
</div>

<table>
  <thead>
    <tr>
      <th>Data</th>
      <th>Dia</th>
      <th>Hospital</th>
      <th>Turno</th>
      <th class="num">Início</th>
      <th class="num">Dur.</th>
      <th>Adicionais</th>
      <th class="num">Bruto</th>
      <th class="num">Desc.</th>
      <th class="num">Líquido</th>
    </tr>
  </thead>
  <tbody>
    ${linhas.map(l => {
      const adicionais = l.remu?.breakdown?.filter(b => b.tipo !== 'base').map(b => `${b.horas}h ${b.tipo}`).join(', ') || '—';
      return `<tr>
        <td>${dt(l.bloco.data)}</td>
        <td>${dow(l.bloco.data)}</td>
        <td><span class="swatch" style="background:${l.hospital?.cor || '#999'}"></span>${l.hospital?.abrev || '?'}</td>
        <td>${l.bloco.turno || '—'}${l.bloco.turnoIncerto ? ' <span class="small">(incerto)</span>' : ''}</td>
        <td class="num">${window.fmtHora(l.bloco.horaInicio)}</td>
        <td class="num">${l.bloco.duracao}h</td>
        <td class="small">${adicionais}</td>
        <td class="num">${l.remu ? fmt(l.remu.bruto) : '—'}</td>
        <td class="num small">${l.remu?.descontoValor > 0 ? '−' + fmt(l.remu.descontoValor) : '—'}</td>
        <td class="num" style="font-weight:600">${l.remu ? fmt(l.remu.liquido) : '—'}</td>
      </tr>`;
    }).join('')}
  </tbody>
  <tfoot>
    <tr style="font-weight:700; background:#FAF7F2">
      <td colspan="7">Total</td>
      <td class="num">${fmt(totais.totalBruto)}</td>
      <td class="num">${fmt(totais.totalBruto - totais.totalLiquido)}</td>
      <td class="num" style="color:#5A6E50">${fmt(totais.totalLiquido)}</td>
    </tr>
  </tfoot>
</table>

<div class="por-hospital">
  <div class="resumo-lbl" style="margin-bottom: 6pt">Por hospital</div>
  ${Object.values(totais.porHospital).map(g => `
    <div class="por-hosp-item">
      <span><span class="swatch" style="background:${g.cor || '#999'}"></span><strong>${g.hospitalAbrev}</strong> — ${g.plantoes} plantões · ${g.horas}h</span>
      <span class="num"><strong>${fmt(g.bruto)}</strong> · <span style="color:#5A6E50">${fmt(g.liquido)} líq.</span></span>
    </div>
  `).join('')}
</div>

<div class="footer">
  Colo Ritmo · Gerado em ${new Date().toLocaleString('pt-BR')} · Cálculo baseado nas regras de remuneração cadastradas em cada hospital.
</div>

<div class="no-print" style="margin-top: 18pt; text-align: center">
  <button onclick="window.print()" style="padding:10pt 24pt;font-size:11pt;background:#3A2E2A;color:#FFF;border:none;border-radius:4pt;cursor:pointer">
    Imprimir / Salvar como PDF
  </button>
</div>

<script>setTimeout(() => window.print(), 600);</script>
</body></html>`;
  w.document.write(html);
  w.document.close();
};

// ---- Casa (origem dos deslocamentos) -------------------------------
// SQNW 306 Bloco I · Asa Noroeste, Brasília. Coordenadas aproximadas.
window.CASA_DEFAULT = {
  cep: "70684-245",
  endereco: "SQNW 306 Bloco I · Asa Noroeste",
  lat: -15.762,
  lng: -47.915,
};
const _casaSalva = (typeof localStorage !== 'undefined') ? (() => {
  try { const raw = localStorage.getItem('colo-ritmo-casa'); return raw ? JSON.parse(raw) : null; } catch { return null; }
})() : null;
window.CASA = _casaSalva || window.CASA_DEFAULT;
window.salvarCasa = (casa) => {
  window.CASA = casa;
  try { localStorage.setItem('colo-ritmo-casa', JSON.stringify(casa)); } catch {}
};

// Mescla hospitais persistidos (de versões anteriores do app) com os defaults atuais.
// Adiciona qualquer hospital novo do default que não esteja no persistido (matched by abrev).
// Também copia `turnos` do default pra hospitais persistidos que não têm (migração suave).
window.mesclarHospitais = (persistidos, defaults) => {
  const defs = defaults || HOSPITAIS_DEFAULT;
  if (!persistidos || persistidos.length === 0) return defs;
  const porAbrev = new Map(defs.map(d => [d.abrev?.toUpperCase(), d]));
  const enriquecidos = persistidos.map(h => {
    const def = porAbrev.get(h.abrev?.toUpperCase());
    if (def && !h.turnos && def.turnos) return { ...h, turnos: def.turnos };
    return h;
  });
  const presentes = new Set(persistidos.map(h => h.abrev?.toUpperCase()));
  const novos = defs.filter(d => !presentes.has(d.abrev?.toUpperCase()))
    .map(d => ({ ...d, id: Math.max(0, ...persistidos.map(h => h.id)) + (d.id || 1) }));
  return [...enriquecidos, ...novos];
};

// Dado um hospital + nome de turno ('manha', 'tarde', 'noite', 'noitinha'),
// retorna { horaInicio, duracao } ou null se o hospital não cadastrou esse turno.
window.resolverTurno = (hospital, turnoNome) => {
  if (!hospital || !hospital.turnos || !turnoNome) return null;
  const t = hospital.turnos[turnoNome.toLowerCase()];
  if (!t) return null;
  return { horaInicio: t.inicio, duracao: t.duracao };
};

// ---- Datas ----------------------------------------------------------
window.DIAS_SEMANA = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
window.DIAS_COMPLETO = ['Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado', 'Domingo'];
window.MESES = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
window.MESES_CURTO = ['jan','fev','mar','abr','mai','jun','jul','ago','set','out','nov','dez'];

// Helpers de data — todas operam em horário local
window.toISO = (d) => {
  const yyyy = d.getFullYear();
  const mm = String(d.getMonth()+1).padStart(2,'0');
  const dd = String(d.getDate()).padStart(2,'0');
  return `${yyyy}-${mm}-${dd}`;
};
window.fromISO = (iso) => {
  const [y,m,d] = iso.split('-').map(Number);
  return new Date(y, m-1, d);
};
window.addDias = (iso, n) => {
  const d = window.fromISO(iso);
  d.setDate(d.getDate() + n);
  return window.toISO(d);
};
// 0=Seg, 6=Dom (estilo BR)
window.diaSemanaBR = (iso) => {
  const d = window.fromISO(iso);
  return (d.getDay() + 6) % 7;
};
// Início da semana (segunda-feira) que contém esta data
window.inicioSemana = (iso) => {
  const dow = window.diaSemanaBR(iso);
  return window.addDias(iso, -dow);
};
window.mesmaData = (a, b) => a === b;
window.diasNoMes = (ano, mes) => new Date(ano, mes+1, 0).getDate();
window.fmtDataCurto = (iso) => {
  const d = window.fromISO(iso);
  return `${d.getDate()} ${window.MESES_CURTO[d.getMonth()]}`;
};
window.fmtDataLongo = (iso) => {
  const d = window.fromISO(iso);
  return `${d.getDate()} de ${window.MESES[d.getMonth()]} de ${d.getFullYear()}`;
};
window.fmtIntervaloSemana = (isoSeg) => {
  const fim = window.addDias(isoSeg, 6);
  const a = window.fromISO(isoSeg), b = window.fromISO(fim);
  if (a.getMonth() === b.getMonth()) {
    return `${a.getDate()} — ${b.getDate()} ${window.MESES_CURTO[a.getMonth()]} · ${a.getFullYear()}`;
  }
  return `${a.getDate()} ${window.MESES_CURTO[a.getMonth()]} — ${b.getDate()} ${window.MESES_CURTO[b.getMonth()]} · ${b.getFullYear()}`;
};

// "Hoje" — calculado dinamicamente
window.HOJE_ISO = window.toISO(new Date());
window.ANO_PROTOTIPO = new Date().getFullYear();

// ---- Blocos iniciais — semente pra primeira sessão da Mariana --------
// O cloud (KV no Worker) é a fonte da verdade. Esses blocos só são gravados
// na primeira vez que ela abre com uma userKey nova (sem dado salvo).
// Cobrem 3 escalas reais extraídas dos PDFs da Mariana:
//   - HSL abril/2026 (escala padrão UTIP Santa Lúcia)
//   - HSL maio/2026 (escala definitiva — Manhã = 7-13h, 6h)
//   - HCB maio/2026 (Hospital da Criança de Brasília)
//
// Conflitos conhecidos (intencionalmente preservados — app sinaliza pra ela
// resolver com a coordenação):
//   - Sáb 9 mai: HCB Manhã+Tarde × HSL Manhã+Tarde 2 (mesmos horários)
//   - Sex 15 mai: HSL Noitinha 19-00 × HCB Noite 19-07
window.SEMENTE_BLOCOS = [
  // ===================== HSL · Abril/2026 (padrão) =====================
  { id: 1, tipo: 'plantao', hospitalId: 1, data: '2026-04-04', horaInicio: 19, duracao: 12, observacao: 'HSL · Sábado · Noite' },
  { id: 2, tipo: 'plantao', hospitalId: 1, data: '2026-04-06', horaInicio: 19, duracao: 12, observacao: 'HSL · Segunda · Noite' },
  { id: 3, tipo: 'plantao', hospitalId: 1, data: '2026-04-13', horaInicio: 19, duracao: 5,  observacao: 'HSL · Segunda · Noitinha' },
  { id: 4, tipo: 'plantao', hospitalId: 1, data: '2026-04-17', horaInicio: 19, duracao: 5,  observacao: 'HSL · Sexta · Noitinha' },
  { id: 5, tipo: 'plantao', hospitalId: 1, data: '2026-04-18', horaInicio: 19, duracao: 5,  observacao: 'HSL · Sábado · Noitinha' },
  { id: 6, tipo: 'plantao', hospitalId: 1, data: '2026-04-20', horaInicio: 19, duracao: 5,  observacao: 'HSL · Segunda · Noitinha' },
  { id: 7, tipo: 'plantao', hospitalId: 1, data: '2026-04-27', horaInicio: 19, duracao: 5,  observacao: 'HSL · Segunda · Noitinha' },

  // ===================== HSL · Maio/2026 (definitiva) ==================
  { id: 8,  tipo: 'plantao', hospitalId: 1, data: '2026-05-01', horaInicio: 19, duracao: 5,  observacao: 'HSL · Sexta · Noitinha' },
  { id: 9,  tipo: 'plantao', hospitalId: 1, data: '2026-05-02', horaInicio: 19, duracao: 12, observacao: 'HSL · Sábado · Noite' },
  { id: 10, tipo: 'plantao', hospitalId: 1, data: '2026-05-04', horaInicio: 7,  duracao: 6,  observacao: 'HSL · Segunda · Manhã (dupla com Bruna)' },
  { id: 11, tipo: 'plantao', hospitalId: 1, data: '2026-05-04', horaInicio: 19, duracao: 12, observacao: 'HSL · Segunda · Noite' },
  { id: 12, tipo: 'plantao', hospitalId: 1, data: '2026-05-08', horaInicio: 19, duracao: 5,  observacao: 'HSL · Sexta · Noitinha (* extra FDS)' },
  { id: 13, tipo: 'plantao', hospitalId: 1, data: '2026-05-09', horaInicio: 7,  duracao: 6,  observacao: 'HSL · Sábado · Manhã (dupla com Anna) — ⚠ conflita com HCB' },
  { id: 14, tipo: 'plantao', hospitalId: 1, data: '2026-05-09', horaInicio: 13, duracao: 6,  observacao: 'HSL · Sábado · Tarde 2 — ⚠ conflita com HCB' },
  { id: 15, tipo: 'plantao', hospitalId: 1, data: '2026-05-11', horaInicio: 19, duracao: 5,  observacao: 'HSL · Segunda · Noitinha' },
  { id: 16, tipo: 'plantao', hospitalId: 1, data: '2026-05-15', horaInicio: 19, duracao: 5,  observacao: 'HSL · Sexta · Noitinha — ⚠ conflita com HCB Noite' },
  { id: 17, tipo: 'plantao', hospitalId: 1, data: '2026-05-17', horaInicio: 19, duracao: 5,  observacao: 'HSL · Domingo · Noitinha' },
  { id: 18, tipo: 'plantao', hospitalId: 1, data: '2026-05-18', horaInicio: 19, duracao: 5,  observacao: 'HSL · Segunda · Noitinha' },
  { id: 19, tipo: 'plantao', hospitalId: 1, data: '2026-05-24', horaInicio: 19, duracao: 5,  observacao: 'HSL · Domingo · Noitinha (* extra FDS)' },
  { id: 20, tipo: 'plantao', hospitalId: 1, data: '2026-05-25', horaInicio: 19, duracao: 5,  observacao: 'HSL · Segunda · Noitinha' },
  { id: 21, tipo: 'plantao', hospitalId: 1, data: '2026-05-29', horaInicio: 19, duracao: 5,  observacao: 'HSL · Sexta · Noitinha' },

  // ===================== HCB · Maio/2026 ===============================
  { id: 22, tipo: 'plantao', hospitalId: 4, data: '2026-05-07', horaInicio: 19, duracao: 12, observacao: 'HCB · Quinta · Noite' },
  { id: 23, tipo: 'plantao', hospitalId: 4, data: '2026-05-08', horaInicio: 13, duracao: 6,  observacao: 'HCB · Sexta · Tarde' },
  { id: 24, tipo: 'plantao', hospitalId: 4, data: '2026-05-09', horaInicio: 7,  duracao: 6,  observacao: 'HCB · Sábado · Manhã — ⚠ conflita com HSL' },
  { id: 25, tipo: 'plantao', hospitalId: 4, data: '2026-05-09', horaInicio: 13, duracao: 6,  observacao: 'HCB · Sábado · Tarde — ⚠ conflita com HSL' },
  { id: 26, tipo: 'plantao', hospitalId: 4, data: '2026-05-14', horaInicio: 13, duracao: 6,  observacao: 'HCB · Quinta · Tarde' },
  { id: 27, tipo: 'plantao', hospitalId: 4, data: '2026-05-15', horaInicio: 19, duracao: 12, observacao: 'HCB · Sexta · Noite — ⚠ conflita com HSL Noitinha' },
  { id: 28, tipo: 'plantao', hospitalId: 4, data: '2026-05-26', horaInicio: 19, duracao: 12, observacao: 'HCB · Terça · Noite' },
  { id: 29, tipo: 'plantao', hospitalId: 4, data: '2026-05-29', horaInicio: 7,  duracao: 6,  observacao: 'HCB · Sexta · Manhã' },
  { id: 30, tipo: 'plantao', hospitalId: 4, data: '2026-05-29', horaInicio: 13, duracao: 6,  observacao: 'HCB · Sexta · Tarde' },
];
// Alias retrocompatível (Tweaks "Resetar exemplo" referencia BLOCOS_INICIAIS)
window.BLOCOS_INICIAIS = window.SEMENTE_BLOCOS;

// ---- Recorrências ---------------------------------------------------
// Cada recorrência gera blocos sintéticos. Tipos: 'weekly' | 'biweekly' | 'monthly'.
// monthlyWeek: 1-5 (qual semana do mês). diaSemana: 0-6 (Seg-Dom).
window.RECORRENCIAS_INICIAIS = [
  // Toda 1ª terça do mês, 7h-19h, HBDF
  // { id: 'r1', kind: 'monthly', monthlyWeek: 1, diaSemana: 1, horaInicio: 7, duracao: 12, hospitalId: 2, ate: '2026-12-31' },
];

// Expande recorrências em blocos pro intervalo [iniISO, fimISO]
window.expandirRecorrencias = (recs, iniISO, fimISO) => {
  if (!recs || recs.length === 0) return [];
  const out = [];
  let nextId = 100000;
  const ini = window.fromISO(iniISO);
  const fim = window.fromISO(fimISO);
  for (const r of recs) {
    const limite = r.ate ? window.fromISO(r.ate) : fim;
    const fimReal = limite < fim ? limite : fim;
    const exc = new Set(r.exceptions || []);
    const desde = r.desde ? window.fromISO(r.desde) : null;
    const pushOcc = (d) => {
      const iso = window.toISO(d);
      if (exc.has(iso)) return;
      if (desde && d < desde) return;
      out.push({
        id: `${r.id}-${iso}`,
        tipo: 'plantao',
        hospitalId: r.hospitalId,
        data: iso,
        horaInicio: r.horaInicio,
        duracao: r.duracao,
        fromRecorrencia: r.id,
      });
    };
    if (r.kind === 'weekly' || r.kind === 'biweekly') {
      let d = new Date(ini);
      const step = r.kind === 'weekly' ? 7 : 14;
      while (((d.getDay()+6)%7) !== r.diaSemana && d <= fimReal) {
        d.setDate(d.getDate()+1);
      }
      while (d <= fimReal) {
        pushOcc(d);
        d.setDate(d.getDate() + step);
      }
    } else if (r.kind === 'monthly') {
      const startMonth = new Date(ini.getFullYear(), ini.getMonth(), 1);
      const endMonth = new Date(fimReal.getFullYear(), fimReal.getMonth(), 1);
      let m = new Date(startMonth);
      while (m <= endMonth) {
        const primeiroDoMes = new Date(m.getFullYear(), m.getMonth(), 1);
        const dowPrim = (primeiroDoMes.getDay()+6)%7;
        const offset = (r.diaSemana - dowPrim + 7) % 7;
        const dia = 1 + offset + (r.monthlyWeek - 1) * 7;
        const d = new Date(m.getFullYear(), m.getMonth(), dia);
        if (d.getMonth() === m.getMonth() && d >= ini && d <= fimReal) {
          pushOcc(d);
        }
        m.setMonth(m.getMonth()+1);
      }
    }
  }
  return out;
};

// Texto curto descrevendo a recorrência ("Toda terça", "Quinzenal · sex", "1ª terça do mês")
window.descreverRecorrencia = (r) => {
  if (!r) return '';
  const dia = window.DIAS_COMPLETO[r.diaSemana] || '';
  const ord = ['1ª', '2ª', '3ª', '4ª', '5ª'][(r.monthlyWeek || 1) - 1];
  if (r.kind === 'weekly')   return `Toda ${dia.toLowerCase()}`;
  if (r.kind === 'biweekly') return `A cada 2 ${dia.toLowerCase()}s`;
  if (r.kind === 'monthly')  return `${ord} ${dia.toLowerCase()} do mês`;
  return '';
};

// ---- Deslocamentos (cálculo automático) -----------------------------

// Distância em km entre dois pontos (lat, lng) — fórmula de Haversine
window.haversineKm = (a, b) => {
  if (!a || !b || a.lat == null || b.lat == null) return null;
  const R = 6371;
  const toRad = (x) => x * Math.PI / 180;
  const dLat = toRad(b.lat - a.lat);
  const dLng = toRad(b.lng - a.lng);
  const lat1 = toRad(a.lat);
  const lat2 = toRad(b.lat);
  const sinDLat = Math.sin(dLat / 2);
  const sinDLng = Math.sin(dLng / 2);
  const A = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLng * sinDLng;
  return 2 * R * Math.atan2(Math.sqrt(A), Math.sqrt(1 - A));
};

// Multiplicador de trânsito por horário em Brasília
// horaDecimal: 0..24 (suporta 7.5 = 7h30)
// dataISO: YYYY-MM-DD pra checar dia da semana
window.multiplicadorTransito = (horaDecimal, dataISO) => {
  const h = ((horaDecimal % 24) + 24) % 24;
  const dow = window.diaSemanaBR(dataISO); // 0=Seg ... 6=Dom
  const ehFds = dow >= 5;
  let mult;
  if (h < 5)       mult = 0.7;   // madrugada — vazio
  else if (h < 7)  mult = 0.85;
  else if (h < 9)  mult = 1.5;   // rush manhã
  else if (h < 12) mult = 1.0;
  else if (h < 14) mult = 1.15;  // almoço
  else if (h < 17) mult = 1.0;
  else if (h < 19) mult = 1.5;   // rush tarde
  else if (h < 22) mult = 1.1;
  else             mult = 0.8;
  if (ehFds) mult *= 0.75;       // fim-de-semana, Brasília esvazia
  return mult;
};

// Tempo em minutos entre dois hospitais, ajustado pelo trânsito
window.tempoEntreHospitaisMin = (h1, h2, horaInicio, dataISO) => {
  if (!h1 || !h2) return null;
  if (h1.lat == null || h2.lat == null) {
    // Fallback: média dos deslocCasa (chute grosso)
    const a = h1.deslocCasa, b = h2.deslocCasa;
    if (a == null || b == null) return null;
    const base = Math.max(15, Math.round((a + b) / 2 * 0.6));
    return Math.round(base * window.multiplicadorTransito(horaInicio, dataISO));
  }
  const km = window.haversineKm(h1, h2) * 1.4; // routing factor urbano
  const speedKmh = 30;
  const baseMin = (km / speedKmh) * 60;
  const mult = window.multiplicadorTransito(horaInicio, dataISO);
  return Math.max(5, Math.min(90, Math.round(baseMin * mult)));
};

// Tempo em minutos entre casa e um hospital, ajustado pelo trânsito
// Usa hospital.deslocCasa como baseline (já está em min, condições normais)
window.tempoCasaHospitalMin = (h, horaInicio, dataISO) => {
  if (!h) return null;
  if (h.deslocCasa != null) {
    const mult = window.multiplicadorTransito(horaInicio, dataISO);
    return Math.max(5, Math.min(90, Math.round(h.deslocCasa * mult)));
  }
  // Fallback: usa lat/lng se houver
  const casa = window.CASA;
  if (!casa || !h.lat) return null;
  const km = window.haversineKm(casa, h) * 1.4;
  const baseMin = (km / 30) * 60;
  const mult = window.multiplicadorTransito(horaInicio, dataISO);
  return Math.max(5, Math.min(90, Math.round(baseMin * mult)));
};

// Converte minutos absolutos (desde âncora 2020-01-01) → { data, horaInicio }
const _ANCORA_DESL = '2020-01-01';
const _minAbsParaDataHora = (minAbs) => {
  const diasDesde = Math.floor(minAbs / (24 * 60));
  const minDoDia = minAbs - diasDesde * 24 * 60;
  const horaInicio = minDoDia / 60;
  const d = window.fromISO(_ANCORA_DESL);
  d.setDate(d.getDate() + diasDesde);
  return { data: window.toISO(d), horaInicio };
};

// Gera deslocamentos automáticos. Idempotente: limpa os auto antigos e re-gera.
// Preserva deslocamentos manuais (sem flag auto).
//
// Regras:
//  - Antes de cada plantão: insere deslocamento da origem (casa ou hospital anterior)
//  - Origem = casa se não há plantão anterior próximo (gap >= homeThresholdH)
//  - Origem = hospital anterior se gap < homeThresholdH e hospital diferente
//  - Sem deslocamento se mesmo hospital
//  - Depois do último plantão de cada "sequência" (run de plantões com gap < home),
//    insere retorno pra casa
window.gerarDeslocamentosAuto = (blocos, hospitais, opcoes = {}) => {
  const HOME_THRESHOLD_H = opcoes.homeThresholdH ?? 4;
  // Remove auto antigos, preserva manuais
  const limpos = blocos.filter(b => !(b.tipo === 'deslocamento' && b.auto));
  const plantoes = limpos
    .filter(b => b.tipo === 'plantao')
    .map(b => ({ ...b, _ini: window.intervaloMin(b)[0], _fim: window.intervaloMin(b)[1] }))
    .sort((a, b) => a._ini - b._ini);

  if (plantoes.length === 0) return limpos;

  const deslocs = [];
  let nextId = Math.max(0, ...limpos.map(b => typeof b.id === 'number' ? b.id : 0)) + 1;

  // ---- Deslocamentos antes de cada plantão (chegada) -----------------
  for (let i = 0; i < plantoes.length; i++) {
    const p = plantoes[i];
    const hosp = hospitais.find(h => h.id === p.hospitalId);
    if (!hosp) continue;
    const prev = plantoes[i - 1];
    const gapMin = prev ? p._ini - prev._fim : Infinity;
    const gapH = gapMin / 60;

    let origem; // 'casa' | { hospitalId } | null
    if (!prev || gapH >= HOME_THRESHOLD_H) {
      origem = 'casa';
    } else if (prev.hospitalId !== p.hospitalId) {
      origem = { hospitalId: prev.hospitalId };
    } else {
      origem = null;
    }
    if (!origem) continue;

    let tempoMin;
    if (origem === 'casa') {
      tempoMin = window.tempoCasaHospitalMin(hosp, p.horaInicio, p.data);
    } else {
      const hOrig = hospitais.find(h => h.id === origem.hospitalId);
      tempoMin = window.tempoEntreHospitaisMin(hOrig, hosp, p.horaInicio, p.data);
    }
    if (!tempoMin || tempoMin <= 0) continue;
    // Não gerar deslocamento maior que o gap disponível
    if (prev && tempoMin > gapMin) tempoMin = Math.max(5, gapMin);

    const { data, horaInicio } = _minAbsParaDataHora(p._ini - tempoMin);
    deslocs.push({
      id: nextId++,
      tipo: 'deslocamento',
      hospitalDestinoId: p.hospitalId,
      hospitalOrigemId: origem === 'casa' ? null : origem.hospitalId,
      deCasa: origem === 'casa',
      paraCasa: false,
      data,
      horaInicio,
      duracao: tempoMin / 60,
      auto: true,
    });
  }

  // ---- Retornos pra casa: depois do último de cada sequência ---------
  let i = 0;
  while (i < plantoes.length) {
    let j = i;
    while (j + 1 < plantoes.length && (plantoes[j+1]._ini - plantoes[j]._fim) / 60 < HOME_THRESHOLD_H) {
      j++;
    }
    const ultimo = plantoes[j];
    const proximo = plantoes[j + 1];
    const gapApos = proximo ? (proximo._ini - ultimo._fim) / 60 : Infinity;
    if (gapApos >= HOME_THRESHOLD_H) {
      const hUlt = hospitais.find(h => h.id === ultimo.hospitalId);
      if (hUlt) {
        const horaFim = ((ultimo.horaInicio + ultimo.duracao) % 24 + 24) % 24;
        const tempoMin = window.tempoCasaHospitalMin(hUlt, horaFim, ultimo.data);
        if (tempoMin && tempoMin > 0) {
          const { data, horaInicio } = _minAbsParaDataHora(ultimo._fim);
          deslocs.push({
            id: nextId++,
            tipo: 'deslocamento',
            hospitalOrigemId: ultimo.hospitalId,
            hospitalDestinoId: null,
            paraCasa: true,
            deCasa: false,
            data,
            horaInicio,
            duracao: tempoMin / 60,
            auto: true,
          });
        }
      }
    }
    i = j + 1;
  }

  return [...limpos, ...deslocs];
};

// ---- Helpers de filtro ---------------------------------------------
window.blocosNaSemana = (todos, isoSeg) => {
  const fim = window.addDias(isoSeg, 6);
  return todos.filter(b => b.data >= isoSeg && b.data <= fim);
};
window.blocosNoMes = (todos, ano, mes) => {
  const ini = window.toISO(new Date(ano, mes, 1));
  const fim = window.toISO(new Date(ano, mes+1, 0));
  return todos.filter(b => b.data >= ini && b.data <= fim);
};
window.blocosNoAno = (todos, ano) => {
  const ini = `${ano}-01-01`;
  const fim = `${ano}-12-31`;
  return todos.filter(b => b.data >= ini && b.data <= fim);
};

window.getHospital = (id, lista) => {
  const arr = lista || window.HOSPITAIS;
  return arr.find(h => h.id === id) || arr[0];
};

// ---- Carga ----------------------------------------------------------
window.classifyCarga = (h) => {
  if (h < 48) return { zone: 'ok', label: 'Carga saudável', sub: 'Você está dentro de um ritmo sustentável.' };
  if (h < 60) return { zone: 'warn', label: 'Atenção', sub: 'Sua semana está apertada — proteja seu descanso onde puder.' };
  return { zone: 'err', label: 'Pesada', sub: 'Sua semana está pesada. Considere proteger um descanso.' };
};

// Cadeias contínuas — adapta para datas. Converte cada bloco para minuto
// "global" desde uma âncora antiga.
window.calcCadeias = (blocos) => {
  if (!blocos || blocos.length === 0) return [];
  const ANCORA = '2020-01-01';
  const trab = blocos
    .filter(b => b.tipo === 'plantao' || b.tipo === 'deslocamento')
    .map(b => {
      const dias = Math.round((window.fromISO(b.data) - window.fromISO(ANCORA)) / 86400000);
      const ini = dias * 24 * 60 + b.horaInicio * 60;
      return { ...b, iniMin: ini, fimMin: ini + b.duracao * 60 };
    })
    .sort((a, b) => a.iniMin - b.iniMin);

  const cadeias = [];
  let atual = null;
  for (const b of trab) {
    if (!atual) { atual = { iniMin: b.iniMin, fimMin: b.fimMin, blocos: [b] }; continue; }
    const gap = b.iniMin - atual.fimMin;
    if (gap < 6 * 60) {
      atual.fimMin = Math.max(atual.fimMin, b.fimMin);
      atual.blocos.push(b);
    } else {
      if ((atual.fimMin - atual.iniMin) > 24 * 60) cadeias.push(atual);
      atual = { iniMin: b.iniMin, fimMin: b.fimMin, blocos: [b] };
    }
  }
  if (atual && (atual.fimMin - atual.iniMin) > 24 * 60) cadeias.push(atual);
  return cadeias.map(c => ({
    ...c,
    horas: (c.fimMin - c.iniMin) / 60,
    data: c.blocos[0].data,
  }));
};

window.calcCargaTotal = (blocos) =>
  blocos
    .filter(b => b.tipo === 'plantao' || b.tipo === 'deslocamento')
    .reduce((acc, b) => acc + b.duracao, 0);

window.fmtHora = (h) => {
  const horas = Math.floor(h);
  const mins = Math.round((h - horas) * 60);
  return `${String(horas % 24).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
};

// ---- Conflitos ------------------------------------------------------
// Retorna intervalo do bloco em "minutos absolutos" desde âncora 2020-01-01
window.intervaloMin = (b) => {
  const ANCORA = '2020-01-01';
  const dias = Math.round((window.fromISO(b.data) - window.fromISO(ANCORA)) / 86400000);
  const ini = dias * 24 * 60 + b.horaInicio * 60;
  return [ini, ini + b.duracao * 60];
};

// Detecta TODOS os grupos de conflito existentes nos blocos.
// Retorna: array de grupos. Cada grupo = array de 2+ blocos que se sobrepõem temporalmente.
// Plantões/sonos/bloqueios contam; deslocamentos auto, cedidos e trocados são ignorados.
window.detectarTodosConflitos = (blocos) => {
  const ativos = (blocos || []).filter(b =>
    (b.tipo === 'plantao' || b.tipo === 'sono' || b.tipo === 'bloqueio')
  );
  const grupos = [];
  const grupoDe = new Map(); // bloco.id → grupo

  for (let i = 0; i < ativos.length; i++) {
    const a = ativos[i];
    const [aIni, aFim] = window.intervaloMin(a);
    for (let j = i + 1; j < ativos.length; j++) {
      const b = ativos[j];
      const [bIni, bFim] = window.intervaloMin(b);
      if (aIni < bFim && bIni < aFim) {
        // sobreposição
        const ga = grupoDe.get(a.id);
        const gb = grupoDe.get(b.id);
        if (ga && gb && ga !== gb) {
          // unir os dois grupos
          for (const x of gb) { ga.push(x); grupoDe.set(x.id, ga); }
          const idx = grupos.indexOf(gb); if (idx >= 0) grupos.splice(idx, 1);
        } else if (ga) {
          if (!ga.find(x => x.id === b.id)) { ga.push(b); grupoDe.set(b.id, ga); }
        } else if (gb) {
          if (!gb.find(x => x.id === a.id)) { gb.push(a); grupoDe.set(a.id, gb); }
        } else {
          const novo = [a, b];
          grupos.push(novo);
          grupoDe.set(a.id, novo);
          grupoDe.set(b.id, novo);
        }
      }
    }
  }
  // Ordena cada grupo por horaInicio + data
  for (const g of grupos) {
    g.sort((x, y) => window.intervaloMin(x)[0] - window.intervaloMin(y)[0]);
  }
  // Ordena grupos pela data do primeiro bloco
  grupos.sort((a, b) => window.intervaloMin(a[0])[0] - window.intervaloMin(b[0])[0]);
  return grupos;
};

// Conta conflitos PENDENTES (grupos onde nem todos foram aceitos manualmente)
window.contarConflitosPendentes = (blocos) => {
  const grupos = window.detectarTodosConflitos(blocos);
  return grupos.filter(g => !g.every(b => b.aceitouConflito)).length;
};

// IDs dos blocos que estão em algum conflito ativo (pra outline na grade)
window.idsBlocosEmConflito = (blocos) => {
  const grupos = window.detectarTodosConflitos(blocos);
  const ids = new Set();
  for (const g of grupos) {
    const aceito = g.every(b => b.aceitouConflito);
    for (const b of g) ids.add(`${b.id}:${aceito ? 'aceito' : 'pendente'}`);
  }
  return ids;
};

// Detectar sobreposições com outros plantões/deslocamentos (excluindo blocos cedidos/trocados)
window.detectarConflitos = (novo, blocos, ignoreId) => {
  if (!['plantao','deslocamento','sono','bloqueio'].includes(novo.tipo)) return [];
  const [iniNovo, fimNovo] = window.intervaloMin(novo);
  return blocos.filter(b => {
    if (b.id === ignoreId) return false;
    if (b.tipo === 'cedido' || b.tipo === 'trocado') return false;
    const [ini, fim] = window.intervaloMin(b);
    return ini < fimNovo && fim > iniNovo;
  });
};

// Calcular cadeia projetada caso adicionemos `novo`
window.cadeiaSeAdicionar = (novo, blocos) => {
  const proj = [...blocos.filter(b => b.id !== novo.id), novo];
  const cadeias = window.calcCadeias(proj);
  // Achar a cadeia que contém o novo bloco
  const [iniNovo] = window.intervaloMin(novo);
  return cadeias.find(c => iniNovo >= c.iniMin && iniNovo < c.fimMin);
};

// ---- Estatísticas reais --------------------------------------------
// Para uma janela [iniISO, fimISO], calcula carga total e cadeia max por semana
window.estatsPorSemana = (blocos, semanasAtras = 8, refISO) => {
  const ref = refISO || window.HOJE_ISO;
  const fimSemAtual = window.inicioSemana(ref);
  const out = [];
  for (let i = semanasAtras - 1; i >= 0; i--) {
    const semIni = window.addDias(fimSemAtual, -i * 7);
    const blocosSem = window.blocosNaSemana(blocos, semIni);
    const total = window.calcCargaTotal(blocosSem);
    const cadeias = window.calcCadeias(blocosSem);
    const maiorCadeia = cadeias.reduce((m, c) => Math.max(m, c.horas), 0);
    out.push({
      semanaInicio: semIni,
      total, maiorCadeia,
      label: window.fmtIntervaloSemana(semIni).split(' · ')[0],
      isCurrent: i === 0,
    });
  }
  return out;
};

// Domingos completos no mês (sem plantão/deslocamento)
window.domingosLivresMes = (blocos, ano, mes) => {
  let livres = 0;
  const ultimo = new Date(ano, mes+1, 0).getDate();
  for (let d = 1; d <= ultimo; d++) {
    const dt = new Date(ano, mes, d);
    if ((dt.getDay() + 6) % 7 === 6) {
      const iso = window.toISO(dt);
      const trab = blocos.some(b => b.data === iso && (b.tipo === 'plantao' || b.tipo === 'deslocamento'));
      if (!trab) livres++;
    }
  }
  return livres;
};

// ---- ICS (Google/Apple Calendar) ----------------------------------

// Escapa string pra ICS: vírgulas, ponto-e-vírgulas, barras invertidas e quebras de linha
const _icsEscape = (s) => String(s || '')
  .replace(/\\/g, '\\\\')
  .replace(/;/g, '\\;')
  .replace(/,/g, '\\,')
  .replace(/\r?\n/g, '\\n');

// Formata Date como YYYYMMDDTHHMMSS (sem TZ — usado com TZID separado)
const _icsDateLocal = (d) => {
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}T${pad(d.getHours())}${pad(d.getMinutes())}00`;
};

// Formata Date como YYYYMMDDTHHMMSSZ (UTC) — pra DTSTAMP
const _icsDateUTC = (d) => {
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getUTCFullYear()}${pad(d.getUTCMonth()+1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
};

// Constrói Date local a partir de data ISO (YYYY-MM-DD) + hora decimal
const _dataMaisHora = (dataISO, horaDecimal) => {
  const [y, m, d] = dataISO.split('-').map(Number);
  const horas = Math.floor(horaDecimal);
  const mins = Math.round((horaDecimal - horas) * 60);
  return new Date(y, m-1, d, horas, mins, 0, 0);
};

// Gera string .ics com plantões, sonos e bloqueios. Pula deslocamentos auto e cessões/trocas
// (ruído visual no calendário). UID estável por bloco — re-importar atualiza por UID.
window.gerarICS = (blocos, hospitais) => {
  const HOSPS = hospitais || window.HOSPITAIS;
  const exportaveis = (blocos || []).filter(b =>
    b.tipo === 'plantao' || b.tipo === 'sono' || b.tipo === 'bloqueio'
  );

  const linhas = [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    'PRODID:-//Colo Ritmo//Colo Pediatria//PT-BR',
    'CALSCALE:GREGORIAN',
    'METHOD:PUBLISH',
    'NAME:Colo Ritmo · Plantões',
    'X-WR-CALNAME:Colo Ritmo · Plantões',
    'X-WR-TIMEZONE:America/Sao_Paulo',
    'X-WR-CALDESC:Plantões e bloqueios sincronizados do Colo Ritmo',
    // Definição da timezone Brasília (sem horário de verão)
    'BEGIN:VTIMEZONE',
    'TZID:America/Sao_Paulo',
    'BEGIN:STANDARD',
    'DTSTART:19700101T000000',
    'TZOFFSETFROM:-0300',
    'TZOFFSETTO:-0300',
    'TZNAME:BRT',
    'END:STANDARD',
    'END:VTIMEZONE',
  ];

  const agora = _icsDateUTC(new Date());

  for (const b of exportaveis) {
    const inicio = _dataMaisHora(b.data, b.horaInicio);
    const fim = new Date(inicio.getTime() + b.duracao * 60 * 60 * 1000);
    let summary, description, location, categoria;

    if (b.tipo === 'plantao') {
      const h = window.getHospital(b.hospitalId, HOSPS);
      summary = `${h.abrev} · Plantão`;
      const linhasDesc = [
        `${h.nome}`,
        `${b.duracao}h (${window.fmtHora(b.horaInicio)} — ${window.fmtHora((b.horaInicio + b.duracao) % 24)})`,
      ];
      if (b.viaTroca) linhasDesc.push(`Recebido em troca: ${b.trocaCom || ''}`);
      if (b.observacao) linhasDesc.push(b.observacao);
      description = linhasDesc.join('\n');
      location = h.endereco || '';
      categoria = 'Plantão';
    } else if (b.tipo === 'sono') {
      summary = 'Sono protegido';
      description = `Janela de descanso planejada · ${b.duracao}h`;
      location = '';
      categoria = 'Descanso';
    } else {
      summary = b.motivo ? `Bloqueio · ${b.motivo}` : 'Bloqueio';
      description = `Indisponível para plantão · ${b.duracao}h${b.motivo ? '\n' + b.motivo : ''}`;
      location = '';
      categoria = 'Bloqueio';
    }

    linhas.push(
      'BEGIN:VEVENT',
      `UID:${b.tipo}-${b.id}@coloritmo.app`,
      `DTSTAMP:${agora}`,
      `DTSTART;TZID=America/Sao_Paulo:${_icsDateLocal(inicio)}`,
      `DTEND;TZID=America/Sao_Paulo:${_icsDateLocal(fim)}`,
      `SUMMARY:${_icsEscape(summary)}`,
      `DESCRIPTION:${_icsEscape(description)}`,
      ...(location ? [`LOCATION:${_icsEscape(location)}`] : []),
      `CATEGORIES:${categoria}`,
      'END:VEVENT',
    );
  }

  linhas.push('END:VCALENDAR');
  // ICS exige CRLF como line separator
  return linhas.join('\r\n') + '\r\n';
};

// Faz parse simplificado de .ics → array de eventos { uid, inicio, fim, summary, description, location }
// Suporta DTSTART/DTEND com TZID, sem TZ (floating), e Z (UTC). Não suporta RRULE/recorrência.
window.parsearICS = (texto) => {
  // ICS line unfolding: linhas que começam com espaço/tab são continuação da anterior
  const cru = String(texto || '').replace(/\r\n[ \t]/g, '').replace(/\n[ \t]/g, '');
  const linhas = cru.split(/\r?\n/);
  const eventos = [];
  let atual = null;

  const parseDateTime = (valor, params) => {
    // valor: "20260427T190000" ou "20260427T220000Z" ou "20260427"
    if (!valor) return null;
    if (valor.length === 8) {
      // Data sem hora → meia-noite local
      const y = +valor.slice(0,4), m = +valor.slice(4,6), d = +valor.slice(6,8);
      return new Date(y, m-1, d);
    }
    const y = +valor.slice(0,4), mo = +valor.slice(4,6), d = +valor.slice(6,8);
    const hh = +valor.slice(9,11), mm = +valor.slice(11,13), ss = +valor.slice(13,15) || 0;
    if (valor.endsWith('Z')) {
      return new Date(Date.UTC(y, mo-1, d, hh, mm, ss));
    }
    // TZID ou floating → trata como local. Pra simplicidade, ignora TZID (assume Brasília).
    return new Date(y, mo-1, d, hh, mm, ss);
  };

  const unescape = (s) => String(s || '')
    .replace(/\\n/g, '\n').replace(/\\,/g, ',').replace(/\\;/g, ';').replace(/\\\\/g, '\\');

  for (const linha of linhas) {
    if (linha === 'BEGIN:VEVENT') {
      atual = {};
    } else if (linha === 'END:VEVENT') {
      if (atual && atual.inicio && atual.fim) eventos.push(atual);
      atual = null;
    } else if (atual) {
      const m = linha.match(/^([A-Z][A-Z0-9-]*)((?:;[^:]*)?):(.*)$/);
      if (!m) continue;
      const [, prop, paramsRaw, valor] = m;
      const params = {};
      if (paramsRaw) {
        for (const p of paramsRaw.slice(1).split(';')) {
          const [k, v] = p.split('=');
          if (k) params[k.toUpperCase()] = v;
        }
      }
      switch (prop) {
        case 'UID': atual.uid = valor; break;
        case 'SUMMARY': atual.summary = unescape(valor); break;
        case 'DESCRIPTION': atual.description = unescape(valor); break;
        case 'LOCATION': atual.location = unescape(valor); break;
        case 'CATEGORIES': atual.categories = valor; break;
        case 'DTSTART': atual.inicio = parseDateTime(valor, params); atual.allDay = valor.length === 8; break;
        case 'DTEND': atual.fim = parseDateTime(valor, params); break;
        case 'RRULE': atual.hasRRule = true; break; // marca mas não expande
      }
    }
  }
  return eventos;
};

// Converte eventos parseados de ICS em "candidatos a bloqueio" pro app.
// - Pula eventos > 24h (provável férias/viagem) — viram um bloqueio único do dia inteiro
// - Pula eventos com UID começando com `plantao-` ou `sono-` ou `bloqueio-` (vieram do próprio app)
// - Eventos all-day viram bloqueios das 0h por 24h
window.eventosICSParaBlocos = (eventos) => {
  return (eventos || []).map((ev, idx) => {
    if (ev.uid && /^(plantao|sono|bloqueio)-\d+@coloritmo/.test(ev.uid)) return null;
    if (!ev.inicio || !ev.fim) return null;
    const dataISO = window.toISO(ev.inicio);
    let horaInicio = ev.inicio.getHours() + ev.inicio.getMinutes()/60;
    let duracaoH = (ev.fim - ev.inicio) / (60*60*1000);
    if (ev.allDay) { horaInicio = 0; duracaoH = 24; }
    if (duracaoH <= 0) duracaoH = 1;
    if (duracaoH > 24) duracaoH = 24; // bloqueio máximo 24h por bloco
    return {
      idx,
      original: ev,
      bloco: {
        tipo: 'bloqueio',
        data: dataISO,
        horaInicio: Math.round(horaInicio * 4) / 4, // arredonda pra 15min
        duracao: Math.round(duracaoH * 4) / 4,
        motivo: ev.summary || 'Evento do calendário',
      },
    };
  }).filter(Boolean);
};

// ---- Persistência (localStorage como cache + Cloudflare KV como fonte) ---
// v2 (maio/2026): bumpada após troca de BLOCOS_INICIAIS pra escalas reais
// e migração de localStorage-only pra cloud sync via Cloudflare Workers.
const STORAGE_KEY = 'colo-ritmo-v2';
try {
  if (typeof localStorage !== 'undefined') {
    localStorage.removeItem('colo-ritmo-v1');
    localStorage.removeItem('plantao-claro-v2');
    localStorage.removeItem('plantao-claro-casa');
  }
} catch (_) {}

// ---- Supabase (Auth + Postgres com RLS) -----------------------------
window.SUPABASE_URL = 'https://xlefxpcmruhuyexdvzru.supabase.co';
window.SUPABASE_KEY = 'sb_publishable_lrEzOdS4RnrwsDmCUsXEuQ_ElUajQ3W';

// Cliente único (carregado depois do supabase-js via CDN)
window.criarSupabase = () => {
  if (window._supabaseClient) return window._supabaseClient;
  if (!window.supabase) throw new Error('supabase-js não carregou');
  window._supabaseClient = window.supabase.createClient(
    window.SUPABASE_URL,
    window.SUPABASE_KEY,
    {
      auth: {
        persistSession: true,
        autoRefreshToken: true,
        detectSessionInUrl: true,
        // Implicit flow (não PKCE) pra magic link funcionar cross-device:
        // Mariana digita email no desktop, abre link no celular, login estabelece
        // no celular sem precisar de code_verifier que estaria só no desktop.
        // Tradeoff: tokens vão no fragment URL (não query string), seguro o suficiente
        // pra esse contexto pessoal. PKCE seria pra fluxos OAuth com servidor.
        flowType: 'implicit',
        storage: window.localStorage,
        storageKey: 'colo-ritmo-auth',
      },
    },
  );
  return window._supabaseClient;
};

// Login com magic link (envia email)
window.loginMagicLink = async (email) => {
  const sb = window.criarSupabase();
  return await sb.auth.signInWithOtp({
    email,
    options: { emailRedirectTo: window.location.origin },
  });
};

// Logout
window.logout = async () => {
  const sb = window.criarSupabase();
  await sb.auth.signOut();
  // Limpa cache local pra evitar exibir dado de outra usuária
  try { localStorage.removeItem(STORAGE_KEY); } catch (_) {}
};

// =====================================================================
// Admin & roles (depende do schema supabase-admin.sql)
// =====================================================================

// Carrega perfil da usuária logada (role, nome, ics_token)
window.carregarMeuPerfil = async () => {
  const sb = window.criarSupabase();
  try {
    const { data: { user } } = await sb.auth.getUser();
    if (!user) return null;
    const { data, error } = await sb.from('user_profiles')
      .select('user_id, role, nome, ics_token, created_at')
      .eq('user_id', user.id)
      .maybeSingle();
    if (error) throw error;
    return data;
  } catch (e) {
    console.warn('[colo-ritmo] carregarMeuPerfil falhou:', e.message);
    return null;
  }
};

// Lista todos os médicos + admin (só quem é admin enxerga via RLS)
window.listarMedicos = async () => {
  const sb = window.criarSupabase();
  try {
    const { data: profiles, error } = await sb.from('user_profiles')
      .select('user_id, role, nome, created_at')
      .order('role', { ascending: false })
      .order('created_at', { ascending: true });
    if (error) throw error;
    // Pega emails dos usuários via auth.users (não temos acesso direto via RLS;
    // workaround: armazenar email no perfil futuramente. Por agora retorna só profiles)
    return { ok: true, medicos: profiles || [] };
  } catch (e) {
    return { ok: false, medicos: [], erro: e.message };
  }
};

// Atualiza role de um user (admin only — RLS bloqueia se não for admin)
window.atualizarRole = async (userId, novaRole) => {
  const sb = window.criarSupabase();
  try {
    const { error } = await sb.from('user_profiles')
      .update({ role: novaRole })
      .eq('user_id', userId);
    if (error) throw error;
    return { ok: true };
  } catch (e) {
    return { ok: false, erro: e.message };
  }
};

// Atualiza nome do perfil (qualquer user pode mudar o próprio; admin pode mudar de qualquer)
window.atualizarNomePerfil = async (userId, nome) => {
  const sb = window.criarSupabase();
  try {
    const { error } = await sb.from('user_profiles')
      .update({ nome })
      .eq('user_id', userId);
    if (error) throw error;
    return { ok: true };
  } catch (e) {
    return { ok: false, erro: e.message };
  }
};

// Carrega estado de um médico específico (admin only via RLS)
window.carregarEstadoDeUser = async (userId) => {
  const sb = window.criarSupabase();
  try {
    const { data, error } = await sb.from('user_state')
      .select('state, updated_at')
      .eq('user_id', userId)
      .maybeSingle();
    if (error) throw error;
    return { ok: true, estado: data?.state || null, updated_at: data?.updated_at };
  } catch (e) {
    return { ok: false, erro: e.message };
  }
};

// Reenviar magic link (qualquer email cadastrado pode receber novo link)
window.dispararMagicLink = async (email) => {
  const sb = window.criarSupabase();
  try {
    const { error } = await sb.auth.signInWithOtp({
      email,
      options: { emailRedirectTo: window.location.origin },
    });
    if (error) throw error;
    return { ok: true };
  } catch (e) {
    return { ok: false, erro: e.message };
  }
};

// Backup JSON do estado de um user (formato completo do user_state.state)
window.baixarBackupDeUser = async (userId, label) => {
  const r = await window.carregarEstadoDeUser(userId);
  if (!r.ok || !r.estado) return { ok: false, erro: r.erro || 'sem dados' };
  const blob = new Blob([JSON.stringify(r.estado, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  const safeName = (label || userId).toString().replace(/[^a-z0-9.-]/gi, '_');
  a.download = `colo-ritmo-backup-${safeName}-${new Date().toISOString().slice(0,10)}.json`;
  document.body.appendChild(a);
  a.click();
  setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100);
  return { ok: true };
};

// Carrega o estado do user logado (com fallback pra localStorage cache se offline)
window.carregarEstadoSupabase = async () => {
  const sb = window.criarSupabase();
  try {
    const { data, error } = await sb.from('user_state').select('state').maybeSingle();
    if (error) throw error;
    if (!data) return { _origem: 'novo', blocos: null, hospitais: null, recorrencias: [], casa: null };
    return { ...(data.state || {}), _origem: 'cloud' };
  } catch (e) {
    console.warn('[colo-ritmo] Supabase GET falhou, usando cache:', e.message || e);
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (raw) return { ...JSON.parse(raw), _origem: 'cache' };
    } catch (_) {}
    return { _origem: 'erro', blocos: null, hospitais: null, recorrencias: [], casa: null, erro: e.message };
  }
};

// Salva via upsert no Postgres (RLS garante que só salva o próprio row)
window.salvarEstadoSupabase = async (estado) => {
  const sb = window.criarSupabase();
  const payload = {
    schemaVersion: 1,
    savedAt: new Date().toISOString(),
    ...estado,
  };
  // Cache local sempre primeiro
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch (_) {}
  try {
    const { data: { user } } = await sb.auth.getUser();
    if (!user) throw new Error('sem usuária autenticada');
    const { error } = await sb.from('user_state')
      .upsert({ user_id: user.id, state: payload }, { onConflict: 'user_id' });
    if (error) throw error;
    return { ok: true, status: 'cloud' };
  } catch (e) {
    console.warn('[colo-ritmo] Supabase PUT falhou (cache OK):', e.message || e);
    return { ok: false, status: 'offline', erro: e.message };
  }
};
// =====================================================================
// Solver de simulação de escala — feature "Fazer escala"
// =====================================================================
//
// Algoritmo gulose com 3 variantes de scoring. Para cada variante:
//   1. Gera candidatos (dia × hospital × turno) viáveis
//   2. Pontua candidatos segundo a estratégia
//   3. Seleciona em ordem decrescente, respeitando hard constraints
//   4. Garante mínimos por hospital (preenche se faltar, com penalty)
//   5. Calcula score detalhado e violações
//
// Hard constraints (nunca violadas):
//   - Não conflita com agenda real (plantões já existentes)
//   - Não usa dia em `preferencias.bloqueios`
//   - Respeita `intervaloMinH` entre 2 plantões consecutivos
//   - Não cria cadeia > `preferencias.cadeiaMaxH` (default 24)
//   - Respeita `regrasEscala.diasIndisponiveis` por hospital
//   - Só usa turnos em `regrasEscala.turnosPermitidos`
//   - Não excede `plantoesMax` ou `horasMax` por hospital
//
// Soft constraints (geram penalty/score):
//   - Mínimos: plantoesMin, fdsMin, horasMin
//   - prefereLivre (dias da semana que ela quer folga)
//   - qualidadeRequerida: # de janelas livres >=16h

// Helpers internos -----------------------------------------------------
const _diasDoMes = (mesAlvo /* "YYYY-MM" */) => {
  const [y, m] = mesAlvo.split('-').map(Number);
  const total = new Date(y, m, 0).getDate();
  const dias = [];
  for (let d = 1; d <= total; d++) {
    dias.push(window.toISO(new Date(y, m - 1, d)));
  }
  return dias;
};
const _ehFimDeSemana = (iso) => {
  const dow = window.diaSemanaBR(iso);
  return dow === 5 || dow === 6;
};

// Gera todos os candidatos viáveis (não viola hard constraints).
const _gerarCandidatos = (hospitais, preferencias, mesAlvo, agendaReal) => {
  const dias = _diasDoMes(mesAlvo);
  const bloqueios = new Set(preferencias?.bloqueios || []);
  const candidatos = [];
  for (const dataISO of dias) {
    if (bloqueios.has(dataISO)) continue;
    const dow = window.diaSemanaBR(dataISO);
    for (const h of hospitais) {
      const r = h.regrasEscala;
      if (!r?.ativo) continue;
      if ((r.diasIndisponiveis || []).includes(dow)) continue;
      const turnosOk = r.turnosPermitidos || Object.keys(h.turnos || {});
      for (const tNome of turnosOk) {
        const t = h.turnos?.[tNome];
        if (!t) continue;
        // Conflita com agenda real?
        const candidatoBloco = {
          data: dataISO, horaInicio: t.inicio, duracao: t.duracao,
          hospitalId: h.id, tipo: 'plantao',
        };
        const choque = (agendaReal || []).some(b => {
          if (b.tipo !== 'plantao' || b.data !== dataISO) return false;
          const [aIni, aFim] = window.intervaloMin(candidatoBloco);
          const [bIni, bFim] = window.intervaloMin(b);
          return aIni < bFim && bIni < aFim;
        });
        if (choque) continue;
        candidatos.push({
          dataISO, dow, hospitalId: h.id, hospital: h, turno: tNome,
          horaInicio: t.inicio, duracao: t.duracao,
          ehFDS: _ehFimDeSemana(dataISO),
        });
      }
    }
  }
  return candidatos;
};

// Verifica se adicionar candidato viola intervalo mínimo / cadeia max.
const _violaIntervalo = (candidato, selecionados, intervaloMinH, cadeiaMaxH) => {
  const novoBloco = {
    data: candidato.dataISO, horaInicio: candidato.horaInicio,
    duracao: candidato.duracao, tipo: 'plantao',
  };
  const [novoIni, novoFim] = window.intervaloMin(novoBloco);
  for (const s of selecionados) {
    const sBloco = { data: s.dataISO, horaInicio: s.horaInicio, duracao: s.duracao, tipo: 'plantao' };
    const [sIni, sFim] = window.intervaloMin(sBloco);
    // Sobreposição direta — não deveria acontecer, mas guarda
    if (novoIni < sFim && sIni < novoFim) return true;
    // Gap entre os dois
    const gapH = Math.min(Math.abs(novoIni - sFim), Math.abs(sIni - novoFim)) / 60;
    if (gapH < intervaloMinH) return true;
  }
  // Cadeia máxima — adiciona o candidato e checa se alguma cadeia ultrapassa
  const todos = [...selecionados.map(s => ({
    data: s.dataISO, horaInicio: s.horaInicio, duracao: s.duracao, tipo: 'plantao',
  })), novoBloco];
  const cadeias = window.calcCadeias(todos);
  if (cadeias.some(c => c.horas > cadeiaMaxH)) return true;
  return false;
};

// Calcula valor $ de um candidato (1 plantão), usando hospital.remuneracao.
// Retorna 0 se hospital sem calculadora ativa.
const _valorCandidato = (cand) => {
  if (!cand.hospital?.remuneracao?.ativo) return 0;
  const blocoFake = {
    tipo: 'plantao', data: cand.dataISO,
    horaInicio: cand.horaInicio, duracao: cand.duracao,
    hospitalId: cand.hospitalId,
  };
  const r = window.calcRemuneracao(blocoFake, cand.hospital);
  return r.bruto || 0;
};

// Score de cada variante. Retorna número (maior = melhor escolha pra essa variante).
// Também leva em conta meta financeira se preferencias.metaFinanceira.ativo.
const _scoreVariante = (cand, selecionados, variante, prefs) => {
  const prefereLivre = new Set(prefs?.prefereLivre || []);
  let score = 0;

  // Penalidade base: dia da semana que ela prefere livre
  if (prefereLivre.has(cand.dow)) score -= 5;

  if (variante === 'equilibrada') {
    const dataN = window.fromISO(cand.dataISO).getTime();
    const minGap = selecionados.length === 0 ? 30 :
      Math.min(...selecionados.map(s => Math.abs(dataN - window.fromISO(s.dataISO).getTime()) / 86400000));
    score += Math.min(minGap, 10);
    if (cand.ehFDS) score += 1;
  } else if (variante === 'concentrada') {
    const dataN = window.fromISO(cand.dataISO).getTime();
    let bonus = 0;
    for (const s of selecionados) {
      const gapDias = Math.abs(dataN - window.fromISO(s.dataISO).getTime()) / 86400000;
      if (gapDias === 1) bonus += 4;
      else if (gapDias === 2) bonus += 2;
      else if (gapDias > 7) bonus -= 1;
    }
    score += bonus;
    if (cand.ehFDS) score -= 2;
  } else if (variante === 'espalhada') {
    const dataN = window.fromISO(cand.dataISO).getTime();
    const minGap = selecionados.length === 0 ? 30 :
      Math.min(...selecionados.map(s => Math.abs(dataN - window.fromISO(s.dataISO).getTime()) / 86400000));
    score += minGap * 2;
    if (cand.ehFDS) score -= 3;
  }

  // Meta financeira: bonifica candidatos que ajudam a atingir alvo.
  // Se já passou do max, penaliza candidatos caros. Se faltam $, bonifica os mais caros.
  const meta = prefs?.metaFinanceira;
  if (meta?.ativo && meta?.valorMin > 0) {
    const valorAtual = selecionados.reduce((a, s) => a + (s._valor || 0), 0);
    const valorCand = _valorCandidato(cand);
    if (valorCand > 0) {
      const faltaMin = Math.max(0, meta.valorMin - valorAtual);
      const sobraMax = meta.valorMax > 0 ? Math.max(0, valorAtual + valorCand - meta.valorMax) : 0;
      // Bonus proporcional: candidato vale +N pontos se ainda faltam mínimos
      // Escala: 1 ponto a cada R$200 que ajuda
      if (faltaMin > 0) {
        score += Math.min(8, valorCand / 200);
      }
      if (sobraMax > 0) {
        // Já passou do teto — penaliza candidatos caros (mas não bloqueia, é soft)
        score -= Math.min(8, valorCand / 200);
      }
    }
  }
  return score;
};

// PRNG simples com seed pra perturbação reproduzível
const _seededRandom = (seed) => {
  let s = seed % 2147483647;
  if (s <= 0) s += 2147483646;
  return () => { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; };
};

// Calcula resumo por hospital + violações depois de selecionar.
window.recalcularScoreProposta = (blocosPropostos, hospitais, preferencias) => {
  const ativos = (hospitais || []).filter(window.temRegrasAtivas);
  // Reconstrói "selecionados" no formato interno do solver
  const selecionados = (blocosPropostos || []).map(b => ({
    dataISO: b.data, hospitalId: b.hospitalId,
    horaInicio: b.horaInicio, duracao: b.duracao,
    turno: b.turno,
    ehFDS: _ehFimDeSemana(b.data),
  }));
  return _avaliarSelecao(selecionados, ativos, preferencias || {});
};

const _avaliarSelecao = (selecionados, hospitais, prefs) => {
  const porHospital = {};
  for (const h of hospitais) {
    if (!h.regrasEscala?.ativo) continue;
    const sel = selecionados.filter(s => s.hospitalId === h.id);
    const fds = sel.filter(s => s.ehFDS).length;
    const horas = sel.reduce((a, s) => a + s.duracao, 0);
    porHospital[h.id] = {
      hospitalNome: h.abrev || h.nome,
      plantoes: sel.length, fds, horas,
      regras: h.regrasEscala,
    };
  }

  // ---- Score breakdown granular (0-100 cada componente) ----
  // Mínimos atendidos (peso 50): porcentagem de mínimos cumpridos
  let totalMin = 0, atendMin = 0;
  for (const id in porHospital) {
    const x = porHospital[id]; const r = x.regras;
    [['plantoes','plantoesMin'], ['fds','fdsMin'], ['horas','horasMin']].forEach(([k, mk]) => {
      const min = r[mk] || 0;
      if (min > 0) {
        totalMin++;
        if (x[k] >= min) atendMin++;
        else atendMin += Math.max(0, x[k] / min); // crédito parcial
      }
    });
  }
  const minimosScore = totalMin === 0 ? 100 : Math.round((atendMin / totalMin) * 100);

  // Janelas de qualidade
  const ordenados = [...selecionados].sort((a, b) => {
    const ka = window.fromISO(a.dataISO).getTime() + a.horaInicio * 3600000;
    const kb = window.fromISO(b.dataISO).getTime() + b.horaInicio * 3600000;
    return ka - kb;
  });
  let janelas = 0;
  for (let i = 1; i < ordenados.length; i++) {
    const prev = ordenados[i-1];
    const curr = ordenados[i];
    const fimPrev = window.fromISO(prev.dataISO).getTime() + (prev.horaInicio + prev.duracao) * 3600000;
    const iniCurr = window.fromISO(curr.dataISO).getTime() + curr.horaInicio * 3600000;
    if ((iniCurr - fimPrev) / 3600000 >= 16) janelas++;
  }
  const qualidadeRequerida = prefs?.qualidadeRequerida || 4;
  const qualidadeScore = qualidadeRequerida === 0 ? 100
    : Math.round(Math.min(100, (janelas / qualidadeRequerida) * 100));

  // Preferências respeitadas (prefereLivre)
  const prefereLivre = new Set(prefs?.prefereLivre || []);
  let totalPrefDias = 0, respeitadoPref = 0;
  if (prefereLivre.size > 0) {
    for (const s of selecionados) {
      totalPrefDias++;
      if (!prefereLivre.has(s.dow)) respeitadoPref++;
    }
  }
  const prefsScore = totalPrefDias === 0 ? 100 : Math.round((respeitadoPref / totalPrefDias) * 100);

  // ---- Score Financeiro (se meta ativa) ----
  // Calcula bruto/líquido total da seleção
  let valorBrutoTotal = 0, valorLiquidoTotal = 0;
  const valorPorHospital = {};
  for (const s of selecionados) {
    const h = hospitais.find(x => x.id === s.hospitalId);
    if (!h?.remuneracao?.ativo) continue;
    const r = window.calcRemuneracao({
      tipo:'plantao', data: s.dataISO, horaInicio: s.horaInicio,
      duracao: s.duracao, hospitalId: s.hospitalId,
    }, h);
    valorBrutoTotal += r.bruto;
    valorLiquidoTotal += r.liquido;
    if (!valorPorHospital[h.abrev]) valorPorHospital[h.abrev] = 0;
    valorPorHospital[h.abrev] += r.bruto;
  }
  const meta = prefs?.metaFinanceira;
  let valorScore = null; // null = sem meta ativa, não conta no total
  if (meta?.ativo && (meta.valorMin > 0 || meta.valorMax > 0)) {
    const min = meta.valorMin || 0;
    const max = meta.valorMax || min * 1.5;
    if (valorBrutoTotal >= min && valorBrutoTotal <= max) {
      valorScore = 100; // dentro da janela alvo
    } else if (valorBrutoTotal < min) {
      // Penaliza proporcional ao quanto falta — atinge 0 se ganha 0
      valorScore = Math.round((valorBrutoTotal / min) * 100);
    } else {
      // Acima do max — penaliza proporcional
      const sobra = valorBrutoTotal - max;
      valorScore = Math.max(0, Math.round(100 - (sobra / max) * 100));
    }
    if (valorBrutoTotal < min) {
      violacoes.push(`Bruto ${window.fmtBRL(valorBrutoTotal)} abaixo da meta mínima ${window.fmtBRL(min)}`);
    } else if (max > 0 && valorBrutoTotal > max * 1.1) {
      violacoes.push(`Bruto ${window.fmtBRL(valorBrutoTotal)} acima da meta máxima ${window.fmtBRL(max)}`);
    }
  }

  // Score total: ponderado. Com meta financeira: mín 40%, qual 25%, prefs 15%, $ 20%.
  // Sem meta: mín 50%, qual 30%, prefs 20%.
  const total = valorScore != null
    ? Math.round(minimosScore * 0.4 + qualidadeScore * 0.25 + prefsScore * 0.15 + valorScore * 0.2)
    : Math.round(minimosScore * 0.5 + qualidadeScore * 0.3 + prefsScore * 0.2);

  // Violações detalhadas (texto pra UI)
  const violacoes = [];
  for (const id in porHospital) {
    const x = porHospital[id]; const r = x.regras;
    if (x.plantoes < (r.plantoesMin || 0)) violacoes.push(`${x.hospitalNome}: ${x.plantoes} plantões (mín ${r.plantoesMin})`);
    if (x.plantoes > (r.plantoesMax || 99)) violacoes.push(`${x.hospitalNome}: ${x.plantoes} plantões (máx ${r.plantoesMax})`);
    if (x.fds < (r.fdsMin || 0)) violacoes.push(`${x.hospitalNome}: ${x.fds} FDS (mín ${r.fdsMin})`);
    if (x.fds > (r.fdsMax || 99)) violacoes.push(`${x.hospitalNome}: ${x.fds} FDS (máx ${r.fdsMax})`);
    if (x.horas < (r.horasMin || 0)) violacoes.push(`${x.hospitalNome}: ${x.horas}h (mín ${r.horasMin}h)`);
    if (x.horas > (r.horasMax || 999)) violacoes.push(`${x.hospitalNome}: ${x.horas}h (máx ${r.horasMax}h)`);
  }
  if (janelas < qualidadeRequerida) {
    violacoes.push(`Apenas ${janelas} janela(s) de descanso longas (alvo ${qualidadeRequerida})`);
  }

  return {
    porHospital, violacoes, janelasLongas: janelas,
    total,
    breakdown: { minimosScore, qualidadeScore, prefsScore, valorScore },
    financeiro: {
      bruto: Math.round(valorBrutoTotal * 100) / 100,
      liquido: Math.round(valorLiquidoTotal * 100) / 100,
      porHospital: valorPorHospital,
      meta: meta?.ativo ? { min: meta.valorMin || 0, max: meta.valorMax || 0 } : null,
    },
  };
};

// Algoritmo principal — para uma variante. Aceita seed pra perturbar scoring.
const _resolverVariante = (variante, hospitaisAtivos, preferencias, mesAlvo, agendaReal, seed = 0) => {
  const candidatos = _gerarCandidatos(hospitaisAtivos, preferencias, mesAlvo, agendaReal);
  const intervaloMinH = preferencias?.descansoMinH ?? 12;
  const cadeiaMaxH = preferencias?.cadeiaMaxH ?? 24;
  const rand = _seededRandom(seed || 1);
  const selecionados = [];

  const tentaSelecionar = (qualidadeRelaxada) => {
    while (true) {
      const ranked = candidatos
        .filter(c => {
          const h = c.hospital;
          const r = h.regrasEscala;
          const selH = selecionados.filter(s => s.hospitalId === h.id);
          if (selH.length >= (r.plantoesMax || 99)) return false;
          if (selH.reduce((a,s) => a + s.duracao, 0) + c.duracao > (r.horasMax || 999)) return false;
          if (selH.filter(s => s.ehFDS).length >= (r.fdsMax || 99) && c.ehFDS) return false;
          if (_violaIntervalo(c, selecionados, intervaloMinH, cadeiaMaxH)) return false;
          if (selecionados.some(s => s.dataISO === c.dataISO && s.hospitalId === c.hospitalId && s.turno === c.turno)) return false;
          return true;
        })
        .map(c => {
          const baseScore = _scoreVariante(c, selecionados, variante, preferencias);
          // Perturbação por seed — empates desfeitos de forma reproduzível
          const jitter = (rand() - 0.5) * (seed > 0 ? 4 : 0.5);
          return { c, s: baseScore + jitter };
        })
        .sort((a, b) => b.s - a.s);

      if (ranked.length === 0) break;
      const melhor = ranked[0];

      const algumFaltaMinimo = hospitaisAtivos.some(h => {
        const sel = selecionados.filter(s => s.hospitalId === h.id);
        return sel.length < (h.regrasEscala.plantoesMin || 0);
      });
      // Verifica também meta financeira mínima
      const meta = preferencias?.metaFinanceira;
      const valorAtual = selecionados.reduce((a, s) => a + (s._valor || 0), 0);
      const faltaMetaMin = meta?.ativo && meta?.valorMin > 0 && valorAtual < meta.valorMin;
      // Threshold mais frouxo se ainda faltam mínimos OU meta financeira OU se backtrack
      const threshold = (algumFaltaMinimo || faltaMetaMin) ? -20 : (qualidadeRelaxada ? -5 : 0);
      if (melhor.s < threshold) break;

      // Stop se já passou do max financeiro (hard-ish)
      if (meta?.ativo && meta?.valorMax > 0 && valorAtual >= meta.valorMax * 1.05) break;

      // Anota valor do candidato escolhido pra usar em iterações futuras
      melhor.c._valor = _valorCandidato(melhor.c);
      selecionados.push(melhor.c);
    }
  };

  // Primeira passada
  tentaSelecionar(false);

  // Backtracking: se mínimos não atendidos, relaxa threshold e tenta preencher
  const aindaFaltaMinimo = hospitaisAtivos.some(h => {
    const sel = selecionados.filter(s => s.hospitalId === h.id);
    return sel.length < (h.regrasEscala.plantoesMin || 0);
  });
  if (aindaFaltaMinimo) {
    tentaSelecionar(true);
  }

  const blocosPropostos = selecionados.map(s => ({
    hospitalId: s.hospitalId,
    data: s.dataISO,
    horaInicio: s.horaInicio,
    duracao: s.duracao,
    turno: s.turno,
  }));
  const avaliacao = _avaliarSelecao(selecionados, hospitaisAtivos, preferencias);
  return {
    variante,
    blocosPropostos,
    score: {
      total: avaliacao.total,
      breakdown: avaliacao.breakdown,
      porHospital: avaliacao.porHospital,
      janelasLongas: avaliacao.janelasLongas,
      financeiro: avaliacao.financeiro,
    },
    violacoes: avaliacao.violacoes,
  };
};

// Mede similaridade entre 2 propostas (% de blocos idênticos em data+hospital+turno).
const _similaridade = (a, b) => {
  if (!a.length && !b.length) return 1;
  const ka = new Set(a.map(x => `${x.data}|${x.hospitalId}|${x.turno}`));
  const kb = new Set(b.map(x => `${x.data}|${x.hospitalId}|${x.turno}`));
  let intersec = 0;
  ka.forEach(k => { if (kb.has(k)) intersec++; });
  const unionSize = ka.size + kb.size - intersec;
  return unionSize === 0 ? 1 : intersec / unionSize;
};

// API principal: gera 3 propostas (Equilibrada, Concentrada, Espalhada).
// Aceita seed pra reroll — chamar gerarPropostasEscala(..., seed) com seed diferente
// gera variações ligeiras pra mesma config.
window.gerarPropostasEscala = (hospitais, preferencias, mesAlvo, agendaReal, seed = 0) => {
  const ativos = (hospitais || []).filter(window.temRegrasAtivas);
  if (ativos.length === 0) {
    return { ok: false, erro: 'Nenhum hospital com regras de simulação ativas. Vá em Hospitais → Ligar simulação.' };
  }
  const variantes = ['equilibrada', 'concentrada', 'espalhada'];
  const propostas = variantes.map((v, i) =>
    _resolverVariante(v, ativos, preferencias || {}, mesAlvo, agendaReal || [], seed + i * 31)
  );

  // Diversidade: se duas propostas são >80% idênticas, regenera a pior com perturbação extra
  for (let i = 0; i < propostas.length; i++) {
    for (let j = i + 1; j < propostas.length; j++) {
      const sim = _similaridade(propostas[i].blocosPropostos, propostas[j].blocosPropostos);
      if (sim > 0.8) {
        const piorIdx = propostas[i].score.total < propostas[j].score.total ? i : j;
        const novaSeed = (seed + 999 + piorIdx * 17) || 1;
        propostas[piorIdx] = _resolverVariante(
          variantes[piorIdx], ativos, preferencias || {}, mesAlvo, agendaReal || [], novaSeed
        );
      }
    }
  }

  // Ordena por score total desc
  propostas.sort((a, b) => b.score.total - a.score.total);
  return {
    ok: true, propostas,
    regrasUsadas: ativos.map(h => ({ hospitalId: h.id, abrev: h.abrev, regras: h.regrasEscala })),
    seed,
  };
};

// =====================================================================
// Imprimir / PDF — agenda real (não proposta) — Semana ou Mês inteiro
// Layout limpo pra geladeira: lista por dia com hospital, hora, duração.
// Mariana: clica → abre janela → "Salvar como PDF" no diálogo do navegador.
// =====================================================================
window.imprimirAgenda = (modo, blocos, hospitais, refISO) => {
  const w = window.open('', '_blank', 'width=900,height=1100');
  if (!w) { alert('Pop-up bloqueado. Libere pop-ups pra imprimir.'); return; }

  const ref = refISO || window.HOJE_ISO;
  let titulo, dias, subtitulo;
  if (modo === 'semana') {
    const ini = window.inicioSemana(ref);
    dias = Array.from({length: 7}, (_, i) => window.addDias(ini, i));
    const fim = dias[6];
    const dIni = window.fromISO(ini);
    const dFim = window.fromISO(fim);
    titulo = `Semana de ${dIni.getDate()}/${String(dIni.getMonth()+1).padStart(2,'0')} a ${dFim.getDate()}/${String(dFim.getMonth()+1).padStart(2,'0')}`;
    subtitulo = `${dIni.getFullYear()}`;
  } else {
    const d = window.fromISO(ref);
    const y = d.getFullYear(), m = d.getMonth();
    const totalDias = new Date(y, m+1, 0).getDate();
    dias = Array.from({length: totalDias}, (_, i) => window.toISO(new Date(y, m, i+1)));
    titulo = `${window.MESES[m]} ${y}`;
    subtitulo = '';
  }

  // Filtra blocos relevantes (plantões e bloqueios; deslocamentos e sono não imprimem)
  const blocosImpressao = (blocos || []).filter(b =>
    (b.tipo === 'plantao' || b.tipo === 'bloqueio') &&
    dias.includes(b.data)
  );

  // Agrupa por dia
  const porDia = {};
  for (const d of dias) porDia[d] = [];
  for (const b of blocosImpressao) porDia[b.data]?.push(b);
  // Ordena cada dia por horário
  Object.values(porDia).forEach(arr => arr.sort((a,b) => a.horaInicio - b.horaInicio));

  // Calendário grid pra modo mês
  const primeiroDow = window.diaSemanaBR(dias[0]);

  const totalPlantoes = blocosImpressao.filter(b => b.tipo === 'plantao').length;
  const totalHoras = blocosImpressao.filter(b => b.tipo === 'plantao').reduce((a,b) => a + b.duracao, 0);

  const html = `<!doctype html><html lang="pt-BR"><head>
<meta charset="utf-8"><title>Agenda — ${titulo}</title>
<style>
  @page { size: A4; margin: 14mm; }
  * { box-sizing: border-box; }
  body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #3A2E2A; margin: 0; padding: 18px; line-height: 1.4; }
  h1 { font-size: 24pt; margin: 0 0 4pt; font-weight: 600; letter-spacing: -0.02em; }
  .subt { color: #7A6A60; font-size: 12pt; margin: 0 0 14pt; }
  .meta { display:flex; gap: 16pt; margin: 8pt 0 18pt; padding: 8pt 12pt; background: #FBF1E1; border-radius: 4pt; font-size: 10pt; }
  .meta strong { font-size: 14pt; font-weight: 600; }
  ${modo === 'mes' ? `
    .cal { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2pt; }
    .cal-h { font-size: 9pt; font-weight: 600; text-align: center; color: #7A6A60; padding: 4pt 0; }
    .cal-d {
      border: 1px solid #DDD; border-radius: 3pt;
      padding: 4pt; min-height: 64pt;
      font-size: 9pt; position: relative;
    }
    .cal-d.fds { background: #FAF7F2; }
    .cal-num { font-weight: 700; font-size: 11pt; }
    .cal-bloco {
      margin-top: 2pt; padding: 1pt 4pt; font-size: 8pt;
      border-left: 2pt solid; border-radius: 2pt;
    }
  ` : `
    .dia { display: grid; grid-template-columns: 80pt 1fr; gap: 12pt; padding: 8pt 0; border-bottom: 1px solid #EEE; page-break-inside: avoid; }
    .dia.fds { background: #FAF7F2; padding-left: 8pt; padding-right: 8pt; border-radius: 3pt; }
    .dia-data { font-size: 11pt; font-weight: 600; color: #7A6A60; }
    .dia-data .num { display: block; font-size: 22pt; font-weight: 700; color: #3A2E2A; line-height: 1; margin-top: 2pt; }
    .dia-blocos { display: grid; gap: 4pt; }
    .bloco {
      padding: 4pt 10pt; border-left: 3pt solid;
      border-radius: 2pt; font-size: 11pt;
      display: flex; gap: 8pt; align-items: baseline;
    }
    .bloco strong { font-size: 11pt; min-width: 56pt; }
    .bloco-vazio { color: #BBB; font-size: 10pt; font-style: italic; padding: 2pt 0; }
  `}
  @media print { body { padding: 0; } .no-print { display: none !important; } }
</style></head><body>

<h1>${titulo}</h1>
${subtitulo ? `<div class="subt">${subtitulo}</div>` : ''}

<div class="meta">
  <div><strong>${totalPlantoes}</strong> plantões</div>
  <div><strong>${totalHoras}</strong>h totais</div>
</div>

${modo === 'mes' ? `
  <div class="cal">
    ${['Seg','Ter','Qua','Qui','Sex','Sáb','Dom'].map(d => `<div class="cal-h">${d}</div>`).join('')}
    ${Array.from({length: primeiroDow}).map(() => '<div></div>').join('')}
    ${dias.map(iso => {
      const dow = window.diaSemanaBR(iso);
      const ehFDS = dow === 5 || dow === 6;
      const dia = parseInt(iso.split('-')[2]);
      const blocosDia = porDia[iso] || [];
      return `<div class="cal-d ${ehFDS ? 'fds' : ''}">
        <div class="cal-num">${dia}</div>
        ${blocosDia.map(b => {
          const h = hospitais.find(x => x.id === b.hospitalId);
          if (b.tipo === 'bloqueio') {
            return `<div class="cal-bloco" style="background:#F5F5F5;border-color:#999;color:#666"><strong>BLOQ</strong> ${b.motivo || ''}</div>`;
          }
          return `<div class="cal-bloco" style="background:${h?.corWash||'#F5F5F5'};border-color:${h?.cor||'#999'};color:${h?.corDeep||'#333'}">
            <strong>${h?.abrev||'?'}</strong> ${window.fmtHora(b.horaInicio)}–${window.fmtHora((b.horaInicio + b.duracao) % 24)}
          </div>`;
        }).join('')}
      </div>`;
    }).join('')}
  </div>
` : `
  ${dias.map(iso => {
    const d = window.fromISO(iso);
    const dow = window.diaSemanaBR(iso);
    const ehFDS = dow === 5 || dow === 6;
    const blocosDia = porDia[iso] || [];
    const diaNomeCompleto = window.DIAS_COMPLETO[dow];
    return `<div class="dia ${ehFDS ? 'fds' : ''}">
      <div class="dia-data">
        ${diaNomeCompleto}
        <span class="num">${d.getDate()}</span>
        <small>${window.MESES_CURTO[d.getMonth()]}</small>
      </div>
      <div class="dia-blocos">
        ${blocosDia.length === 0 ? '<div class="bloco-vazio">— livre</div>' : blocosDia.map(b => {
          const h = hospitais.find(x => x.id === b.hospitalId);
          if (b.tipo === 'bloqueio') {
            return `<div class="bloco" style="background:#F5F5F5;border-color:#999;color:#666"><strong>BLOQUEIO</strong> ${b.motivo || ''}</div>`;
          }
          return `<div class="bloco" style="background:${h?.corWash||'#F5F5F5'};border-color:${h?.cor||'#999'};color:${h?.corDeep||'#333'}">
            <strong>${h?.abrev||'?'}</strong>
            <span>${window.fmtHora(b.horaInicio)}–${window.fmtHora((b.horaInicio + b.duracao) % 24)} · ${b.duracao}h</span>
            ${b.tipo === 'cedido' ? '<span style="color:#999;font-size:9pt">(cedido)</span>' : ''}
            ${b.viaTroca ? '<span style="color:#5A4E8C;font-size:9pt">(via troca)</span>' : ''}
          </div>`;
        }).join('')}
      </div>
    </div>`;
  }).join('')}
`}

<div style="margin-top: 24pt; padding-top: 8pt; border-top: 1px solid #DDD; font-size: 9pt; color: #7A6A60">
  Colo Ritmo · Gerado em ${new Date().toLocaleString('pt-BR')}
</div>

<div class="no-print" style="margin-top: 24pt; text-align: center">
  <button onclick="window.print()" style="padding:10pt 24pt;font-size:11pt;background:#3A2E2A;color:#FFF;border:none;border-radius:4pt;cursor:pointer">
    Imprimir / Salvar como PDF
  </button>
</div>

<script>setTimeout(() => window.print(), 600);</script>
</body></html>`;
  w.document.write(html);
  w.document.close();
};

// Rótulos legíveis pra UI
window.LABEL_VARIANTE = {
  equilibrada: 'Equilibrada',
  concentrada: 'Concentrada',
  espalhada: 'Espalhada',
};
window.DESCRICAO_VARIANTE = {
  equilibrada: 'Plantões distribuídos pelo mês, sem aglomerar.',
  concentrada: 'Plantões em ondas — mais blocos longos livres.',
  espalhada: 'Maximiza dias diferentes, evita seguidos.',
};

// =====================================================================
// Propostas (sandbox de simulação de escala — feature "Fazer escala")
// =====================================================================
// Schema da tabela `public.propostas` em schemas/supabase-propostas.sql
//
// Modelo da proposta no app (camelCase, traduzido do snake_case do DB):
//   {
//     id:               UUID (do Postgres)
//     mesAlvo:          "2026-06"
//     variante:         "A" | "B" | "C" (ou rótulo livre)
//     titulo:           string opcional
//     blocosPropostos:  Array<{ hospitalId, data, horaInicio, duracao, turno }>
//     regrasUsadas:     snapshot das regras dos hospitais quando simulou
//     preferencias:     { bloqueios, prefereLivre, cadeiaMaxH, descansoMinH, ... }
//     score:            { total, regras: {...}, prefs: {...}, violacoes: [...] }
//     status:           'rascunho' | 'enviada' | 'adotada' | 'arquivada'
//     notas:            string
//     criadaEm:         ISO timestamp
//     atualizadaEm:     ISO timestamp
//   }

const _rowToProposta = (r) => ({
  id: r.id,
  mesAlvo: r.mes_alvo,
  variante: r.variante,
  titulo: r.titulo,
  blocosPropostos: r.blocos_propostos || [],
  regrasUsadas: r.regras_usadas || {},
  preferencias: r.preferencias || {},
  score: r.score || {},
  status: r.status,
  notas: r.notas,
  criadaEm: r.created_at,
  atualizadaEm: r.updated_at,
});

const _propostaToRow = (p) => ({
  // user_id é preenchido pelo cliente (auth.uid() não funciona em insert sem default)
  mes_alvo: p.mesAlvo,
  variante: p.variante || 'A',
  titulo: p.titulo || null,
  blocos_propostos: p.blocosPropostos || [],
  regras_usadas: p.regrasUsadas || {},
  preferencias: p.preferencias || {},
  score: p.score || {},
  status: p.status || 'rascunho',
  notas: p.notas || null,
});

// Lista todas as propostas da usuária. Filtros opcionais: mesAlvo, status, incluirArquivadas.
// Ordena por created_at desc (mais recente primeiro).
// Auto-arquiva rascunhos > 90 dias em background na primeira chamada da sessão.
window.listarPropostas = async ({ mesAlvo = null, status = null, incluirArquivadas = false } = {}) => {
  const sb = window.criarSupabase();
  try {
    let q = sb.from('propostas').select('*').order('created_at', { ascending: false });
    if (mesAlvo) q = q.eq('mes_alvo', mesAlvo);
    if (status) q = q.eq('status', status);
    if (!status && !incluirArquivadas) q = q.neq('status', 'arquivada');
    const { data, error } = await q;
    if (error) throw error;
    const propostas = (data || []).map(_rowToProposta);

    // Auto-arquiva rascunhos antigos (>90 dias) — assíncrono, não bloqueia retorno
    if (!window.__archivedThisSession) {
      window.__archivedThisSession = true;
      const limite = new Date(Date.now() - 90 * 24 * 3600 * 1000);
      const candidatos = propostas.filter(p =>
        p.status === 'rascunho' && new Date(p.criadaEm) < limite
      );
      if (candidatos.length > 0) {
        Promise.all(candidatos.map(p =>
          window.atualizarProposta(p.id, { status: 'arquivada' })
        )).then(() => console.log(`[colo-ritmo] auto-arquivou ${candidatos.length} rascunho(s) > 90d`));
      }
    }
    return { ok: true, propostas };
  } catch (e) {
    console.warn('[colo-ritmo] listarPropostas falhou:', e.message || e);
    return { ok: false, propostas: [], erro: e.message };
  }
};

// Cria nova proposta. Retorna a proposta com id atribuído pelo DB.
window.criarProposta = async (proposta) => {
  const sb = window.criarSupabase();
  try {
    const { data: { user } } = await sb.auth.getUser();
    if (!user) throw new Error('sem usuária autenticada');
    const row = { user_id: user.id, ..._propostaToRow(proposta) };
    const { data, error } = await sb.from('propostas').insert(row).select('*').single();
    if (error) throw error;
    return { ok: true, proposta: _rowToProposta(data) };
  } catch (e) {
    console.warn('[colo-ritmo] criarProposta falhou:', e.message || e);
    return { ok: false, erro: e.message };
  }
};

// Atualiza campos de uma proposta existente (parcial).
window.atualizarProposta = async (id, campos) => {
  const sb = window.criarSupabase();
  try {
    const row = _propostaToRow({ ...campos });
    // Só inclui keys que foram passadas em `campos` (evita sobrescrever com defaults)
    const update = {};
    if ('mesAlvo' in campos) update.mes_alvo = row.mes_alvo;
    if ('variante' in campos) update.variante = row.variante;
    if ('titulo' in campos) update.titulo = row.titulo;
    if ('blocosPropostos' in campos) update.blocos_propostos = row.blocos_propostos;
    if ('regrasUsadas' in campos) update.regras_usadas = row.regras_usadas;
    if ('preferencias' in campos) update.preferencias = row.preferencias;
    if ('score' in campos) update.score = row.score;
    if ('status' in campos) update.status = row.status;
    if ('notas' in campos) update.notas = row.notas;
    const { data, error } = await sb.from('propostas').update(update).eq('id', id).select('*').single();
    if (error) throw error;
    return { ok: true, proposta: _rowToProposta(data) };
  } catch (e) {
    console.warn('[colo-ritmo] atualizarProposta falhou:', e.message || e);
    return { ok: false, erro: e.message };
  }
};

// Apaga uma proposta. Use status='arquivada' se quiser preservar histórico.
window.apagarProposta = async (id) => {
  const sb = window.criarSupabase();
  try {
    const { error } = await sb.from('propostas').delete().eq('id', id);
    if (error) throw error;
    return { ok: true };
  } catch (e) {
    console.warn('[colo-ritmo] apagarProposta falhou:', e.message || e);
    return { ok: false, erro: e.message };
  }
};

// =====================================================================

window.carregarEstado = () => {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    return JSON.parse(raw);
  } catch (_) { return null; }
};
window.salvarEstado = (estado) => {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(estado)); }
  catch (_) {}
};
window.limparEstado = () => {
  try { localStorage.removeItem(STORAGE_KEY); } catch(_){}
};

// Estado inicial: combina default + persistido
const persisted = window.carregarEstado();
window.HOSPITAIS = persisted?.hospitais || HOSPITAIS_DEFAULT;

// Histórico das últimas semanas — vazio por enquanto. Será preenchido
// dinamicamente quando o app tiver dados de mais de 1 mês acumulados.
window.SEMANAS_HISTORICO = [];
