/**
 * ============================================================
 * EDUMANAGER — SERVER SELF-HOSTED
 * ============================================================
 * SUBSTITUIÇÃO CIRÚRGICA:
 *   - @supabase/supabase-js → pg (PostgreSQL direto)
 *   - Supabase Storage      → MinIO (S3-compatible)
 * 
 * TODAS AS ROTAS mantêm a mesma assinatura e resposta.
 * O frontend NÃO percebe a diferença.
 * ============================================================
 */
import express from 'express';
import cors from 'cors';
import { jsPDF } from 'jspdf';
import 'jspdf-autotable';
// fetch nativo do Node 22 será utilizado automaticamente
import fs from 'fs';
import { fileURLToPath } from 'url';
import multer from 'multer';
import sharp from 'sharp';
import jwt from 'jsonwebtoken';
import cron from 'node-cron';
import path from 'path';
import crypto from 'crypto';

// === Novos módulos Self-Hosted (substituem Supabase) ===
import {
  getSchoolData, saveSchoolData, pool,
  insertCobrancas, updateCobranca, deleteCobranca,
  getCobrancaByPaymentId, getCobrancasByOrQuery,
  getCobrancasByAlunoId, getCobrancasAtrasadas, getCobrancasPendentes,
  getCobrancasByInstallmentId, updateCobrancaLinkCarne,
  updateCobrancaByField,
  initNotasTable, getNotasByAluno, upsertNota,
  syncJsonToRelationalTables,
  getFuncionarios, getCategoriasFuncionarios,
  insertFuncionario, updateFuncionario, deleteFuncionario,
  insertCategoriaFuncionario, updateCategoriaFuncionario, deleteCategoriaFuncionario,
  getCursos, insertCurso, updateCurso, deleteCurso,
  getTurmas, insertTurma, updateTurma, deleteTurma,
  getDisciplinas, insertDisciplina, updateDisciplina, deleteDisciplina,
  getAlunos, insertAluno, updateAluno, deleteAluno,
  getModelosContrato, insertModeloContrato, updateModeloContrato, deleteModeloContrato,
  getContratos, insertContrato, updateContrato, deleteContrato,
  getAulasByTurma, getAllAulas, insertAulas, deleteAulas,
  getFrequencias, insertFrequencia, updateFrequencia, deleteFrequencia,
  getProvas, getQuestoesDaProva, insertProva, updateProva, deleteProva, syncQuestoesProva
} from './services/database.js';
import { uploadLogo as uploadLogoToStorage, uploadCarne as uploadCarneToStorage, uploadReceipt as uploadReceiptToStorage, getMinioStats, s3Client, getBucketObjects, deleteMinioObject, uploadFile } from './services/storage.js';
import { GetObjectCommand } from '@aws-sdk/client-s3';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'EduManager-JWT-Secret-2026!';

// === ASAAS: URL base dinâmica inteligente ===
const ASAAS_KEY = process.env.ASAAS_API_KEY || '';
const ASAAS_BASE_URL = process.env.ASAAS_API_URL || (ASAAS_KEY.startsWith('$a') ? 'https://api.asaas.com' : 'https://sandbox.asaas.com/api');

app.use(express.json({ limit: '50mb' }));
app.use(cors());

const cancelCache = new Set();

// === Gerador da Página Pública de Pré-Matrícula ===
function getPreMatriculaHTML(slug) {
  return `<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Pré-Matrícula Online</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f8fafc;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.container{max-width:640px;width:100%;background:#fff;border-radius:24px;box-shadow:0 25px 50px -12px rgba(0,0,0,.1);overflow:hidden}
.header{padding:40px 32px 24px;text-align:center}
.header img{max-height:80px;max-width:240px;margin:0 auto 24px;display:block;object-fit:contain}
.header h1{font-size:28px;font-weight:900;color:#0f172a;margin-bottom:8px}
.header p{font-size:14px;color:#64748b;line-height:1.6}
.form{padding:0 32px 32px}
.field{margin-bottom:20px}
.field label{display:block;font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px}
.field label .req{color:#ef4444;margin-left:2px}
.field input,.field select,.field textarea{width:100%;padding:14px 16px;background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:12px;font-size:14px;font-family:'Inter',sans-serif;font-weight:500;color:#0f172a;transition:.2s;outline:none}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--primary,#4f46e5);box-shadow:0 0 0 3px rgba(79,70,229,.1)}
.field textarea{resize:vertical;min-height:80px}
.btn{display:block;width:100%;padding:16px;background:var(--primary,#4f46e5);color:#fff;border:none;border-radius:14px;font-size:15px;font-weight:800;cursor:pointer;transition:.2s;text-transform:uppercase;letter-spacing:1px}
.btn:hover{opacity:.9;transform:translateY(-1px)}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
.success{text-align:center;padding:60px 32px}
.success .icon{width:64px;height:64px;background:#d1fae5;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;font-size:28px}
.success h2{font-size:22px;font-weight:800;color:#0f172a;margin-bottom:8px}
.success p{color:#64748b;font-size:14px;line-height:1.6}
.error-page{text-align:center;padding:80px 32px}
.error-page h2{font-size:22px;font-weight:800;color:#0f172a;margin-bottom:8px}
.error-page p{color:#94a3b8;font-size:14px}
.loading{text-align:center;padding:80px 32px;color:#94a3b8}
.top-bar{height:5px;width:100%;background:var(--primary,#4f46e5)}
</style>
</head>
<body>
<div class="container" id="app"><div class="loading">Carregando formulário...</div></div>
<script>
const slug='${slug}';
const app=document.getElementById('app');

function maskPhone(v){
  if(!v)return "";
  v=v.replace(/\D/g,"");
  if(v.length>11) v=v.substring(0,11);
  if(v.length>10){
    return '(' + v.substring(0,2) + ') ' + v.substring(2,7) + '-' + v.substring(7);
  } else if(v.length>6){
    return '(' + v.substring(0,2) + ') ' + v.substring(2,6) + '-' + v.substring(6);
  } else if(v.length>2){
    return '(' + v.substring(0,2) + ') ' + v.substring(2);
  }
  return v;
}

function maskCPF(v){
  if(!v)return "";
  v=v.replace(/\D/g,"");
  if(v.length>11) v=v.substring(0,11);
  if(v.length>9){
    return v.substring(0,3) + '.' + v.substring(3,6) + '.' + v.substring(6,9) + '-' + v.substring(9);
  } else if(v.length>6){
    return v.substring(0,3) + '.' + v.substring(3,6) + '.' + v.substring(6);
  } else if(v.length>3){
    return v.substring(0,3) + '.' + v.substring(3);
  }
  return v;
}

function maskCEP(v){
  if(!v)return "";
  v=v.replace(/\D/g,"");
  if(v.length>8) v=v.substring(0,8);
  if(v.length>5){
    return v.substring(0,5) + '-' + v.substring(5);
  }
  return v;
}

// Global fallback event listener
document.addEventListener('input',function(e){
  if(!e.target)return;
  const dt=e.target.getAttribute('data-type');
  if(dt==='phone' || dt==='cpf' || dt==='cep'){
    const originalVal = e.target.value;
    let newVal = originalVal;
    if(dt==='phone') newVal = maskPhone(originalVal);
    else if(dt==='cpf') newVal = maskCPF(originalVal);
    else if(dt==='cep') newVal = maskCEP(originalVal);
    
    if (newVal !== originalVal) {
      const start = e.target.selectionStart;
      e.target.value = newVal;
      try {
        const diff = newVal.length - originalVal.length;
        e.target.setSelectionRange(start + diff, start + diff);
      } catch(err) {}
    }
  }
});

async function init(){
  try{
    const res=await fetch('/api/prematricula/public/'+slug);
    if(!res.ok){app.innerHTML='<div class="error-page"><h2>Formulário indisponível</h2><p>Este formulário não está publicado ou o link é inválido.</p></div>';return}
    const data=await res.json();
    if(data.escola && data.escola.logo) {
      const img = new Image();
      img.crossOrigin = "anonymous";
      img.onload = function() {
        try {
          const canvas = document.createElement('canvas');
          const size = Math.min(img.width, img.height);
          canvas.width = size;
          canvas.height = size;
          const ctx = canvas.getContext('2d');
          if (ctx) {
            ctx.beginPath();
            ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
            ctx.clip();
            ctx.drawImage(img, (img.width - size) / 2, (img.height - size) / 2, size, size, 0, 0, size, size);
            
            let link = document.querySelector("link[rel~='icon']");
            if (!link) {
              link = document.createElement('link');
              link.type = 'image/x-icon';
              link.rel = 'icon';
              document.getElementsByTagName('head')[0].appendChild(link);
            }
            link.href = canvas.toDataURL('image/png');
          }
        } catch (e) {
          let link = document.querySelector("link[rel~='icon']");
          if (!link) {
            link = document.createElement('link');
            link.type = 'image/x-icon';
            link.rel = 'icon';
            document.getElementsByTagName('head')[0].appendChild(link);
          }
          link.href = data.escola.logo;
        }
      };
      img.onerror = function() {
        let link = document.querySelector("link[rel~='icon']");
        if (!link) {
          link = document.createElement('link');
          link.type = 'image/x-icon';
          link.rel = 'icon';
          document.getElementsByTagName('head')[0].appendChild(link);
        }
        link.href = data.escola.logo;
      };
      img.src = data.escola.logo;
    }
    if(data.draft){
      app.innerHTML='<div class="error-page"><h2>Todas as turmas estão lotadas</h2><p style="margin-top:12px;font-weight:600;color:#64748b">Em breve abriremos novas vagas. Fique atento!</p></div>';
      return;
    }
    const c=data.config;
    document.documentElement.style.setProperty('--primary',c.corPrimaria||'#4f46e5');
    let html='<div class="top-bar"></div><div class="header">';
    if(data.escola.logo)html+='<img src="'+data.escola.logo+'" alt="Logo">';
    html+='<h1>'+c.titulo+'</h1><p>'+c.descricao+'</p></div>';
    html+='<form class="form" id="preForm" onsubmit="return submitForm(event)">';
    
    // Render fields dynamically in their exact database order
    data.campos.forEach(f=>{
      if(f.tipo==='banner'){
        if(f.placeholder){
          html+='<div style="text-align:center;margin-bottom:20px"><img src="'+f.placeholder+'" alt="Promoção" style="width:100%;height:auto;border-radius:16px;box-shadow:0 4px 12px rgba(0,0,0,.05);display:block;margin:0 auto"></div>';
        }
      }else{
        html+='<div class="field"><label>'+f.label+(f.obrigatorio?' <span class="req">*</span>':'')+'</label>';
        
        if(f.id==='_nome'){
          html+='<input name="_nome"'+(f.obrigatorio?' required':'')+' placeholder="'+(f.placeholder||'Seu nome completo')+'">';
        }else if(f.id==='_email'){
          html+='<input name="_email" type="email" placeholder="'+(f.placeholder||'seu@email.com')+'">';
        }else if(f.id==='_telefone'){
          html+='<input name="_telefone" data-type="phone" placeholder="'+(f.placeholder||'(00) 00000-0000')+'" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">';
        }else if(f.id==='_turma'){
          if(data.turmas.length>0){
            html+='<select name="_turma"'+(f.obrigatorio?' required':'')+'><option value="">Selecione a turma...</option>';
            data.turmas.forEach(t=>{
              const info=t.horario?' ('+t.horario+')':'';
              const label=t.esgotado?t.nome+info+' [ESGOTADO]':t.nome+info;
              const disabled=t.esgotado?' disabled':'';
              html+='<option value="'+t.id+'" data-nome="'+t.nome+'"'+disabled+'>'+label+'</option>';
            });
            html+='</select>';
          }else{
            html+='<p style="color:#ef4444;font-size:12px;font-weight:600">Nenhuma turma disponível.</p>';
          }
        }else{
          // Custom fields
          const isTextarea = f.tipo === 'textarea' || (f.label && /observa/i.test(f.label));
          let tipoDetectado = f.tipo;
          if (f.label) {
            const lbl = f.label.toLowerCase();
            if (lbl.includes('cpf')) {
              tipoDetectado = 'cpf';
            } else if (lbl.includes('telefone') || lbl.includes('whatsapp') || lbl.includes('celular') || lbl.includes('phone')) {
              tipoDetectado = 'phone';
            } else if (lbl.includes('cep')) {
              tipoDetectado = 'cep';
            }
          }
          
          if(f.tipo==='file'){
            html+='<input type="file" name="'+f.id+'"'+(f.obrigatorio?' required':'')+' accept="image/*,application/pdf">';
          }else if(f.tipo==='select'){
            html+='<select name="'+f.id+'"'+(f.obrigatorio?' required':'')+'><option value="">Selecione...</option>';
            (f.opcoes||[]).forEach(o=>{html+='<option value="'+o+'">'+o+'</option>'});
            html+='</select>';
          }else if(isTextarea){
            html+='<textarea name="'+f.id+'"'+(f.obrigatorio?' required':'')+' placeholder="'+(f.placeholder||'')+'"></textarea>';
          }else{
            let t=tipoDetectado;
            if(t==='phone'||t==='cpf'||t==='cep')t='text';
            let dt=(tipoDetectado==='phone'||tipoDetectado==='cpf'||tipoDetectado==='cep')?(' data-type="'+tipoDetectado+'" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"'):'';
            html+='<input type="'+t+'" name="'+f.id+'"'+(f.obrigatorio?' required':'')+dt+' placeholder="'+(f.placeholder||'')+'">';
          }
        }
        html+='</div>';
      }
    });
    
    html+='<button type="submit" class="btn" id="submitBtn">Enviar Pré-Matrícula</button></form>';
    app.innerHTML=html;
    
    // Format any prefilled inputs on load
    app.querySelectorAll('[data-type]').forEach(function(el){
      const dt = el.getAttribute('data-type');
      if (el.value) {
        if(dt==='phone') el.value = maskPhone(el.value);
        if(dt==='cpf') el.value = maskCPF(el.value);
        if(dt==='cep') el.value = maskCEP(el.value);
      }
    });

    window._successMsg=c.mensagemSucesso||'Pré-matrícula enviada com sucesso!';
  }catch(e){app.innerHTML='<div class="error-page"><h2>Erro</h2><p>Não foi possível carregar o formulário.</p></div>'}
}
async function submitForm(e){
  e.preventDefault();
  const form=document.getElementById('preForm');
  const btn=document.getElementById('submitBtn');
  btn.disabled=true;btn.textContent='Enviando...';
  const fd=new FormData(form);
  const nome=fd.get('_nome'),email=fd.get('_email'),telefone=fd.get('_telefone');
  const turmaSelect=form.querySelector('[name="_turma"]');
  const turmaId=turmaSelect?turmaSelect.value:'';
  const turmaNome=turmaSelect?turmaSelect.options[turmaSelect.selectedIndex]?.dataset?.nome||'':'';
  const respostas={};
  
  // Upload file inputs to MinIO first
  const fileInputs = form.querySelectorAll('input[type="file"]');
  for (const input of fileInputs) {
    const file = input.files[0];
    if (file) {
      btn.textContent = 'Enviando arquivo...';
      const fileFd = new FormData();
      fileFd.append('file', file);
      try {
        const uploadRes = await fetch('/api/prematricula/upload', {
          method: 'POST',
          body: fileFd
        });
        if (!uploadRes.ok) throw new Error();
        const uploadData = await uploadRes.json();
        respostas[input.name] = uploadData.url;
      } catch(err) {
        alert('Falha ao enviar arquivo. Tente novamente.');
        btn.disabled=false;btn.textContent='Enviar Pré-Matrícula';
        return false;
      }
    }
  }

  for(const[k,v]of fd.entries()){
    if(!k.startsWith('_') && !(v instanceof File)){
      respostas[k]=v;
    }
  }
  try{
    const res=await fetch('/api/prematricula/submit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({nome,email,telefone,turmaId,turmaNome,respostas})});
    if(res.ok){
      app.innerHTML='<div class="top-bar"></div><div class="success"><div class="icon">✅</div><h2>Enviado com sucesso!</h2><p>'+window._successMsg+'</p></div>';
    }else{
      const err=await res.json();alert(err.error||'Erro ao enviar.');btn.disabled=false;btn.textContent='Enviar Pré-Matrícula';
    }
  }catch(err){alert('Erro de conexão.');btn.disabled=false;btn.textContent='Enviar Pré-Matrícula'}
  return false;
}
init();
</script>
</body></html>`;
}
const sentCache = new Set();
const lockCache = new Set();
let activeCronJob = null; // Referência global para o agendamento preventivo
let activeCronJobOverdue = null; // Referência global para o agendamento de inadimplência
let activeCronJobBirthday = null; // Referência global para o agendamento de aniversário

// === Funções Auxiliares de Notificação ===
async function createAdminNotification(titulo, mensagem, metadata = {}) {
  try {
    await pool.query(
      'INSERT INTO notificacoes (aluno_id, titulo, mensagem, anexo) VALUES ($1, $2, $3, $4)',
      ['admin', titulo, mensagem, JSON.stringify(metadata)]
    );
    console.log(`[Notification] Alerta Admin criado: ${titulo}`);
  } catch (err) {
    console.error('[Notification] Erro ao criar alerta admin:', err.message);
  }
}

// Proxy de Imagens do MinIO (acesso público via backend)
app.get(/^\/storage\/([^\/]+)\/(.+)$/, async (req, res) => {
  try {
    const bucket = req.params[0];
    const key = req.params[1]; // Captura tudo que vem após o bucket (incluindo barras)
    
    const command = new GetObjectCommand({ Bucket: bucket, Key: key });
    const data = await s3Client.send(command);
    
    res.set('Content-Type', data.ContentType || 'image/jpeg');
    res.set('Cache-Control', 'public, max-age=86400');
    data.Body.pipe(res);
  } catch (e) {
    console.error(`[Storage Proxy] Erro ao buscar: ${req.params.bucket}/${req.params[0]}`, e.message);
    res.status(404).send('Arquivo não encontrado');
  }
});

const multerStorage = multer.memoryStorage();
const upload = multer({ storage: multerStorage, limits: { fileSize: 10 * 1024 * 1024 } });

// ============================================================
// ROTA NOVA: Login Administrativo (JWT)
// ============================================================
app.post('/api/auth/login', async (req, res) => {
  try {
    const { username, password } = req.body;
    if (!username || !password) {
      return res.status(400).json({ error: 'Usuário e senha são obrigatórios' });
    }

    const { rows } = await pool.query(
      'SELECT * FROM usuarios WHERE username = $1',
      [username]
    );

    const user = rows[0];
    if (!user || user.password !== password) {
      return res.status(401).json({ error: 'Credenciais inválidas' });
    }

    const token = jwt.sign(
      { userId: user.id, username: user.username, role: user.role },
      JWT_SECRET,
      { expiresIn: '24h' }
    );

    res.json({ token, user: { id: user.id, name: user.display_name || user.username, role: user.role } });
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTA NOVA: API para o dbService.ts do Frontend
// GET  /api/school-data → fetchFromCloud()
// PUT  /api/school-data → saveToCloud()
// ============================================================
app.get('/api/school-data', async (req, res) => {
  try {
    const data = await getSchoolData();
    
    // Injetar dados migrados diretamente do PostgreSQL
    try {
      data.attendance = await getFrequencias();
    } catch (e) {
      console.error('[SQL] Falha ao carregar frequencias do banco:', e);
    }
    
    // Normalizar URLs do MinIO para proxy relativo
    // Converte URLs como https://storageedu.xxx/bucket/file para /storage/bucket/file
    const MINIO_PUBLIC_URL = process.env.MINIO_PUBLIC_URL || '';
    const normalizeUrl = (url) => {
      if (!url || typeof url !== 'string') return url;
      // Se já é uma URL relativa de proxy, manter
      if (url.startsWith('/storage/')) return url;
      // Se é a URL pública do MinIO, converter para proxy
      if (MINIO_PUBLIC_URL && url.startsWith(MINIO_PUBLIC_URL)) {
        return url.replace(MINIO_PUBLIC_URL, '/storage');
      }
      // Fallback: URL com http://localhost:9000 ou http://minio:9000
      const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/);
      if (match && (url.includes('minio') || url.includes('storageedu') || url.includes(':9000'))) {
        return `/storage/${match[1]}`;
      }
      return url;
    };
    
    // Normalizar fotos de alunos
    if (data.students) {
      data.students.forEach(s => { if (s.photo) s.photo = normalizeUrl(s.photo); });
    }
    // Normalizar logo
    if (data.logo) data.logo = normalizeUrl(data.logo);
    if (data.profile?.logo) data.profile.logo = normalizeUrl(data.profile.logo);
    // Normalizar fotos nos registros de presença
    if (data.attendance) {
      data.attendance.forEach(a => { if (a.photo) a.photo = normalizeUrl(a.photo); });
    }
    // Normalizar imagens de exames
    if (data.exams) {
      data.exams.forEach(e => {
        if (e.questions) e.questions.forEach(q => { if (q.image) q.image = normalizeUrl(q.image); });
      });
    }
    
    res.json({ data });
  } catch (error) {
    console.error('Erro ao buscar school_data:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/school-data', async (req, res) => {
  try {
    const schoolData = req.body;
    if (!schoolData) return res.status(400).json({ error: 'Dados não fornecidos' });

    // Verificação de timestamp para evitar regressão
    const current = await getSchoolData();
    const cloudTimestamp = current.lastUpdated ? new Date(current.lastUpdated).getTime() : 0;
    const localTimestamp = schoolData.lastUpdated ? new Date(schoolData.lastUpdated).getTime() : 0;

    if (cloudTimestamp > localTimestamp) {
      return res.status(409).json({ success: false, reason: 'newer_version' });
    }

    // Inicialização de colunas necessárias para automação
    pool.query(`
      DO $$ 
      BEGIN 
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='pre_warnings_count') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN pre_warnings_count INTEGER DEFAULT 0;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='last_pre_warning_at') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN last_pre_warning_at TIMESTAMP WITH TIME ZONE;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='overdue_warnings_count') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN overdue_warnings_count INTEGER DEFAULT 0;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='last_overdue_warning_at') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN last_overdue_warning_at TIMESTAMP WITH TIME ZONE;
        END IF;
      END $$;
    `).catch(err => console.error('[PostgreSQL] Erro ao inicializar colunas de automação:', err));

    schoolData.lastUpdated = new Date().toISOString();
    await saveSchoolData(schoolData);
    
    // Sincronização em tempo real (JSON -> Relacional)
    syncJsonToRelationalTables().catch(err => console.error('[Real-time Sync] Erro:', err.message));
    
    res.json({ 
      success: true, 
      message: 'Dados salvos com sucesso',
      lastUpdated: schoolData.lastUpdated
    });
  } catch (error) {
    console.error('Erro ao salvar school-data:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

app.get('/api/system-stats', async (req, res) => {
  let postgresStats = { dbSize: 'N/A', tableCount: '0' };
  try {
    const dbResult = await pool.query(`
      SELECT pg_size_pretty(pg_database_size(current_database())) as db_size,
             (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public') as table_count
    `);
    postgresStats = {
      dbSize: dbResult.rows[0].db_size,
      tableCount: dbResult.rows[0].table_count
    };
  } catch(e) {
    console.error('System Stats (Postgres) Error:', e);
  }
  
  let minioStats = { error: true, message: 'Not initialized' };
  try {
    minioStats = await getMinioStats();
  } catch(e) {
    console.error('System Stats (MinIO) Error:', e);
    minioStats = { error: true, message: e.message };
  }
  
  res.json({
    postgres: postgresStats,
    minio: minioStats
  });
});

// ============================================================
// Database Explorer
// ============================================================
app.get('/api/database/tables', async (req, res) => {
  try {
    const query = `
      SELECT 
        relname as table_name,
        pg_size_pretty(pg_total_relation_size(relid)) as total_size,
        pg_total_relation_size(relid) as raw_size,
        n_live_tup as row_count
      FROM pg_stat_user_tables
      ORDER BY raw_size DESC;
    `;
    const result = await pool.query(query);
    res.json({ tables: result.rows });
  } catch (error) {
    console.error('Erro ao listar tabelas:', error);
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/database/tables/:tableName/data', async (req, res) => {
  try {
    const { tableName } = req.params;
    
    // Basic validation to prevent SQL injection on table name
    if (!/^[a-zA-Z0-9_]+$/.test(tableName)) {
      return res.status(400).json({ error: 'Nome de tabela inválido' });
    }

    const query = `SELECT * FROM "${tableName}" LIMIT 100;`;
    const result = await pool.query(query);
    res.json({ rows: result.rows, fields: result.fields.map(f => f.name) });
  } catch (error) {
    console.error(`Erro ao buscar dados da tabela ${req.params.tableName}:`, error);
    res.status(500).json({ error: error.message });
  }
});

// ============================================================
// MinIO Explorer
// ============================================================
app.get('/api/storage/buckets/:bucketName/objects', async (req, res) => {
  try {
    const { bucketName } = req.params;
    const objects = await getBucketObjects(bucketName);
    res.json({ objects });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.delete('/api/storage/buckets/:bucketName/objects', async (req, res) => {
  try {
    const { bucketName } = req.params;
    const { key } = req.body;
    if (!key) return res.status(400).json({ error: 'Key is required' });
    
    await deleteMinioObject(bucketName, key);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// ============================================================
// Rota para buscar submissões (acertos/erros) do aluno
// ============================================================
app.get('/api/student-submissions/:studentId', async (req, res) => {
  try {
    const { studentId } = req.params;
    const { rows } = await pool.query(
      'SELECT prova_id as "prova_id", acertos, erros FROM provas_submissoes WHERE TRIM(aluno_id) = TRIM($1)',
      [String(studentId).trim()]
    );
    res.json({ submissions: rows });
  } catch (err) {
    console.error('Erro ao buscar submissões do aluno:', err);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE NOTAS (NOVA TABELA)
// ============================================================
app.get('/api/notas/:alunoId', async (req, res) => {
  try {
    const { rows: dbNotas } = await pool.query(
      'SELECT id, aluno_id as "aluno_id", disciplina_id as "disciplina_id", periodo_id as "periodo_id", prova_id as "prova_id", valor as "valor" FROM notas_boletim WHERE TRIM(aluno_id) = TRIM($1)',
      [String(req.params.alunoId).trim()]
    );
    // Garantir cast numérico para evitar erro de .toFixed no frontend
    const notas = dbNotas.map(n => ({ ...n, valor: Number(n.valor) }));
    res.json({ notas });
  } catch (err) {
    console.error('Erro ao buscar notas do aluno:', err);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/notas', async (req, res) => {
  try {
    const { notas } = req.body;
    if (!Array.isArray(notas)) return res.status(400).json({ error: 'Formato inválido' });
    
    for (const nota of notas) {
      if (nota.valor !== null && nota.valor !== '' && !isNaN(Number(nota.valor))) {
        await upsertNota({
          aluno_id: String(nota.aluno_id),
          disciplina_id: String(nota.disciplina_id),
          periodo_id: String(nota.periodo_id),
          prova_id: nota.prova_id ? String(nota.prova_id) : null,
          valor: Number(nota.valor)
        });
      }
    }
    
    // Opcionalmente implementar delete para notas que o professor limpou (vazio)
    
    res.json({ success: true });
  } catch (err) {
    console.error('Erro ao salvar notas manuais:', err);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE CURSOS (MIGRAÇÃO FASE 3)
// ============================================================
app.get('/api/cursos', async (req, res) => {
  try {
    const cursos = await getCursos();
    res.json({ cursos });
  } catch (error) {
    console.error('Erro ao buscar cursos:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/cursos', async (req, res) => {
  try {
    await insertCurso(req.body);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao criar curso:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/cursos/:id', async (req, res) => {
  try {
    await updateCurso(req.params.id, req.body);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar curso:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/cursos/:id', async (req, res) => {
  try {
    await deleteCurso(req.params.id);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar curso:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE DISCIPLINAS (MIGRAÇÃO FASE 3)
// ============================================================
app.get('/api/disciplinas', async (req, res) => {
  try {
    const disciplinas = await getDisciplinas();
    res.json({ disciplinas });
  } catch (error) {
    console.error('Erro ao buscar disciplinas:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/disciplinas', async (req, res) => {
  try {
    await insertDisciplina(req.body);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao criar disciplina:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/disciplinas/:id', async (req, res) => {
  try {
    await updateDisciplina(req.params.id, req.body);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar disciplina:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/disciplinas/:id', async (req, res) => {
  try {
    await deleteDisciplina(req.params.id);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar disciplina:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE ALUNOS (MIGRAÇÃO FASE 4)
// ============================================================
app.get('/api/fix-alunos-migracao', async (req, res) => {
  try {
    const result = await pool.query(`
      UPDATE alunos a
      SET
          data_nascimento = NULLIF(s.elem->>'data_nascimento', '')::timestamp,
          rg = s.elem->>'rg',
          rua = s.elem->>'rua',
          numero = s.elem->>'numero',
          bairro = s.elem->>'bairro',
          cidade = s.elem->>'cidade',
          estado = s.elem->>'estado',
          cep = s.elem->>'cep'
      FROM (
          SELECT jsonb_array_elements(data->'students') as elem
          FROM school_data
          WHERE id = 1
      ) s
      WHERE a.id = s.elem->>'id'
      RETURNING a.nome;
    `);
    res.json({ success: true, message: `Foram atualizados ${result.rowCount} alunos com os dados do JSON original!` });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/alunos', async (req, res) => {
  try {
    const alunos = await getAlunos();
    res.json({ alunos });
  } catch (error) {
    console.error('Erro ao buscar alunos:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/alunos', async (req, res) => {
  try {
    const student = req.body;
    await insertAluno(student);
    
    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbAlunos = await getAlunos();
    appData.students = dbAlunos;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao criar aluno:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/alunos/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const student = req.body;
    await updateAluno(id, student);

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbAlunos = await getAlunos();
    appData.students = dbAlunos;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar aluno:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/alunos/:id', async (req, res) => {
  try {
    const { id } = req.params;
    
    // 1. Relational cleanups in Postgres
    await pool.query('DELETE FROM alunos_cobrancas WHERE aluno_id = $1', [id]);
    await pool.query('DELETE FROM contratos WHERE aluno_id = $1', [id]);
    await pool.query('DELETE FROM frequencias WHERE aluno_id = $1', [id]);
    await pool.query('DELETE FROM notificacoes WHERE aluno_id = $1', [id]);
    await pool.query('DELETE FROM provas_submissoes WHERE aluno_id = $1', [id]);
    await pool.query('DELETE FROM notas_boletim WHERE aluno_id = $1', [id]);
    await deleteAluno(id);

    // 2. Reverse sync to legacy JSON
    const appData = await getSchoolData();
    appData.students = appData.students.filter(s => s.id !== id);
    appData.payments = appData.payments.filter(p => p.studentId !== id);
    appData.contracts = appData.contracts.filter(c => c.studentId !== id);
    if (appData.attendance) appData.attendance = appData.attendance.filter(a => a.studentId !== id);
    if (appData.notifications) appData.notifications = appData.notifications.filter(n => n.studentId !== id);
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar aluno:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});


// ============================================================
// ROTAS DE CONTRATOS E MODELOS
// ============================================================
app.get('/api/modelos-contrato', async (req, res) => {
  try { res.json({ modelos: await getModelosContrato() }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
});
app.post('/api/modelos-contrato', async (req, res) => {
  try { 
    await insertModeloContrato(req.body); 
    const appData = await getSchoolData();
    appData.contractTemplates = await getModelosContrato();
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);
    res.json({ success: true }); 
  } catch (e) { res.status(500).json({ error: 'Erro' }); }
});
app.put('/api/modelos-contrato/:id', async (req, res) => {
  try { 
    await updateModeloContrato(req.params.id, req.body); 
    const appData = await getSchoolData();
    appData.contractTemplates = await getModelosContrato();
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);
    res.json({ success: true }); 
  } catch (e) { res.status(500).json({ error: 'Erro' }); }
});
app.delete('/api/modelos-contrato/:id', async (req, res) => {
  try { 
    await deleteModeloContrato(req.params.id); 
    const appData = await getSchoolData();
    appData.contractTemplates = await getModelosContrato();
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);
    res.json({ success: true }); 
  } catch (e) { res.status(500).json({ error: 'Erro' }); }
});

app.get('/api/contratos', async (req, res) => {
  try { res.json({ contratos: await getContratos() }); } catch (e) { res.status(500).json({ error: 'Erro' }); }
});
app.post('/api/contratos', async (req, res) => {
  try { 
    await insertContrato(req.body); 
    const appData = await getSchoolData();
    appData.contracts = await getContratos();
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);
    res.json({ success: true }); 
  } catch (e) { res.status(500).json({ error: 'Erro' }); }
});
app.put('/api/contratos/:id', async (req, res) => {
  try { 
    await updateContrato(req.params.id, req.body); 
    const appData = await getSchoolData();
    appData.contracts = await getContratos();
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);
    res.json({ success: true }); 
  } catch (e) { res.status(500).json({ error: 'Erro' }); }
});
app.delete('/api/contratos/:id', async (req, res) => {
  try { 
    await deleteContrato(req.params.id); 
    const appData = await getSchoolData();
    appData.contracts = await getContratos();
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);
    res.json({ success: true }); 
  } catch (e) { res.status(500).json({ error: 'Erro' }); }
});

// ============================================================
// ROTAS DE AULAS E CRONOGRAMA
// ============================================================
app.get('/api/aulas', async (req, res) => {
  try {
    const { turma_id } = req.query;
    const aulas = turma_id ? await getAulasByTurma(turma_id) : await getAllAulas();
    res.json({ aulas });
  } catch (error) {
    console.error('Erro ao buscar aulas:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/aulas/lote', async (req, res) => {
  try {
    const { aulas } = req.body;
    await insertAulas(aulas);

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbAulas = await getAllAulas();
    appData.lessons = dbAulas;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao inserir aulas em lote:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/aulas/lote', async (req, res) => {
  try {
    const { ids } = req.body;
    await deleteAulas(ids);

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbAulas = await getAllAulas();
    appData.lessons = dbAulas;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar aulas em lote:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE FREQUÊNCIAS (CHAMADA)
// ============================================================
app.get('/api/frequencias', async (req, res) => {
  try {
    const frequencias = await getFrequencias();
    res.json({ frequencias });
  } catch (error) {
    console.error('Erro ao buscar frequencias:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/frequencias', async (req, res) => {
  try {
    await insertFrequencia(req.body);

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbFrequencias = await getFrequencias();
    appData.attendance = dbFrequencias;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao inserir frequencia:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/frequencias/:id', async (req, res) => {
  try {
    await updateFrequencia(req.params.id, req.body);

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbFrequencias = await getFrequencias();
    appData.attendance = dbFrequencias;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar frequencia:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/frequencias/:id', async (req, res) => {
  try {
    await deleteFrequencia(req.params.id);

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbFrequencias = await getFrequencias();
    appData.attendance = dbFrequencias;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar frequencia:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE AVALIAÇÕES (MIGRAÇÃO FASE 5)
// ============================================================
app.get('/api/provas', async (req, res) => {
  try {
    const provas = await getProvas();
    res.json({ provas });
  } catch (error) {
    console.error('Erro ao buscar provas:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.get('/api/provas/:id/questoes', async (req, res) => {
  try {
    const questoes = await getQuestoesDaProva(req.params.id);
    res.json({ questoes });
  } catch (error) {
    console.error('Erro ao buscar questoes:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/provas', async (req, res) => {
  try {
    const prova = req.body;
    await insertProva(prova);

    // Sync questions if provided
    if (prova.questions && prova.questions.length > 0) {
      await syncQuestoesProva(prova.id, prova.questions);
    }

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbProvas = await getProvas();
    appData.exams = dbProvas;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao criar prova:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/provas/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const prova = req.body;
    await updateProva(id, prova);

    // Sync questions if provided
    if (prova.questions) {
      await syncQuestoesProva(id, prova.questions);
    }

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbProvas = await getProvas();
    appData.exams = dbProvas;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar prova:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/provas/:id', async (req, res) => {
  try {
    const { id } = req.params;
    // Cleanup relational dependencies
    await pool.query('DELETE FROM questoes_provas WHERE prova_id = $1', [id]);
    await pool.query('DELETE FROM provas_submissoes WHERE prova_id = $1', [id]);
    await deleteProva(id);

    // Reverse sync to legacy JSON
    const appData = await getSchoolData();
    appData.exams = (appData.exams || []).filter(e => e.id !== id);
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar prova:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/provas/:id/questoes', async (req, res) => {
  try {
    const { questoes } = req.body;
    await syncQuestoesProva(req.params.id, questoes || []);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao sincronizar questoes:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE TURMAS (MIGRAÇÃO FASE 3)
// ============================================================
app.get('/api/turmas', async (req, res) => {
  try {
    const turmas = await getTurmas();
    res.json({ turmas });
  } catch (error) {
    console.error('Erro ao buscar turmas:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/turmas', async (req, res) => {
  try {
    await insertTurma(req.body);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao criar turma:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/turmas/:id', async (req, res) => {
  try {
    await updateTurma(req.params.id, req.body);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar turma:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/turmas/:id', async (req, res) => {
  try {
    await deleteTurma(req.params.id);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar turma:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// ROTAS DE FUNCIONÁRIOS (MIGRAÇÃO FASE 1)
// ============================================================

app.get('/api/categorias_funcionarios', async (req, res) => {
  try {
    const categorias = await getCategoriasFuncionarios();
    res.json({ categorias });
  } catch (error) {
    console.error('Erro ao buscar categorias:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/categorias_funcionarios', async (req, res) => {
  try {
    const data = req.body;
    await insertCategoriaFuncionario(data);
    res.json({ success: true, categoria: data });
  } catch (error) {
    console.error('Erro ao criar categoria:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/categorias_funcionarios/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const { nome } = req.body;
    await updateCategoriaFuncionario(id, nome);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar categoria:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/categorias_funcionarios/:id', async (req, res) => {
  try {
    const { id } = req.params;
    await deleteCategoriaFuncionario(id);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar categoria:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.get('/api/funcionarios', async (req, res) => {
  try {
    const funcionarios = await getFuncionarios();
    res.json({ funcionarios });
  } catch (error) {
    console.error('Erro ao buscar funcionarios:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.post('/api/funcionarios', async (req, res) => {
  try {
    const data = req.body;
    await insertFuncionario(data);
    res.json({ success: true, funcionario: data });
  } catch (error) {
    console.error('Erro ao criar funcionario:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/funcionarios/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const updateData = req.body;
    await updateFuncionario(id, updateData);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao atualizar funcionario:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.delete('/api/funcionarios/:id', async (req, res) => {
  try {
    const { id } = req.params;
    await deleteFuncionario(id);
    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao deletar funcionario:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

// ============================================================
// Upload de Logo (MinIO em vez de Supabase Storage)
// ============================================================
app.post('/api/upload/logo', upload.single('logo'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
    }

    const compressedBuffer = await sharp(req.file.buffer)
      .resize(500, 500, { fit: 'inside', withoutEnlargement: true })
      .webp({ quality: 60 })
      .toBuffer();

    const url = await uploadLogoToStorage(compressedBuffer, 'image/webp');
    return res.status(200).json({ url });
  } catch (error) {
    console.error('Erro ao processar logo:', error);
    return res.status(500).json({ error: 'Erro interno ao processar a imagem.' });
  }
});

// ============================================================
// Upload de Foto de Aluno (MinIO)
// ============================================================
app.post('/api/upload/student-photo', upload.single('photo'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
    }

    const { uploadStudentPhoto } = await import('./services/storage.js');
    const url = await uploadStudentPhoto(req.file.buffer, req.file.mimetype);
    return res.status(200).json({ url });
  } catch (error) {
    console.error('Erro ao processar foto:', error);
    return res.status(500).json({ error: 'Erro interno.' });
  }
});

// ============================================================
// Upload de Logo da Escola (MinIO)
// ============================================================
app.post('/api/upload/logo', upload.single('logo'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
    }

    const { uploadLogo } = await import('./services/storage.js');
    const url = await uploadLogo(req.file.buffer, req.file.mimetype);
    return res.status(200).json({ url });
  } catch (error) {
    console.error('Erro ao processar logo:', error);
    return res.status(500).json({ error: 'Erro interno.' });
  }
});

// ============================================================
// Upload de Imagem de Avaliação (MinIO)
// ============================================================
app.post('/api/upload/exam-image', upload.single('photo'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
    }

    const { uploadExamImage } = await import('./services/storage.js');
    const url = await uploadExamImage(req.file.buffer, req.file.mimetype);
    return res.status(200).json({ url });
  } catch (error) {
    console.error('Erro ao processar imagem de avaliação:', error);
    return res.status(500).json({ error: 'Erro interno.' });
  }
});

// ============================================================
// Formatação de Data
// ============================================================
function formatCobrancaDate(dateStr) {
  if (!dateStr) return '';
  const [Ano, Mes, Dia] = dateStr.split('-');
  if (!Dia) return dateStr;
  return `${Dia}/${Mes}/${Ano}`;
}

// ============================================================
// Integração WhatsApp Evolution API
// (Mesma lógica, trocando supabase por database.js)
// ============================================================
async function sendEvolutionMessage(asaasPaymentId, eventType, fallbackValorArg = null, fallbackVencimentoArg = null) {
  try {
    let cob = null;
    for (let i = 0; i < 3; i++) {
      cob = await getCobrancaByPaymentId(asaasPaymentId);
      if (cob) break;
      if (i < 2) await new Promise(r => setTimeout(r, 1000));
    }

    if (!cob) return console.log(`[Evolution] Cobrança não encontrada: ${asaasPaymentId}`);

    let fallbackValor = fallbackValorArg || cob.valor;
    let fallbackVencimento = fallbackVencimentoArg || cob.vencimento;
    let fallbackDescricao = 'serviços educacionais';

    const appData = await getSchoolData();
    if (!appData) return console.log('[WhatsApp] school_data não encontrado');

    const evoConfig = appData.evolutionConfig;
    const templates = appData.messageTemplates;

    if (!evoConfig || !evoConfig.apiUrl || !evoConfig.apiKey || !evoConfig.instanceName) {
      return console.log('[WhatsApp] Credenciais Evolution não configuradas.');
    }

    const normalizedEvent = (eventType === 'PAYMENT_RECEIVED' || eventType === 'PAYMENT_CONFIRMED') ? 'PAYMENT_RECEIVED' : eventType;
    const cacheKey = `${asaasPaymentId}_${normalizedEvent}`;
    if (sentCache.has(cacheKey)) return;
    sentCache.add(cacheKey);
    setTimeout(() => sentCache.delete(cacheKey), 30000);

    let aluno = appData.students?.find(s => s.id === cob.aluno_id);
    
    // Fallback: Se não achar no JSON, busca na tabela SQL de alunos
    if (!aluno) {
      const { rows } = await pool.query('SELECT * FROM alunos WHERE id = $1', [cob.aluno_id]);
      if (rows[0]) {
        const a = rows[0];
        aluno = {
          id: a.id,
          name: a.nome,
          phone: a.telefone,
          guardianPhone: a.telefone_responsavel,
          birthDate: a.data_nascimento,
          enrollmentNumber: a.numero_matricula
        };
      }
    }

    if (!aluno) return console.log('[WhatsApp] Aluno não encontrado:', cob.aluno_id);

    let age = 18; // Padrão adulto para evitar travas se bday faltar
    if (aluno.birthDate) {
      const bDate = new Date(aluno.birthDate);
      const today = new Date();
      age = today.getFullYear() - bDate.getFullYear();
      const m = today.getMonth() - bDate.getMonth();
      if (m < 0 || (m === 0 && today.getDate() < bDate.getDate())) age--;
    }

    const isMinor = age < 18;
    // Seleção resiliente: Tenta responsável se menor, mas aceita o do aluno se o do pai faltar (e vice-versa)
    const targetPhone = isMinor 
      ? (aluno.guardianPhone || aluno.telefone_responsavel || aluno.phone || aluno.telefone)
      : (aluno.phone || aluno.telefone || aluno.guardianPhone || aluno.telefone_responsavel);

    const targetName = (isMinor && (aluno.nome_responsavel || aluno.guardianName)) ? (aluno.nome_responsavel || aluno.guardianName) : (aluno.name || aluno.nome);
    
    if (!targetPhone) return console.log('[WhatsApp] Sem telefone.');

    let cleanPhone = targetPhone.replace(/\D/g, '');
    if (cleanPhone.length === 10 || cleanPhone.length === 11) cleanPhone = '55' + cleanPhone;

    let descricao = fallbackDescricao;
    let pdfUrl = cob.link_carne || cob.link_boleto || '';
    let isCarneCompleto = false;

    const pResp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${asaasPaymentId}`, {
      headers: { 'access_token': process.env.ASAAS_API_KEY }
    });

    if (pResp.ok) {
      const pData = await pResp.json();
      if (pData.description) descricao = pData.description;
      if (pData.value) fallbackValor = pData.value;
      if (pData.dueDate) fallbackVencimento = pData.dueDate;

      if (descricao.includes('Parcela')) {
        if (eventType === 'PAYMENT_CREATED') descricao = descricao.replace(' de ', ' a ');
        else if (['PAYMENT_RECEIVED', 'PAYMENT_CONFIRMED', 'PAYMENT_UPDATED'].includes(eventType)) {
          descricao = descricao.replace(/Parcela (\d+) a (\d+)/g, 'Parcela $1 de $2');
        }
      }

      if (pData.installment && eventType === 'PAYMENT_CREATED') {
        if (pData.installmentNumber > 1) return;
        isCarneCompleto = true;
        pdfUrl = `${ASAAS_BASE_URL}/v3/installments/${pData.installment}/paymentBook`;
      } else {
        pdfUrl = pData.transactionReceiptUrl || pData.bankSlipUrl || pData.invoiceUrl || pdfUrl;
      }
    }

    const fbAVencer = 'Olá {nome}, lembramos que sua cobrança referente a {descricao} no valor de R$ {valor} vencerá em {vencimento}. Segue o PDF abaixo:';

    let templateText = '';
    if (eventType === 'PAYMENT_CREATED') templateText = templates?.boletoGerado || fbGerado;
    else if (eventType === 'PAYMENT_RECEIVED' || eventType === 'PAYMENT_CONFIRMED') templateText = templates?.pagamentoConfirmado || fbPago;
    else if (eventType === 'PAYMENT_OVERDUE') templateText = templates?.boletoVencido || fbAtrasado;
    else if (eventType === 'PAYMENT_DELETED') templateText = templates?.cobrancaCancelada || fbCancelado;
    else if (eventType === 'PAYMENT_UPDATED') templateText = templates?.cobrancaAtualizada || fbAtualizado;
    else if (eventType === 'PAYMENT_UPCOMING') templateText = templates?.boletoAVencer || fbAVencer;

    if (!templateText) return;

    const valNum = parseFloat(fallbackValor);
    const valorFormatado = !isNaN(valNum) ? valNum.toFixed(2).replace('.', ',') : '—';

    let msgFinal = templateText
      .replace(/{nome}/g, targetName)
      .replace(/{nome_aluno}/g, aluno.name || aluno.nome)
      .replace(/{matricula}/g, aluno.enrollmentNumber || aluno.numero_matricula || aluno.matricula || '—')
      .replace(/{valor}/g, valorFormatado)
      .replace(/{vencimento}/g, formatCobrancaDate(typeof fallbackVencimento === 'string' ? fallbackVencimento : (fallbackVencimento instanceof Date ? fallbackVencimento.toISOString().split('T')[0] : '')))
      .replace(/{link_boleto}/g, pdfUrl)
      .replace(/{descricao}/g, descricao);

    const isPaymentConfirmation = ['PAYMENT_RECEIVED', 'PAYMENT_CONFIRMED'].includes(eventType);
    const isCreationEvent = eventType === 'PAYMENT_CREATED';

    // 1. GERAÇÃO DE PDF (Recibo ou Boleto)
    let base64Pdf = null;
    let fileName = `Documento-${targetName.replace(/\s+/g, '')}.pdf`;

    if (isPaymentConfirmation) {
      // GERAÇÃO DE RECIBO PROFISSIONAL (BACKEND - NODE COMPATIBLE)
      try {
        const doc = new jsPDF();
        const profile = appData.profile || {};
        
        // Moldura e Cabeçalho
        doc.setDrawColor(0);
        doc.setLineWidth(0.5);
        doc.rect(10, 10, 190, 100); 
        
        doc.setFontSize(14);
        doc.setFont('helvetica', 'bold');
        doc.text(profile.name || 'EduManager School', 105, 25, { align: 'center' });
        
        doc.setFontSize(9);
        doc.setFont('helvetica', 'normal');
        doc.text(`CNPJ: ${profile.cnpj || '---'} | Contato: ${profile.phone || ''}`, 105, 32, { align: 'center' });
        
        doc.setFontSize(16);
        doc.text('RECIBO DE PAGAMENTO', 105, 50, { align: 'center' });
        
        doc.setFontSize(11);
        doc.text(`Recebemos de: ${aluno.name || aluno.nome}`, 20, 65);
        doc.text(`A quantia de: R$ ${valorFormatado}`, 20, 75);
        doc.text(`Referente a: ${descricao}`, 20, 85);
        
        const dataHj = new Date().toLocaleDateString('pt-BR');
        const dataPagamento = fallbackVencimento ? formatCobrancaDate(typeof fallbackVencimento === 'string' ? fallbackVencimento : dataHj) : dataHj;
        doc.text(`Data do Pagamento: ${dataPagamento}`, 20, 95);
        
        doc.line(60, 105, 150, 105);
        doc.setFontSize(8);
        doc.text('Autenticação Digital EduManager', 105, 108, { align: 'center' });

        const pdfArrayBuffer = doc.output('arraybuffer');
        const pdfBuffer = Buffer.from(pdfArrayBuffer);
        
        // Upload para o MinIO (Pasta recibos) — apenas para envio via WhatsApp
        const minioFileName = `recibos/recibo_${asaasPaymentId}.pdf`;
        const minioUrl = await uploadReceiptToStorage(minioFileName, pdfBuffer);
        
        // NÃO sobrescrever transaction_receipt_url — o link do Asaas (público) é salvo pelo webhook
        // O MinIO é usado apenas para o envio do PDF via WhatsApp

        base64Pdf = pdfBuffer.toString('base64');
        fileName = `Recibo-${targetName.replace(/\s+/g, '')}.pdf`;
      } catch (pdfErr) {
        console.error('[WhatsApp] Erro ao gerar PDF de recibo:', pdfErr.message);
      }
    } else if (pdfUrl && !['PAYMENT_DELETED'].includes(eventType)) {
      // TENTA BUSCAR PDF EXTERNO (BOLETO/CARNÊ)
      for (let attempt = 1; attempt <= 3; attempt++) {
        try {
          const fetchOptions = { headers: { 'Accept': 'application/pdf' } };
          if (pdfUrl.includes('asaas.com')) fetchOptions.headers['access_token'] = process.env.ASAAS_API_KEY;
          const pdfResp = await fetch(pdfUrl, fetchOptions);
          if (pdfResp.ok && pdfResp.headers.get('content-type')?.includes('pdf')) {
            const arrayBuffer = await pdfResp.arrayBuffer();
            base64Pdf = Buffer.from(arrayBuffer).toString('base64');
            break;
          }
          if (attempt < 3) await new Promise(r => setTimeout(r, 3000));
        } catch (err) {
          if (attempt < 3) await new Promise(r => setTimeout(r, 3000));
        }
      }
    }

    if ((isCreationEvent || isPaymentConfirmation || eventType === 'PAYMENT_UPDATED' || eventType === 'PAYMENT_UPCOMING') && !base64Pdf && pdfUrl) {
      if (!templateText.includes('{link_boleto}')) {
        msgFinal += `\n\n📄 Acesse aqui seu documento:\n${pdfUrl}`;
      }
    }

    let endpoint = 'sendText';
    let payload = {};

    if (base64Pdf) {
      endpoint = 'sendMedia';
      if (isCarneCompleto) fileName = `Carne-${targetName.replace(/\s+/g, '')}.pdf`;
      payload = { 
        number: cleanPhone, 
        options: { delay: 1200, presence: "composing" }, 
        mediatype: "document", 
        mimetype: "application/pdf", 
        fileName, 
        media: base64Pdf, 
        caption: msgFinal 
      };
    } else {
      payload = { number: cleanPhone, text: msgFinal };
    }

    const url = `${evoConfig.apiUrl.replace(/\/$/, '')}/message/${endpoint}/${evoConfig.instanceName}`;
    const sendResp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': evoConfig.apiKey }, body: JSON.stringify(payload) });

    if (sendResp.ok) {
      console.log(`[WhatsApp] ✅ Enviado para ${cleanPhone} (${eventType})`);
      return true;
    } else {
      console.error(`[WhatsApp] ❌ Erro ao enviar WhatsApp:`, sendResp.status);
      return false;
    }
  } catch (error) {
    console.error('[WhatsApp] Erro interno:', error.message);
    return false;
  }
}

// ============================================================
// Webhook Asaas (Substituídas chamadas supabase por database.js)
// ============================================================
app.post('/api/webhook_asaas', async (req, res) => {
  const tokenRecebido = req.headers['asaas-access-token'];
  if (tokenRecebido !== process.env.ASAAS_WEBHOOK_TOKEN) {
    addLog('Webhook', 'Auth Negada', 'Token inválido');
    return res.status(401).json({ error: 'Não autorizado' });
  }

  try {
    const payload = req.body;
    if (payload.dateCreated) {
      const diffHours = (Date.now() - new Date(payload.dateCreated).getTime()) / (1000 * 60 * 60);
      if (diffHours > 24) return res.status(200).send('OK');
    }

    const asaasPaymentId = payload.payment.id;
    let updateData = {};
    const targetName = payload.payment.customerName || 'Cliente';

    switch (payload.event) {
      case 'PAYMENT_CREATED':
        setTimeout(() => sendEvolutionMessage(asaasPaymentId, 'PAYMENT_CREATED'), 2000);
        return res.status(200).json({ message: 'OK' });

      case 'PAYMENT_RECEIVED':
      case 'PAYMENT_CONFIRMED':
        updateData = {
          status: 'PAGO',
          valor_pago: payload.payment.value,
          data_pagamento: payload.payment.confirmedDate || payload.payment.paymentDate || new Date().toISOString().split('T')[0]
        };
        
        // Preservar o valor original da parcela — não inflar com desconto
        try {
          const cobRes = await pool.query('SELECT valor, discount, amount_original FROM alunos_cobrancas WHERE asaas_payment_id = $1', [asaasPaymentId]);
          if (cobRes.rows.length > 0) {
            const cob = cobRes.rows[0];
            const storedValor = Number(cob.valor || 0);
            const discount = Number(cob.discount || 0);
            const receivedValue = Number(payload.payment.value);

            // Se o Asaas enviou o valor líquido (menor que o registrado), 
            // registramos como valor_pago e preservamos o valor da parcela
            if (receivedValue < storedValor) {
              updateData.valor_pago = receivedValue;
            }
          }
        } catch (e) { console.error('[Webhook:Recovery] Erro:', e.message); }

        if (payload.payment.transactionReceiptUrl) {
          updateData.transaction_receipt_url = payload.payment.transactionReceiptUrl;
        }
        
        // Alerta no Sino (Admin)
        createAdminNotification(
          '✅ Pagamento Confirmado',
          `Recebemos R$ ${Number(payload.payment.value).toFixed(2)} de ${targetName}.`,
          { type: 'finance', status: 'paid', paymentId: asaasPaymentId }
        );

        sendEvolutionMessage(asaasPaymentId, 'PAYMENT_RECEIVED');
        break;

      case 'PAYMENT_OVERDUE':
        updateData = { status: 'ATRASADO' };
        
        // Alerta no Sino (Admin)
        createAdminNotification(
          '⚠️ Pagamento em Atraso',
          `A cobrança de ${targetName} no valor de R$ ${Number(payload.payment.value).toFixed(2)} está vencida.`,
          { type: 'finance', status: 'overdue', paymentId: asaasPaymentId }
        );

        sendEvolutionMessage(asaasPaymentId, 'PAYMENT_OVERDUE');
        break;

      case 'PAYMENT_UPDATED':
        updateData = {
          valor: payload.payment.value,
          vencimento: payload.payment.dueDate,
          link_boleto: payload.payment.bankSlipUrl || payload.payment.link || null
        };
        
        // Alerta no Sino (Admin)
        createAdminNotification(
          '📝 Cobrança Alterada',
          `A cobrança de ${targetName} foi atualizada no Asaas para R$ ${Number(payload.payment.value).toFixed(2)}.`,
          { type: 'finance', status: 'updated', paymentId: asaasPaymentId }
        );
        if (payload.event === 'PAYMENT_UPDATED') sendEvolutionMessage(asaasPaymentId, 'PAYMENT_UPDATED');
        break;

      case 'PAYMENT_DELETED':
      case 'PAYMENT_CANCELED':
        // Alerta no Sino (Admin)
        createAdminNotification(
          '🗑️ Cobrança Removida',
          `A cobrança de ${targetName} (R$ ${Number(payload.payment.value).toFixed(2)}) foi excluída no Asaas.`,
          { type: 'finance', status: 'deleted', paymentId: asaasPaymentId }
        );
        const installmentId = payload.payment.installment;
        if (installmentId) {
          if (cancelCache.has(installmentId)) {
            await deleteCobranca(asaasPaymentId);
            return res.status(200).send('OK');
          }
          cancelCache.add(installmentId);
          setTimeout(() => cancelCache.delete(installmentId), 60000);
        }
        const sent = await sendEvolutionMessage(asaasPaymentId, 'PAYMENT_DELETED');
        await deleteCobranca(asaasPaymentId);
        
        if (sent) {
          createAdminNotification('✅ WhatsApp Enviado', `O aluno ${targetName} foi notificado sobre o cancelamento da cobrança.`, { type: 'whatsapp', status: 'success' });
          addLog('WhatsApp', 'Cancelamento Enviado', { aluno: targetName, asaasPaymentId });
        } else {
          createAdminNotification('⚠️ Falha no WhatsApp', `Não foi possível enviar a notificação de cancelamento para ${targetName}.`, { type: 'whatsapp', status: 'error' });
          addLog('WhatsApp', 'Erro no Cancelamento', { aluno: targetName, asaasPaymentId });
        }

        return res.status(200).send('OK');

      default:
        return res.status(200).json({ message: 'Evento ignorado' });
    }

    await updateCobranca(asaasPaymentId, updateData);

    // Sincronização em tempo real com o JSON legado
    try {
      const appData = await getSchoolData();
      const pIdx = appData.payments?.findIndex(p => p.asaasPaymentId === asaasPaymentId);
      if (pIdx !== undefined && pIdx !== -1) {
        const p = appData.payments[pIdx];
        const statusStr = (updateData.status || '').toLowerCase();
        const newStatus = statusStr === 'pago' ? 'paid' :
                          statusStr === 'atrasado' ? 'overdue' :
                          statusStr === 'cancelado' ? 'cancelled' : 'pending';
        
        // Se for um evento de atualização de pagamento, atualiza o valor.
        // Se for só confirmação de recebimento, preserva o 'amount' bruto original para não causar double-discount.
        const shouldUpdateAmount = payload.event === 'PAYMENT_UPDATED' && updateData.valor;
        
        appData.payments[pIdx] = { 
          ...p, 
          status: newStatus,
          amount: shouldUpdateAmount ? updateData.valor : p.amount,
          valor_pago: updateData.valor_pago || p.valor_pago || 0,
          dueDate: updateData.vencimento || p.dueDate,
          paidDate: updateData.data_pagamento || p.paidDate
        };
        appData.lastUpdated = new Date().toISOString();
        await saveSchoolData(appData);
        console.log(`[Webhook:Sync] JSON atualizado para boleto ${asaasPaymentId} (Amount: ${shouldUpdateAmount ? 'Atualizado' : 'Preservado'})`);
      }
    } catch (syncErr) {
      console.error('[Webhook:Sync] Erro ao sincronizar JSON:', syncErr.message);
    }

    addLog('Webhook', `Sucesso ${payload.event}`, { asaasPaymentId });
    return res.status(200).json({ message: 'OK' });
  } catch (error) {
    console.error('Webhook erro:', error);
    return res.status(500).json({ error: 'Erro interno' });
  }
});

// Admin Raw Cobrancas para a Aba Financeiro
app.get('/api/admin/cobrancas', async (req, res) => {
  try {
    const result = await pool.query('SELECT * FROM alunos_cobrancas ORDER BY vencimento DESC');
    // Garantir que valores numéricos sejam retornados como Number para evitar bugs de string no front
    const rows = result.rows.map(r => ({
      ...r,
      valor: Number(r.valor)
    }));
    res.json(rows);
  } catch(e) {
    res.status(500).json({error: e.message});
  }
});

app.delete('/api/admin/cobrancas', async (req, res) => {
  try {
    const { ids } = req.body;
    if (!Array.isArray(ids)) return res.status(400).end();
    await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = ANY($1) OR local_id = ANY($1)', [ids]);
    res.json({ success: true });
  } catch(e) {
     res.status(500).json({error: e.message});
  }
});

app.delete('/api/admin/cobrancas/:id', async (req, res) => {
  try {
    await pool.query('DELETE FROM alunos_cobrancas WHERE asaas_payment_id = $1 OR local_id = $1', [req.params.id]);
    res.json({ success: true });
  } catch(e) {
    res.status(500).json({error: e.message});
  }
});

// Fase 2: Escrita dupla — Manager pode atualizar campos ricos diretamente no SQL
app.put('/api/admin/cobrancas/:id', async (req, res) => {
  try {
    const { valor, vencimento, description, type, discount, installment_number, total_installments, amount_original } = req.body;
    
    const updates = [];
    const values = [];
    let paramIdx = 1;

    if (valor !== undefined) { updates.push(`valor = $${paramIdx++}`); values.push(valor); }
    if (vencimento !== undefined) { updates.push(`vencimento = $${paramIdx++}`); values.push(vencimento); }
    if (description !== undefined) { updates.push(`description = $${paramIdx++}`); values.push(description); }
    if (type !== undefined) { updates.push(`type = $${paramIdx++}`); values.push(type); }
    if (discount !== undefined) { updates.push(`discount = $${paramIdx++}`); values.push(discount); }
    if (installment_number !== undefined) { updates.push(`installment_number = $${paramIdx++}`); values.push(installment_number); }
    if (total_installments !== undefined) { updates.push(`total_installments = $${paramIdx++}`); values.push(total_installments); }
    if (amount_original !== undefined) { updates.push(`amount_original = $${paramIdx++}`); values.push(amount_original); }

    if (updates.length === 0) return res.status(400).json({ error: 'Nenhum campo para atualizar.' });
 
    values.push(req.params.id);
    await pool.query(
      `UPDATE alunos_cobrancas SET ${updates.join(', ')} WHERE asaas_payment_id = $${paramIdx} OR local_id = $${paramIdx}`,
      values
    );
    
    res.json({ success: true });
  } catch(e) {
    console.error('[Admin:Cobrancas:PUT] Erro:', e.message);
    res.status(500).json({ error: e.message });
  }
});

// Webhook Evolution
app.post('/api/webhooks/evolution', (req, res) => {
  try {
    const payload = req.body;
    let messageData = payload.data || payload;
    if (messageData.status === 'READ') {
      const phone = messageData.key?.remoteJid || 'Desconhecido';
      console.log(`👀 [WhatsApp STATUS] Mensagem LIDA: ${phone.split('@')[0]}`);
    }
    res.status(200).send('OK');
  } catch (err) {
    res.status(500).send('Erro');
  }
});

// ============================================================
// Gerar Cobrança
// ============================================================
app.post('/api/gerar_cobranca', async (req, res) => {
  try {
    const { aluno_id, nome, cpf, email, valor, vencimento, multa, juros, desconto, telefone, cep, endereco, numero, bairro, descricao, parcelas, nascimento } = req.body;

    let customerId = '';
    const searchRes = await fetch(`${ASAAS_BASE_URL}/v3/customers?cpfCnpj=${cpf}`, { method: 'GET', headers: { 'access_token': process.env.ASAAS_API_KEY } });
    if (searchRes.ok) {
      const searchData = await searchRes.json();
      if (searchData.data?.length > 0) customerId = searchData.data[0].id;
    }

    if (!customerId) {
      const customerRes = await fetch(`${ASAAS_BASE_URL}/v3/customers`, {
        method: 'POST', headers: { 'Content-Type': 'application/json', 'access_token': process.env.ASAAS_API_KEY },
        body: JSON.stringify({ name: nome, cpfCnpj: cpf, email, mobilePhone: telefone, postalCode: cep, address: endereco, addressNumber: numero, province: bairro, birthDate: nascimento })
      });
      if (!customerRes.ok) {
        const errorData = await customerRes.json();
        throw new Error(errorData.errors?.[0]?.description || 'Falha ao criar cliente');
      }
      customerId = (await customerRes.json()).id;
    }

    const asaasPayload = { customer: customerId, billingType: 'BOLETO', dueDate: vencimento, description: descricao ? `${descricao} - Microtec Informática Cursos` : 'Mensalidade - Microtec Informática Cursos' };
    const isInstallment = parcelas && parseInt(parcelas) > 1;
    if (isInstallment) { asaasPayload.installmentCount = parseInt(parcelas); asaasPayload.installmentValue = parseFloat(valor); }
    else { asaasPayload.value = parseFloat(valor); }

    const fineValue = parseFloat(multa); const interestValue = parseFloat(juros); const discountValue = parseFloat(desconto);
    if (!isNaN(fineValue) && fineValue > 0) asaasPayload.fine = { value: fineValue, type: 'PERCENTAGE' };
    if (!isNaN(interestValue) && interestValue > 0) asaasPayload.interest = { value: interestValue, type: 'PERCENTAGE' };
    if (!isNaN(discountValue) && discountValue > 0) asaasPayload.discount = { value: discountValue, dueDateLimitDays: 0, type: 'FIXED' };

    const paymentRes = await fetch(`${ASAAS_BASE_URL}/v3/payments`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'access_token': process.env.ASAAS_API_KEY }, body: JSON.stringify(asaasPayload) });
    if (!paymentRes.ok) { const e = await paymentRes.json(); throw new Error(e.errors?.[0]?.description || 'Falha Asaas'); }

    const paymentData = await paymentRes.json();
    let paymentsToSave = [];
    const instId = formatInstallmentId(paymentData.installment);

    if (isInstallment && instId) {
      const installmentsRes = await fetch(`${ASAAS_BASE_URL}/v3/payments?installment=${instId}&limit=100`, { headers: { 'access_token': process.env.ASAAS_API_KEY } });
      if (installmentsRes.ok) {
        const installmentsData = await installmentsRes.json();
        paymentsToSave = installmentsData.data.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate)).map(p => ({
          aluno_id, asaas_customer_id: customerId, asaas_payment_id: p.id, asaas_installment_id: instId, installment: instId, valor: p.value, vencimento: p.dueDate, link_boleto: p.bankSlipUrl
        }));
      } else throw new Error('Falha ao buscar parcelas');
    } else {
      paymentsToSave = [{ aluno_id, asaas_customer_id: customerId, asaas_payment_id: paymentData.id, installment: null, valor: paymentData.value || valor, vencimento: paymentData.dueDate || vencimento, link_boleto: paymentData.bankSlipUrl }];
    }

    await insertCobrancas(paymentsToSave);

    if (paymentsToSave.length > 0) {
      sendEvolutionMessage(paymentsToSave[0].asaas_payment_id, 'PAYMENT_CREATED').catch(e => console.error('Erro disparo:', e));
    }

    return res.status(200).json({ success: true, installment: instId || null, payments: paymentsToSave, bankSlipUrl: paymentsToSave[0]?.link_boleto, paymentId: paymentsToSave[0]?.asaas_payment_id });
  } catch (error) {
    console.error('Erro gerar cobrança:', error);
    return res.status(500).json({ error: error.message });
  }
});

// ============================================================
// Notificar Alunos sobre Avaliação
// ============================================================
app.post('/api/exames/notificar', async (req, res) => {
  const { examId } = req.body;
  if (!examId) return res.status(400).json({ error: 'ID do exame obrigatório.' });

  try {
    const appData = await getSchoolData();
    const exam = (appData.exams || []).find(e => e.id === examId);
    if (!exam) return res.status(404).json({ error: 'Exame não encontrado.' });

    const classObj = (appData.classes || []).find(c => c.id === exam.classId);
    if (!classObj) return res.status(404).json({ error: 'Turma não encontrada.' });

    const subjectObj = (appData.subjects || []).find(s => s.id === exam.subjectId);
    const materia = subjectObj ? subjectObj.name : 'sua disciplina';

    const alunos = (appData.students || []).filter(s => s.classId === classObj.id && s.status === 'active');
    if (alunos.length === 0) return res.status(400).json({ error: 'Nenhum aluno ativo nesta turma.' });

    const evoConfig = appData.evolutionConfig;
    const msgTemplate = (appData.messageTemplates?.novaAvaliacao) || "Olá {nome}, uma nova {tipo_avaliacao} ({titulo_avaliacao}) de {materia} foi publicada no portal do aluno. Acesse e realize o mais breve possível!";

    const tipoAvaliacao = exam.evaluationType === 'activity' ? 'atividade' : 'prova';

    // 1. Inserir notificações no PostgreSQL (Sino do Portal)
    for (const aluno of alunos) {
      await pool.query(
        `INSERT INTO notificacoes (aluno_id, titulo, mensagem, lida) VALUES ($1, $2, $3, false)`,
        [aluno.id, "Nova Avaliação Disponível!", `A ${tipoAvaliacao} "${exam.title}" já está disponível no seu portal.`]
      );
    }

    // 2. Disparo de WhatsApp em Background
    if (evoConfig?.apiUrl && evoConfig?.apiKey && evoConfig?.instanceName) {
      // Background async function
      (async () => {
        for (let i = 0; i < alunos.length; i++) {
          const aluno = alunos[i];
          const telefone = aluno.phone || aluno.guardianPhone;
          if (!telefone) continue;

          let cleanPhone = telefone.replace(/\D/g, '');
          if (cleanPhone.length === 10 || cleanPhone.length === 11) cleanPhone = '55' + cleanPhone;

          const msg = msgTemplate
            .replace(/{nome}/g, aluno.name.split(' ')[0])
            .replace(/{matricula}/g, aluno.enrollmentNumber || '—')
            .replace(/{tipo_avaliacao}/g, tipoAvaliacao)
            .replace(/{titulo_avaliacao}/g, exam.title)
            .replace(/{materia}/g, materia)
            .replace(/{escola}/g, appData.profile?.name || 'nossa escola');

          try {
            const url = `${evoConfig.apiUrl.replace(/\/$/, '')}/message/sendText/${evoConfig.instanceName}`;
            await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': evoConfig.apiKey }, body: JSON.stringify({ number: cleanPhone, text: msg }) });
          } catch (error) { 
            console.error(`[Notificar Avaliação] Erro ${aluno.name}:`, error.message); 
          }
          if (i < alunos.length - 1) await new Promise(r => setTimeout(r, Math.floor(Math.random() * 30000) + 15000));
        }
      })();
    }

    return res.status(200).json({ success: true, message: 'Notificações criadas e disparos iniciados.' });
  } catch (error) {
    console.error('Erro ao notificar exames:', error);
    return res.status(500).json({ error: error.message });
  }
});

// ============================================================
// Notificações do Sistema (Painel Admin)
// ============================================================
app.get('/api/notificacoes/admin', async (req, res) => {
  try {
    const { rows } = await pool.query(
      'SELECT id, aluno_id as "studentId", titulo as title, mensagem as message, lida as read, anexo as attachment, created_at as "createdAt" FROM notificacoes WHERE aluno_id = $1 ORDER BY created_at DESC',
      ['admin']
    );
    res.json({ notifications: rows });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.put('/api/notificacoes/ler/:id', async (req, res) => {
  try {
    const { id } = req.params;
    await pool.query('UPDATE notificacoes SET lida = true WHERE id = $1', [id]);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.delete('/api/notificacoes/limpar-lidas', async (req, res) => {
  try {
    await pool.query('DELETE FROM notificacoes WHERE aluno_id = $1 AND lida = true', ['admin']);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.put('/api/notificacoes/remover-anexo/:id', async (req, res) => {
  try {
    const { id } = req.params;
    await pool.query('UPDATE notificacoes SET anexo = NULL WHERE id = $1', [id]);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// ============================================================
// Disparo em Massa
// ============================================================
app.post('/api/enviar-massa', upload.single('attachment'), (req, res) => {
  let { alunos, mensagem, delay } = req.body;
  if (typeof alunos === 'string') {
    try { alunos = JSON.parse(alunos); } catch (e) { alunos = []; }
  }
  if (!alunos || !Array.isArray(alunos) || alunos.length === 0) return res.status(400).json({ error: 'Nenhum aluno.' });
  res.status(200).json({ success: true, message: 'Background iniciado.' });
  
  const fileData = req.file ? {
    buffer: req.file.buffer.toString('base64'),
    mimetype: req.file.mimetype,
    originalname: req.file.originalname
  } : null;

  processarFilaWhatsApp(alunos, mensagem, parseInt(delay) || 60, fileData);
});

async function processarFilaWhatsApp(alunos, mensagemTemplate, customDelay = 60, fileData = null) {
  const appData = await getSchoolData();
  const evoConfig = appData?.evolutionConfig;
  if (!evoConfig?.apiUrl || !evoConfig?.apiKey || !evoConfig?.instanceName) return;

  for (let i = 0; i < alunos.length; i++) {
    const aluno = alunos[i];
    const msg = mensagemTemplate.replace(/{nome}/g, aluno.nome).replace(/{matricula}/g, aluno.matricula || '—');
    try {
      let cleanPhone = aluno.telefone.replace(/\D/g, '');
      if (cleanPhone.length === 10 || cleanPhone.length === 11) cleanPhone = '55' + cleanPhone;
      
      let url, payload;
      if (fileData) {
        url = `${evoConfig.apiUrl.replace(/\/$/, '')}/message/sendMedia/${evoConfig.instanceName}`;
        payload = {
           number: cleanPhone,
           options: { delay: 1200, presence: "composing" },
           mediatype: fileData.mimetype.includes('pdf') ? 'document' : 'image',
           mimetype: fileData.mimetype,
           fileName: fileData.originalname,
           media: fileData.buffer,
           caption: msg
        };
      } else {
        url = `${evoConfig.apiUrl.replace(/\/$/, '')}/message/sendText/${evoConfig.instanceName}`;
        payload = { number: cleanPhone, text: msg };
      }
      
      await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': evoConfig.apiKey }, body: JSON.stringify(payload) });
    } catch (error) { console.error(`[Massa] Erro ${aluno.nome}:`, error.message); }
    if (i < alunos.length - 1) {
      // Delay base informado pelo usuário + variância aleatória de 0-30s para evitar padrões robóticos
      const delayMs = (customDelay * 1000) + (Math.floor(Math.random() * 30000));
      await new Promise(r => setTimeout(r, delayMs));
    }
  }
}

// ============================================================
// Logs
// ============================================================
const apiLogs = [];
function addLog(service, action, details) {
  apiLogs.unshift({ date: new Date().toISOString(), service, action, details });
  if (apiLogs.length > 200) apiLogs.pop();
}
app.get('/api/logs', (req, res) => res.json(apiLogs));

const isUUID = (str) => typeof str === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
const formatInstallmentId = (id) => { if (!id) return id; if (id.startsWith('inst_')) return id.replace('inst_', 'ins_'); return id; };

// ============================================================
// Exclusão de Cobrança
// ============================================================
app.post('/api/excluir_cobranca', async (req, res) => {
  try {
    const { id } = req.body;
    if (!id) return res.status(400).json({ error: 'ID não fornecido' });

    const isManual = id.startsWith('pay-');

    if (isManual) {
      await pool.query('DELETE FROM alunos_cobrancas WHERE local_id = $1', [id]);
      return res.status(200).json({ message: 'Excluído na base local' });
    }

    const parcelas = await getCobrancasByOrQuery(id);
    let isSinglePayment = id.startsWith('pay_');

    if (!isSinglePayment) {
      const asaasTargetId = formatInstallmentId(id);
      const resp = await fetch(`${ASAAS_BASE_URL}/v3/installments/${asaasTargetId}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } });
      if (!resp.ok) { const e = await resp.json().catch(() => ({})); return res.status(400).json({ error: e.errors?.[0]?.description || 'Falha Asaas' }); }
      addLog('Asaas', 'Exclusão Parcelamento OK', { id: asaasTargetId });
      // Deletar localmente apenas via webhook para não apagar antes de enviar o WhatsApp (Regra 34)
    } else {
      const resp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${id}`, { method: 'DELETE', headers: { 'access_token': process.env.ASAAS_API_KEY } });
      if (!resp.ok) { const e = await resp.json().catch(() => ({})); return res.status(400).json({ error: e.errors?.[0]?.description || 'Falha Asaas' }); }
      
      addLog('Asaas', 'Exclusão Cobrança OK', { id });
      // Deletar localmente apenas via webhook para não apagar antes de enviar o WhatsApp (Regra 34)
    }

    return res.status(200).json({ message: 'Exclusão solicitada ao Asaas. A remoção local ocorrerá via Webhook.' });
  } catch (error) {
    console.error('[Exclusão] Erro:', error);
    return res.status(500).json({ error: 'Erro interno.' });
  }
});

// ============================================================
// Carnês e Links
// ============================================================
app.get('/api/parcelamentos/:id/carne', async (req, res) => {
  try {
    const id = req.params.id;
    const parcelas = await getCobrancasByOrQuery(id);
    let instId = (!id.startsWith('pay_')) ? id : null;
    if (!instId && parcelas?.length > 0) { const p = parcelas.find(x => x.asaas_installment_id); if (p) instId = p.asaas_installment_id; }

    if (instId) {
      const asaasTargetInstId = formatInstallmentId(instId);
      const pSaved = parcelas?.find(x => x.link_carne);
      if (pSaved?.link_carne) return res.status(200).json({ status: 'success', type: 'pdf', url: pSaved.link_carne });

      const ar = await fetch(`${ASAAS_BASE_URL}/v3/installments/${asaasTargetInstId}/paymentBook`, { headers: { 'access_token': process.env.ASAAS_API_KEY, 'Accept': 'application/pdf' } });
      if (ar.ok && ar.headers.get('content-type')?.includes('pdf')) {
        const buffer = Buffer.from(await ar.arrayBuffer());
        const fileName = `carne_${asaasTargetInstId}.pdf`;
        const publicUrl = await uploadCarneToStorage(fileName, buffer);
        await updateCobrancaLinkCarne(instId, publicUrl);
        return res.status(200).json({ status: 'success', type: 'pdf', url: publicUrl });
      }
    }

    const boletos = parcelas ? parcelas.map((c, i) => ({ id: c.id, numero: i + 1, vencimento: c.vencimento, valor: c.valor, linkBoleto: c.link_boleto, status: c.status, asaasPaymentId: c.asaas_payment_id })) : [];
    return res.status(200).json({ status: 'success', type: 'fallback', boletos });
  } catch (error) { return res.status(500).json({ error: 'Erro interno.' }); }
});

app.get('/api/cobrancas/:id/link', async (req, res) => {
  try {
    const p = await fetch(`${ASAAS_BASE_URL}/v3/payments/${req.params.id}`, { headers: { 'access_token': process.env.ASAAS_API_KEY } });
    if (!p.ok) return res.status(404).json({ error: 'Não encontrada.' });
    const d = await p.json();
    return res.status(200).json({ bankSlipUrl: d.bankSlipUrl || d.invoiceUrl, transactionReceiptUrl: d.transactionReceiptUrl });
  } catch (error) { return res.status(500).json({ error: 'Erro interno.' }); }
});

app.patch('/api/alunos/:id/rematricular', async (req, res) => {
  try {
    const { id } = req.params;
    // 1. Update in Postgres
    await pool.query("UPDATE alunos SET status = 'active', motivo_cancelamento = NULL WHERE id = $1", [id]);
    
    // 2. Reverse sync to legacy JSON
    const appData = await getSchoolData();
    const dbAlunos = await getAlunos();
    appData.students = dbAlunos;
    appData.lastUpdated = new Date().toISOString();
    await saveSchoolData(appData);

    res.json({ success: true });
  } catch (error) {
    console.error('Erro ao rematricular aluno:', error);
    res.status(500).json({ error: 'Erro interno' });
  }
});

app.put('/api/cobrancas/:id', async (req, res) => {
  try {
    const { id } = req.params;
    const { valor, vencimento } = req.body;
    let targetAsaasId = id;

    if (isUUID(id)) {
      const parcelas = await getCobrancasByOrQuery(id);
      if (parcelas.length > 0 && parcelas[0].asaas_payment_id) targetAsaasId = parcelas[0].asaas_payment_id;
    }

    const isAsaasPayment = targetAsaasId && targetAsaasId.startsWith('pay_');

    if (isAsaasPayment) {
      const aResp = await fetch(`${ASAAS_BASE_URL}/v3/payments/${targetAsaasId}`, {
        method: 'POST', headers: { 'Content-Type': 'application/json', 'access_token': process.env.ASAAS_API_KEY },
        body: JSON.stringify({ value: valor, dueDate: vencimento })
      });
      if (!aResp.ok) { const err = await aResp.json().catch(() => ({})); return res.status(400).json({ error: err.errors?.[0]?.description || 'Erro Asaas' }); }
    }

    const queryField = isUUID(id) ? 'id' : (id.startsWith('pay_') ? 'asaas_payment_id' : 'local_id');
    await updateCobrancaByField(queryField, id, { valor, vencimento });

    res.json({ message: 'Editado com sucesso' });
  } catch (e) { 
    console.error('[cobrancas:PUT] Erro:', e);
    res.status(500).json({ error: 'Erro interno.' }); 
  }
});

app.get('/api/alunos/:id/carne', async (req, res) => {
  try {
    const cobrancas = await getCobrancasByAlunoId(req.params.id);
    const withInstallment = cobrancas.filter(c => c.asaas_installment_id);
    if (withInstallment.length === 0) return res.status(404).json({ error: 'Nenhum carnê.' });

    const latestInstId = withInstallment[withInstallment.length - 1].asaas_installment_id;
    const asaasTargetInstId = formatInstallmentId(latestInstId);

    const binResp = await fetch(`${ASAAS_BASE_URL}/v3/installments/${asaasTargetInstId}/paymentBook`, { headers: { 'access_token': process.env.ASAAS_API_KEY, 'Accept': 'application/pdf' } });
    if (binResp.ok && binResp.headers.get('content-type')?.includes('pdf')) {
      const buffer = Buffer.from(await binResp.arrayBuffer());
      const fileName = `carne_${asaasTargetInstId}.pdf`;
      const publicUrl = await uploadCarneToStorage(fileName, buffer);
      await updateCobrancaLinkCarne(latestInstId, publicUrl);
      return res.status(200).json({ status: 'success', type: 'pdf', url: publicUrl });
    }

    const allCobs = await getCobrancasByInstallmentId(latestInstId);
    const boletos = allCobs.map((c, i) => ({ id: c.id, numero: i + 1, vencimento: c.vencimento, valor: c.valor, linkBoleto: c.link_boleto, status: c.status, asaasPaymentId: c.asaas_payment_id }));
    return res.status(200).json({ status: 'success', type: 'fallback', boletos });
  } catch (error) { return res.status(500).json({ error: 'Erro interno.' }); }
});

// ============================================================
// INICIALIZAÇÃO
// ============================================================
// ============================================================
// LÓGICA REUTILIZÁVEL DE DISPARO DE COBRANÇAS
// ============================================================
// Helpers de Data
// ============================================================
const getLocalSafeDate = (val) => {
  if (!val) return null;
  const d = new Date(val);
  if (isNaN(d.getTime())) return null;
  
  // Se for string YYYY-MM-DD pura, o JS cria em UTC. 
  // Forçamos para os componentes locais para evitar o deslocamento de -1 dia.
  if (typeof val === 'string' && val.includes('-') && !val.includes('T') && !val.includes(':')) {
    const parts = val.split(' ')[0].split('-');
    if (parts.length === 3) {
      return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]), 0, 0, 0, 0);
    }
  }
  
  // Para objetos Date ou strings com tempo, extraímos o dia civil local
  return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
};

// ============================================================
async function executarRotinaCobrancas(tipo = 'ambos') {
  const appData = await getSchoolData();
  const rules = appData?.messageTemplates?.automationRules || {};
  const sendDaysBefore = parseInt(rules.sendDaysBefore) || 3;
  const maxPreWarnings = parseInt(rules.maxPreWarnings) || 1;
  const sendDaysAfter = parseInt(rules.sendDaysAfter) || 1;
  const repeatEveryDays = parseInt(rules.repeatEveryDays) || 3;

  let enviadasAtraso = 0;
  let enviadasAviso = 0;

  const allPayments = appData.payments || [];
  const hoje = getLocalSafeDate(new Date());

  // 1. Processar Atrasados
  if (tipo === 'atrasado' || tipo === 'ambos') {
    const atrasados = allPayments.filter(p => (p.status || '').toUpperCase() === 'ATRASADO');

    for (const pJSON of atrasados) {
      if (!pJSON.asaasPaymentId || !pJSON.dueDate) continue;

      // Pegamos os contadores do SQL para este boleto específico
      const cob = await getCobrancaByPaymentId(pJSON.asaasPaymentId);
      if (!cob) continue; // Sincronização cuidará de criar no SQL depois
      
      const vencimento = getLocalSafeDate(pJSON.dueDate);
      if (!vencimento) continue;
      
      const diffDiasAtraso = Math.floor((hoje.getTime() - vencimento.getTime()) / (1000 * 60 * 60 * 24));

      if (diffDiasAtraso >= sendDaysAfter) {
        const lastWarn = getLocalSafeDate(cob.last_overdue_warning_at);
        
        const diasDesdeUltimoAviso = lastWarn 
            ? Math.floor((hoje.getTime() - lastWarn.getTime()) / (1000 * 60 * 60 * 24)) 
            : null;

        const jaEnviadoHoje = !rules.ignoreDailyLock && lastWarn && lastWarn.toDateString() === hoje.toDateString();

        if (!jaEnviadoHoje && (diasDesdeUltimoAviso === null || diasDesdeUltimoAviso >= repeatEveryDays)) {
          const sent = await sendEvolutionMessage(cob.asaas_payment_id, 'PAYMENT_OVERDUE', cob.valor, cob.vencimento);
          
          if (sent) {
            const currentCount = parseInt(cob.overdue_warnings_count) || 0;
            await pool.query(
              'UPDATE alunos_cobrancas SET overdue_warnings_count = $1, last_overdue_warning_at = NOW() WHERE asaas_payment_id = $2',
              [currentCount + 1, cob.asaas_payment_id]
            );
            
            enviadasAtraso++; 
          }
        }
      }
    }
  }

  // 2. Processar A Vencer (Lembretes Preventivos)
  if (tipo === 'preventivo' || tipo === 'ambos') {
    const pendentes = allPayments.filter(p => ['PENDENTE', 'PENDING'].includes((p.status || '').toUpperCase()));

    for (const pJSON of pendentes) {
      if (!pJSON.asaasPaymentId || !pJSON.dueDate) continue;
      
      // Pegamos os contadores do SQL para este boleto específico
      const cob = await getCobrancaByPaymentId(pJSON.asaasPaymentId);
      if (!cob) continue;
      
      const vencimento = getLocalSafeDate(pJSON.dueDate);
      if (!vencimento) continue;
      
      const diffDias = Math.ceil((vencimento.getTime() - hoje.getTime()) / (1000 * 60 * 60 * 24));
      const sendOnDueDate = rules.sendOnDueDate !== false;
      
      if ((diffDias > 0 && diffDias <= sendDaysBefore) || (diffDias === 0 && sendOnDueDate)) {
        const currentCount = parseInt(cob.pre_warnings_count) || 0;
        
        if (currentCount < maxPreWarnings) {
          const lastWarn = getLocalSafeDate(cob.last_pre_warning_at);
          const jaEnviadoHoje = !rules.ignoreDailyLock && lastWarn && lastWarn.toDateString() === hoje.toDateString();

          if (!jaEnviadoHoje) {
            const sent = await sendEvolutionMessage(cob.asaas_payment_id, 'PAYMENT_UPCOMING', cob.valor, cob.vencimento);
            
            if (sent) {
              await pool.query(
                'UPDATE alunos_cobrancas SET pre_warnings_count = $1, last_pre_warning_at = NOW() WHERE asaas_payment_id = $2',
                [currentCount + 1, cob.asaas_payment_id]
              );
              enviadasAviso++;
            }
          }
        }
      }
    }
  }

  return { enviadasAtraso, enviadasAviso };
}

// ============================================================
// Rotina Automática de Aniversários
// ============================================================
async function executarRotinaAniversarios() {
  try {
    const appData = await getSchoolData();
    if (!appData) return 0;

    const evoConfig = appData.evolutionConfig;
    if (!evoConfig?.apiUrl || !evoConfig?.apiKey || !evoConfig?.instanceName) {
      console.log('[Cron:Aniversário] ⚠️ Evolution API não configurada.');
      return 0;
    }

    const templates = appData.messageTemplates || {};
    const templateMsg = templates.felizAniversario || "Olá {nome}, a equipe da {escola} passa para te desejar um Feliz Aniversário! Muita saúde, paz e conquistas neste novo ciclo! 🎂🎈";
    const escolaNome = appData.profile?.name || '';
    
    // Busca alunos de forma híbrida (JSON e SQL)
    const { rows: sqlStudents } = await pool.query(`
      SELECT id, nome as name, email, telefone as phone, data_nascimento, status, nome_responsavel as "guardianName", telefone_responsavel as "guardianPhone"
      FROM alunos
      WHERE status = 'active'
    `);

    const jsonStudents = appData.students || [];
    const studentMap = new Map();

    // Adiciona alunos do JSON
    for (const s of jsonStudents) {
      if (s.status === 'active' && s.id) {
        studentMap.set(String(s.id), {
          id: String(s.id),
          name: s.name,
          phone: s.phone,
          guardianPhone: s.guardianPhone,
          guardianName: s.guardianName,
          birthDate: s.birthDate
        });
      }
    }

    // Sobrescreve/adiciona do SQL
    for (const s of sqlStudents) {
      if (s.id) {
        let birthDateStr = null;
        if (s.data_nascimento) {
          const d = new Date(s.data_nascimento);
          if (!isNaN(d.getTime())) {
            const year = d.getFullYear();
            const month = String(d.getMonth() + 1).padStart(2, '0');
            const day = String(d.getDate()).padStart(2, '0');
            birthDateStr = `${year}-${month}-${day}`;
          }
        }
        const existing = studentMap.get(String(s.id)) || {};
        studentMap.set(String(s.id), {
          id: String(s.id),
          name: s.name || existing.name,
          phone: s.phone || existing.phone,
          guardianPhone: s.guardianPhone || existing.guardianPhone,
          guardianName: s.guardianName || existing.guardianName,
          birthDate: birthDateStr || existing.birthDate
        });
      }
    }

    // Filtra aniversariantes do dia
    const today = new Date();
    const todayDay = today.getDate();
    const todayMonth = today.getMonth() + 1;

    const aniversariantes = [];
    for (const s of studentMap.values()) {
      if (!s.birthDate) continue;
      const parts = s.birthDate.split('-');
      if (parts.length === 3) {
        const bdayDay = parseInt(parts[2]);
        const bdayMonth = parseInt(parts[1]);
        if (bdayDay === todayDay && bdayMonth === todayMonth) {
          aniversariantes.push(s);
        }
      }
    }

    if (aniversariantes.length === 0) {
      console.log('[Cron:Aniversário] ℹ️ Nenhum aniversariante hoje.');
      return 0;
    }

    console.log(`[Cron:Aniversário] 🎉 Encontrados ${aniversariantes.length} aniversariante(s) hoje.`);

    let enviadas = 0;
    for (let i = 0; i < aniversariantes.length; i++) {
      const s = aniversariantes[i];
      const nomePrimeiro = s.name.split(' ')[0];
      const msg = templateMsg
        .replace(/{nome}/g, nomePrimeiro)
        .replace(/{escola}/g, escolaNome);

      if (!s.phone) continue;

      let cleanPhone = s.phone.replace(/\D/g, '');
      if (cleanPhone.length === 10 || cleanPhone.length === 11) cleanPhone = '55' + cleanPhone;

      try {
        const url = `${evoConfig.apiUrl.replace(/\/$/, '')}/message/sendText/${evoConfig.instanceName}`;
        const resp = await fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'apikey': evoConfig.apiKey
          },
          body: JSON.stringify({ number: cleanPhone, text: msg })
        });
        if (resp.ok) {
          enviadas++;
        } else {
          console.error(`[Cron:Aniversário] Falha ao enviar para ${cleanPhone}: status ${resp.status}`);
        }
      } catch (err) {
        console.error(`[Cron:Aniversário] Erro ao enviar para ${cleanPhone}:`, err.message);
      }

      if (i < aniversariantes.length - 1) {
        await new Promise(r => setTimeout(r, 30000));
      }
    }

    return enviadas;
  } catch (error) {
    console.error('[Cron:Aniversário] Erro na rotina de aniversário:', error.message);
    return 0;
  }
}

// ============================================================
// AGENDADOR AUTOMÁTICO (node-cron) — Suporte a múltiplos tipos
// ============================================================
function agendarRotina(tipo, hora, minuto) {
  const label = tipo === 'preventivo' ? 'Preventivo' : tipo === 'atrasado' ? 'Inadimplência' : 'Aniversário';

  // Cancela job anterior do mesmo tipo
  if (tipo === 'preventivo' && activeCronJob) {
    activeCronJob.stop();
    activeCronJob = null;
    console.log(`[Cron:${label}] ⏹ Rotina anterior cancelada.`);
  } else if (tipo === 'atrasado' && activeCronJobOverdue) {
    activeCronJobOverdue.stop();
    activeCronJobOverdue = null;
    console.log(`[Cron:${label}] ⏹ Rotina anterior cancelada.`);
  } else if (tipo === 'aniversario' && activeCronJobBirthday) {
    activeCronJobBirthday.stop();
    activeCronJobBirthday = null;
    console.log(`[Cron:${label}] ⏹ Rotina anterior cancelada.`);
  }

  const h = parseInt(hora);
  const m = parseInt(minuto);
  if (isNaN(h) || isNaN(m) || h < 0 || h > 23 || m < 0 || m > 59) {
    console.error(`[Cron:${label}] Horário inválido:`, hora, minuto);
    return;
  }

  const cronExpression = `${m} ${h} * * *`;
  const job = cron.schedule(cronExpression, async () => {
    console.log(`[Cron:${label}] ⏰ Rotina automática iniciada às ${new Date().toLocaleTimeString('pt-BR')}`);
    try {
      if (tipo === 'aniversario') {
        const count = await executarRotinaAniversarios();
        console.log(`[Cron:${label}] ✅ Concluído: ${count} mensagens processadas.`);
      } else {
        const cronTipo = tipo === 'preventivo' ? 'preventivo' : 'atrasado';
        const resultado = await executarRotinaCobrancas(cronTipo);
        const count = tipo === 'preventivo' ? resultado.enviadasAviso : resultado.enviadasAtraso;
        console.log(`[Cron:${label}] ✅ Concluído: ${count} mensagens processadas.`);
      }
    } catch (error) {
      console.error(`[Cron:${label}] ❌ Erro na rotina automática:`, error.message);
    }
  }, { timezone: 'America/Sao_Paulo' });

  if (tipo === 'preventivo') activeCronJob = job;
  else if (tipo === 'atrasado') activeCronJobOverdue = job;
  else if (tipo === 'aniversario') activeCronJobBirthday = job;

  console.log(`[Cron:${label}] ✅ Rotina agendada para ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')} (America/Sao_Paulo)`);
}

async function syncPaymentsWithAsaasAPI() {
  try {
    console.log(`[Asaas:Sync] 🚀 Iniciando Sincronização JSON-First...`);
    
    // 1. Carregamos o JSON principal
    const appData = await getSchoolData();
    if (!appData || !appData.payments) {
      console.error('[Asaas:Sync] ❌ JSON school_data ou payments não localizado.');
      return 0;
    }

    // 2. URLs de busca (Usando a chave global ASAAS_KEY e ASAAS_BASE_URL)
    const url = `${ASAAS_BASE_URL}/v3/payments?limit=100&status=RECEIVED&paymentDate%5Bge%5D=2026-01-01`;
    const urlConfirmed = `${ASAAS_BASE_URL}/v3/payments?limit=100&status=CONFIRMED`;
    
    const fetchPayments = async (targetUrl) => {
      try {
        const response = await fetch(targetUrl, { 
          headers: { 'access_token': process.env.ASAAS_API_KEY || ASAAS_KEY } 
        });
        if (!response.ok) {
          const errText = await response.text();
          console.error(`[Asaas:Sync] Erro na API (${response.status}):`, errText);
          return [];
        }
        const data = await response.json();
        return data.data || [];
      } catch (e) { 
        console.error(`[Asaas:Sync] Falha de rede na URL ${targetUrl}:`, e.message);
        return []; 
      }
    };

    const received = await fetchPayments(url);
    const confirmed = await fetchPayments(urlConfirmed);
    const allRecent = [...received, ...confirmed];

    if (allRecent.length === 0) {
      console.log('[Asaas:Sync] ℹ Nenhum pagamento confirmado/recebido no Asaas.');
      return 0;
    }
    
    const statusMap = { 
      'RECEIVED': 'PAGO', 
      'CONFIRMED': 'PAGO', 
      'RECEIVED_IN_CASH': 'PAGO', 
      'OVERDUE': 'ATRASADO',
      'REFUNDED': 'CANCELADO'
    };

    const jsonStatusMap = {
      'PAGO': 'paid',
      'ATRASADO': 'overdue',
      'CANCELADO': 'cancelled'
    };

    let totalUpdated = 0;
    
    for (const payment of allRecent) {
      const internalStatus = statusMap[payment.status];
      if (!internalStatus) continue;

      const valorNum = Number(payment.value);

      // A. Atualiza SQL (Prioridade Máxima)
      const receivedValue = (internalStatus === 'paid') ? valorNum : 0;
      await pool.query(`
        INSERT INTO alunos_cobrancas (asaas_payment_id, valor, vencimento, status, data_pagamento, valor_pago, amount_original)
        VALUES ($1, $2, $3, $4, $5, $6, $2)
        ON CONFLICT (asaas_payment_id) DO UPDATE SET 
          status = EXCLUDED.status, 
          data_pagamento = EXCLUDED.data_pagamento,
          valor_pago = CASE WHEN EXCLUDED.valor_pago > 0 THEN EXCLUDED.valor_pago ELSE alunos_cobrancas.valor_pago END,
          valor = COALESCE(NULLIF(EXCLUDED.valor, 0), alunos_cobrancas.valor)
      `, [payment.id, valorNum, payment.dueDate, internalStatus, payment.confirmedDate || payment.paymentDate, receivedValue]).catch(() => {});

      // B. Atualiza JSON
      const pIdx = appData.payments.findIndex(p => p.asaasPaymentId === payment.id);
      if (pIdx !== -1) {
        const newStatus = jsonStatusMap[internalStatus];
        const p = appData.payments[pIdx];
        let changed = false;

        if (p.status !== newStatus) {
          p.status = newStatus;
          changed = true;
        }

        // [Bugfix Crítico]: Não sobrescrever o valor BRUTO com o valor LÍQUIDO (descontado) do Asaas
        const currentAmount = Number(p.amount || 0);
        const currentDiscount = Number(p.discount || 0);
        const isNetValueOverwrite = valorNum < currentAmount && Math.abs((currentAmount - currentDiscount) - valorNum) < 0.01;

        if (p.amount !== valorNum && !isNetValueOverwrite) {
          p.amount = valorNum;
          changed = true;
        }

        // Adicionar valor_pago ao JSON para o Manager ler
        if (receivedValue > 0 && Number(p.valor_pago || 0) !== receivedValue) {
          p.valor_pago = receivedValue;
          changed = true;
        }

        const newPaidDate = payment.confirmedDate || payment.paymentDate;
        if (newPaidDate && p.paidDate !== newPaidDate) {
          p.paidDate = newPaidDate;
          changed = true;
        }

        if (changed) totalUpdated++;
      }
    }
    
    if (totalUpdated > 0) {
      appData.lastUpdated = new Date().toISOString();
      await saveSchoolData(appData);
      console.log(`[Asaas:Sync] ✅ Sucesso! ${totalUpdated} pagamentos atualizados.`);
    }

    return totalUpdated;
  } catch (err) {
    console.error('[Asaas:Sync] ❌ Erro Fatal:', err.message);
    throw err;
  }
}

async function syncRelationalToJsonPayments() {
  try {
    const { rows: cloudPayments } = await pool.query('SELECT * FROM alunos_cobrancas');
    const appData = await getSchoolData();
    let updatedCount = 0;

    if (!appData || !appData.payments) return;

    const updatedPayments = appData.payments.map(p => {
      const match = cloudPayments.find(cp => {
        if (p.asaasPaymentId && cp.asaas_payment_id === p.asaasPaymentId) return true;
        if (p.id && cp.local_id === p.id) return true;
        return false;
      });
      if (match) {
        const statusStr = (match.status || '').toLowerCase();
        const newStatus = statusStr === 'pago' ? 'paid' :
                          statusStr === 'atrasado' ? 'overdue' :
                          statusStr === 'cancelado' ? 'cancelled' : 'pending';

        const hasChanges = p.status !== newStatus || 
                           Number(p.valor_pago || 0) !== Number(match.valor_pago || 0) ||
                           Number(p.amount || 0) !== Number(match.valor || 0);

        if (hasChanges) {
          updatedCount++;
          return { 
            ...p, 
            status: newStatus, 
            paidDate: match.data_pagamento || p.paidDate,
            valor_pago: Number(match.valor_pago || 0),
            amount: Number(match.valor || p.amount || 0)
          };
        }
      }
      return p;
    });

    if (updatedCount > 0) {
      appData.payments = updatedPayments;
      appData.lastUpdated = new Date().toISOString();
      await saveSchoolData(appData);
      console.log(`[Sync:SQL->JSON] ✅ ${updatedCount} status de pagamentos sincronizados com sucesso.`);
    }
    return updatedCount;
  } catch (err) {
    console.error('[Sync:SQL->JSON] ❌ Erro na sincronização reversa:', err.message);
    return 0;
  }
}

async function inicializarAgendamento() {
  try {
    // Inicialização DB para colunas de automação (garantir no boot)
    await pool.query(`
      DO $$ 
      BEGIN 
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='pre_warnings_count') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN pre_warnings_count INTEGER DEFAULT 0;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='last_pre_warning_at') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN last_pre_warning_at TIMESTAMP WITH TIME ZONE;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='overdue_warnings_count') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN overdue_warnings_count INTEGER DEFAULT 0;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='last_overdue_warning_at') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN last_overdue_warning_at TIMESTAMP WITH TIME ZONE;
        END IF;

        -- ===== FASE 1: Colunas ricas para migração financeira SQL-First =====
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='description') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN description TEXT;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='type') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN type TEXT DEFAULT 'monthly';
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='discount') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN discount NUMERIC(10,2) DEFAULT 0;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='installment_number') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN installment_number INTEGER;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='total_installments') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN total_installments INTEGER;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='contract_id') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN contract_id TEXT;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='asaas_payment_url') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN asaas_payment_url TEXT;
        END IF;
        IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='alunos_cobrancas' AND column_name='amount_original') THEN
          ALTER TABLE alunos_cobrancas ADD COLUMN amount_original NUMERIC(10,2);
        END IF;

        -- Garantir índice de unicidade para o UPSERT funcionar
        IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'alunos_cobrancas' AND indexname = 'idx_asaas_payment_id_unique') THEN
          CREATE UNIQUE INDEX idx_asaas_payment_id_unique ON alunos_cobrancas(asaas_payment_id);
        END IF;
      END $$;
    `).catch(err => console.error('[PostgreSQL] Erro boot automação:', err));

    // Inicialização da Tabela de Notas e Migração Automática
    await initNotasTable();
    
    // Sincronização de Integridade (JSON -> Tabelas Relacionais)
    await syncJsonToRelationalTables();

    // Sincronização Reversa (SQL -> JSON) - Garante que status pagos no DB reflitam no painel administrativo
    await syncRelationalToJsonPayments();

    const appData = await getSchoolData();
    
    // Migração: Se existirem notas no JSON, movemos para a tabela e removemos do JSON
    if (appData.grades && appData.grades.length > 0) {
      console.log(`[Migração] Migrando ${appData.grades.length} notas do JSON para o PostgreSQL...`);
      for (const grade of appData.grades) {
        try {
          await upsertNota({
            aluno_id: String(grade.studentId),
            disciplina_id: String(grade.subjectId),
            periodo_id: String(grade.period),
            prova_id: grade.examId ? String(grade.examId) : null,
            valor: Number(grade.value)
          });
        } catch(err) {
          console.error('[Migração] Erro ao migrar nota:', err);
        }
      }
      appData.grades = []; // Limpa o JSON após migrar
      appData.lastUpdated = new Date().toISOString();
      await saveSchoolData(appData);
      console.log('[Migração] Migração de notas concluída com sucesso!');
    }
    const rules = appData?.messageTemplates?.automationRules || {};
    
    // Preventivo
    if (rules.autoScheduleEnabled && rules.autoScheduleTime) {
      const [h, m] = rules.autoScheduleTime.split(':');
      agendarRotina('preventivo', h, m);
    } else {
      console.log('[Cron:Preventivo] ℹ Agendamento desativado.');
    }

    // Inadimplência
    if (rules.autoScheduleOverdueEnabled && rules.autoScheduleOverdueTime) {
      const [h, m] = rules.autoScheduleOverdueTime.split(':');
      agendarRotina('atrasado', h, m);
    } else {
      console.log('[Cron:Inadimplência] ℹ Agendamento desativado.');
    }

    // Aniversário
    if (rules.autoScheduleBirthdayEnabled && rules.autoScheduleBirthdayTime) {
      const [h, m] = rules.autoScheduleBirthdayTime.split(':');
      agendarRotina('aniversario', h, m);
    } else {
      console.log('[Cron:Aniversário] ℹ Agendamento desativado.');
    }
  } catch (e) {
    console.error('[Cron] Erro ao inicializar agendamento:', e.message);
  }
}

async function startServer() {

  // Rota para zerar contadores de avisos
  app.post('/api/admin/reset-cobrancas-counters', async (req, res) => {
    try {
      await pool.query('UPDATE alunos_cobrancas SET pre_warnings_count = 0, last_pre_warning_at = NULL, overdue_warnings_count = 0, last_overdue_warning_at = NULL');
      return res.json({ success: true, message: 'Contadores zerados com sucesso!' });
    } catch (e) {
      return res.status(500).json({ error: e.message });
    }
  });

  // Disparo Manual de Inadimplência e Lembretes
  app.post('/api/disparar_cobrancas', async (req, res) => {
    try {
      const tipo = req.query.tipo || 'ambos';
      const resultado = await executarRotinaCobrancas(tipo);

      let msg = '';
      if (tipo === 'atrasado') msg = `${resultado.enviadasAtraso} mensagens de atraso processadas.`;
      else if (tipo === 'preventivo') msg = `${resultado.enviadasAviso} lembretes preventivos processados.`;
      else msg = `${resultado.enviadasAtraso} mensagens de atraso e ${resultado.enviadasAviso} lembretes preventivos processados.`;

      return res.status(200).json({ message: msg });
    } catch (error) { 
      console.error('[Disparo] Erro:', error);
      return res.status(500).json({ error: 'Erro interno.' }); 
    }
  });

  // Endpoint para forçar sincronização direta com a API do Asaas (Aba Financeiro)
  app.post('/api/admin/sync-asaas-full', async (req, res) => {
    try {
      const updatedCount = await syncPaymentsWithAsaasAPI();
      const appData = await getSchoolData(); // Busca o JSON já atualizado
      res.json({ success: true, updatedCount, data: appData });
    } catch (e) {
      console.error('[Asaas:FullSync] Erro:', e.message);
      res.status(500).json({ error: e.message });
    }
  });

  // Endpoint para forçar sincronização SQL -> JSON (Aba Financeiro)
  app.post('/api/admin/sync-finance-json', async (req, res) => {
    try {
      const updatedCount = await syncRelationalToJsonPayments();
      res.json({ success: true, updatedCount });
    } catch (e) {
      res.status(500).json({ error: e.message });
    }
  });

  // API para gerenciar o agendamento (suporte a preventivo, atrasado e aniversário)
  app.get('/api/cron/status', (req, res) => {
    res.json({ 
      preventive: !!activeCronJob, 
      overdue: !!activeCronJobOverdue,
      birthday: !!activeCronJobBirthday
    });
  });

  app.post('/api/cron/schedule', async (req, res) => {
    try {
      const { enabled, time, tipo } = req.body;
      const appData = await getSchoolData();
      if (!appData.messageTemplates) appData.messageTemplates = {};
      if (!appData.messageTemplates.automationRules) appData.messageTemplates.automationRules = {};
      
      if (tipo === 'atrasado') {
        appData.messageTemplates.automationRules.autoScheduleOverdueEnabled = !!enabled;
        appData.messageTemplates.automationRules.autoScheduleOverdueTime = time || '09:00';
      } else if (tipo === 'aniversario') {
        appData.messageTemplates.automationRules.autoScheduleBirthdayEnabled = !!enabled;
        appData.messageTemplates.automationRules.autoScheduleBirthdayTime = time || '09:00';
      } else {
        appData.messageTemplates.automationRules.autoScheduleEnabled = !!enabled;
        appData.messageTemplates.automationRules.autoScheduleTime = time || '09:00';
      }

      appData.lastUpdated = new Date().toISOString();
      await saveSchoolData(appData);

      if (enabled && time) {
        const [h, m] = time.split(':');
        agendarRotina(tipo, h, m);
      } else {
        if (tipo === 'atrasado') {
          if (activeCronJobOverdue) { activeCronJobOverdue.stop(); activeCronJobOverdue = null; }
        } else if (tipo === 'aniversario') {
          if (activeCronJobBirthday) { activeCronJobBirthday.stop(); activeCronJobBirthday = null; }
        } else {
          if (activeCronJob) { activeCronJob.stop(); activeCronJob = null; }
        }
      }

      res.json({ 
        success: true, 
        preventive: !!activeCronJob, 
        overdue: !!activeCronJobOverdue,
        birthday: !!activeCronJobBirthday
      });
    } catch (error) {
      console.error('[Cron] Erro ao salvar agendamento:', error);
      res.status(500).json({ error: 'Erro interno.' });
    }
  });

  // Imprimir Carnê
  app.get('/api/imprimir-carne/:installmentId', async (req, res) => {
    try {
      const { installmentId } = req.params;
      const parcelas = await getCobrancasByOrQuery(installmentId);
      let instId = (!installmentId.startsWith('pay_')) ? installmentId : null;
      if (!instId && parcelas?.length > 0) { const p = parcelas.find(x => x.asaas_installment_id); if (p) instId = p.asaas_installment_id; }

      const asaasTargetInstId = formatInstallmentId(instId || installmentId);
      const pSaved = parcelas?.find(x => x.link_carne);
      if (pSaved?.link_carne) return res.redirect(pSaved.link_carne);

      let asaasUrl = `${ASAAS_BASE_URL}/v3/installments/${asaasTargetInstId}/paymentBook`;
      const { sort, order } = req.query;
      const params = new URLSearchParams();
      if (sort) params.append('sort', sort);
      if (order) params.append('order', order);
      if (params.toString()) asaasUrl += `?${params.toString()}`;

      const response = await fetch(asaasUrl, { headers: { 'access_token': process.env.ASAAS_API_KEY, 'Accept': 'application/pdf' } });
      if (response.ok && response.headers.get('content-type')?.includes('pdf')) {
        const buffer = Buffer.from(await response.arrayBuffer());
        const fileName = `carne_${asaasTargetInstId}.pdf`;

        // Upload assíncrono para MinIO
        uploadCarneToStorage(fileName, buffer).then(publicUrl => {
          updateCobrancaLinkCarne(instId, publicUrl).catch(() => {});
        }).catch(() => {});

        res.setHeader('Content-Type', 'application/pdf');
        res.setHeader('Content-Disposition', 'inline; filename="carne.pdf"');
        return res.send(buffer);
      } else {
        return res.status(response.status).send('Falha Asaas');
      }
    } catch (error) { return res.status(500).json({ error: 'Erro interno.' }); }
  });

  // ===================================================
  // PRÉ-MATRÍCULA — ROTAS CRUD (SQL-First)
  // ===================================================

  // Config
  app.get('/api/prematricula/config', async (req, res) => {
    try {
      const { rows } = await pool.query('SELECT * FROM prematricula_config WHERE id = 1');
      const r = rows[0];
      if (!r) return res.json({ config: null });
      res.json({ config: {
        id: r.id, titulo: r.titulo, descricao: r.descricao, slug: r.slug,
        status: r.status, corPrimaria: r.cor_primaria, logoUrl: r.logo_url,
        mensagemSucesso: r.mensagem_sucesso,
        turmasPermitidas: r.turmas_permitidas || [],
        exibirEmail: r.exibir_email !== false,
        exibirTelefone: r.exibir_telefone !== false,
        bannerUrl: r.banner_url || ''
      }});
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  app.put('/api/prematricula/config', async (req, res) => {
    try {
      const c = req.body;
      await pool.query(
        `UPDATE prematricula_config SET titulo=$1, descricao=$2, slug=$3, status=$4,
         cor_primaria=$5, logo_url=$6, mensagem_sucesso=$7, turmas_permitidas=$8,
         exibir_email=$9, exibir_telefone=$10, banner_url=$11, updated_at=NOW() WHERE id=1`,
        [
          c.titulo, c.descricao, c.slug, c.status, c.corPrimaria || '#4f46e5',
          c.logoUrl || '', c.mensagemSucesso || '', c.turmasPermitidas || [],
          c.exibirEmail !== false, c.exibirTelefone !== false, c.bannerUrl || ''
        ]
      );
      res.json({ success: true });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  // Campos
  app.get('/api/prematricula/campos', async (req, res) => {
    try {
      const { rows } = await pool.query('SELECT * FROM prematricula_campos ORDER BY ordem ASC');
      const active = rows.filter(r => r.ativo !== false);
      res.json({
        campos: active.map(r => ({
          id: r.id, label: r.label, tipo: r.tipo, placeholder: r.placeholder,
          obrigatorio: r.obrigatorio, opcoes: r.opcoes || [], ordem: r.ordem, ativo: r.ativo
        })),
        todosCampos: rows.map(r => ({
          id: r.id, label: r.label, tipo: r.tipo, placeholder: r.placeholder,
          obrigatorio: r.obrigatorio, opcoes: r.opcoes || [], ordem: r.ordem, ativo: r.ativo
        }))
      });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  app.put('/api/prematricula/campos/reordenar', async (req, res) => {
    try {
      const { ordem } = req.body;
      if (!Array.isArray(ordem)) return res.status(400).json({ error: 'Formato inválido.' });
      
      const client = await pool.connect();
      try {
        await client.query('BEGIN');
        for (const item of ordem) {
          await client.query('UPDATE prematricula_campos SET ordem = $1 WHERE id = $2', [item.ordem, item.id]);
        }
        await client.query('COMMIT');
      } catch (e) {
        await client.query('ROLLBACK');
        throw e;
      } finally {
        client.release();
      }
      res.json({ success: true });
    } catch (e) {
      console.error(e);
      res.status(500).json({ error: e.message });
    }
  });

  app.post('/api/prematricula/campos', async (req, res) => {
    try {
      const c = req.body;
      await pool.query(
        `INSERT INTO prematricula_campos (id, label, tipo, placeholder, obrigatorio, opcoes, ordem, ativo)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
        [c.id, c.label, c.tipo, c.placeholder || '', c.obrigatorio || false, c.opcoes || [], c.ordem || 0, true]
      );
      res.json({ success: true });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  app.put('/api/prematricula/campos/:id', async (req, res) => {
    try {
      const c = req.body;
      await pool.query(
        `UPDATE prematricula_campos SET label=$1, tipo=$2, placeholder=$3, obrigatorio=$4, opcoes=$5, ordem=$6 WHERE id=$7`,
        [c.label, c.tipo, c.placeholder, c.obrigatorio, c.opcoes || [], c.ordem, req.params.id]
      );
      res.json({ success: true });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  app.delete('/api/prematricula/campos/:id', async (req, res) => {
    try {
      await pool.query('UPDATE prematricula_campos SET ativo = false WHERE id = $1', [req.params.id]);
      res.json({ success: true });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  // Inscrições
  app.get('/api/prematricula/inscricoes', async (req, res) => {
    try {
      const { rows } = await pool.query('SELECT * FROM prematriculas ORDER BY created_at DESC');
      res.json({ inscricoes: rows.map(r => ({
        id: r.id, nome: r.nome, email: r.email, telefone: r.telefone,
        turmaId: r.turma_id, turmaNome: r.turma_nome, respostas: r.respostas || {},
        status: r.status, observacoes: r.observacoes, createdAt: r.created_at
      }))});
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  app.delete('/api/prematricula/inscricoes/:id', async (req, res) => {
    try {
      await pool.query('DELETE FROM prematriculas WHERE id = $1', [req.params.id]);
      res.json({ success: true });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  // Upload público de arquivos de pré-matrícula para o MinIO
  const prematriculaUpload = multer({
    storage: multer.memoryStorage(),
    limits: { fileSize: 10 * 1024 * 1024 } // 10MB
  });

  app.post('/api/prematricula/upload', prematriculaUpload.single('file'), async (req, res) => {
    try {
      if (!req.file) {
        return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
      }
      const fileName = `pre_${Date.now()}_${Math.random().toString(36).substring(7)}_${req.file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
      const url = await uploadFile('prematriculas', fileName, req.file.buffer, req.file.mimetype);
      res.json({ url });
    } catch (e) {
      console.error('[PreMatricula:Upload] Erro de upload:', e);
      res.status(500).json({ error: 'Erro ao fazer upload do arquivo no storage local.' });
    }
  });

  // Submissão pública
  app.post('/api/prematricula/submit', async (req, res) => {
    try {
      const { nome, email, telefone, turmaId, turmaNome, respostas } = req.body;
      if (!nome) return res.status(400).json({ error: 'Nome é obrigatório.' });
      
      const config = (await pool.query('SELECT status FROM prematricula_config WHERE id = 1')).rows[0];
      if (!config || config.status !== 'published') return res.status(403).json({ error: 'Formulário não disponível.' });
      
      // Impedir duplicidade por e-mail ou telefone
      if ((email && email.trim()) || (telefone && telefone.trim())) {
        let dupQuery = 'SELECT id FROM prematriculas WHERE 1=0';
        const params = [];
        if (email && email.trim()) {
          dupQuery += ' OR LOWER(email) = LOWER($' + (params.length + 1) + ')';
          params.push(email.trim());
        }
        if (telefone && telefone.trim()) {
          const cleanPhone = telefone.replace(/\D/g, '');
          if (cleanPhone) {
            dupQuery += " OR REGEXP_REPLACE(telefone, '\\D', '', 'g') = $" + (params.length + 1);
            params.push(cleanPhone);
          }
        }
        const { rows: dupRows } = await pool.query(dupQuery, params);
        if (dupRows.length > 0) {
          return res.status(400).json({ error: 'Você já realizou uma pré-matrícula com esses dados de contato.' });
        }
      }
      
      // Validação rigorosa de vagas
      if (turmaId) {
        const { rows: tRows } = await pool.query('SELECT vagas_prematricula FROM turmas WHERE id = $1', [turmaId]);
        if (tRows.length > 0 && tRows[0].vagas_prematricula !== null) {
          const limit = tRows[0].vagas_prematricula;
          const { rows: countRows } = await pool.query("SELECT COUNT(*) as count FROM prematriculas WHERE status != 'rejeitado' AND turma_id = $1", [turmaId]);
          const count = parseInt(countRows[0].count);
          if (count >= limit) {
            return res.status(400).json({ error: 'Desculpe, esta turma já preencheu todas as vagas de pré-matrícula.' });
          }
        }
      }
      
      const id = crypto.randomUUID();
      await pool.query(
        `INSERT INTO prematriculas (id, nome, email, telefone, turma_id, turma_nome, respostas, status)
         VALUES ($1, $2, $3, $4, $5, $6, $7, 'pendente')`,
        [id, nome, email || '', telefone || '', turmaId || null, turmaNome || '', JSON.stringify(respostas || {})]
      );
      res.json({ success: true, id });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  });

  async function handlePublicPreMatricula(req, res, slug) {
    try {
      const { rows: cfgRows } = await pool.query(
        'SELECT * FROM prematricula_config WHERE (slug = $1 OR id = 1) ORDER BY id ASC LIMIT 1',
        [slug]
      );
      if (cfgRows.length === 0) return res.status(404).json({ error: 'Formulário não encontrado.' });
      const cfg = cfgRows[0];

      if (cfg.status === 'draft') {
        return res.json({ draft: true });
      }

      const { rows: camposRows } = await pool.query('SELECT * FROM prematricula_campos WHERE ativo = true ORDER BY ordem ASC');
      
      // Buscar contagens de pré-matrículas ativas por turma para calcular as vagas restantes
      const countsResult = await pool.query("SELECT turma_id, COUNT(*) as count FROM prematriculas WHERE status != 'rejeitado' GROUP BY turma_id");
      const prematriculaCounts = {};
      countsResult.rows.forEach(r => {
        if (r.turma_id) {
          prematriculaCounts[r.turma_id] = parseInt(r.count);
        }
      });

      const turmasResult = await pool.query('SELECT id, nome, vagas_prematricula, horario, dia_semana, horario_inicio_padrao, horario_fim_padrao FROM turmas ORDER BY nome ASC');
      
      let turmasRows = turmasResult.rows;
      if (cfg.turmas_permitidas && cfg.turmas_permitidas.length > 0) {
        turmasRows = turmasRows.filter(t => cfg.turmas_permitidas.includes(t.id));
      }

      const appData = await getSchoolData();
      res.json({
        config: {
          titulo: cfg.titulo, descricao: cfg.descricao, corPrimaria: cfg.cor_primaria,
          logoUrl: cfg.logo_url, mensagemSucesso: cfg.mensagem_sucesso,
          bannerUrl: cfg.banner_url
        },
        campos: camposRows.map(r => ({
          id: r.id, label: r.label, tipo: r.tipo, placeholder: r.placeholder,
          obrigatorio: r.obrigatorio, opcoes: r.opcoes || []
        })),
        turmas: turmasRows.map(t => {
          const count = prematriculaCounts[t.id] || 0;
          const vagas = t.vagas_prematricula;
          const esgotado = vagas !== null && count >= vagas;
          const horarioStr = t.horario_inicio_padrao && t.horario_fim_padrao 
            ? `${t.horario_inicio_padrao} às ${t.horario_fim_padrao}` 
            : '';
          return {
            id: t.id,
            nome: t.nome,
            horario: horarioStr,
            vagas: vagas,
            vagasRestantes: vagas !== null ? Math.max(0, vagas - count) : null,
            esgotado: esgotado
          };
        }),
        escola: { nome: appData?.profile?.name || 'EduManager', logo: appData?.logo || '' }
      });
    } catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
  }

  // Rota sem slug (usa slug padrão)
  app.get('/api/prematricula/public', (req, res) => handlePublicPreMatricula(req, res, 'pre-matricula'));
  // Rota com slug explícito
  app.get('/api/prematricula/public/:slug', (req, res) => handlePublicPreMatricula(req, res, req.params.slug));

  // Middleware para servir a página HTML pública de forma 100% dinâmica baseada na slug do banco
  app.use(async (req, res, next) => {
    // Ignorar APIs, Storage ou arquivos de assets estáticos (com extensão)
    if (req.path.startsWith('/api') || req.path.startsWith('/storage') || req.path.includes('.')) {
      return next();
    }
    const slug = req.path.substring(1); // Remove a primeira barra
    if (!slug) return next();

    try {
      // Verifica se a slug atual corresponde a alguma configuração de pré-matrícula publicada
      const { rows } = await pool.query(
        'SELECT slug FROM prematricula_config WHERE slug = $1 LIMIT 1',
        [slug]
      );
      if (rows.length > 0) {
        // Se bater, serve a página pública renderizando a slug dinâmica!
        return res.send(getPreMatriculaHTML(slug));
      }
    } catch (e) {
      console.error('[PreMatricula:Router] Erro de roteamento dinâmico:', e);
    }
    next();
  });

  // ===================================================
  // SERVE FRONTEND (Final Catch-all)
  // ===================================================
  const distPath = path.join(__dirname, 'dist');
  if (fs.existsSync(distPath)) {
    app.use(express.static(distPath));
    app.use((req, res, next) => {
      if (req.path.startsWith('/api') || req.path.startsWith('/storage')) return next();
      res.sendFile(path.join(distPath, 'index.html'));
    });
  } else {
    try {
      const vite = await import('vite').then(m => m.createServer({ 
        server: { middlewareMode: true }, 
        appType: 'spa' 
      }));
      app.use(vite.middlewares);
    } catch (e) {
      console.warn('Vite dev server not available and dist folder missing.');
    }
  }

  app.listen(PORT, '0.0.0.0', () => {
    console.log(`🚀 EduManager Self-Hosted na porta ${PORT}`);
    // Inicializa agendamento automático após servidor subir
    inicializarAgendamento();
  });
}

startServer();
