// ─── Visualizador de Exames v3 ─────────────────────────────────────────────
const { useState: useStateEV, useRef: useRefEV, useEffect: useEffectEV } = React;

// ── Helpers de formatação para cópia ────────────────────────────────────────

// Normaliza nome de exame para correspondência (remove acentos, caixa, e toda
// pontuação — parênteses, hífens etc. — virando espaço).
function normNome(nome) {
  return (nome || '')
    .toLowerCase()
    .normalize('NFD').replace(/[̀-ͯ]/g, '')
    .replace(/[^a-z0-9\s]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

// Chave de dedup de um exame: usa a abreviação canônica (mapa de sinônimos)
// quando reconhecida, senão cai para o nome normalizado. Evita duplicar o
// mesmo exame quando a IA extrai nomes diferentes para o mesmo valor (ex:
// "CKD-EPI 2021" e "Estimativa da Filtração Glomerular CKD-EPI 2021").
function chaveDedupExame(nome) {
  return (typeof ABREV !== 'undefined' && ABREV[normNome(nome)]) || normNome(nome);
}

// Override de categoria por nome de exame. A IA às vezes classifica um exame na
// seção errada (ex: ureia caindo em "Bioquímica" em vez de "Função Renal"). Move
// o exame para o bloco da categoria correta, criando-o se necessário.
const OVERRIDE_CATEGORIA_EV = [
  ['Função Renal', ['ureia', 'ureia serica']],
  ['Bioquímica', ['acido urico', 'ac urico', 'uricemia']],
  ['Vitaminas e Hormônios', [
    'tsh', 'hormonio tireoestimulante', 'hormonio tireoestimulante ultrassensivel tsh',
    't4 livre', 't4l', 'tiroxina livre', 't4 l', 'tiroxina livre t4 livre', 'ft4', 't4 livre ft4', 'tiroxina t4 livre', 'tiroxina livre t4', 't4 livre tiroxina livre',
    't3', 'triiodotironina', 't3 total',
    'pth', 'paratormonio', 'hormonio da paratireoide',
    'cortisol', 'cortisol serico', 'cortisol basal', 'cortisol matinal',
    'acido folico', 'ac folico', 'folato',
    'vitamina b12', 'vit b12', 'b12', 'cobalamina',
    'vitamina d', 'vit d', '25 oh vitamina d', '25 hidroxivitamina d', '25 oh vit d',
  ]],
];
function categoriaOverrideEV(nomeExame) {
  const n = normNome(nomeExame);
  for (const [categoria, nomes] of OVERRIDE_CATEGORIA_EV) {
    if (nomes.some(p => n === normNome(p))) return categoria;
  }
  return null;
}
// Normaliza o NOME da categoria de um bloco. A IA às vezes cria a categoria
// "Hormônios" sozinha em vez de "Vitaminas e Hormônios"; unificamos para que
// todos os exames apareçam juntos no mesmo bloco.
function normCategoriaEV(categoria) {
  const n = normNome(categoria);
  if (n === 'hormonios' || n === 'vitaminas' || n === 'hormonios e vitaminas' || n === 'hormonal' || n === 'hormonais' || n === 'exames hormonais') {
    return 'Vitaminas e Hormônios';
  }
  return categoria;
}
function reclassificarLabBlocks(blocks) {
  if (!Array.isArray(blocks) || blocks.length === 0) return blocks;
  // 1) Mescla blocos com mesma categoria + mesma data (a IA por vezes separa o
  //    mesmo laudo em dois blocos), sem duplicar exames de nome igual.
  //    Blocos com datas diferentes permanecem separados (um bloco por data).
  const porChave = new Map(); // categoria||data → bloco mesclado
  const ordem = [];
  for (const block of blocks) {
    const categoria = normCategoriaEV(block.categoria);
    const chave = (categoria || '') + '||' + (block.data_coleta || '');
    if (!porChave.has(chave)) {
      const novo = { ...block, categoria, exames: [] };
      porChave.set(chave, novo);
      ordem.push(novo);
    }
    const destino = porChave.get(chave);
    for (const ex of (Array.isArray(block.exames) ? block.exames : [])) {
      const jaExiste = destino.exames.some(e => chaveDedupExame(e && e.nome) === chaveDedupExame(ex && ex.nome));
      if (!jaExiste) destino.exames.push(ex);
    }
  }

  // 2) Extrai exames com override de categoria e os insere no primeiro bloco
  //    compatível (logo após Creatinina, quando aplicável).
  const primeiroBlockPorCategoria = new Map();
  for (const block of ordem) {
    if (!primeiroBlockPorCategoria.has(block.categoria)) primeiroBlockPorCategoria.set(block.categoria, block);
  }
  for (const block of ordem) {
    const exames = block.exames;
    const ficam = [];
    for (const ex of exames) {
      const destino = categoriaOverrideEV(ex && ex.nome);
      if (destino && destino !== block.categoria) {
        if (!primeiroBlockPorCategoria.has(destino)) {
          const novo = { categoria: destino, data_coleta: block.data_coleta, exames: [] };
          primeiroBlockPorCategoria.set(destino, novo);
          ordem.push(novo);
        }
        const blockDestino = primeiroBlockPorCategoria.get(destino);
        const idxCreatinina = blockDestino.exames.findIndex(e => normNome(e && e.nome) === 'creatinina');
        if (idxCreatinina !== -1) {
          blockDestino.exames.splice(idxCreatinina + 1, 0, ex);
        } else {
          blockDestino.exames.push(ex);
        }
      } else {
        ficam.push(ex);
      }
    }
    block.exames = ficam;
  }
  return ordem.filter(b => (b.exames || []).length > 0);
}

// Mapa nome normalizado → abreviação do modelo médico. (Hem = hemácias/eritrócitos;
// Hg = hemoglobina — campos distintos no modelo.)
const ABREV = (() => {
  const pares = [
    ['Hem', ['hemacias', 'hemacia', 'eritrocitos', 'contagem de hemacias']],
    ['Hg', ['hemoglobina', 'hb', 'hgb']],
    ['Ht', ['hematocrito', 'ht', 'hto']],
    ['CHCM', ['chcm', 'concentracao de hemoglobina corpuscular media']],
    ['HCM', ['hcm', 'hemoglobina corpuscular media']],
    ['VCM', ['vcm', 'volume corpuscular medio']],
    ['RDW', ['rdw']],
    ['Plaq', ['plaquetas', 'plaqueta', 'contagem de plaquetas']],
    ['VPM', ['vpm', 'mpv', 'volume plaquetario medio']],
    ['Ferritina', ['ferritina']],
    ['Ferro', ['ferro', 'ferro serico']],
    ['UIBIC', ['uibic', 'capacidade latente de ligacao do ferro']],
    ['TIBC', ['tibc', 'capacidade total de ligacao do ferro', 'ctlf']],
    ['IST', ['ist', 'indice de saturacao de transferrina', 'saturacao de transferrina']],
    ['Leuco', ['leucocitos', 'leucograma', 'global de leucocitos', 'contagem de leucocitos']],
    ['Neut', ['neutrofilos', 'neutrofilo', 'segmentados']],
    ['Eos', ['eosinofilos', 'eosinofilo']],
    ['Bas', ['basofilos', 'basofilo']],
    ['Linf', ['linfocitos', 'linfocito']],
    ['Mo', ['monocitos', 'monocito']],
    ['PCR', ['pcr', 'proteina c reativa']],
    ['VSH', ['vsh', 'vhs', 'velocidade de hemossedimentacao', 'hemossedimentacao', 'hemosedimentacao']],
    ['FR', ['fr', 'fator reumatoide']],
    ['Anti-CCP3', ['anti ccp3', 'anti ccp', 'anticorpos anti ccp3', 'anticorpo anti ccp3']],
    ['AgHBs', ['aghbs', 'antigeno de superficie do virus da hepatite b', 'antigeno de superficie do virus da hepatite b aghbs', 'hbsag']],
    ['Anti-HBs', ['anti hbs', 'anticorpo contra antigeno de superficie', 'anticorpo contra antigeno de superficie anti hbs']],
    ['Ur', ['ureia', 'ureia serica']],
    ['Cr', ['creatinina', 'creatinina serica']],
    ['eGFR', ['egfr', 'tfg estimada', 'taxa de filtracao glomerular estimada', 'rfg estimado', 'ckd epi', 'estimativa da filtracao glomerular ckd epi 2021', 'ckd epi 2021', 'estimativa da filtracao glomerular']],
    ['eGFR(Afro)', [
      'ckd epi 2009 afrodescendente', 'ckd epi 2009 afro descendente',
      'estimativa da filtracao glomerular afrodescendente', 'estimativa da filtracao glomerular afro descendente',
    ]],
    ['eGFR(NãoAfro)', [
      'ckd epi 2009 nao afrodescendente', 'ckd epi 2009 nao afro descendente',
      'estimativa da filtracao glomerular nao afrodescendente', 'estimativa da filtracao glomerular nao afro descendente',
    ]],
    ['Na', ['sodio', 'na']],
    ['K', ['potassio', 'k']],
    ['Cl', ['cloro', 'cloreto']],
    ['Ca', ['calcio', 'calcio total', 'calcio serico']],
    ['Mg', ['magnesio', 'mg']],
    ['Fosf', ['fosforo', 'fosfato', 'fosf', 'fosforo inorganico']],
    ['CO2', ['co2', 'dioxido de carbono', 'dioxido de carbono total', 'co2 total']],
    ['FA', ['fa', 'fal', 'fosfatase alcalina']],
    ['Ác. Úr.', ['acido urico', 'ac urico', 'uricemia']],
    ['RAC', ['rac', 'relacao albumina creatinina', 'albumina creatinina urinaria', 'microalbuminuria', 'rel alb cr urin']],
    ['RPC', ['rpc', 'relacao proteina creatinina', 'rel prot cr urin']],
    ['Prot Ur 24h', ['proteinuria de 24h', 'proteinuria 24h', 'proteinuria 24 horas', 'proteina urinaria 24h', 'prot ur 24h']],
    ['CT', ['colesterol total', 'colesterol']],
    ['HDL', ['hdl', 'hdl colesterol', 'colesterol hdl']],
    ['Não-HDL', ['nao hdl', 'colesterol nao hdl', 'nao hdl colesterol']],
    ['LDL', ['ldl', 'ldl colesterol', 'colesterol ldl', 'ldl colesterol calculado', 'ldl colesterol calculado']],
    ['VLDL', ['vldl', 'colesterol vldl', 'vldl colesterol']],
    ['TG', ['triglicerideos', 'triglicerides', 'tg']],
    ['CPK', ['cpk', 'ck', 'creatinoquinase', 'creatina quinase']],
    ['GJ', ['glicemia de jejum', 'glicemia', 'glicose', 'glicose de jejum', 'glj']],
    ['HbA1c', ['hba1c', 'hemoglobina glicada', 'hemoglobina glicosilada', 'glicada', 'hemoglobina glicada hba1c', 'hemoglobina glicada a1c', 'a1c']],
    ['Glicose Média Estimada', ['glicose media estimada', 'gme', 'media glicemica estimada', 'glicose media estimada gme']],
    ['TOTG', ['totg', 'teste de tolerancia a glicose', 'tolerancia a glicose', 'curva glicemica']],
    ['TGO', ['tgo', 'ast', 'aspartato amino transferase', 'aspartato aminotransferase', 'transaminase oxalacetica tgo aspartato amino transferase']],
    ['TGP', ['tgp', 'alt', 'alanina amino transferase', 'alanina aminotransferase', 'transaminase piruvica tgp alanina amino transferase']],
    ['BT', ['bilirrubina total', 'bt']],
    ['BD', ['bilirrubina direta', 'bd']],
    ['BI', ['bilirrubina indireta', 'bi']],
    ['INR', ['inr', 'rni', 'tap inr', 'tempo de protrombina']],
    ['ALB', ['albumina', 'albumina serica']],
    ['Ác. Fól.', ['acido folico', 'ac folico', 'folato']],
    ['Vit B-12', ['vitamina b12', 'vit b12', 'b12', 'cobalamina', 'vitamina b 12']],
    ['25-OH VIT D', ['vitamina d', 'vit d', '25 oh vitamina d', '25 hidroxivitamina d', '25 oh vit d', '25 hidroxi vitamina d']],
    ['TSH', ['tsh', 'hormonio tireoestimulante', 'hormonio tireoestimulante ultrassensivel tsh']],
    ['T4 L', ['t4 livre', 't4l', 'tiroxina livre', 't4 l', 'tiroxina livre t4 livre', 'ft4', 't4 livre ft4', 'tiroxina t4 livre', 'tiroxina livre t4', 't4 livre tiroxina livre']],
    ['T3', ['t3', 'triiodotironina', 't3 total']],
    ['PTH', ['pth', 'paratormonio', 'hormonio da paratireoide']],
  ];
  const map = {};
  for (const [abrev, sins] of pares) for (const s of sins) map[normNome(s)] = abrev;
  return map;
})();

// Linhas do modelo médico — sequência de abreviações por linha. Itens ausentes são
// suprimidos; linha sem nenhum item é omitida.
const LINHAS_LAB = [
  ['Hem', 'Hg', 'Ht', 'CHCM', 'HCM', 'VCM', 'RDW', 'Plaq', 'VPM'],
  ['Ferritina', 'Ferro', 'UIBIC', 'TIBC', 'IST'],
  ['Leuco', 'Neut', 'Eos', 'Bas', 'Linf', 'Mo'],
  ['PCR', 'VSH'],
  ['Ur', 'Cr', 'eGFR', 'Na', 'K', 'Cl', 'Ca', 'Mg', 'Fosf', 'Ác. Úr.', 'RAC', 'RPC', 'Prot Ur 24h'],
  ['GJ', 'HbA1c', 'Glicose Média Estimada', 'TOTG'],
  ['CT', 'HDL', 'Não-HDL', 'LDL', 'VLDL', 'TG'],
  ['CPK'],
  ['TGO', 'TGP', 'BT', 'BD', 'BI', 'INR', 'ALB'],
  ['Ác. Fól.', 'Vit B-12', '25-OH VIT D'],
  ['TSH', 'T4 L', 'PTH'],
];

// Sumário de urina (SU): rótulo + sinônimos.
const SU_CAMPOS = [
  ['Dens', ['densidade', 'densidade urinaria']],
  ['pH', ['ph', 'ph urinario']],
  ['Cor', ['cor', 'coloracao']],
  ['Prot', ['proteinas', 'proteina urinaria', 'proteinuria']],
  ['Corpos. Cetônicos', ['corpos cetonicos', 'cetonuria', 'cetona']],
  ['Pig. Biliares', ['pigmentos biliares', 'bilirrubina urinaria']],
  ['Nitrito', ['nitrito', 'nitritos']],
  ['Filamentos de Muco', ['filamentos de muco', 'muco']],
  ['Hemácias', ['hemacias urinarias', 'hematuria', 'hemacias na urina']],
  ['Células Epiteliais de Descamação', ['celulas epiteliais', 'celulas epiteliais de descamacao', 'celulas de descamacao']],
];

// Rastreios: rótulo + sinônimos.
const RASTREIOS = [
  ['MMG', ['mamografia', 'mmg']],
  ['USG de mamas', ['ultrassom de mamas', 'usg de mamas', 'usg mamas', 'ultrassonografia mamaria', 'ecografia mamaria']],
  ['COLONO', ['colonoscopia', 'colono']],
  ['DMO', ['dmo', 'densitometria', 'densitometria ossea']],
  ['PSOF', ['psof', 'pesquisa de sangue oculto', 'sangue oculto nas fezes', 'sangue oculto']],
  ['PSA', ['psa', 'antigeno prostatico especifico', 'antigeno prostatico', 'psa total']],
  ['Papanicolau', ['papanicolau', 'colpocitologia', 'citologia oncotica', 'preventivo']],
];

// Classificação dos exames descritivos (laudos de imagem/cardio/etc.) em seções.
const SECOES_DESCRITIVAS = [
  ['Exames cardiológicos', ['ecocardiograma', 'eco transtoracico', 'ecodopplercardiograma', 'eletrocardiograma', 'ecg', 'holter', 'mapa 24', 'mapa de 24', 'doppler de carotidas', 'doppler carotidas', 'cateterismo', 'cintilografia miocardica', 'cintilografia cardiaca', 'ressonancia cardiaca', 'ergometrico', 'teste ergometrico', 'teste de esforco', 'angiotomografia de coronarias', 'escore de calcio']],
  ['Outros Exames', ['endoscopia', 'eda', 'colonoscopia', 'histopatologico', 'anatomopatologico', 'biopsia', 'core biopsy', 'puncao', 'polissonografia', 'espirometria', 'prova de funcao pulmonar', 'liquor', 'liquido cefalorraquidiano']],
  ['Exames de Imagem', ['ultrassonografia', 'ultrassom', 'usg', 'us de', 'us bolsa', 'us parede', 'ecografia', 'doppler', 'ressonancia', 'rm de', 'rnm', 'tomografia', 'tc de', 'angiotomografia', 'angiografia', 'angiorressonancia', 'raio x', 'rx de', 'radiografia', 'pet', 'pet ct', 'cintilografia', 'densitometria']],
  ['Pareceres, relatórios médicos ou de outros profissionais, resumos de alta hospitalar', ['parecer', 'relatorio medico', 'relatorio', 'resumo de alta', 'alta hospitalar', 'sumario de alta', 'avaliacao de', 'laudo de']],
];

function classificarSecao(nome) {
  const n = normNome(nome);
  for (const [secao, palavras] of SECOES_DESCRITIVAS) {
    if (palavras.some(p => n.includes(normNome(p)))) return secao;
  }
  return 'Outros Exames';
}

// Siglas médicas conhecidas que, se aparecerem entre parênteses no nome do exame
// (ex: "Aspartato Aminotransferase (TGO)"), devem ser usadas como abreviação —
// mesmo que o nome completo não esteja no mapa ABREV. A IA copia o exame pela sigla
// como é mais conhecido no meio de saúde. Mapeia a sigla detectada → forma de exibição.
const SIGLAS_CONHECIDAS = {
  'tgo': 'TGO', 'ast': 'TGO',
  'tgp': 'TGP', 'alt': 'TGP',
  'ggt': 'GGT', 'gama gt': 'GGT', 'gamagt': 'GGT',
  'fa': 'FA', 'fal': 'FA',
  'ldh': 'LDH', 'dhl': 'LDH',
  'cpk': 'CPK', 'ck': 'CPK',
  'tsh': 'TSH', 't4l': 'T4 L', 't4 l': 'T4 L', 't4 livre': 'T4 L', 'ft4': 'T4 L', 't3': 'T3', 't4': 'T4', 'pth': 'PTH',
  'lh': 'LH', 'fsh': 'FSH', 'shbg': 'SHBG', 'dheas': 'DHEAS', 'dhea s': 'DHEAS',
  'pcr': 'PCR', 'vhs': 'VSH', 'vsh': 'VSH',
  'hba1c': 'HbA1c', 'a1c': 'HbA1c', 'gme': 'Glicose Média Estimada',
  'hdl': 'HDL', 'ldl': 'LDL', 'vldl': 'VLDL',
  'rdw': 'RDW', 'vcm': 'VCM', 'hcm': 'HCM', 'chcm': 'CHCM', 'vpm': 'VPM', 'mpv': 'VPM',
  'egfr': 'eGFR', 'tfg': 'eGFR',
  'rac': 'RAC', 'rpc': 'RPC',
  'psa': 'PSA', 'inr': 'INR', 'pth intacto': 'PTH',
  'tp': 'TP', 'ttpa': 'TTPa',
};

// Extrai uma sigla entre parênteses do nome do exame, se houver uma conhecida.
// Ex: "Aspartato Aminotransferase (TGO)" → "TGO". Pega a ÚLTIMA ocorrência
// (a sigla costuma vir no fim). Retorna a forma de exibição ou null.
function siglaEntreParenteses(nome) {
  const matches = String(nome || '').match(/\(([^)]+)\)/g);
  if (!matches) return null;
  for (let i = matches.length - 1; i >= 0; i--) {
    const conteudo = normNome(matches[i]); // remove parênteses e normaliza
    if (SIGLAS_CONHECIDAS[conteudo]) return SIGLAS_CONHECIDAS[conteudo];
  }
  return null;
}

// Abreviação de um exame: 1) match direto no mapa; 2) sigla entre parênteses no
// próprio nome (ex: "(TGO)"); 3) o nome original se nada casar.
function abrevExame(nome) {
  const direto = ABREV[normNome(nome)];
  if (direto) return direto;
  const sigla = siglaEntreParenteses(nome);
  if (sigla) return sigla;
  return null; // não reconhecido como item de linha fixa
}

function abreviar(nome) {
  return ABREV[normNome(nome)] || siglaEntreParenteses(nome) || nome;
}

// Seta de status, igual ao modelo médico.
function setaStatus(st) {
  return st === 'alto' ? ' ↑' : st === 'baixo' ? ' ↓' : '';
}
// Valor de um exame com seta e marca de baixa confiança.
function valorComSeta(ex) {
  const duvida = ex.confianca === 'baixa' ? ' (?)' : '';
  return (ex.valor || '—') + setaStatus(ex.status) + duvida;
}

// Monta as linhas de uma data no bloco laboratório (modelo médico).
// Itens ausentes são suprimidos; linha sem nenhum item presente é omitida.
function fmtCompacto(exames) {
  // indexa por abreviação do modelo (apenas as que entram nas linhas fixas)
  const porAbrev = {};
  const usadosKeys = new Set();
  for (const ex of exames) {
    const abrev = abrevExame(ex.nome); // match direto OU sigla entre parênteses
    if (abrev && !porAbrev[abrev]) porAbrev[abrev] = ex;
  }

  const linhas = [];

  // Linhas numéricas do template
  for (const abrevs of LINHAS_LAB) {
    const partes = [];
    for (const a of abrevs) {
      const ex = porAbrev[a];
      if (ex) { partes.push(a + ': ' + valorComSeta(ex)); usadosKeys.add(normNome(ex.nome)); }
    }
    if (partes.length) linhas.push(partes.join(' / '));
  }

  // Sumário de urina (SU)
  const suPartes = [];
  for (const [rotulo, sins] of SU_CAMPOS) {
    const ex = exames.find(e => sins.map(normNome).includes(normNome(e.nome)));
    if (ex) { suPartes.push(rotulo + ': ' + (ex.valor || '—')); usadosKeys.add(normNome(ex.nome)); }
  }
  if (suPartes.length) linhas.push('SU ' + suPartes.join(' / '));

  // Outros laboratoriais (não usados nas linhas fixas nem no SU). Usa a abreviação
  // mais conhecida quando houver (ex: SHBG, LH, GGT via sigla), senão o nome.
  const extras = exames.filter(ex => !usadosKeys.has(normNome(ex.nome)));
  if (extras.length > 0) {
    linhas.push('Outros Exames Laboratoriais: ' + extras.map(ex => abreviar(ex.nome) + ': ' + valorComSeta(ex)).join(' / '));
  }

  return linhas.join('\n');
}

// Formata todos os labBlocks de uma submission. Um bloco [DATA] por data coleta,
// mais recente primeiro. Retorna a seção Laboratório (ou '' se vazia).
function fmtTodosLabBlocks(labBlocks) {
  const porData = {};
  for (const block of labBlocks) {
    const data = block.data_coleta || 'sem data';
    if (!porData[data]) porData[data] = [];
    porData[data] = porData[data].concat(Array.isArray(block.exames) ? block.exames : []);
  }

  const datas = Object.keys(porData).sort((a, b) => {
    if (a === 'sem data') return 1;
    if (b === 'sem data') return -1;
    return b.localeCompare(a);
  });

  const blocos = [];
  for (const data of datas) {
    const compacto = fmtCompacto(porData[data]);
    if (!compacto) continue;
    const cab = data === 'sem data' ? '[sem data]' : '[' + fmtDate(data) + ']';
    blocos.push(cab + '\n' + compacto);
  }
  if (!blocos.length) return '';
  return 'Laboratório\n' + blocos.join('\n\n');
}

// Cópia de um único bloco (botão de cópia de cada categoria individual)
function fmtLabBlock(block) {
  const exames = Array.isArray(block.exames) ? block.exames : [];
  const compacto = fmtCompacto(exames);
  const cab = block.data_coleta ? '[' + fmtDate(block.data_coleta) + ']\n' : '';
  return 'Laboratório\n' + cab + compacto;
}

// Demais exames (botão da seção "Demais exames"): um por linha "- Nome: resultado"
function fmtDemaisBlock(demais) {
  const linhas = demais.filter(Boolean).map(ex => '- ' + (ex.nome) + ': ' + valorComSeta(ex));
  if (linhas.length === 0) return '';
  return 'Demais exames\n' + linhas.join('\n');
}

// Para o "Copiar tudo": classifica os exames descritivos (origem demais) em seções
// do modelo (Imagem, Cardiológicos, Rastreios, Outros, Pareceres) e formata.
// Cada seção só aparece quando tem conteúdo. dataFallback = data do envio, usada
// quando o item não traz data própria (compatibilidade com dados antigos).
function fmtDescritivosSecoes(demais, dataFallback) {
  const itens = (Array.isArray(demais) ? demais : []).filter(e => e && e.nome && e.nome !== '_meta');
  const porSecao = {};
  const rastreios = [];
  const rastreioFeito = new Set();

  for (const ex of itens) {
    const key = normNome(ex.nome);
    const rast = RASTREIOS.find(([, sins]) => sins.map(normNome).includes(key));
    if (rast) {
      if (!rastreioFeito.has(rast[0])) { rastreioFeito.add(rast[0]); rastreios.push({ rotulo: rast[0], ex }); }
      continue;
    }
    const secao = classificarSecao(ex.nome);
    (porSecao[secao] = porSecao[secao] || []).push(ex);
  }

  let out = '';

  // Formata uma seção descritiva agrupando os itens por data (mais recente
  // primeiro), com cabeçalho [DATA] por grupo — igual ao bloco Laboratório.
  // itens: [{ data, rotulo, valorStr }]. Sem data → "[sem data]", por último.
  const blocoPorData = (titulo, itens) => {
    if (!itens.length) return;
    const grupos = {};
    for (const it of itens) {
      const d = it.data || '';
      (grupos[d] = grupos[d] || []).push(it);
    }
    const datasOrd = Object.keys(grupos).sort((a, b) => {
      if (!a) return 1; if (!b) return -1;       // sem data por último
      return new Date(b) - new Date(a);
    });
    out += titulo + ':\n';
    out += datasOrd.map(d => {
      const cab = d ? '[' + fmtDate(d) + ']' : '[sem data]';
      const linhas = grupos[d].map(it => '- ' + it.rotulo + ': ' + it.valorStr).join('\n');
      return cab + '\n' + linhas;
    }).join('\n\n') + '\n\n';
  };

  // Mapeia os exames de uma seção para { data, rotulo, valorStr }. A data vem do
  // item (data_coleta) ou, se ausente (dados antigos), do fallback do envio.
  const mapDesc = (arr) => arr.map(e => ({
    data: e.data_coleta || dataFallback || '',
    rotulo: e.nome,
    valorStr: e.valor || '—',
  }));
  const mapRast = (arr) => arr.map(r => ({
    data: r.ex.data_coleta || dataFallback || '',
    rotulo: r.rotulo,
    valorStr: valorComSeta(r.ex),
  }));

  blocoPorData('Exames de Imagem', mapDesc(porSecao['Exames de Imagem'] || []));
  blocoPorData('Exames cardiológicos', mapDesc(porSecao['Exames cardiológicos'] || []));
  blocoPorData('Exames de Rastreios', mapRast(rastreios));
  blocoPorData('Outros Exames', mapDesc(porSecao['Outros Exames'] || []));
  blocoPorData('Pareceres, relatórios médicos ou de outros profissionais, resumos de alta hospitalar',
    mapDesc(porSecao['Pareceres, relatórios médicos ou de outros profissionais, resumos de alta hospitalar'] || []));

  return out.trimEnd();
}

function copyText(text) {
  return navigator.clipboard.writeText(text).catch(() => {
    const el = document.createElement('textarea');
    el.value = text; el.style.position = 'fixed'; el.style.opacity = '0';
    document.body.appendChild(el); el.select(); document.execCommand('copy');
    document.body.removeChild(el);
  });
}

// Botão de copiar com feedback visual (ícone pequeno, para headers de seção)
const CopyBtn = ({ getText, title }) => {
  const [copied, setCopied] = useStateEV(false);
  const handle = (e) => {
    e.stopPropagation();
    copyText(getText()).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); });
  };
  return (
    <button onClick={handle} title={title || 'Copiar'}
      style={{width:24,height:24,borderRadius:6,border:'none',background:'rgba(255,255,255,0.5)',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0,transition:'background 0.15s',padding:0}}>
      {copied
        ? <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#2C4A35" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
        : <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
      }
    </button>
  );
};

// Botão "Copiar tudo" com label, no topo da aba de interpretação
const CopyAllBtn = ({ labBlocks, demais }) => {
  const [copied, setCopied] = useStateEV(false);
  const handle = () => {
    const parts = [];
    if (labBlocks.length > 0) {
      const lab = fmtTodosLabBlocks(labBlocks);
      if (lab) parts.push(lab);
    }
    // Fallback de data p/ exames descritivos sem data própria (dados antigos):
    // usa a data mais recente entre os lab_blocks do mesmo envio.
    const datasLab = labBlocks.map(b => b.data_coleta).filter(Boolean).sort();
    const dataFallback = datasLab.length ? datasLab[datasLab.length - 1] : null;
    const descritivos = fmtDescritivosSecoes(demais, dataFallback);
    if (descritivos) parts.push(descritivos);
    const texto = parts.join('\n\n');
    copyText(texto).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); });
  };
  return (
    <button onClick={handle}
      style={{display:'flex',alignItems:'center',gap:5,padding:'5px 10px',borderRadius:8,border:'1.5px solid #DDD8CE',background:'#fff',cursor:'pointer',fontFamily:'inherit',fontSize:11,fontWeight:600,color: copied ? '#2C4A35' : '#4A6355',transition:'all 0.15s'}}>
      {copied
        ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#2C4A35" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
        : <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
      }
      {copied ? 'Copiado!' : 'Copiar tudo'}
    </button>
  );
};

const BLOCK_COLORS = [
  { accent: '#5C8A6E', bg: '#EAF2ED', border: '#B8D9C4', text: '#2C4A35', headerBg: '#F0F7F3' },
  { accent: '#C8973A', bg: '#FBF5E6', border: '#E8D5A0', text: '#8A6A20', headerBg: '#FDF8EE' },
  { accent: '#4A7A8A', bg: '#EBF5F8', border: '#B0D4DC', text: '#2A5A68', headerBg: '#F0F7F9' },
  { accent: '#B84040', bg: '#FFF0F0', border: '#F5C5C5', text: '#8A2020', headerBg: '#FFF5F5' },
  { accent: '#7A5C8A', bg: '#F5F0FA', border: '#D4C0E0', text: '#4A2C6A', headerBg: '#F8F4FC' },
  { accent: '#6A8A3C', bg: '#EFF5E8', border: '#C0D8A0', text: '#3A5C1C', headerBg: '#F3F8ED' },
];

const STATUS_STYLE = {
  alto:          { color: '#B84040', bg: '#FFF0F0', border: '#F5C5C5', arrow: '↑' },
  baixo:         { color: '#2A5A68', bg: '#EBF5F8', border: '#B0D4DC', arrow: '↓' },
  normal:        { color: '#2C4A35', bg: '#EAF2ED', border: '#B8D9C4', arrow: null },
  indisponivel:  { color: '#8FAA96', bg: '#F5F0E8', border: '#DDD8CE', arrow: null },
};

// Detecta se `b` é o complementar percentual/absoluto de `a`.
// Ex: "Neutrófilos" ↔ "Neutrófilos Relativo", "Neutrófilos %", "% Neutrófilos", "Neutrófilos - Valor Abs."
function ePar(nomeA, nomeB) {
  const a = normNome(nomeA);
  const b = normNome(nomeB);
  if (a === b) return false;
  // sufixos/prefixos que indicam a versão complementar
  const extras = [' relativo',' absoluto',' valor abs',' valor absoluto',' abs',' %',' percent','relativo ','absoluto ','% '];
  for (const e of extras) {
    if (b === a + e || b === e.trimStart() + a || a === b + e || a === e.trimStart() + b) return true;
  }
  // b começa com a (ou vice-versa) e a diferença é apenas o sufixo de % ou abs
  if (b.startsWith(a + ' ') || a.startsWith(b + ' ')) return true;
  return false;
}

// Exibe e permite editar inline um valor de exame.
// onSave(novoValor, novoStatus) é chamado quando o médico confirma a edição.
const ExamValorEdit = ({ exame, onSave }) => {
  const [editing, setEditing] = useStateEV(false);
  const [draft, setDraft] = useStateEV('');
  const [draftStatus, setDraftStatus] = useStateEV('');
  const [saving, setSaving] = useStateEV(false);
  const inputRef = useRefEV(null);
  const containerRef = useRefEV(null);
  const blurTimerRef = useRefEV(null);
  const draftRef = useRefEV('');
  const draftStatusRef = useRefEV('');

  const setDraftSynced = (v) => { draftRef.current = v; setDraft(v); };
  const setDraftStatusSynced = (v) => { draftStatusRef.current = v; setDraftStatus(v); };

  const startEdit = () => {
    setDraftSynced(exame.valor || '');
    setDraftStatusSynced(exame.status || 'indisponivel');
    setEditing(true);
    setTimeout(() => inputRef.current && inputRef.current.focus(), 30);
  };

  const cancel = () => {
    clearTimeout(blurTimerRef.current);
    setEditing(false);
  };

  const confirm = async () => {
    if (saving) return;
    const novoValor = draftRef.current.trim();
    const novoStatus = draftStatusRef.current;
    if (novoValor === (exame.valor || '') && novoStatus === exame.status) { setEditing(false); return; }
    setSaving(true);
    try {
      await onSave(novoValor, novoStatus);
      setEditing(false);
    } finally {
      setSaving(false);
    }
  };

  const onKeyDown = (e) => {
    if (e.key === 'Enter') { e.preventDefault(); clearTimeout(blurTimerRef.current); confirm(); }
    if (e.key === 'Escape') cancel();
  };

  // Controla blur com timeout: cancela se o foco vai para outro elemento dentro do container
  const handleBlur = () => {
    blurTimerRef.current = setTimeout(() => {
      if (containerRef.current && !containerRef.current.contains(document.activeElement)) {
        confirm();
      }
    }, 150);
  };

  const handleFocus = () => {
    clearTimeout(blurTimerRef.current);
  };

  if (!editing) {
    const stCur = STATUS_STYLE[exame.status] || STATUS_STYLE.indisponivel;
    return (
      <span
        title="Clique para editar"
        onClick={startEdit}
        style={{display:'inline-flex',alignItems:'center',gap:4,cursor:'pointer',borderRadius:5,padding:'1px 4px',transition:'background 0.15s'}}
        onMouseEnter={e=>e.currentTarget.style.background='rgba(92,138,110,0.1)'}
        onMouseLeave={e=>e.currentTarget.style.background='transparent'}>
        <span style={{fontSize:13,fontWeight:700,color:stCur.color}}>{exame.valor || '—'}</span>
        {exame.unidade && <span style={{fontSize:11,color:'#8FAA96'}}>{exame.unidade}</span>}
        {stCur.arrow && <span style={{fontSize:11,fontWeight:800,color:stCur.color,lineHeight:1}}>{stCur.arrow}</span>}
        <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#8FAA96" strokeWidth="2" style={{opacity:0.6,flexShrink:0}}>
          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
        </svg>
      </span>
    );
  }

  return (
    <span ref={containerRef} style={{display:'inline-flex',alignItems:'center',gap:4,flexWrap:'wrap'}}>
      <input
        ref={inputRef}
        value={draft}
        onChange={e => setDraftSynced(e.target.value)}
        onKeyDown={onKeyDown}
        onBlur={handleBlur}
        onFocus={handleFocus}
        placeholder="valor"
        style={{width:80,padding:'2px 6px',borderRadius:6,border:'1.5px solid #5C8A6E',fontSize:13,fontWeight:700,fontFamily:'inherit',color:'#1A2B1E',outline:'none',background:'#fff'}}
      />
      <select
        value={draftStatus}
        onChange={e => setDraftStatusSynced(e.target.value)}
        onBlur={handleBlur}
        onFocus={handleFocus}
        style={{padding:'2px 4px',borderRadius:6,border:'1.5px solid #5C8A6E',fontSize:11,fontFamily:'inherit',color:'#4A6355',cursor:'pointer',background:'#fff'}}>
        <option value="normal">Normal</option>
        <option value="alto">Alto</option>
        <option value="baixo">Baixo</option>
        <option value="indisponivel">Indisponível</option>
      </select>
      <button
        onMouseDown={e => { e.preventDefault(); clearTimeout(blurTimerRef.current); confirm(); }}
        style={{padding:'2px 8px',borderRadius:6,border:'none',background:'#5C8A6E',color:'#fff',fontSize:11,fontWeight:600,cursor:'pointer',lineHeight:'18px'}}>
        OK
      </button>
      {saving && <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#5C8A6E" strokeWidth="2.5" style={{animation:'evSpin 1s linear infinite'}}><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0"/></svg>}
    </span>
  );
};

// Exibe um campo de exame individual (linha de resultado).
// `par` é opcional: exame complementar (percentual ↔ absoluto) exibido na mesma linha.
// `onSaveExame(idx, novoValor, novoStatus)` persiste a edição no pai.
const ExamRow = ({ exame, par, onSaveExame, onSavePar }) => {
  const st = STATUS_STYLE[exame.status] || STATUS_STYLE.indisponivel;
  const baixaConfianca = exame.confianca === 'baixa' || (par && par.confianca === 'baixa');
  const stPar = par ? (STATUS_STYLE[par.status] || STATUS_STYLE.indisponivel) : null;

  // Decide qual dos dois é o percentual e qual é o absoluto, para ordenar bem.
  const isPercent = (ex) => (ex.unidade || '').includes('%') || normNome(ex.nome).includes('relativ') || normNome(ex.nome).endsWith(' %');
  let primeiro = exame, segundo = par;
  let onSavePrimeiro = onSaveExame, onSaveSegundo = onSavePar;
  if (par && isPercent(exame) && !isPercent(par)) {
    primeiro = par; segundo = exame;
    onSavePrimeiro = onSavePar; onSaveSegundo = onSaveExame;
  }

  const stPrimeiro = STATUS_STYLE[primeiro.status] || STATUS_STYLE.indisponivel;

  return (
    <div style={{padding:'7px 10px',borderRadius:9,background:st.bg,border: baixaConfianca ? '1.5px dashed #C8973A' : `1.5px solid ${st.border}`}}>
      <div style={{display:'flex',alignItems:'center',gap:6,flexWrap:'wrap'}}>
        <span style={{fontSize:12,fontWeight:600,color:'#1A2B1E'}}>{exame.nome}</span>

        {/* Valor principal — editável */}
        {onSavePrimeiro
          ? <ExamValorEdit exame={primeiro} onSave={onSavePrimeiro}/>
          : <>
              <span style={{fontSize:13,fontWeight:700,color:stPrimeiro.color}}>{primeiro.valor || '—'}</span>
              {primeiro.unidade && <span style={{fontSize:11,color:'#8FAA96'}}>{primeiro.unidade}</span>}
              {stPrimeiro.arrow && <span style={{fontSize:11,fontWeight:800,color:stPrimeiro.color,lineHeight:1}}>{stPrimeiro.arrow}</span>}
            </>
        }

        {/* Valor do par (percentual ou absoluto) — editável */}
        {segundo && (
          <>
            <span style={{fontSize:11,color:'#C5BFB2',lineHeight:1,fontWeight:300,marginLeft:1}}>·</span>
            {onSaveSegundo
              ? <ExamValorEdit exame={segundo} onSave={onSaveSegundo}/>
              : <>
                  <span style={{fontSize:12,fontWeight:600,color:stPar.color}}>{segundo.valor || '—'}</span>
                  {segundo.unidade && <span style={{fontSize:11,color:'#8FAA96'}}>{segundo.unidade}</span>}
                  {stPar.arrow && <span style={{fontSize:11,fontWeight:800,color:stPar.color,lineHeight:1}}>{stPar.arrow}</span>}
                </>
            }
          </>
        )}

        {baixaConfianca && (
          <span title="A IA não teve certeza ao ler este valor. Confira na imagem do laudo."
            style={{display:'inline-flex',alignItems:'center',gap:3,marginLeft:'auto',padding:'1px 7px',borderRadius:6,background:'#FBF5E6',border:'1px solid #E8D5A0',fontSize:9.5,fontWeight:700,color:'#8A6A20',letterSpacing:'0.01em',whiteSpace:'nowrap'}}>
            <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8973A" strokeWidth="2.5"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
            Conferir
          </span>
        )}
      </div>
      {(exame.referencia || (par && par.referencia)) && (
        <div style={{fontSize:10,color:'#8FAA96',marginTop:2}}>
          {exame.referencia && <>Ref: {exame.referencia}</>}
          {par && par.referencia && exame.referencia !== par.referencia && <>{exame.referencia ? '  |  ' : 'Ref: '}{par.referencia}</>}
        </div>
      )}
    </div>
  );
};

// Banner de qualidade da imagem — aparece quando a IA classifica a foto como
// regular/ruim, alertando o médico a conferir os valores manualmente.
const QUALIDADE_STYLE = {
  ruim:    { bg:'#FFF0F0', border:'#F5C5C5', color:'#8A2020', chipBg:'#FFFFFF', chipBorder:'#F0CFCF', titulo:'Imagem de baixa qualidade', icone:'⚠️' },
  regular: { bg:'#FBF5E6', border:'#E8D5A0', color:'#8A6A20', chipBg:'#FFFFFF', chipBorder:'#EADFC0', titulo:'Imagem com qualidade regular', icone:'⚠️' },
};

// Quebra a string de observações da IA em itens estruturados e escaneáveis.
// A IA concatena tudo num parágrafo repetindo "Conferir manualmente: <exame> (motivo)".
// Aqui separamos em:
//   - itens: [{ exame, motivo }]  (o que precisa de conferência, um por linha)
//   - notas: []                   (observações livres, ex: "Observação do laudo: ...")
function parseObservacoes(txt) {
  const itens = [];
  const notas = [];
  if (!txt || typeof txt !== 'string') return { itens, notas };

  // Divide o parágrafo antes de cada marcador conhecido, preservando o marcador.
  const marcadores = /(?=Conferir manualmente:|Conferir na imagem[^:]*:|Observa[çc][ãa]o do laudo:)/gi;
  const partes = txt.split(marcadores).map(s => s.trim()).filter(Boolean);

  const addItem = (bruto) => {
    const s = bruto.replace(/\.$/, '').trim();
    if (!s) return;
    // Separa "<exame> (motivo)" quando houver parênteses; senão, tudo vira o exame.
    const m = s.match(/^(.*?)\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)\s*$/);
    if (m) itens.push({ exame: m[1].trim(), motivo: m[2].trim() });
    else itens.push({ exame: s, motivo: '' });
  };

  for (const p of partes) {
    if (/^Conferir manualmente:/i.test(p)) {
      addItem(p.replace(/^Conferir manualmente:\s*/i, ''));
    } else if (/^Conferir na imagem[^:]*:/i.test(p)) {
      // Lista separada por ";" — um exame por chip, motivo comum "leituras divergentes".
      const corpo = p.replace(/^Conferir na imagem[^:]*:\s*/i, '').replace(/\.$/, '');
      corpo.split(/;+/).map(s => s.trim()).filter(Boolean)
        .forEach(nome => itens.push({ exame: nome, motivo: 'leituras divergentes da IA' }));
    } else {
      // Observação livre: guarda como nota geral (sem a etiqueta "Observação do laudo:").
      notas.push(p.replace(/^Observa[çc][ãa]o do laudo:\s*/i, '').trim());
    }
  }
  return { itens, notas };
}

const QualidadeBanner = ({ qualidade, observacoes }) => {
  if (!qualidade || qualidade === 'boa') return null;
  const c = QUALIDADE_STYLE[qualidade] || QUALIDADE_STYLE.regular;
  const { itens, notas } = parseObservacoes(observacoes);
  const [aberto, setAberto] = useStateEV(false);
  const LIMITE = 6;
  const visiveis = aberto ? itens : itens.slice(0, LIMITE);
  const restantes = itens.length - visiveis.length;

  return (
    <div style={{background:c.bg,border:`1.5px solid ${c.border}`,borderRadius:12,padding:'12px 14px'}}>
      <div style={{display:'flex',alignItems:'center',gap:7}}>
        <span style={{fontSize:14,lineHeight:1}}>{c.icone}</span>
        <span style={{fontSize:12,fontWeight:700,color:c.color,flex:1}}>{c.titulo}</span>
        {itens.length > 0 && (
          <span style={{fontSize:10.5,fontWeight:700,color:c.color,background:c.chipBg,border:`1px solid ${c.chipBorder}`,borderRadius:20,padding:'2px 9px',whiteSpace:'nowrap'}}>
            {itens.length} {itens.length === 1 ? 'item a conferir' : 'itens a conferir'}
          </span>
        )}
      </div>

      <div style={{fontSize:11,color:c.color,opacity:0.9,lineHeight:1.5,marginTop:6}}>
        A IA pode ter lido alguns valores incorretamente. Confira estes pontos na imagem do laudo:
      </div>

      {itens.length > 0 && (
        <ul style={{listStyle:'none',margin:'8px 0 0',padding:0,display:'flex',flexDirection:'column',gap:5}}>
          {visiveis.map((it, i) => (
            <li key={i} style={{display:'flex',alignItems:'baseline',gap:8,background:c.chipBg,border:`1px solid ${c.chipBorder}`,borderRadius:9,padding:'7px 10px'}}>
              <span style={{width:5,height:5,borderRadius:'50%',background:c.color,flexShrink:0,transform:'translateY(4px)'}}></span>
              <div style={{minWidth:0}}>
                <span style={{fontSize:11.5,fontWeight:700,color:c.color}}>{it.exame}</span>
                {it.motivo && (
                  <span style={{fontSize:11,color:c.color,opacity:0.72,marginLeft:6}}>— {it.motivo}</span>
                )}
              </div>
            </li>
          ))}
        </ul>
      )}

      {itens.length > LIMITE && (
        <button onClick={() => setAberto(a => !a)}
          style={{marginTop:8,border:'none',background:'none',cursor:'pointer',fontFamily:'inherit',fontSize:11,fontWeight:700,color:c.color,padding:0,textDecoration:'underline',textUnderlineOffset:2}}>
          {aberto ? 'Mostrar menos' : `Ver mais ${restantes} ${restantes === 1 ? 'item' : 'itens'}`}
        </button>
      )}

      {notas.length > 0 && (
        <div style={{fontSize:11,color:c.color,marginTop:10,paddingTop:8,borderTop:`1px solid ${c.border}`,lineHeight:1.5}}>
          {notas.map((n, i) => (
            <div key={i} style={{marginTop:i ? 4 : 0}}>{n}</div>
          ))}
        </div>
      )}

      {/* Fallback: se nada foi parseado mas há texto, mostra o original. */}
      {itens.length === 0 && notas.length === 0 && observacoes && (
        <div style={{fontSize:11,color:c.color,marginTop:6,lineHeight:1.5,whiteSpace:'pre-wrap'}}>{observacoes}</div>
      )}
    </div>
  );
};

// Agrupa pares percentual+absoluto para o mesmo item (ex: "Neutrófilos" + "Neutrófilos Relativo").
// Retorna array de { principal, par|null, principalIdx, parIdx } com os índices originais
// para que callbacks de edição possam referenciar o exame correto no array.
function agrupaPares(exames) {
  const usados = new Set();
  const grupos = [];
  for (let i = 0; i < exames.length; i++) {
    if (usados.has(i)) continue;
    const ex = exames[i];
    let parIdx = -1;
    for (let j = i + 1; j < exames.length; j++) {
      if (!usados.has(j) && ePar(ex.nome, exames[j].nome)) { parIdx = j; break; }
    }
    if (parIdx !== -1) {
      usados.add(parIdx);
      grupos.push({ principal: ex, par: exames[parIdx], principalIdx: i, parIdx });
    } else {
      grupos.push({ principal: ex, par: null, principalIdx: i, parIdx: -1 });
    }
    usados.add(i);
  }
  return grupos;
}

// Bloco de uma categoria (Hemograma, Leucograma, etc.)
// onSaveExame(exameIdx, novoValor, novoStatus) — callback para salvar edição de um exame.
const LabBlock = ({ block, colorIdx, onSaveExame }) => {
  const c = BLOCK_COLORS[colorIdx % BLOCK_COLORS.length];
  const exames = Array.isArray(block.exames) ? block.exames : [];
  const [open, setOpen] = useStateEV(true);
  const grupos = agrupaPares(exames);
  return (
    <div style={{background:'#fff',border:`1.5px solid ${c.border}`,borderRadius:14,overflow:'hidden',boxShadow:'0 2px 8px rgba(30,51,38,0.05)'}}>
      <div style={{display:'flex',alignItems:'center',background:c.headerBg,borderBottom:open?`1px solid ${c.border}`:'none'}}>
        <button onClick={() => setOpen(o => !o)}
          style={{flex:1,display:'flex',alignItems:'center',gap:10,padding:'10px 16px',cursor:'pointer',border:'none',background:'none',fontFamily:'inherit',textAlign:'left'}}>
          <div style={{width:3,height:16,borderRadius:2,background:c.accent,flexShrink:0}}></div>
          <span style={{fontSize:12,fontWeight:700,color:c.text,flex:1}}>{block.categoria || 'Resultados'}</span>
          {block.data_coleta && (
            <span style={{fontSize:11,color:c.text,opacity:0.7}}>{fmtDate(block.data_coleta)}</span>
          )}
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={c.accent} strokeWidth="2.5"
            style={{flexShrink:0,transition:'transform 0.2s',transform:open?'rotate(0deg)':'rotate(-90deg)'}}>
            <polyline points="6 9 12 15 18 9"/>
          </svg>
        </button>
        <div style={{paddingRight:10}}>
          <CopyBtn getText={() => fmtLabBlock(block)} title={`Copiar ${block.categoria || 'seção'}`}/>
        </div>
      </div>
      {open && (
        <div style={{padding:'12px 14px',display:'flex',flexDirection:'column',gap:6}}>
          {grupos.length === 0
            ? <p style={{fontSize:12,color:'#8FAA96',fontStyle:'italic',margin:0}}>Sem dados</p>
            : grupos.map(({ principal, par, principalIdx, parIdx }, i) => (
                <ExamRow key={i} exame={principal} par={par}
                  onSaveExame={onSaveExame ? (v, s) => onSaveExame(principalIdx, v, s) : null}
                  onSavePar={onSaveExame && par ? (v, s) => onSaveExame(parIdx, v, s) : null}/>
              ))
          }
        </div>
      )}
    </div>
  );
};

// Bloco de demais exames — suporta formato antigo {nome,valor,...} e novo igual
const DemaisExamesBlock = ({ demais }) => {
  const items = Array.isArray(demais) ? demais.filter(e => e && e.nome !== '_meta') : [];
  const [open, setOpen] = useStateEV(true);
  if (items.length === 0) return null;
  return (
    <div style={{background:'#fff',border:'1.5px solid #DDD8CE',borderRadius:14,overflow:'hidden',boxShadow:'0 2px 8px rgba(30,51,38,0.05)'}}>
      <div style={{display:'flex',alignItems:'center',background:'#FBF5E6',borderBottom:open?'1px solid #DDD8CE':'none'}}>
        <button onClick={() => setOpen(o => !o)}
          style={{flex:1,display:'flex',alignItems:'center',gap:10,padding:'10px 16px',cursor:'pointer',border:'none',background:'none',fontFamily:'inherit',textAlign:'left'}}>
          <div style={{width:3,height:16,borderRadius:2,background:'#C8973A',flexShrink:0}}></div>
          <span style={{fontSize:12,fontWeight:700,color:'#8A6A20',flex:1}}>Demais exames</span>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#C8973A" strokeWidth="2.5"
            style={{flexShrink:0,transition:'transform 0.2s',transform:open?'rotate(0deg)':'rotate(-90deg)'}}>
            <polyline points="6 9 12 15 18 9"/>
          </svg>
        </button>
        <div style={{paddingRight:10}}>
          <CopyBtn getText={() => fmtDemaisBlock(items)} title="Copiar Demais exames"/>
        </div>
      </div>
      {open && (
        <div style={{padding:'12px 14px',display:'flex',flexDirection:'column',gap:6}}>
          {items.map((item, i) => <ExamRow key={i} exame={item}/>)}
        </div>
      )}
    </div>
  );
};

const ZOOM_MIN = 0.5;
const ZOOM_MAX = 4;
const ZOOM_STEP = 0.25;

// Aviso exibido quando o arquivo original não carrega mais do armazenamento
// (ex.: removido/expirado) — os dados já extraídos pela IA continuam intactos.
const FileUnavailable = ({ compact }) => (
  <div style={{padding: compact ? '10px 8px' : '40px 20px',textAlign:'center'}}>
    <div style={{fontSize: compact ? 18 : 28,marginBottom:8}}>⚠️</div>
    <div style={{fontSize: compact ? 10 : 11,color:'#8FAA96',lineHeight:1.4}}>
      Arquivo original indisponível.<br/>A leitura da IA continua abaixo.
    </div>
  </div>
);

const DocViewer = ({ files, activeFile, setActiveFile, onDeleteFile, deletingId, aiPanel }) => {
  const [lightboxOpen, setLightboxOpen] = useStateEV(false);
  const [zoom, setZoom] = useStateEV(1);
  const [pan, setPan] = useStateEV({ x: 0, y: 0 });
  const [hoverId, setHoverId] = useStateEV(null); // thumbnail sob o mouse → mostra o ×
  const [cmpView, setCmpView] = useStateEV('split'); // no mobile: 'split' | 'doc' | 'ia'
  const drag = useRefEV({ active: false, startX: 0, startY: 0, panX: 0, panY: 0 });
  // ids de arquivos cujo blob original não carregou (removido/expirado no storage) —
  // mostra aviso amigável em vez do erro cru do provedor de armazenamento.
  const [brokenFiles, setBrokenFiles] = useStateEV(() => new Set());
  const markBroken = (id) => setBrokenFiles(prev => prev.has(id) ? prev : new Set(prev).add(id));

  // O arquivo exibido no lightbox é sempre o activeFile — assim as setas de
  // navegação apenas trocam o activeFile e o modal acompanha, sem fechar/abrir.
  const lightbox = lightboxOpen ? activeFile : null;

  const resetView = () => { setZoom(1); setPan({ x: 0, y: 0 }); };
  const openLightbox = (file) => { setActiveFile(file); setLightboxOpen(true); resetView(); };
  const closeLightbox = () => { setLightboxOpen(false); resetView(); };
  const zoomIn  = (e) => { e.stopPropagation(); setZoom(z => Math.min(z + ZOOM_STEP, ZOOM_MAX)); };
  const zoomOut = (e) => { e.stopPropagation(); setZoom(z => Math.max(z - ZOOM_STEP, ZOOM_MIN)); };
  const resetZoom = (e) => { e.stopPropagation(); resetView(); };

  // Índice do arquivo ativo e navegação anterior/próximo (para nas pontas —
  // sem circular: no primeiro arquivo não volta, no último não avança).
  const activeIdx = files.findIndex(f => f.id === activeFile?.id);
  const hasNav = files.length > 1;
  const base = activeIdx === -1 ? 0 : activeIdx;
  const canPrev = hasNav && base > 0;
  const canNext = hasNav && base < files.length - 1;
  const goToOffset = (delta, e) => {
    if (e) e.stopPropagation();
    if (!files.length) return;
    const next = base + delta;
    if (next < 0 || next >= files.length) return; // parou na ponta
    setActiveFile(files[next]);
    resetView();
  };
  const goPrev = (e) => goToOffset(-1, e);
  const goNext = (e) => goToOffset(1, e);

  // Teclado: ← → navegam entre arquivos, Esc fecha (só com o lightbox aberto).
  useEffectEV(() => {
    if (!lightboxOpen) return;
    const onKey = (e) => {
      if (e.key === 'Escape') closeLightbox();
      else if (e.key === 'ArrowLeft') goPrev();
      else if (e.key === 'ArrowRight') goNext();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [lightboxOpen, activeIdx, files.length]);

  const handleWheel = (e) => {
    if (lightbox?.type !== 'image') return;
    e.preventDefault();
    setZoom(z => Math.min(Math.max(z - e.deltaY * 0.001, ZOOM_MIN), ZOOM_MAX));
  };

  const onMouseDown = (e) => {
    if (lightbox?.type !== 'image') return;
    e.preventDefault();
    drag.current = { active: true, startX: e.clientX, startY: e.clientY, panX: pan.x, panY: pan.y };
  };
  const onMouseMove = (e) => {
    if (!drag.current.active) return;
    setPan({ x: drag.current.panX + e.clientX - drag.current.startX, y: drag.current.panY + e.clientY - drag.current.startY });
  };
  const onMouseUp = () => { drag.current.active = false; };

  return (
    <div style={{display:'flex',flexDirection:'column',height:'100%',overflow:'hidden'}}>

      {/* Modo de comparação: documento à esquerda, leitura da IA à direita.
          Permite conferir se a IA leu/interpretou corretamente. */}
      {lightbox && (
        <div
          style={{position:'fixed',inset:0,zIndex:9999,background:'rgba(10,20,14,0.92)',display:'flex',flexDirection:'column',overflow:'hidden',userSelect:'none'}}>

          {/* Barra superior */}
          <div style={{display:'flex',alignItems:'center',gap:10,padding:'10px 14px',background:'rgba(0,0,0,0.35)',flexShrink:0,zIndex:2}}>
            <span style={{fontSize:13,fontWeight:600,color:'#fff',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',flex:1,minWidth:0}}>{lightbox.name}</span>

            {/* Contador de arquivos (ex: "2 / 5") — só com mais de um arquivo */}
            {hasNav && (
              <span style={{fontSize:12,fontWeight:600,color:'rgba(255,255,255,0.7)',flexShrink:0,whiteSpace:'nowrap',fontVariantNumeric:'tabular-nums'}}>
                {(activeIdx === -1 ? 0 : activeIdx) + 1} / {files.length}
              </span>
            )}

            {/* Controles de zoom na barra superior — sempre visíveis (nunca cobertos
                pela imagem ampliada). Só aparecem quando o arquivo é imagem. */}
            {lightbox.type === 'image' && cmpView !== 'ia' && (
              <div style={{display:'flex',alignItems:'center',gap:6,background:'rgba(255,255,255,0.1)',borderRadius:9,padding:3,flexShrink:0}}>
                <button onClick={zoomOut}
                  style={{width:28,height:28,borderRadius:7,border:'none',background:'rgba(255,255,255,0.12)',color:'#fff',fontSize:18,cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',lineHeight:1,fontWeight:300}}
                  title="Reduzir">−</button>
                <span onClick={resetZoom}
                  style={{fontSize:12,fontWeight:600,color:'#fff',minWidth:42,textAlign:'center',cursor:'pointer',userSelect:'none'}}
                  title="Resetar zoom">{Math.round(zoom * 100)}%</span>
                <button onClick={zoomIn}
                  style={{width:28,height:28,borderRadius:7,border:'none',background:'rgba(255,255,255,0.12)',color:'#fff',fontSize:18,cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',lineHeight:1,fontWeight:300}}
                  title="Ampliar">+</button>
              </div>
            )}

            {/* Alternador de visão — só aparece em telas estreitas (controlado via CSS) */}
            <div className="cmp-toggle" style={{display:'none',gap:4,background:'rgba(255,255,255,0.1)',borderRadius:9,padding:3}}>
              {[{id:'doc',label:'Documento'},{id:'split',label:'Lado a lado'},{id:'ia',label:'Leitura IA'}].map(o=>(
                <button key={o.id} onClick={()=>setCmpView(o.id)}
                  style={{padding:'5px 9px',borderRadius:7,border:'none',cursor:'pointer',fontFamily:'inherit',fontSize:11,fontWeight:600,background:cmpView===o.id?'#5C8A6E':'transparent',color:cmpView===o.id?'#fff':'rgba(255,255,255,0.7)',transition:'all 0.15s'}}>
                  {o.label}
                </button>
              ))}
            </div>

            <button onClick={closeLightbox}
              style={{width:34,height:34,borderRadius:8,border:'none',background:'rgba(255,255,255,0.12)',color:'#fff',fontSize:20,cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',lineHeight:1,flexShrink:0}}
              title="Fechar">×</button>
          </div>

          {/* Corpo: duas colunas */}
          <div className={`cmp-body cmp-${cmpView}`} style={{flex:1,display:'flex',overflow:'hidden'}}>

            {/* Coluna do documento — 55% */}
            <div className="cmp-doc"
              onWheel={lightbox.type === 'image' ? handleWheel : undefined}
              onMouseMove={onMouseMove}
              onMouseUp={onMouseUp}
              onMouseLeave={onMouseUp}
              style={{flex:'0 0 55%',position:'relative',display:'flex',flexDirection:'column',overflow:'hidden',background:'#1a1a1a'}}>

              {/* Setas de navegação entre arquivos — sobrepostas nas laterais.
                  Trocam o arquivo exibido sem fechar o visualizador. */}
              {hasNav && (
                <>
                  <button onClick={goPrev} disabled={!canPrev} title="Arquivo anterior (←)" aria-label="Arquivo anterior"
                    style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)',zIndex:3,width:44,height:44,borderRadius:'50%',border:'none',background:'rgba(0,0,0,0.5)',color:'#fff',cursor:canPrev?'pointer':'default',display:'flex',alignItems:'center',justifyContent:'center',boxShadow:'0 2px 10px rgba(0,0,0,0.35)',opacity:canPrev?1:0.3,transition:'background 0.15s, opacity 0.15s'}}
                    onMouseEnter={e=>{if(canPrev)e.currentTarget.style.background='rgba(0,0,0,0.75)';}}
                    onMouseLeave={e=>e.currentTarget.style.background='rgba(0,0,0,0.5)'}>
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="15 18 9 12 15 6"/></svg>
                  </button>
                  <button onClick={goNext} disabled={!canNext} title="Próximo arquivo (→)" aria-label="Próximo arquivo"
                    style={{position:'absolute',right:12,top:'50%',transform:'translateY(-50%)',zIndex:3,width:44,height:44,borderRadius:'50%',border:'none',background:'rgba(0,0,0,0.5)',color:'#fff',cursor:canNext?'pointer':'default',display:'flex',alignItems:'center',justifyContent:'center',boxShadow:'0 2px 10px rgba(0,0,0,0.35)',opacity:canNext?1:0.3,transition:'background 0.15s, opacity 0.15s'}}
                    onMouseEnter={e=>{if(canNext)e.currentTarget.style.background='rgba(0,0,0,0.75)';}}
                    onMouseLeave={e=>e.currentTarget.style.background='rgba(0,0,0,0.5)'}>
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="9 18 15 12 9 6"/></svg>
                  </button>
                </>
              )}

              {brokenFiles.has(lightbox.id)
                ? <div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center'}}>
                    <div style={{background:'rgba(255,255,255,0.06)',borderRadius:14,padding:'8px 16px'}}>
                      <FileUnavailable/>
                    </div>
                  </div>
                : lightbox.type === 'image'
                  ? <div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',overflow:'hidden'}}>
                      <img
                        src={lightbox.url} alt={lightbox.name}
                        onMouseDown={onMouseDown}
                        onError={()=>markBroken(lightbox.id)}
                        style={{
                          display:'block',
                          maxWidth:'96%',
                          maxHeight:'96%',
                          objectFit:'contain',
                          transform:`translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
                          transformOrigin:'center center',
                          transition: drag.current.active ? 'none' : 'transform 0.15s',
                          cursor: zoom > 1 ? 'grab' : 'default',
                          borderRadius:6,
                          userSelect:'none',
                        }}
                      />
                    </div>
                  : <iframe
                      src={lightbox.url + '#toolbar=1&navpanes=0&scrollbar=1&view=FitH'}
                      title={lightbox.name}
                      onError={()=>markBroken(lightbox.id)}
                      style={{flex:1,width:'100%',height:'100%',border:'none',display:'block'}}/>
              }
            </div>

            {/* Divisor visual */}
            <div style={{width:3,background:'rgba(255,255,255,0.08)',flexShrink:0}}/>

            {/* Coluna da leitura da IA — 45% */}
            <div className="cmp-ia"
              style={{flex:'0 0 45%',background:'#F8F5EF',overflowY:'auto',display:'flex',flexDirection:'column'}}>
              <div style={{position:'sticky',top:0,background:'#fff',borderBottom:'1px solid #EDE9E2',padding:'10px 16px',fontSize:12,fontWeight:700,color:'#2C4A35',fontFamily:'Lora, serif',zIndex:1,flexShrink:0}}>
                Leitura da IA <span style={{fontWeight:400,color:'#8FAA96'}}>— confira contra o documento</span>
              </div>
              <div style={{flex:1,padding:14,display:'flex',flexDirection:'column',gap:10}}>
                {aiPanel}
              </div>
            </div>
          </div>
        </div>
      )}

      {/* Tira de thumbnails */}
      <div style={{display:'flex',gap:8,padding:'12px',borderBottom:'1px solid #EDE9E2',overflowX:'auto',flexShrink:0,background:'#fff'}}>
        {files.map(f => {
          const active = activeFile?.id === f.id;
          const isDeleting = deletingId === f.id;
          // Mostra o × no hover (desktop) ou quando o thumbnail está selecionado
          // (garante acesso em telas de toque, onde não há hover).
          const showX = hoverId === f.id || active;
          return (
            <div key={f.id}
              onMouseEnter={()=>setHoverId(f.id)}
              onMouseLeave={()=>setHoverId(h=>h===f.id?null:h)}
              style={{position:'relative',flexShrink:0,opacity:isDeleting?0.5:1,transition:'opacity 0.2s'}}>
              <button onClick={()=>setActiveFile(f)} disabled={isDeleting}
                style={{display:'flex',flexDirection:'column',alignItems:'center',gap:4,padding:'6px',borderRadius:10,border:`1.5px solid ${active?'#5C8A6E':'#EDE9E2'}`,background:active?'#EAF2ED':'#F8F5EF',cursor:isDeleting?'default':'pointer',transition:'all 0.15s',fontFamily:'inherit'}}>
                <div style={{position:'relative',width:44,height:54,borderRadius:6,overflow:'hidden',background:f.type==='pdf'?'#FFF0F0':'#EAF2ED',display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>
                  {brokenFiles.has(f.id)
                    ? <span style={{fontSize:16}} title="Arquivo original indisponível">⚠️</span>
                    : f.type === 'image' && f.url
                      ? <img src={f.url} alt={f.name} onError={()=>markBroken(f.id)} style={{width:'100%',height:'100%',objectFit:'cover'}}/>
                      : <span style={{fontSize:10,fontWeight:800,color:'#B84040',letterSpacing:'0.02em'}}>PDF</span>
                  }
                  {/* Spinner sobre a miniatura durante a exclusão */}
                  {isDeleting && (
                    <div style={{position:'absolute',inset:0,background:'rgba(255,255,255,0.6)',display:'flex',alignItems:'center',justifyContent:'center'}}>
                      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#B84040" strokeWidth="2.5" style={{animation:'evSpin 1s linear infinite'}}><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0"/></svg>
                    </div>
                  )}
                </div>
                <span style={{fontSize:9,color:'#8FAA96',maxWidth:52,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',textAlign:'center'}}>{f.name.split('.')[0]}</span>
              </button>
              {/* Botão de apagar — aparece no hover, esconde durante a exclusão */}
              {!isDeleting && (
                <button onClick={e=>{e.stopPropagation();onDeleteFile(f);}}
                  title="Apagar arquivo"
                  style={{position:'absolute',top:-6,right:-6,width:22,height:22,borderRadius:'50%',border:'2px solid #fff',background:'#B84040',color:'#fff',cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',lineHeight:1,padding:0,zIndex:2,boxShadow:'0 2px 6px rgba(184,64,64,0.4)',opacity:showX?1:0,transform:showX?'scale(1)':'scale(0.7)',transition:'opacity 0.15s, transform 0.15s, background 0.15s'}}
                  onMouseEnter={e=>e.currentTarget.style.background='#A03333'}
                  onMouseLeave={e=>e.currentTarget.style.background='#B84040'}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                </button>
              )}
            </div>
          );
        })}
      </div>

      {/* Preview do arquivo selecionado */}
      <div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',background:'#F8F5EF',padding:16,overflow:'auto'}}>
        {activeFile ? (
          <div style={{background:'#fff',borderRadius:14,boxShadow:'0 4px 20px rgba(30,51,38,0.08)',width:'100%',maxWidth:280,overflow:'hidden',border:'1px solid #EDE9E2',cursor:'pointer'}}
            onClick={()=>activeFile.url && openLightbox(activeFile)}>
            <div style={{height:3,background:activeFile.type==='pdf'?'#B84040':'#5C8A6E'}}></div>

            {/* Área de preview */}
            <div style={{position:'relative',background:'#F8F5EF',minHeight:180,display:'flex',alignItems:'center',justifyContent:'center'}}>
              {brokenFiles.has(activeFile.id)
                ? <FileUnavailable compact/>
                : activeFile.type === 'image' && activeFile.url
                  ? <img src={activeFile.url} alt={activeFile.name}
                      onError={()=>markBroken(activeFile.id)}
                      style={{width:'100%',maxHeight:240,objectFit:'contain',display:'block'}}/>
                  : activeFile.type === 'pdf' && activeFile.url
                    ? <iframe src={activeFile.url + '#toolbar=0&navpanes=0'} title={activeFile.name}
                        onError={()=>markBroken(activeFile.id)}
                        style={{width:'100%',height:240,border:'none',pointerEvents:'none'}}/>
                    : <div style={{padding:'40px 20px',textAlign:'center'}}>
                        <div style={{fontSize:28,marginBottom:8}}>📄</div>
                        <div style={{fontSize:11,color:'#8FAA96'}}>Pré-visualização indisponível</div>
                      </div>
              }
              {/* Overlay clicável para expandir */}
              {activeFile.url && (
                <div style={{position:'absolute',inset:0,display:'flex',alignItems:'center',justifyContent:'center',opacity:0,transition:'opacity 0.2s',background:'rgba(28,43,26,0.35)',borderRadius:0}}
                  onMouseEnter={e=>e.currentTarget.style.opacity='1'}
                  onMouseLeave={e=>e.currentTarget.style.opacity='0'}>
                  <div style={{background:'rgba(255,255,255,0.9)',borderRadius:8,padding:'6px 12px',fontSize:11,fontWeight:600,color:'#1A2B1E',display:'flex',alignItems:'center',gap:4}}>
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
                    Expandir
                  </div>
                </div>
              )}
            </div>

            <div style={{borderTop:'1px solid #F5F0E8',padding:'8px 14px',display:'flex',alignItems:'center',justifyContent:'space-between'}}>
              <span style={{fontSize:10,color:'#8FAA96',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',flex:1}}>{activeFile.name}</span>
              <span style={{fontSize:10,color:'#8FAA96',flexShrink:0,marginLeft:6}}>{formatBytes(activeFile.size)}</span>
            </div>
          </div>
        ) : (
          <span style={{fontSize:13,color:'#8FAA96'}}>Selecione um arquivo</span>
        )}
      </div>
    </div>
  );
};

// Modal de confirmação de exclusão de ARQUIVO — nome distinto para não colidir
// com o ConfirmDeleteModal de doctor.jsx (ambos vivem no mesmo escopo global).
const FileDeleteModal = ({ file, deleting, error, onCancel, onConfirm }) => {
  if (!file) return null;
  return (
    <div
      onClick={deleting ? undefined : onCancel}
      style={{position:'fixed',inset:0,zIndex:10000,background:'rgba(10,20,14,0.55)',backdropFilter:'blur(3px)',display:'flex',alignItems:'center',justifyContent:'center',padding:20,animation:'evFade 0.18s ease'}}>
      <div onClick={e=>e.stopPropagation()}
        style={{background:'#fff',borderRadius:18,boxShadow:'0 16px 50px rgba(10,20,14,0.3)',width:'100%',maxWidth:340,overflow:'hidden',animation:'evPop 0.2s cubic-bezier(0.16,1,0.3,1)'}}>

        {/* Topo: ícone de alerta + miniatura */}
        <div style={{padding:'22px 22px 16px',display:'flex',flexDirection:'column',alignItems:'center',textAlign:'center'}}>
          <div style={{width:48,height:48,borderRadius:14,background:'#FFF0F0',display:'flex',alignItems:'center',justifyContent:'center',marginBottom:14}}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#B84040" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
          </div>
          <div style={{fontSize:15,fontWeight:700,color:'#1A2B1E',fontFamily:'Lora, serif',marginBottom:5}}>Apagar este arquivo?</div>
          <div style={{fontSize:12,color:'#8FAA96',lineHeight:1.5}}>Esta ação não pode ser desfeita. O arquivo será removido permanentemente do envio.</div>
        </div>

        {/* Cartão do arquivo */}
        <div style={{margin:'0 22px',padding:'10px 12px',display:'flex',alignItems:'center',gap:10,background:'#F8F5EF',border:'1px solid #EDE9E2',borderRadius:12}}>
          <div style={{width:34,height:42,borderRadius:6,overflow:'hidden',background:file.type==='pdf'?'#FFF0F0':'#EAF2ED',display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}>
            {file.type === 'image' && file.url
              ? <img src={file.url} alt={file.name} style={{width:'100%',height:'100%',objectFit:'cover'}}/>
              : <span style={{fontSize:9,fontWeight:800,color:'#B84040'}}>PDF</span>}
          </div>
          <div style={{minWidth:0,flex:1}}>
            <div style={{fontSize:12,fontWeight:600,color:'#1A2B1E',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{file.name}</div>
            {file.size != null && <div style={{fontSize:10,color:'#8FAA96',marginTop:1}}>{formatBytes(file.size)}</div>}
          </div>
        </div>

        {/* Erro */}
        {error && (
          <div style={{margin:'12px 22px 0',display:'flex',alignItems:'center',gap:8,background:'#FFF0F0',border:'1px solid #F5C5C5',borderRadius:10,padding:'9px 12px',fontSize:11.5,color:'#B84040',lineHeight:1.5}}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{flexShrink:0}}><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
            Não foi possível apagar. Tente novamente.
          </div>
        )}

        {/* Ações */}
        <div style={{display:'flex',gap:8,padding:'18px 22px 20px'}}>
          <button onClick={onCancel} disabled={deleting}
            style={{flex:1,padding:'10px',borderRadius:10,border:'1.5px solid #DDD8CE',background:'#fff',cursor:deleting?'default':'pointer',fontFamily:'inherit',fontSize:12,fontWeight:600,color:'#4A6355',opacity:deleting?0.5:1,transition:'all 0.15s'}}
            onMouseEnter={e=>{if(!deleting)e.currentTarget.style.background='#F8F5EF';}}
            onMouseLeave={e=>{e.currentTarget.style.background='#fff';}}>
            Cancelar
          </button>
          <button onClick={onConfirm} disabled={deleting}
            style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',gap:6,padding:'10px',borderRadius:10,border:'none',background:deleting?'#C97A7A':'#B84040',cursor:deleting?'default':'pointer',fontFamily:'inherit',fontSize:12,fontWeight:600,color:'#fff',transition:'all 0.15s'}}
            onMouseEnter={e=>{if(!deleting)e.currentTarget.style.background='#A03333';}}
            onMouseLeave={e=>{if(!deleting)e.currentTarget.style.background='#B84040';}}>
            {deleting
              ? <><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{animation:'evSpin 1s linear infinite'}}><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0"/></svg>Apagando...</>
              : 'Apagar'}
          </button>
        </div>
      </div>
    </div>
  );
};

// Conteúdo da leitura da IA — extraído para ser reaproveitado tanto no painel
// direito normal quanto no modo de comparação (lightbox lado a lado).
// onSaveLabExame(blockIdx, exameIdx, novoValor, novoStatus) → persiste edição.
const AIReadout = ({ status, hasAI, qualidadeImagem, observacoesIA, labBlocks, demais, onSaveLabExame }) => {
  if (!hasAI) {
    return status === 'erro' ? (
      <div style={{textAlign:'center',padding:'60px 20px'}}>
        <div style={{fontSize:32,marginBottom:12}}>⚠️</div>
        <div style={{fontSize:14,fontWeight:600,color:'#B84040',marginBottom:6,fontFamily:'Lora, serif'}}>Não foi possível processar o documento</div>
        <div style={{fontSize:12,color:'#8FAA96',lineHeight:1.6}}>A IA não conseguiu extrair os dados desta imagem.<br/>Verifique o arquivo na aba de documentos ao lado.</div>
      </div>
    ) : (
      <div style={{textAlign:'center',padding:'60px 20px'}}>
        <div style={{fontSize:32,marginBottom:12}}>⏳</div>
        <div style={{fontSize:14,fontWeight:600,color:'#4A6355',marginBottom:6,fontFamily:'Lora, serif'}}>Aguardando processamento da IA</div>
        <div style={{fontSize:12,color:'#8FAA96',lineHeight:1.6}}>Os documentos foram recebidos.<br/>Os resultados aparecerão aqui em breve.</div>
      </div>
    );
  }
  return (
    <>
      <QualidadeBanner qualidade={qualidadeImagem} observacoes={observacoesIA}/>
      {labBlocks.map((block, i) => (
        <LabBlock key={i} block={block} colorIdx={i}
          onSaveExame={onSaveLabExame ? (exIdx, v, s) => onSaveLabExame(i, exIdx, v, s) : null}/>
      ))}
      {demais.length > 0 && (
        <DemaisExamesBlock demais={demais}/>
      )}
    </>
  );
};

const ExamViewer = ({ submission, onBack, onOpenPatient }) => {
  const [files, setFiles]         = useStateEV(Array.isArray(submission.files) ? submission.files : []);
  const [labBlocks, setLabBlocks] = useStateEV(reclassificarLabBlocks(Array.isArray(submission.labBlocks) ? submission.labBlocks : []));
  const [demais, setDemais]       = useStateEV(Array.isArray(submission.demaisExames) ? submission.demaisExames : []);

  const [activeFile, setActiveFile] = useStateEV(files[0] || null);
  const [tab, setTab]               = useStateEV('docs');
  const [confirmFile, setConfirmFile] = useStateEV(null); // arquivo no modal de confirmação
  const [deleting, setDeleting]     = useStateEV(null);   // fileId sendo deletado
  const [deleteError, setDeleteError] = useStateEV(false); // erro ao apagar (exibido no modal)
  const [reanalyzing, setReanalyzing] = useStateEV(false);
  const [reanalyzeMsg, setReanalyzeMsg] = useStateEV(null);

  const hasAI = labBlocks.length > 0 || demais.length > 0;

  // Metadados de qualidade da imagem (ver backend).
  const meta = (submission._meta) || {};
  const qualidadeImagem = meta.qualidade_imagem || null;
  const observacoesIA   = meta.observacoes || '';

  // Edição de exame de lab_block pelo médico.
  // Atualiza o estado local imediatamente e persiste via PATCH em background.
  const handleSaveLabExame = async (blockIdx, exameIdx, novoValor, novoStatus) => {
    const novosBlocks = labBlocks.map((b, bi) => {
      if (bi !== blockIdx) return b;
      return {
        ...b,
        exames: (b.exames || []).map((ex, ei) => {
          if (ei !== exameIdx) return ex;
          return { ...ex, valor: novoValor, status: novoStatus, confianca: 'alta' };
        }),
      };
    });
    setLabBlocks(novosBlocks);
    await window.API.submissions.patch(submission.id, { lab_blocks: novosBlocks });
  };

  // Abre o modal de confirmação (não apaga ainda).
  const requestDeleteFile = (file) => { setDeleteError(false); setConfirmFile(file); };

  // Executado pelo botão "Apagar" do modal.
  const confirmDeleteFile = async () => {
    const file = confirmFile;
    if (!file) return;
    setDeleteError(false);
    setDeleting(file.id);
    try {
      await window.API.files.remove(file.id);
      const next = files.filter(f => f.id !== file.id);
      setFiles(next);
      if (activeFile?.id === file.id) setActiveFile(next[0] || null);
      setConfirmFile(null);
    } catch (err) {
      console.error('[ExamViewer] erro ao apagar arquivo', err.message);
      setDeleteError(true);
    } finally {
      setDeleting(null);
    }
  };

  const handleReanalyze = async () => {
    setReanalyzing(true);
    setReanalyzeMsg(null);
    try {
      const result = await window.API.reanalyze(submission.id);
      setLabBlocks(reclassificarLabBlocks(result.lab_blocks || []));
      setDemais(result.demais_exames || []);
      setReanalyzeMsg('ok');
      setTab('interp');
    } catch (err) {
      console.error('[ExamViewer] erro ao reanalisar', err.message);
      setReanalyzeMsg('erro');
    } finally {
      setReanalyzing(false);
      setTimeout(() => setReanalyzeMsg(null), 3000);
    }
  };

  return (
    <div style={{height:'100%',display:'flex',flexDirection:'column',overflow:'hidden'}}>

      <style>{`
        @keyframes evSpin{to{transform:rotate(360deg)}}
        @keyframes evFade{from{opacity:0}to{opacity:1}}
        @keyframes evPop{from{opacity:0;transform:scale(0.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}
      `}</style>

      {/* Modal de confirmação de exclusão de arquivo */}
      <FileDeleteModal
        file={confirmFile}
        deleting={deleting === confirmFile?.id}
        error={deleteError}
        onCancel={() => { if (!deleting) { setConfirmFile(null); setDeleteError(false); } }}
        onConfirm={confirmDeleteFile}/>

      {/* Header */}
      <div style={{display:'flex',alignItems:'center',gap:12,padding:'10px 16px',background:'#fff',borderBottom:'1px solid #EDE9E2',flexShrink:0}}>
        <button onClick={onBack}
          style={{display:'flex',alignItems:'center',gap:7,height:36,padding:'0 16px',borderRadius:9,border:'1.5px solid #DDD8CE',background:'#fff',cursor:'pointer',color:'#4A6355',flexShrink:0,fontFamily:'inherit',fontSize:13,fontWeight:600,transition:'all 0.15s'}}
          onMouseEnter={e=>{e.currentTarget.style.background='#EAF2ED';e.currentTarget.style.color='#1A2B1E';}}
          onMouseLeave={e=>{e.currentTarget.style.background='#fff';e.currentTarget.style.color='#4A6355';}}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{flexShrink:0}}><polyline points="15 18 9 12 15 6"/></svg>
          Voltar
        </button>
        <div style={{flex:1,minWidth:0}}>
          {onOpenPatient ? (
            <button onClick={onOpenPatient}
              title="Ver perfil do paciente"
              style={{display:'flex',alignItems:'center',gap:5,maxWidth:'100%',padding:0,border:'none',background:'none',cursor:'pointer',fontFamily:'Lora, serif',fontSize:14,fontWeight:600,color:'#1A2B1E',overflow:'hidden'}}
              onMouseEnter={e=>e.currentTarget.style.color='#2C4A35'}
              onMouseLeave={e=>e.currentTarget.style.color='#1A2B1E'}>
              <span style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{submission.patientName}</span>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{flexShrink:0,opacity:0.6}}><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1"/></svg>
            </button>
          ) : (
            <div style={{fontSize:14,fontWeight:600,color:'#1A2B1E',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',fontFamily:'Lora, serif'}}>{submission.patientName}</div>
          )}
          <div style={{fontSize:11,color:'#8FAA96',marginTop:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
            {fmtDate(submission.patientDob)}{submission.patientCpf ? ' · CPF ' + submission.patientCpf : ''} · {fmtDateTime(submission.submittedAt)}
          </div>
        </div>
      </div>

      {/* Tabs mobile */}
      <div style={{display:'flex',background:'#fff',borderBottom:'1px solid #EDE9E2',flexShrink:0}} className="exam-tabs">
        {[{id:'docs',label:'Documentos'},{id:'interp',label:'Interpretação IA'}].map(t=>(
          <button key={t.id} onClick={()=>setTab(t.id)}
            style={{flex:1,padding:'10px',fontSize:12,fontWeight:600,cursor:'pointer',border:'none',background:'none',fontFamily:'inherit',color:tab===t.id?'#2C4A35':'#8FAA96',borderBottom:`2px solid ${tab===t.id?'#5C8A6E':'transparent'}`,transition:'all 0.15s'}}>
            {t.label}
          </button>
        ))}
      </div>

      {/* Layout lado a lado */}
      <div style={{flex:1,display:'flex',overflow:'hidden'}}>

        {/* Esquerda: documentos */}
        <div style={{width:'40%',borderRight:'1px solid #e5eaf2',overflow:'hidden',display:'flex',flexDirection:'column'}} className={`exam-left ${tab==='docs'?'exam-visible':'exam-hidden'}`}>
          {files.length > 0
            ? <DocViewer files={files} activeFile={activeFile} setActiveFile={setActiveFile} onDeleteFile={requestDeleteFile} deletingId={deleting}
                aiPanel={
                  <AIReadout
                    status={submission.status} hasAI={hasAI}
                    qualidadeImagem={qualidadeImagem} observacoesIA={observacoesIA}
                    labBlocks={labBlocks} demais={demais}
                    onSaveLabExame={handleSaveLabExame}/>
                }/>
            : <div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',background:'#F8F5EF'}}>
                <span style={{fontSize:13,color:'#8FAA96'}}>Sem arquivos</span>
              </div>
          }
          {/* Botão reanalisar */}
          <div style={{padding:'10px 12px',borderTop:'1px solid #EDE9E2',background:'#fff',flexShrink:0}}>
            <button onClick={handleReanalyze} disabled={reanalyzing}
              style={{width:'100%',display:'flex',alignItems:'center',justifyContent:'center',gap:6,padding:'8px',borderRadius:9,border:'1.5px solid #DDD8CE',background: reanalyzeMsg==='ok'?'#EAF2ED': reanalyzeMsg==='erro'?'#FFF0F0':'#fff',cursor:reanalyzing?'default':'pointer',fontFamily:'inherit',fontSize:11,fontWeight:600,color:reanalyzeMsg==='ok'?'#2C4A35':reanalyzeMsg==='erro'?'#B84040':'#4A6355',transition:'all 0.2s',opacity:reanalyzing?0.7:1}}>
              {reanalyzing
                ? <><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{animation:'evSpin 1s linear infinite'}}><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0"/></svg>Analisando...</>
                : reanalyzeMsg==='ok'
                  ? <><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#2C4A35" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>Análise concluída</>
                  : <><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M1 4v6h6"/><path d="M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>Reanalisar com IA</>
              }
            </button>
          </div>
        </div>

        {/* Direita: interpretação */}
        <div style={{flex:1,display:'flex',flexDirection:'column',overflow:'hidden',background:'#F8F5EF'}} className={`exam-right ${tab==='interp'?'exam-visible':'exam-hidden'}`}>
          {hasAI && (
            <div style={{display:'flex',justifyContent:'flex-end',padding:'8px 14px 0',flexShrink:0}}>
              <CopyAllBtn labBlocks={labBlocks} demais={demais}/>
            </div>
          )}
          <div style={{flex:1,overflowY:'auto'}}>
            <div style={{padding:14,display:'flex',flexDirection:'column',gap:10}}>
              <AIReadout
                status={submission.status} hasAI={hasAI}
                qualidadeImagem={qualidadeImagem} observacoesIA={observacoesIA}
                labBlocks={labBlocks} demais={demais}
                onSaveLabExame={handleSaveLabExame}/>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

Object.assign(window, { ExamViewer });
