require("dotenv").config();
const express = require("express");
const path = require("path");
const crypto = require("crypto");
const bcrypt = require("bcryptjs");
const cookieParser = require("cookie-parser");
const { pool, initDb } = require("./db");

const PORT = process.env.PORT || 4000;
const ADMIN_USER = process.env.ADMIN_USER || "admin";
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
const REGISTRO_SECRET = process.env.REGISTRO_SECRET;
if (!ADMIN_PASSWORD) {
  console.error("ERRO: defina ADMIN_PASSWORD no .env antes de iniciar o servidor.");
  process.exit(1);
}
if (!REGISTRO_SECRET) {
  console.error("ERRO: defina REGISTRO_SECRET no .env antes de iniciar o servidor.");
  process.exit(1);
}

const AREAS = ["caixas", "contas", "vendas", "compras", "estoque", "inadimplentes"];
const AREA_LABELS = {
  caixas: "Caixas",
  contas: "Contas",
  vendas: "Vendas",
  compras: "Compras",
  estoque: "Estoque",
  inadimplentes: "Inadimplentes",
};
const AREA_ICONS = {
  caixas: "💰",
  contas: "🧾",
  vendas: "📈",
  compras: "🛒",
  estoque: "📦",
  inadimplentes: "⚠️",
};
const AREA_CRITICAL = { inadimplentes: true };

const app = express();
app.use(express.json({ limit: "60mb" })); // 1a sincronizacao de clientes grandes pode mandar bastante linha por tabela
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use("/assets", express.static(path.join(__dirname, "assets")));

function gerarToken() {
  return crypto.randomBytes(32).toString("hex");
}

// ---------- Espelho por cliente (banco `{codigo}_genius`, tabelas identicas as do Genius) ----------

// Tabelas clonadas do Genius de cada cliente, com a coluna que identifica
// "ultima alteracao" daquela linha (usada pro sync incremental). Tabelas sem
// coluna de alteracao nem de criacao confiavel no Genius (fornecedores/
// filiais/itens_pagar/contas_pagar) sao pequenas o bastante pra re-sincronizar
// por completo a cada ciclo, sem heuristica.
const TABELAS_ESPELHO = {
  // caixa tem a coluna DataHoraAlteracao no schema, mas na pratica ela vem
  // sempre NULL (confirmado direto no Genius de um cliente real -- 104219 de
  // 104219 linhas sem valor) -- essa instalacao nao preenche esse campo.
  // Tratada como Tier 2 (por data de criacao + janela de seguranca), igual
  // vendas/compras, em vez de confiar numa coluna que existe no schema mas
  // nao e alimentada de verdade.
  caixa: { colunaCriacao: "data_caixa" },
  vendas: { colunaCriacao: "data_venda" },
  compras: { colunaCriacao: "data_compra" },
  itens_receber: { colunaAlteracao: "data_ocorrencia" },
  contas_receber: { colunaAlteracao: "data_ocorrencia" },
  clientes: { colunaAlteracao: "data_ocorrencia" },
  produtos_estoque: { colunaAlteracao: "data_ocorrencia" },
  produtos: { colunaAlteracao: "data_ocorrencia" },
  funcionarios: { colunaAlteracao: "data_ocorrencia" },
  itens_pagar: {},
  contas_pagar: {},
  fornecedores: {},
  filiais: {},
};

// Nome de banco/tabela nunca aceita "?" parametrizado (MySQL nao permite
// parametrizar identificador) -- por isso todo valor usado pra montar SQL de
// banco/tabela passa por aqui antes. "codigo" vem de `clientes.codigo`
// (definido pelo admin); nunca confiar que já está seguro sem checar de novo
// aqui, e nome de tabela sempre contra o allowlist fixo acima.
// Mesmo formato/mesma logica (getters locais, nao toISOString, pra nao
// deslocar fuso horario) do formatarDataHoraMysql do agente (queries.js) --
// usado aqui pra devolver o "desde" de /ultima-sincronizacao num formato que
// o MySQL do cliente aceita direto num WHERE `coluna` >= ?.
function formatarDataHoraMysql(d) {
  const dt = new Date(d);
  const y = dt.getFullYear();
  const mo = String(dt.getMonth() + 1).padStart(2, "0");
  const dia = String(dt.getDate()).padStart(2, "0");
  const h = String(dt.getHours()).padStart(2, "0");
  const mi = String(dt.getMinutes()).padStart(2, "0");
  const s = String(dt.getSeconds()).padStart(2, "0");
  return `${y}-${mo}-${dia} ${h}:${mi}:${s}`;
}

function nomeBancoCliente(codigo) {
  if (!/^[A-Za-z0-9]+$/.test(String(codigo || ""))) {
    throw new Error(`Codigo de cliente invalido para nome de banco: ${codigo}`);
  }
  return `${codigo}_genius`;
}

async function tabelaEspelhoExiste(banco, tabela) {
  const [rows] = await pool.query(
    "SELECT 1 FROM information_schema.tables WHERE table_schema=? AND table_name=? LIMIT 1",
    [banco, tabela]
  );
  return rows.length > 0;
}

// Cria o banco do cliente (se ainda nao existir) e a tabela espelho usando o
// DDL que o proprio agente mandou (SHOW CREATE TABLE rodado no Genius real do
// cliente) -- assim a tabela fica sempre fiel ao schema daquele cliente
// especifico, sem hardcode nosso. So ajusta duas coisas: nome totalmente
// qualificado (evita depender de "USE") e remove o AUTO_INCREMENT atual (nao
// faz sentido copiar o contador de outro banco).
async function garantirTabelaEspelho(codigo, tabela, ddlOrigem) {
  if (!(tabela in TABELAS_ESPELHO)) throw new Error(`Tabela desconhecida: ${tabela}`);
  const banco = nomeBancoCliente(codigo);
  await pool.query(`CREATE DATABASE IF NOT EXISTS \`${banco}\``);
  const alvo = `CREATE TABLE IF NOT EXISTS \`${banco}\`.\`${tabela}\``;
  const ddlAjustado = String(ddlOrigem)
    .replace(/^CREATE TABLE `[^`]+`/i, alvo)
    .replace(/\sAUTO_INCREMENT=\d+/i, "");
  if (!ddlAjustado.startsWith(alvo)) {
    throw new Error("DDL recebido do agente em formato inesperado (nao comeca com CREATE TABLE).");
  }
  await pool.query(ddlAjustado);
}

// Grava um lote de linhas (objetos JS vindos do SELECT * do agente) na tabela
// espelho, sem duplicar (ON DUPLICATE KEY UPDATE pela propria Id do Genius,
// que ja e PRIMARY KEY em toda tabela de origem). Quebra em lotes menores
// porque tabela larga (ex: produtos, ~115 colunas) x muita linha (1a
// sincronizacao) estoura o limite de placeholders de uma unica query.
async function inserirLinhasEspelho(banco, tabela, linhas) {
  if (!Array.isArray(linhas) || linhas.length === 0) return;
  const colunas = Object.keys(linhas[0]);
  if (!colunas.every((c) => /^[A-Za-z0-9_]+$/.test(c))) {
    throw new Error(`Nome de coluna invalido recebido para a tabela ${tabela}.`);
  }
  const colunasSql = colunas.map((c) => `\`${c}\``).join(", ");
  const updateSql = colunas.filter((c) => c !== "Id").map((c) => `\`${c}\`=VALUES(\`${c}\`)`).join(", ");
  const TAMANHO_LOTE = 300;
  for (let i = 0; i < linhas.length; i += TAMANHO_LOTE) {
    const lote = linhas.slice(i, i + TAMANHO_LOTE);
    const placeholders = lote.map(() => `(${colunas.map(() => "?").join(", ")})`).join(", ");
    const params = [];
    for (const linha of lote) {
      for (const c of colunas) {
        let v = linha[c];
        // contraparte do serializarLinha() do agente (queries.js): reconstroi
        // o Buffer original da coluna BLOB a partir do base64 marcado.
        if (typeof v === "string" && v.startsWith("__b64__")) v = Buffer.from(v.slice(7), "base64");
        params.push(v ?? null);
      }
    }
    await pool.query(
      `INSERT INTO \`${banco}\`.\`${tabela}\` (${colunasSql}) VALUES ${placeholders}
       ON DUPLICATE KEY UPDATE ${updateSql}`,
      params
    );
  }
}

// ---------- Auth do agente (token por cliente) ----------

async function requireAgente(req, res, next) {
  const codigo = req.params.codigo;
  const token = req.headers["x-agente-token"];
  if (!token) return res.status(401).json({ ok: false, message: "Token do agente ausente." });

  const [rows] = await pool.query("SELECT * FROM clientes WHERE codigo=?", [codigo]);
  const cliente = rows[0];
  if (!cliente || cliente.token_agente !== token) {
    return res.status(401).json({ ok: false, message: "Token do agente invalido." });
  }
  req.cliente = cliente;
  next();
}

// ---------- Auth do admin (basic auth simples, igual ao licencas_server) ----------

function requireAdmin(req, res, next) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith("Basic ")) {
    res.set("WWW-Authenticate", 'Basic realm="Admin SolusysMobile"');
    return res.status(401).send("Autenticacao necessaria.");
  }
  const [user, pass] = Buffer.from(auth.slice(6), "base64").toString().split(":");
  // timingSafeEqual lanca excecao (nao retorna false) se os buffers tiverem
  // tamanho diferente -- por isso o comprimento e conferido antes de comparar.
  const passBuf = Buffer.from(pass || "");
  const senhaOk = passBuf.length === ADMIN_PASSWORD.length && crypto.timingSafeEqual(passBuf, Buffer.from(ADMIN_PASSWORD));
  if (user !== ADMIN_USER || !senhaOk) {
    res.set("WWW-Authenticate", 'Basic realm="Admin SolusysMobile"');
    return res.status(401).send("Usuario ou senha invalidos.");
  }
  next();
}

// ---------- Auth de sessao do painel mobile (cookie) ----------

async function requireSessao(req, res, next) {
  const slug = req.params.slug;
  const token = req.cookies[`sessao_${slug}`];
  if (!token) return res.redirect(`/${slug}`);

  const [rows] = await pool.query(
    `SELECT s.*, c.slug, c.nome, c.liberado, c.codigo, c.atualizado_em FROM sessoes s
     JOIN clientes c ON c.id = s.cliente_id
     WHERE s.token=? AND s.expira_em > NOW()`,
    [token]
  );
  const sessao = rows[0];
  if (!sessao || sessao.slug !== slug || !sessao.liberado) {
    return res.redirect(`/${slug}`);
  }
  req.sessao = sessao;
  next();
}

// =====================================================================
// ROTAS DO AGENTE (chamadas pelo agente local, nunca pelo navegador)
// =====================================================================

// Ciclo de sincronizacao em 3 fases (o agente chama em sequencia):
//   1. /sync/empresas   -- empresas + credenciais (leve, sempre manda tudo)
//   2. /sync/tabela/:t  -- uma chamada por tabela espelhada, so com o que
//                          mudou desde o ultimo sync (ou tudo, na 1a vez)
//   3. /sync/finalizar  -- fecha o heartbeat e limpa "sincronizar agora"
// Separar por tabela evita um unico payload gigante (cliente grande, com
// tabela larga tipo produtos/vendas, facilmente passaria de 100MB se fosse
// tudo numa reqisicao so) e deixa cada tabela isolada se uma falhar.

app.post("/api/agente/:codigo/sync/empresas", requireAgente, async (req, res) => {
  const cliente = req.cliente;
  const { empresas, credenciais } = req.body || {};
  if (!Array.isArray(empresas) || empresas.length === 0) {
    return res.status(400).json({ ok: false, message: "Campo 'empresas' ausente/invalido." });
  }

  const conn = await pool.getConnection();
  try {
    await conn.beginTransaction();

    for (const emp of empresas) {
      if (emp.id_empresa == null) continue;
      await conn.query(
        `INSERT INTO empresas (cliente_id, id_empresa, nome) VALUES (?, ?, ?)
         ON DUPLICATE KEY UPDATE nome=COALESCE(VALUES(nome), nome)`,
        [cliente.id, emp.id_empresa, emp.nome || null]
      );
    }

    if (Array.isArray(credenciais)) {
      await conn.query("DELETE FROM credenciais WHERE cliente_id=?", [cliente.id]);
      for (const c of credenciais) {
        if (!c.login || !c.senha_hash || c.id_empresa == null) continue;
        await conn.query(
          `INSERT INTO credenciais (cliente_id, login, senha_hash, nome_funcionario, id_empresa)
           VALUES (?, ?, ?, ?, ?)`,
          [cliente.id, c.login, c.senha_hash, c.nome_funcionario || null, c.id_empresa]
        );
      }
    }

    await conn.commit();
    res.json({ ok: true });
  } catch (err) {
    await conn.rollback();
    console.error("Erro no sync/empresas do cliente", cliente.codigo, err);
    res.status(500).json({ ok: false, message: "Erro ao gravar empresas/credenciais." });
  } finally {
    conn.release();
  }
});

// payload: { schema?: "CREATE TABLE ...", porEmpresa: { "1": [ {...linha...}, ... ] } }
// "schema" so vem preenchido quando o agente sabe (via /ultima-sincronizacao)
// que o servidor ainda nao tem essa tabela pra esse cliente.
app.post("/api/agente/:codigo/sync/tabela/:tabela", requireAgente, async (req, res) => {
  const cliente = req.cliente;
  const tabela = req.params.tabela;
  if (!(tabela in TABELAS_ESPELHO)) {
    return res.status(400).json({ ok: false, message: "Tabela desconhecida." });
  }
  const { schema, porEmpresa } = req.body || {};
  try {
    if (schema) await garantirTabelaEspelho(cliente.codigo, tabela, schema);
    const banco = nomeBancoCliente(cliente.codigo);
    if (porEmpresa && typeof porEmpresa === "object") {
      for (const linhas of Object.values(porEmpresa)) {
        await inserirLinhasEspelho(banco, tabela, linhas);
      }
    }
    res.json({ ok: true });
  } catch (err) {
    console.error(`Erro no sync/tabela/${tabela} do cliente`, cliente.codigo, err);
    res.status(500).json({ ok: false, message: `Erro ao gravar tabela ${tabela}.` });
  }
});

app.post("/api/agente/:codigo/sync/finalizar", requireAgente, async (req, res) => {
  const cliente = req.cliente;
  // limpa a flag de "sincronizar agora" (se essa sincronizacao foi resposta a ela)
  await pool.query(
    `INSERT INTO sync_pendente (cliente_id, pendente, solicitado_em) VALUES (?, 0, NULL)
     ON DUPLICATE KEY UPDATE pendente=0, solicitado_em=NULL`,
    [cliente.id]
  );
  await pool.query("UPDATE clientes SET atualizado_em=NOW() WHERE id=?", [cliente.id]);
  res.json({ ok: true });
});

// Informa ao agente ate onde cada tabela ja foi sincronizada (por id_empresa),
// pra ele mandar so o que mudou a partir dai. Tabela que ainda nao existe pro
// cliente volta com existe:false (agente entende como "1a vez": manda o
// schema junto e faz backfill completo, sem filtro de data).
app.get("/api/agente/:codigo/ultima-sincronizacao", requireAgente, async (req, res) => {
  const banco = nomeBancoCliente(req.cliente.codigo);
  const tabelas = {};
  for (const [tabela, cfg] of Object.entries(TABELAS_ESPELHO)) {
    const existe = await tabelaEspelhoExiste(banco, tabela);
    if (!existe) {
      tabelas[tabela] = { existe: false, porEmpresa: {} };
      continue;
    }
    const porEmpresa = {};
    const coluna = cfg.colunaAlteracao || cfg.colunaCriacao;
    if (coluna) {
      const [rows] = await pool.query(
        `SELECT id_empresa, MAX(\`${coluna}\`) AS ultima FROM \`${banco}\`.\`${tabela}\` GROUP BY id_empresa`
      );
      // Formata como "YYYY-MM-DD HH:MM:SS" (nao o default do JSON.stringify,
      // que serializaria o Date como ISO com "T"/"Z" -- o MySQL 5.1 do
      // cliente nao reconhece bem esse formato num WHERE `coluna` >= ?, e o
      // filtro incremental deixava de filtrar qualquer coisa, reenviando a
      // tabela inteira em todo ciclo).
      for (const r of rows) porEmpresa[r.id_empresa] = r.ultima ? formatarDataHoraMysql(r.ultima) : null;
    }
    tabelas[tabela] = { existe: true, porEmpresa };
  }
  res.json({ ok: true, tabelas });
});

// Poll leve: o agente confere isso a cada poucos segundos.
app.get("/api/agente/:codigo/pendente", requireAgente, async (req, res) => {
  const [rows] = await pool.query("SELECT pendente FROM sync_pendente WHERE cliente_id=?", [req.cliente.id]);
  res.json({ pendente: rows[0] ? !!rows[0].pendente : false });
});

// Auto-cadastro self-service: o agente descobre codigo/nome sozinho no Genius
// do cliente, mostra uma telinha LOCAL (na propria maquina do cliente) pro
// dono escolher o link (slug), e manda os dois juntos aqui -- o cliente ja
// nasce criado e liberado na hora, sem passar por aprovacao nenhuma do admin.
// Protegido por um segredo compartilhado (nao o token por-cliente, que so
// existe DEPOIS de criado).
app.post("/api/agente/auto-registrar", async (req, res) => {
  const segredo = req.headers["x-registro-secret"];
  if (segredo !== REGISTRO_SECRET) {
    return res.status(401).json({ ok: false, motivo: "segredo_invalido" });
  }
  const { codigo, nome, id_empresa, slug } = req.body || {};
  if (!codigo) return res.status(400).json({ ok: false, motivo: "codigo_ausente" });

  const [jaCliente] = await pool.query("SELECT slug, token_agente FROM clientes WHERE codigo=?", [codigo]);
  if (jaCliente[0]) {
    return res.json({ ok: true, ja_cadastrado: true, slug: jaCliente[0].slug, token_agente: jaCliente[0].token_agente });
  }

  const slugLimpo = String(slug || "").trim().toLowerCase();
  if (!/^[a-z0-9-]{3,40}$/.test(slugLimpo)) {
    return res.status(400).json({ ok: false, motivo: "slug_invalido" });
  }

  const token_agente = gerarToken();
  try {
    await pool.query(
      "INSERT INTO clientes (codigo, slug, nome, liberado, token_agente) VALUES (?, ?, ?, 1, ?)",
      [codigo, slugLimpo, nome || codigo, token_agente]
    );
  } catch (err) {
    if (err.code === "ER_DUP_ENTRY") {
      const [existeSlug] = await pool.query("SELECT id FROM clientes WHERE slug=?", [slugLimpo]);
      if (existeSlug[0]) return res.status(409).json({ ok: false, motivo: "slug_em_uso" });
      // corrida rara: outro processo ja criou esse mesmo codigo entre o SELECT e o INSERT acima
      const [rows] = await pool.query("SELECT slug, token_agente FROM clientes WHERE codigo=?", [codigo]);
      return res.json({ ok: true, ja_cadastrado: true, slug: rows[0].slug, token_agente: rows[0].token_agente });
    }
    throw err;
  }
  res.json({ ok: true, slug: slugLimpo, token_agente });
});

// =====================================================================
// ADMIN (voce, dono do sistema) - cria/gerencia o vinculo codigo<->link
// =====================================================================

app.get("/admin", requireAdmin, async (req, res) => {
  const [clientes] = await pool.query("SELECT * FROM clientes ORDER BY codigo");
  const ultimaPorCliente = {};
  for (const c of clientes) if (c.atualizado_em) ultimaPorCliente[c.id] = c.atualizado_em;

  res.send(paginaAdmin(clientes, ultimaPorCliente));
});

app.post("/admin/criar", requireAdmin, async (req, res) => {
  const { codigo, slug, nome } = req.body;
  if (!codigo || !slug || !nome) return res.status(400).send("Preencha codigo, slug e nome.");
  const token_agente = gerarToken();
  try {
    await pool.query(
      "INSERT INTO clientes (codigo, slug, nome, liberado, token_agente) VALUES (?, ?, ?, 0, ?)",
      [codigo.trim(), slug.trim().toLowerCase(), nome.trim(), token_agente]
    );
  } catch (err) {
    if (err.code === "ER_DUP_ENTRY") return res.status(400).send("Codigo ou link ja existe.");
    throw err;
  }
  res.redirect("/admin");
});

app.post("/admin/:id/liberar", requireAdmin, async (req, res) => {
  await pool.query("UPDATE clientes SET liberado=1 WHERE id=?", [req.params.id]);
  res.redirect("/admin");
});

app.post("/admin/:id/bloquear", requireAdmin, async (req, res) => {
  await pool.query("UPDATE clientes SET liberado=0 WHERE id=?", [req.params.id]);
  res.redirect("/admin");
});

app.post("/admin/:id/excluir", requireAdmin, async (req, res) => {
  await pool.query("DELETE FROM clientes WHERE id=?", [req.params.id]);
  res.redirect("/admin");
});

// =====================================================================
// PAINEL MOBILE (o que o cliente ve no celular)
// =====================================================================

app.get("/", (req, res) => {
  res.send(layout("SolusysMobile", `
    <main style="text-align:center; margin-top:60px">
      <div class="card">
        <h3>SolusysMobile</h3>
        <p class="muted">Acesse pelo link que voce recebeu (ex: /nomedaempresa).</p>
      </div>
    </main>
  `));
});

// Manifest por cliente -- o generico em /assets/manifest.json tinha start_url
// fixo em "/", entao o PWA instalado a partir do link de QUALQUER cliente
// sempre abria a raiz (sem o slug) em vez do link daquele cliente especifico.
app.get("/:slug/manifest.json", async (req, res) => {
  const [rows] = await pool.query("SELECT nome, liberado FROM clientes WHERE slug=?", [req.params.slug]);
  const cliente = rows[0];
  if (!cliente || !cliente.liberado) return res.status(404).json({});
  res.json({
    name: `Solusys Mobile - ${cliente.nome}`,
    short_name: "Solusys Mobile",
    start_url: `/${req.params.slug}`,
    scope: `/${req.params.slug}`,
    display: "standalone",
    background_color: "#f5f6f8",
    theme_color: "#2670c7",
    icons: [
      { src: "/assets/icon-192.png", sizes: "192x192", type: "image/png" },
      { src: "/assets/icon.png", sizes: "512x512", type: "image/png" },
    ],
  });
});

app.get("/:slug", async (req, res, next) => {
  if (["admin", "api", "favicon.ico"].includes(req.params.slug)) return next();
  const [rows] = await pool.query("SELECT * FROM clientes WHERE slug=?", [req.params.slug]);
  const cliente = rows[0];
  if (!cliente || !cliente.liberado) {
    return res.status(404).send(paginaIndisponivel());
  }
  res.send(paginaLogin(cliente, null));
});

app.post("/:slug/login", async (req, res) => {
  const { login, senha } = req.body;
  const [rows] = await pool.query("SELECT * FROM clientes WHERE slug=?", [req.params.slug]);
  const cliente = rows[0];
  if (!cliente || !cliente.liberado) return res.status(404).send(paginaIndisponivel());

  // O MESMO login pode existir em varias empresas desse cliente, com senhas
  // diferentes (comum em clientes com multiplas empresas no mesmo banco) --
  // aceita se a senha bater com QUALQUER uma delas. Uma vez logado, a pessoa
  // pode ver qualquer empresa do cliente (nao amarra a sessao a 1 empresa so).
  const [creds] = await pool.query(
    "SELECT * FROM credenciais WHERE cliente_id=? AND login=?",
    [cliente.id, login]
  );
  const ok = creds.some((c) => bcrypt.compareSync(senha || "", c.senha_hash));
  if (!ok) {
    return res.send(paginaLogin(cliente, "Usuario ou senha invalidos."));
  }

  const token = gerarToken();
  const expira = new Date(Date.now() + 8 * 60 * 60 * 1000); // 8h
  await pool.query(
    "INSERT INTO sessoes (token, cliente_id, login, expira_em) VALUES (?, ?, ?, ?)",
    [token, cliente.id, login, expira]
  );
  res.cookie(`sessao_${cliente.slug}`, token, { httpOnly: true, expires: expira });
  res.redirect(`/${cliente.slug}/painel`);
});

app.post("/:slug/logout", async (req, res) => {
  const token = req.cookies[`sessao_${req.params.slug}`];
  if (token) await pool.query("DELETE FROM sessoes WHERE token=?", [token]);
  res.clearCookie(`sessao_${req.params.slug}`);
  res.redirect(`/${req.params.slug}`);
});

// Cliente com mais de 1 empresa precisa escolher qual ver -- guardado num
// cookie separado da sessao de login (pra poder trocar sem deslogar).
// Cliente com 1 empresa so nunca ve tela de escolha, e selecionada sozinha.
async function resolverEmpresa(req, res, next) {
  const slug = req.params.slug;
  const [empresas] = await pool.query(
    "SELECT id_empresa, nome FROM empresas WHERE cliente_id=? ORDER BY id_empresa",
    [req.sessao.cliente_id]
  );
  if (empresas.length === 0) {
    req.idEmpresa = null;
    req.empresas = [];
    return next();
  }
  if (empresas.length === 1) {
    req.idEmpresa = empresas[0].id_empresa;
    req.empresas = empresas;
    return next();
  }

  const cookieVal = req.cookies[`empresa_${slug}`];
  const escolhida = cookieVal && empresas.find((e) => String(e.id_empresa) === cookieVal);
  if (!escolhida) {
    return res.redirect(`/${slug}/empresas`);
  }
  req.idEmpresa = escolhida.id_empresa;
  req.empresas = empresas;
  next();
}

app.get("/:slug/empresas", requireSessao, async (req, res) => {
  const [empresas] = await pool.query(
    "SELECT id_empresa, nome FROM empresas WHERE cliente_id=? ORDER BY id_empresa",
    [req.sessao.cliente_id]
  );
  // Todas as empresas de um cliente sincronizam juntas no mesmo ciclo do
  // agente -- nao ha "ultima sincronizacao" por empresa de verdade, so por
  // cliente (clientes.atualizado_em, ja disponivel via requireSessao).
  const ultimaPorEmpresa = {};
  if (req.sessao.atualizado_em) {
    for (const e of empresas) ultimaPorEmpresa[e.id_empresa] = req.sessao.atualizado_em;
  }
  res.send(paginaEmpresas(req.sessao, empresas, ultimaPorEmpresa));
});

app.post("/:slug/empresas/escolher", requireSessao, async (req, res) => {
  const { id_empresa } = req.body;
  res.cookie(`empresa_${req.params.slug}`, String(id_empresa), { httpOnly: true, maxAge: 30 * 24 * 60 * 60 * 1000 });
  res.redirect(`/${req.params.slug}/painel`);
});

app.get("/:slug/painel", requireSessao, resolverEmpresa, async (req, res) => {
  if (req.idEmpresa == null) {
    return res.send(paginaPainel(req.sessao, [], null));
  }
  const empresaAtual = req.empresas.find((e) => e.id_empresa === req.idEmpresa);
  res.send(paginaPainel(req.sessao, req.empresas, empresaAtual));
});

app.post("/:slug/sincronizar-agora", requireSessao, async (req, res) => {
  await pool.query(
    `INSERT INTO sync_pendente (cliente_id, pendente, solicitado_em) VALUES (?, 1, NOW())
     ON DUPLICATE KEY UPDATE pendente=1, solicitado_em=NOW()`,
    [req.sessao.cliente_id]
  );
  res.redirect(`/${req.params.slug}/painel`);
});

// Consultado via JS (polling) pelo botao "Sincronizar agora" pra saber quando
// o agente ja pegou a flag e terminou -- so entao a animacao para e a tela recarrega.
app.get("/:slug/sincronizar-status", requireSessao, async (req, res) => {
  const [rows] = await pool.query("SELECT pendente FROM sync_pendente WHERE cliente_id=?", [req.sessao.cliente_id]);
  res.json({ pendente: rows[0] ? !!rows[0].pendente : false });
});

// Le uma area do painel direto das tabelas espelho do cliente (banco
// `{codigo}_genius`). Reproduz exatamente os mesmos JOINs/alias que o agente
// fazia no Genius ao vivo antes (buscarCaixas/buscarContas/etc, ver
// cliente/agente/queries.js) -- so a origem mudou, o formato de linha pro
// calcularResumo/renderTabela e o mesmo de sempre. Caixas/Vendas/Compras
// trazem TODO o historico sincronizado (sem filtro de data aqui): quem filtra
// por periodo e o calcularResumo/calcularResumoMensal, que precisam do ano
// inteiro pra montar a lista de meses.
async function buscarDadosArea(banco, idEmpresa, area) {
  if (area === "caixas") {
    const [rows] = await pool.query(
      `SELECT c.data_caixa, c.Entrada_caixa AS entrada, c.saida_caixa AS saida,
              f.Nome_fun AS funcionario, c.Terminal AS terminal, c.FechamentoTerminal AS fechado
       FROM \`${banco}\`.\`caixa\` c
       LEFT JOIN \`${banco}\`.\`funcionarios\` f ON f.Id = c.Id_funcionario
       WHERE c.id_empresa = ?
       ORDER BY c.data_caixa DESC, c.Id DESC`,
      [idEmpresa]
    );
    return rows;
  }
  if (area === "vendas") {
    const [rows] = await pool.query(
      `SELECT v.DataHoraVenda AS data_hora, cl.nome_cliente AS cliente,
              v.valor_venda AS valor, v.concluida, v.Terminal AS terminal
       FROM \`${banco}\`.\`vendas\` v
       LEFT JOIN \`${banco}\`.\`clientes\` cl ON cl.Id = v.Id_cliente
       WHERE v.id_empresa = ?
       ORDER BY v.DataHoraVenda DESC`,
      [idEmpresa]
    );
    return rows;
  }
  if (area === "compras") {
    const [rows] = await pool.query(
      `SELECT co.data_compra AS data, fo.nome_fornecedor AS fornecedor,
              co.valor_compra AS valor, co.concluida
       FROM \`${banco}\`.\`compras\` co
       LEFT JOIN \`${banco}\`.\`fornecedores\` fo ON fo.Id = co.Id_fornecedor
       WHERE co.id_empresa = ?
       ORDER BY co.data_compra DESC`,
      [idEmpresa]
    );
    return rows;
  }
  if (area === "contas") {
    const [receber] = await pool.query(
      `SELECT 'receber' AS tipo, cl.nome_cliente AS pessoa, ir.vencimento_parcela AS vencimento,
              ir.valor_parcela AS valor, ir.status_parcela AS status
       FROM \`${banco}\`.\`itens_receber\` ir
       JOIN \`${banco}\`.\`contas_receber\` cr ON cr.Id = ir.Id_receber
       LEFT JOIN \`${banco}\`.\`clientes\` cl ON cl.Id = cr.Id_cliente
       WHERE ir.id_empresa = ? AND ir.vencimento_parcela >= CURDATE() - INTERVAL 60 DAY
       ORDER BY ir.vencimento_parcela DESC`,
      [idEmpresa]
    );
    const [pagar] = await pool.query(
      `SELECT 'pagar' AS tipo, fo.nome_fornecedor AS pessoa, ip.vencimento_parcela AS vencimento,
              ip.valor_parcela AS valor, ip.status_parcela AS status
       FROM \`${banco}\`.\`itens_pagar\` ip
       JOIN \`${banco}\`.\`contas_pagar\` cp ON cp.Id = ip.Id_pagar
       LEFT JOIN \`${banco}\`.\`fornecedores\` fo ON fo.Id = cp.Id_fornecedor
       WHERE ip.id_empresa = ? AND ip.vencimento_parcela >= CURDATE() - INTERVAL 60 DAY
       ORDER BY ip.vencimento_parcela DESC`,
      [idEmpresa]
    );
    return [...receber, ...pagar];
  }
  if (area === "estoque") {
    const [rows] = await pool.query(
      `SELECT p.nome_produto AS produto, pe.Estoque_produto AS estoque,
              pe.estoquemin_produto AS estoque_minimo
       FROM \`${banco}\`.\`produtos_estoque\` pe
       JOIN \`${banco}\`.\`produtos\` p ON p.Id = pe.id_produto
       WHERE pe.id_empresa = ? AND pe.ativo <> 'False'
       ORDER BY p.nome_produto`,
      [idEmpresa]
    );
    return rows;
  }
  if (area === "inadimplentes") {
    const [rows] = await pool.query(
      `SELECT cl.nome_cliente AS cliente, cl.tele_cliente AS telefone,
              ir.vencimento_parcela AS vencimento, ir.valor_parcela AS valor,
              DATEDIFF(CURDATE(), ir.vencimento_parcela) AS dias_atraso
       FROM \`${banco}\`.\`itens_receber\` ir
       JOIN \`${banco}\`.\`contas_receber\` cr ON cr.Id = ir.Id_receber
       JOIN \`${banco}\`.\`clientes\` cl ON cl.Id = cr.Id_cliente
       WHERE ir.id_empresa = ? AND ir.status_parcela <> 'Recebido'
         AND ir.vencimento_parcela < CURDATE()
       ORDER BY ir.vencimento_parcela ASC`,
      [idEmpresa]
    );
    return rows;
  }
  return [];
}

for (const area of AREAS) {
  app.get(`/:slug/${area}`, requireSessao, resolverEmpresa, async (req, res) => {
    if (req.idEmpresa == null) return res.redirect(`/${req.params.slug}/painel`);
    const banco = nomeBancoCliente(req.sessao.codigo);
    let dados = [];
    try {
      dados = await buscarDadosArea(banco, req.idEmpresa, area);
    } catch (err) {
      // Cliente ainda nao fez a 1a sincronizacao dessa area -- banco/tabela
      // do espelho nem existe ainda. Trata como lista vazia (mesmo
      // comportamento de antes, quando nao havia snapshot ainda).
      if (err.code !== "ER_NO_SUCH_TABLE" && err.code !== "ER_BAD_DB_ERROR") throw err;
    }
    const periodo = calcularPeriodo(req.query);
    res.send(paginaArea(req.sessao, area, dados, req.sessao.atualizado_em, periodo));
  });
}

// =====================================================================
// HTML (templates simples, sem framework - mesmo estilo do VPMGourmet)
// =====================================================================

function layout(titulo, corpo, slug = null) {
  return `<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#2670c7" />
<link rel="manifest" href="${slug ? `/${slug}/manifest.json` : "/assets/manifest.json"}" />
<link rel="icon" href="/assets/icon.png" />
<link rel="apple-touch-icon" href="/assets/icon-192.png" />
<title>${titulo} - SolusysMobile</title>
<style>
  /* Paleta/tokens seguindo o mesmo padrao visual do app VPMGourmet (irmao
     deste produto) -- nomes de variavel antigos mantidos (--good/--critical/
     --warning/--blue/--gridline/--surface-1/--page-plane/--border/--muted)
     porque o JS (calcularResumo/CORES_GRAFICO/svgBarrasAgrupadas) referencia
     eles direto; só os VALORES mudaram pra bater com o VPMGourmet. */
  :root {
    color-scheme: light;
    --surface-1:      #ffffff;
    --surface-2:      #eef1f4;
    --page-plane:     #f5f6f8;
    --text-primary:   #1f2937;
    --text-secondary: #6b7280;
    --muted:          #6b7280;
    --gridline:       #dcdfe4;
    --border:         #dcdfe4;
    --blue:           #2563eb;
    --good:           #2670c7;
    --good-bg:        rgba(38,112,199,0.12);
    --warning:        #e67e22;
    --warning-bg:     #fff8f0;
    --critical:       #ef4444;
    --critical-bg:    rgba(239,68,68,0.10);
    --radius:         14px;
    --shadow:         0 8px 24px rgba(0,0,0,0.08);
  }
  @media (prefers-color-scheme: dark) {
    :root {
      color-scheme: dark;
      --surface-1:      #26282c;
      --surface-2:      #35383e;
      --page-plane:     #121212;
      --text-primary:   #f2f3f5;
      --text-secondary: #b0b6bd;
      --muted:          #b0b6bd;
      --gridline:       #52565e;
      --border:         #52565e;
      --blue:           #60a5fa;
      --good:           #5b9bdb;
      --good-bg:        rgba(91,155,219,0.18);
      --warning:        #f0a050;
      --warning-bg:     rgba(230,126,34,0.12);
      --critical:       #f87171;
      --critical-bg:    rgba(239,68,68,0.16);
      --shadow:         0 8px 24px rgba(0,0,0,0.6);
    }
    .card, .stat-tile { border: 1px solid var(--border); }
  }
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
  .hidden { display: none !important; }
  html, body { overflow-x: hidden; }
  body {
    margin: 0;
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    background: var(--page-plane);
    color: var(--text-primary);
    -webkit-font-smoothing: antialiased;
  }
  header {
    background: var(--surface-1);
    border-bottom: 1px solid var(--border);
    padding: calc(10px + env(safe-area-inset-top)) 16px 10px;
    display: flex; align-items: center; gap: 12px;
    position: sticky; top: 0; z-index: 10;
  }
  header .brand { font-weight: 700; font-size: 1.05rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  header nav { display: flex; align-items: center; gap: 8px; }
  .back-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; border-radius: 12px; flex-shrink: 0;
    background: var(--surface-2); border: none; color: var(--text-primary);
    font-size: 18px; cursor: pointer; text-decoration: none;
    transition: filter 0.12s;
  }
  .back-btn:hover { filter: brightness(0.96); }
  main { max-width: 480px; margin: 0 auto; padding: 16px; }
  .card {
    background: var(--surface-1);
    border-radius: var(--radius);
    padding: 18px;
    margin-bottom: 14px;
    box-shadow: var(--shadow);
  }
  .input {
    width: 100%; border: 1px solid var(--gridline); border-radius: 12px;
    padding: 12px; font-size: 16px; margin-bottom: 10px;
    background: var(--surface-1); color: var(--text-primary);
  }
  .input:focus { outline: none; border-color: var(--blue); }
  .check-row { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); font-size: 0.85rem; cursor: pointer; }
  .check-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--good); cursor: pointer; }
  @keyframes girar-spinner { to { transform: rotate(360deg); } }
  .spinner {
    display: inline-block; width: 14px; height: 14px; margin-right: 7px; vertical-align: -2px;
    border: 2px solid rgba(255,255,255,0.4); border-top-color: #fff; border-radius: 50%;
    animation: girar-spinner 0.6s linear infinite;
  }
  button:disabled { opacity: 0.75; cursor: default; }
  button, .btn {
    width: 100%; border: 0; border-radius: 12px; padding: 13px;
    font-size: 16px; font-weight: 600; cursor: pointer;
    font-family: inherit; transition: filter 0.12s;
  }
  button:hover, .btn:hover { filter: brightness(1.06); }
  .btn-primary { background: var(--good); color: #fff; }
  .btn-danger { background: var(--critical); color: #fff; }
  .btn-secondary {
    background: var(--surface-2); color: var(--text-primary); border: 1.5px solid var(--border);
    box-shadow: 0 3px 8px rgba(0,0,0,0.12);
  }
  .muted { color: var(--muted); font-size: 0.85rem; }
  .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  .stat-tile {
    display: flex; align-items: center; gap: 14px;
    background: var(--surface-1); border-radius: var(--radius);
    padding: 16px; margin-bottom: 10px; text-decoration: none; color: var(--text-primary);
    box-shadow: var(--shadow); transition: transform 0.12s, box-shadow 0.12s;
  }
  .stat-tile:hover { transform: translateY(-1px); }
  .stat-tile .icon {
    width: 46px; height: 46px; border-radius: 12px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center; font-size: 23px;
    background: var(--good-bg); color: var(--good);
  }
  .stat-tile.critical .icon { background: var(--critical-bg); color: var(--critical); }
  .stat-tile .info { flex: 1; min-width: 0; }
  .stat-tile .info .nome { font-weight: 700; font-size: 1.15rem; }
  .stat-tile .info .sub { color: var(--muted); font-size: 0.88rem; margin-top: 2px; }
  .stat-tile .chevron { color: var(--muted); font-size: 1.1rem; flex-shrink: 0; }
  .resumo-linha {
    display: flex; justify-content: space-between; align-items: center; gap: 12px;
    padding: 12px 0; border-bottom: 1px solid var(--gridline);
  }
  .resumo-linha:last-child { border-bottom: 0; }
  .resumo-linha .rotulo { color: var(--muted); font-size: 0.95rem; }
  .resumo-linha .valor { font-size: 1.3rem; font-weight: 800; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
  .resumo-linha .valor.good { color: var(--good); }
  .resumo-linha .valor.critical { color: var(--critical); }
  .resumo-linha .valor.warning { color: var(--warning); }
  .resumo-linha .valor.blue { color: var(--blue); }
  .mes-bloco { border-bottom: 1px solid var(--gridline); }
  .mes-bloco:last-child { border-bottom: 0; }
  .mes-linha {
    display: flex; align-items: center; gap: 10px; padding: 14px 16px; cursor: pointer;
  }
  .mes-nome { font-weight: 700; flex: 1; }
  .mes-valor { font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
  .mes-valor.good { color: var(--good); }
  .mes-valor.critical { color: var(--critical); }
  .mes-valor.warning { color: var(--warning); }
  .mes-valor.blue { color: var(--blue); }
  .mes-chevron { color: var(--muted); font-size: 0.8rem; flex-shrink: 0; }
  .mes-detalhes { padding: 0 16px 12px; background: var(--page-plane); }
  .periodo-row { display: flex; gap: 6px; margin-bottom: 14px; overflow-x: auto; }
  .periodo-btn {
    flex-shrink: 0; padding: 8px 14px; border-radius: 999px; font-size: 0.85rem; font-weight: 600;
    background: var(--surface-2); color: var(--text-secondary); text-decoration: none; border: 1px solid var(--border);
    cursor: pointer; font-family: inherit; width: auto;
  }
  .periodo-btn.ativo { background: var(--good); color: #fff; border-color: var(--good); }
  .grafico-card { padding: 16px 14px 12px; }
  .grafico-card .titulo { font-weight: 700; font-size: 0.85rem; margin-bottom: 10px; }
  .legenda { margin-top: 8px; }
  .legenda span.item { display: inline-flex; align-items: center; gap: 5px; margin-right: 14px; }
  .legenda .ponto { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
  table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
  th, td { text-align: left; padding: 9px 8px; border-bottom: 1px solid var(--gridline); white-space: nowrap; }
  th { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.03em; }
  td.numerico { font-variant-numeric: tabular-nums; }
  tbody tr:hover { background: var(--page-plane); }
  tbody tr.linha-critica { background: var(--critical-bg); }
  .erro { background: var(--critical-bg); color: var(--critical); padding: 11px 14px; border-radius: 12px; margin-bottom: 14px; font-size: 0.88rem; }
  .logo-badge {
    width: 52px; height: 52px; border-radius: var(--radius); background: var(--good); color: #fff;
    display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 1.3rem;
    margin: 0 auto 14px;
  }
  .login-logo { width: 64px; height: 64px; object-fit: contain; display: block; margin: 0 auto 6px; }
  .login-brand { font-weight: 800; font-size: 1.05rem; color: var(--text-primary); margin-bottom: 16px; }
  .powered-by {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    margin-top: 18px; color: var(--muted); font-size: 0.78rem;
  }
  .powered-by img { width: 16px; height: 16px; object-fit: contain; vertical-align: -3px; margin: 0 1px; }
  .powered-by b { color: var(--text-secondary); }
  /* Telas maiores (tablet/desktop -- o painel roda em qualquer navegador,
     nao so celular): da mais respiro em vez de ficar uma coluna estreita
     esticada no meio da tela. */
  @media (min-width: 700px) {
    main { max-width: 640px; padding: 24px; }
  }
  /* Celulares bem pequenos: aperta um pouco o cabecalho. */
  @media (max-width: 360px) {
    header { padding-left: 12px; padding-right: 12px; }
    header .brand { font-size: 0.95rem; }
    main { padding: 12px; }
  }
</style>
</head>
<body>
${corpo}
</body>
</html>`;
}

function paginaIndisponivel() {
  return layout("Indisponivel", `
    <main style="text-align:center; margin-top:60px">
      <div class="card">
        <div class="logo-badge">!</div>
        <h3 style="margin-top:0">Link indisponivel</h3>
        <p class="muted">Esse endereco nao existe ou o acesso foi bloqueado. Fale com o suporte.</p>
      </div>
    </main>
  `);
}

function paginaLogin(cliente, erro) {
  return layout("Login", `
    <header><div class="brand">${escapeHtml(cliente.nome)}</div></header>
    <main>
      <div class="card" style="text-align:center">
        <img src="/assets/icon.png" alt="Solusys Mobile" class="login-logo" />
        <div class="login-brand">Solusys Mobile</div>
        <h3 style="margin:0 0 4px">Controle de Acesso</h3>
        <p class="muted" style="margin-bottom:18px">Use o mesmo usuario e senha do sistema.</p>
        ${erro ? `<div class="erro">${escapeHtml(erro)}</div>` : ""}
        <form method="post" action="/${cliente.slug}/login" style="text-align:left" id="loginForm">
          <input class="input" name="login" id="loginInput" placeholder="Usuario" autofocus />
          <input class="input" name="senha" id="senhaInput" type="password" placeholder="Senha" />
          <label class="check-row" style="margin:-2px 0 14px">
            <input type="checkbox" id="lembrarCheckbox" />
            Lembrar usuario e senha
          </label>
          <button class="btn-primary" type="submit">Entrar</button>
        </form>
        <script>
        (function() {
          var KEY = "solusys_login_" + ${JSON.stringify(cliente.slug)};
          var loginEl = document.getElementById("loginInput");
          var senhaEl = document.getElementById("senhaInput");
          var checkEl = document.getElementById("lembrarCheckbox");
          try {
            var salvo = JSON.parse(localStorage.getItem(KEY) || "null");
            if (salvo) {
              loginEl.value = salvo.login || "";
              senhaEl.value = salvo.senha || "";
              checkEl.checked = true;
            }
          } catch (e) {}
          document.getElementById("loginForm").addEventListener("submit", function() {
            try {
              if (checkEl.checked) {
                localStorage.setItem(KEY, JSON.stringify({ login: loginEl.value, senha: senhaEl.value }));
              } else {
                localStorage.removeItem(KEY);
              }
            } catch (e) {}
          });
        })();
        </script>
      </div>
      <div class="powered-by">
        <span>desenvolvido por <img src="/assets/logo.png" alt="" /> <b>VPMSolution</b></span>
      </div>
    </main>
  `, cliente.slug);
}

function paginaPainel(sessao, empresas = [], empresaAtual = null) {
  const ultimaGeral = sessao.atualizado_em ? new Date(sessao.atualizado_em).getTime() : 0;

  const links = AREAS.map((area) => {
    const critico = AREA_CRITICAL[area] ? " critical" : "";
    return `<a class="stat-tile${critico}" href="/${sessao.slug}/${area}">
      <div class="icon">${AREA_ICONS[area]}</div>
      <div class="info">
        <div class="nome">${AREA_LABELS[area]}</div>
      </div>
      <div class="chevron">›</div>
    </a>`;
  }).join("");

  const trocarEmpresa = empresas.length > 1
    ? `<a href="/${sessao.slug}/empresas" class="muted" style="display:block; margin-bottom:14px; text-decoration:none">🔀 ${escapeHtml(nomeEmpresa(empresaAtual))} (trocar empresa)</a>`
    : "";

  return layout("Painel", `
    <header>
      <div class="brand">${escapeHtml(sessao.nome)}</div>
      <nav><form method="post" action="/${sessao.slug}/logout" style="display:inline"><button class="btn-secondary" style="width:auto; padding:6px 12px; font-size:0.85rem">Sair</button></form></nav>
    </header>
    <main>
      ${trocarEmpresa}
      <button class="btn-primary" type="button" id="btnSincronizar" onclick="sincronizarAgora()">
        <span id="btnSincronizarTexto">Sincronizar agora</span>
      </button>
      <p class="muted" style="text-align:center; margin:6px 0 14px">
        ${ultimaGeral ? "Última sincronização: " + formatarDataHora(ultimaGeral) : "Ainda sem sincronizacao"}
      </p>
      ${links}
    </main>
    <script>
    function sincronizarAgora() {
      var btn = document.getElementById("btnSincronizar");
      var txt = document.getElementById("btnSincronizarTexto");
      btn.disabled = true;
      txt.innerHTML = '<span class="spinner"></span>Sincronizando...';
      fetch(${JSON.stringify(`/${sessao.slug}/sincronizar-agora`)}, { method: "POST" }).catch(function () {});
      var tentativas = 0;
      var maxTentativas = 15; // ~30s (intervalo de 2s)
      var intervalo = setInterval(function () {
        tentativas++;
        fetch(${JSON.stringify(`/${sessao.slug}/sincronizar-status`)})
          .then(function (r) { return r.json(); })
          .then(function (data) {
            if (!data.pendente || tentativas >= maxTentativas) {
              clearInterval(intervalo);
              window.location.reload();
            }
          })
          .catch(function () {
            if (tentativas >= maxTentativas) { clearInterval(intervalo); window.location.reload(); }
          });
      }, 2000);
    }
    </script>
  `, sessao.slug);
}

function paginaEmpresas(sessao, empresas, ultimaPorEmpresa = {}) {
  const links = empresas.map((e) => {
    const ultima = ultimaPorEmpresa[e.id_empresa];
    return `
    <form method="post" action="/${sessao.slug}/empresas/escolher">
      <input type="hidden" name="id_empresa" value="${e.id_empresa}" />
      <button class="stat-tile" style="width:100%; border:1px solid var(--border); font-weight:400; cursor:pointer" type="submit">
        <div class="icon">🏢</div>
        <div class="info">
          <div class="nome">${escapeHtml(nomeEmpresa(e))}</div>
          <div class="sub">${ultima ? "Atualizado: " + formatarDataHora(ultima) : "Ainda sem sincronizacao"}</div>
        </div>
        <div class="chevron">›</div>
      </button>
    </form>
  `;
  }).join("");

  return layout("Empresas", `
    <header>
      <a class="back-btn" href="/${sessao.slug}/painel">←</a>
      <div class="brand">${escapeHtml(sessao.nome)}</div>
    </header>
    <main>
      <p class="muted">Escolha a empresa:</p>
      ${links}
    </main>
  `, sessao.slug);
}

function nomeEmpresa(e) {
  if (!e) return "";
  return e.nome || `Empresa ${e.id_empresa}`;
}

// ---------- Formatacao de valores ----------

function formatarMoeda(v) {
  const n = Number(v) || 0;
  return n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}

function formatarDataCurta(d) {
  if (!d) return "-";
  return new Date(d).toLocaleDateString("pt-BR");
}

// Formato exigido pelo value= de <input type="date"> (YYYY-MM-DD)
function formatarDataInput(d) {
  const dt = new Date(d);
  const y = dt.getFullYear();
  const m = String(dt.getMonth() + 1).padStart(2, "0");
  const dia = String(dt.getDate()).padStart(2, "0");
  return `${y}-${m}-${dia}`;
}

function formatarBool(v) {
  if (v === true || v === 1 || v === "1" || v === "True" || v === "true") return "Sim";
  if (v === false || v === 0 || v === "0" || v === "False" || v === "false" || v == null) return "Nao";
  return String(v);
}

function chaveDia(d) {
  const dt = new Date(d);
  const y = dt.getFullYear();
  const m = String(dt.getMonth() + 1).padStart(2, "0");
  const dia = String(dt.getDate()).padStart(2, "0");
  return `${y}-${m}-${dia}`;
}

// ---------- Periodo selecionavel (dia/semana/mes/ano/personalizado) ----------

const AREAS_COM_PERIODO = ["caixas", "vendas", "compras"];
const PERIODO_LABEL = { dia: "Dia", semana: "Semana", mes: "Mês", ano: "Ano" };

function calcularPeriodo(query) {
  const valido = ["dia", "semana", "mes", "ano", "personalizado"].includes(query.periodo) ? query.periodo : "mes";
  const agora = new Date();
  let tipo = valido;
  let inicio;
  let fim = agora;

  if (tipo === "dia") {
    // Dia especifico (escolhido no calendario) ou hoje por padrao -- usa o
    // dia inteiro (00h-23h59) em vez de "ate agora", senao um dia passado
    // acabaria incluindo tudo entre ele e hoje (bug: fim=agora sempre).
    const base = query.data ? new Date(`${query.data}T00:00:00`) : agora;
    if (isNaN(base)) {
      inicio = new Date(agora.getFullYear(), agora.getMonth(), agora.getDate());
    } else {
      inicio = new Date(base.getFullYear(), base.getMonth(), base.getDate());
    }
    fim = new Date(inicio.getFullYear(), inicio.getMonth(), inicio.getDate(), 23, 59, 59, 999);
  } else if (tipo === "semana") {
    inicio = new Date(agora.getFullYear(), agora.getMonth(), agora.getDate() - 6);
  } else if (tipo === "ano") {
    // Ano escolhido, ou o ano atual por padrao. O dropdown (paginaArea) so
    // oferece anos com dado de verdade pra esse cliente -- aqui so precisa
    // barrar ano invalido/futuro, nao tem mais teto fixo (a sincronizacao
    // hoje traz o historico completo, nao so os ultimos N anos). Ano
    // passado usa o ano inteiro; ano atual vai so ate agora.
    const anoAtual = agora.getFullYear();
    let ano = query.ano ? parseInt(query.ano, 10) : anoAtual;
    if (isNaN(ano) || ano < 2000 || ano > anoAtual) ano = anoAtual;
    inicio = new Date(ano, 0, 1);
    fim = ano === anoAtual ? agora : new Date(ano, 11, 31, 23, 59, 59, 999);
  } else if (tipo === "personalizado") {
    const i = query.inicio ? new Date(`${query.inicio}T00:00:00`) : null;
    const f = query.fim ? new Date(`${query.fim}T23:59:59`) : null;
    if (i && f && !isNaN(i) && !isNaN(f) && i <= f) {
      inicio = i;
      fim = f;
    } else {
      // datas invalidas/faltando -- cai pro mes atual
      tipo = "mes";
      inicio = new Date(agora.getFullYear(), agora.getMonth(), 1);
    }
  } else {
    inicio = new Date(agora.getFullYear(), agora.getMonth(), 1);
  }

  return { tipo, inicio, fim, ano: inicio.getFullYear() };
}

// ---------- Resumo (totais do periodo) por area ----------

// Campo de data usado por cada area de fluxo -- reaproveitado tanto pelo
// resumo/grafico (calcularResumo) quanto pela tabela de lancamentos
// (paginaArea), pra os dois respeitarem o mesmo filtro de periodo.
const AREA_CAMPO_DATA = { caixas: "data_caixa", vendas: "data_hora", compras: "data" };

function filtrarPorPeriodo(dados, campo, periodo) {
  return dados.filter((r) => {
    if (!r[campo]) return false;
    const d = new Date(r[campo]);
    return d >= periodo.inicio && d <= periodo.fim;
  });
}

function calcularResumo(area, dados, periodo) {
  const noPeriodo = (campo) => filtrarPorPeriodo(dados, campo, periodo);
  const soma = (linhas, campo) => linhas.reduce((s, r) => s + (Number(r[campo]) || 0), 0);

  if (area === "caixas") {
    const linhas = noPeriodo("data_caixa");
    const entrada = soma(linhas, "entrada");
    const saida = soma(linhas, "saida");
    const saldo = entrada - saida;
    return {
      tiles: [
        { rotulo: "Entradas no periodo", valor: formatarMoeda(entrada), cor: "good" },
        { rotulo: "Saidas no periodo", valor: formatarMoeda(saida), cor: "critical" },
        { rotulo: "Saldo do periodo", valor: formatarMoeda(saldo), cor: saldo >= 0 ? "good" : "critical" },
      ],
      grafico: montarSerie(dados, "data_caixa", [
        { campo: "entrada", cor: "good", nome: "Entrada" },
        { campo: "saida", cor: "critical", nome: "Saida" },
      ], periodo),
    };
  }

  if (area === "vendas") {
    const linhas = noPeriodo("data_hora");
    const total = soma(linhas, "valor");
    return {
      tiles: [
        { rotulo: "Vendido no periodo", valor: formatarMoeda(total), cor: "good" },
        { rotulo: "Vendas no periodo", valor: String(linhas.length), cor: "blue" },
        { rotulo: "Ticket medio", valor: formatarMoeda(linhas.length ? total / linhas.length : 0), cor: "blue" },
      ],
      grafico: montarSerie(dados, "data_hora", [{ campo: "valor", cor: "blue", nome: "Vendas" }], periodo),
    };
  }

  if (area === "compras") {
    const linhas = noPeriodo("data");
    const total = soma(linhas, "valor");
    return {
      tiles: [
        { rotulo: "Comprado no periodo", valor: formatarMoeda(total), cor: "warning" },
        { rotulo: "Compras no periodo", valor: String(linhas.length), cor: "blue" },
      ],
      grafico: montarSerie(dados, "data", [{ campo: "valor", cor: "warning", nome: "Compras" }], periodo),
    };
  }

  if (area === "contas") {
    const receber = dados.filter((r) => r.tipo === "receber");
    const pagar = dados.filter((r) => r.tipo === "pagar");
    return {
      tiles: [
        { rotulo: "Total a receber", valor: formatarMoeda(soma(receber, "valor")), cor: "good" },
        { rotulo: "Total a pagar", valor: formatarMoeda(soma(pagar, "valor")), cor: "critical" },
      ],
      grafico: null,
    };
  }

  if (area === "inadimplentes") {
    return {
      tiles: [
        { rotulo: "Total em atraso", valor: formatarMoeda(soma(dados, "valor")), cor: "critical" },
        { rotulo: "Clientes inadimplentes", valor: String(dados.length), cor: "critical" },
      ],
      grafico: null,
    };
  }

  if (area === "estoque") {
    const abaixoMin = dados.filter((r) => Number(r.estoque) < Number(r.estoque_minimo));
    return {
      tiles: [
        { rotulo: "Produtos cadastrados", valor: String(dados.length), cor: "blue" },
        { rotulo: "Abaixo do minimo", valor: String(abaixoMin.length), cor: abaixoMin.length ? "critical" : "good" },
      ],
      grafico: null,
    };
  }

  return { tiles: [], grafico: null };
}

// Quebra o ano selecionado em 12 linhas (uma por mes), reaproveitando
// calcularResumo pra cada mes individualmente -- ganha de graca o mesmo
// conjunto de tiles (Saldo/Entradas/Saidas etc) pra usar no "expandir".
function calcularResumoMensal(area, dados, ano) {
  const meses = [];
  for (let m = 0; m < 12; m++) {
    const inicioMes = new Date(ano, m, 1);
    const fimMes = new Date(ano, m + 1, 0, 23, 59, 59, 999);
    const resumo = calcularResumo(area, dados, { inicio: inicioMes, fim: fimMes });
    // "total" da linha colapsada do mes: prefere a tile de Saldo (o numero
    // liquido, mais representativo pra escanear rapido) quando existir,
    // senao cai pra primeira tile (ex: "Vendido"/"Comprado" em vendas/compras).
    const tileSaldo = resumo.tiles.find((t) => t.rotulo.startsWith("Saldo"));
    meses.push({
      nome: MESES_ABREV[m],
      total: tileSaldo || resumo.tiles[0] || { valor: "-", cor: "blue" },
      tiles: resumo.tiles,
    });
  }
  return meses;
}

// Agrupa valores por dia (ultimos N dias) para o grafico de barras.
// Agrupa por dia se o periodo for curto (ate ~62 dias, cabe legivel no grafico)
// ou por mes se for mais longo (ex: periodo "ano" ou personalizado grande) --
// senao o grafico de "ano" teria 365 barrinhas ilegiveis.
function montarSerie(dados, campoData, seriesCfg, periodo) {
  const umDiaMs = 24 * 60 * 60 * 1000;
  const spanDias = Math.ceil((periodo.fim - periodo.inicio) / umDiaMs) + 1;

  if (spanDias <= 62) {
    const chaves = [];
    const cursor = new Date(periodo.inicio.getFullYear(), periodo.inicio.getMonth(), periodo.inicio.getDate());
    const fimDia = new Date(periodo.fim.getFullYear(), periodo.fim.getMonth(), periodo.fim.getDate());
    while (cursor <= fimDia) {
      chaves.push(chaveDia(cursor));
      cursor.setDate(cursor.getDate() + 1);
    }
    const porDia = {};
    for (const chave of chaves) porDia[chave] = {};
    for (const r of dados) {
      const raw = r[campoData];
      if (!raw) continue;
      const chave = chaveDia(raw);
      if (!(chave in porDia)) continue;
      for (const s of seriesCfg) {
        porDia[chave][s.campo] = (porDia[chave][s.campo] || 0) + (Number(r[s.campo]) || 0);
      }
    }
    return {
      granularidade: "dia",
      labels: chaves,
      series: seriesCfg.map((s) => ({ nome: s.nome, cor: s.cor, valores: chaves.map((c) => porDia[c][s.campo] || 0) })),
    };
  }

  const meses = [];
  const cursorMes = new Date(periodo.inicio.getFullYear(), periodo.inicio.getMonth(), 1);
  const fimMes = new Date(periodo.fim.getFullYear(), periodo.fim.getMonth(), 1);
  while (cursorMes <= fimMes) {
    meses.push(`${cursorMes.getFullYear()}-${String(cursorMes.getMonth() + 1).padStart(2, "0")}`);
    cursorMes.setMonth(cursorMes.getMonth() + 1);
  }
  const porMes = {};
  for (const m of meses) porMes[m] = {};
  for (const r of dados) {
    const raw = r[campoData];
    if (!raw) continue;
    const d = new Date(raw);
    const chave = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
    if (!(chave in porMes)) continue;
    for (const s of seriesCfg) {
      porMes[chave][s.campo] = (porMes[chave][s.campo] || 0) + (Number(r[s.campo]) || 0);
    }
  }
  return {
    granularidade: "mes",
    labels: meses,
    series: seriesCfg.map((s) => ({ nome: s.nome, cor: s.cor, valores: meses.map((c) => porMes[c][s.campo] || 0) })),
  };
}

function caminhoBarraTopoArredondado(x, y, w, h, r) {
  if (h <= 0.5) return "";
  const raio = Math.max(0, Math.min(r, w / 2, h));
  return `M${x},${(y + h).toFixed(1)} L${x},${(y + raio).toFixed(1)} Q${x},${y.toFixed(1)} ${(x + raio).toFixed(1)},${y.toFixed(1)} L${(x + w - raio).toFixed(1)},${y.toFixed(1)} Q${(x + w).toFixed(1)},${y.toFixed(1)} ${(x + w).toFixed(1)},${(y + raio).toFixed(1)} L${(x + w).toFixed(1)},${(y + h).toFixed(1)} Z`;
}

const CORES_GRAFICO = { good: "var(--good)", critical: "var(--critical)", blue: "var(--blue)", warning: "var(--warning)" };
const MESES_ABREV = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];

function svgBarrasAgrupadas(serie) {
  const dias = serie.labels;
  const n = dias.length;
  if (n === 0 || serie.series.every((s) => s.valores.every((v) => !v))) {
    return "";
  }

  const largura = 320, altura = 150;
  const padL = 4, padR = 4, padT = 10, padB = 20;
  const plotW = largura - padL - padR;
  const plotH = altura - padT - padB;
  const grupoW = plotW / n;
  const numSeries = serie.series.length;
  const barGap = 2;
  const barW = Math.max(2, (grupoW - 6) / numSeries - barGap);
  const max = Math.max(1, ...serie.series.flatMap((s) => s.valores));
  const baseY = padT + plotH;

  let barras = "";
  dias.forEach((dia, i) => {
    const xGrupo = padL + i * grupoW + (grupoW - (barW * numSeries + barGap * (numSeries - 1))) / 2;
    serie.series.forEach((s, si) => {
      const v = s.valores[i] || 0;
      const h = (v / max) * plotH;
      const x = xGrupo + si * (barW + barGap);
      const y = baseY - h;
      const cor = CORES_GRAFICO[s.cor] || CORES_GRAFICO.blue;
      barras += `<path d="${caminhoBarraTopoArredondado(x, y, barW, h, 2.5)}" fill="${cor}" />`;
    });
  });

  const passoLabel = n > 8 ? Math.ceil(n / 6) : 1;
  let labels = "";
  dias.forEach((chave, i) => {
    if (i % passoLabel !== 0 && i !== n - 1) return;
    const x = padL + i * grupoW + grupoW / 2;
    let texto;
    if (serie.granularidade === "mes") {
      const [ano, mes] = chave.split("-");
      texto = `${MESES_ABREV[Number(mes) - 1]}/${ano.slice(2)}`;
    } else {
      const [, mes, d] = chave.split("-");
      texto = `${d}/${mes}`;
    }
    labels += `<text x="${x.toFixed(1)}" y="${altura - 5}" font-size="9" fill="var(--muted)" text-anchor="middle">${texto}</text>`;
  });

  const legenda = serie.series.length > 1
    ? `<div class="legenda">${serie.series.map((s) =>
        `<span class="item"><span class="ponto" style="background:${CORES_GRAFICO[s.cor] || CORES_GRAFICO.blue}"></span><span class="muted" style="font-size:0.78rem">${escapeHtml(s.nome)}</span></span>`
      ).join("")}</div>`
    : "";

  const titulo = serie.granularidade === "mes" ? `Por mês (${n} meses)` : `Últimos ${n} dias`;

  return `
    <div class="card grafico-card">
      <div class="titulo">${titulo}</div>
      <svg viewBox="0 0 ${largura} ${altura}" style="width:100%; height:auto; display:block">
        <line x1="${padL}" y1="${baseY}" x2="${largura - padR}" y2="${baseY}" stroke="var(--gridline)" stroke-width="1" />
        ${barras}
        ${labels}
      </svg>
      ${legenda}
    </div>
  `;
}

// ---------- Tabela formatada por area ----------

const AREA_COLUMNS = {
  caixas: [
    { chave: "data_caixa", rotulo: "Data", tipo: "data" },
    { chave: "entrada", rotulo: "Entrada", tipo: "moeda" },
    { chave: "saida", rotulo: "Saida", tipo: "moeda" },
    { chave: "funcionario", rotulo: "Funcionario", tipo: "texto" },
    { chave: "terminal", rotulo: "Terminal", tipo: "texto" },
    { chave: "fechado", rotulo: "Fechado", tipo: "bool" },
  ],
  contas: [
    { chave: "tipo", rotulo: "Tipo", tipo: "tipoConta" },
    { chave: "pessoa", rotulo: "Pessoa", tipo: "texto" },
    { chave: "vencimento", rotulo: "Vencimento", tipo: "data" },
    { chave: "valor", rotulo: "Valor", tipo: "moeda" },
    { chave: "status", rotulo: "Status", tipo: "texto" },
  ],
  vendas: [
    { chave: "data_hora", rotulo: "Data/Hora", tipo: "dataHora" },
    { chave: "cliente", rotulo: "Cliente", tipo: "texto" },
    { chave: "valor", rotulo: "Valor", tipo: "moeda" },
    { chave: "concluida", rotulo: "Concluida", tipo: "bool" },
    { chave: "terminal", rotulo: "Terminal", tipo: "texto" },
  ],
  compras: [
    { chave: "data", rotulo: "Data", tipo: "data" },
    { chave: "fornecedor", rotulo: "Fornecedor", tipo: "texto" },
    { chave: "valor", rotulo: "Valor", tipo: "moeda" },
    { chave: "concluida", rotulo: "Concluida", tipo: "bool" },
  ],
  estoque: [
    { chave: "produto", rotulo: "Produto", tipo: "texto" },
    { chave: "estoque", rotulo: "Estoque", tipo: "numero" },
    { chave: "estoque_minimo", rotulo: "Minimo", tipo: "numero" },
  ],
  inadimplentes: [
    { chave: "cliente", rotulo: "Cliente", tipo: "texto" },
    { chave: "telefone", rotulo: "Telefone", tipo: "texto" },
    { chave: "vencimento", rotulo: "Vencimento", tipo: "data" },
    { chave: "valor", rotulo: "Valor", tipo: "moeda" },
    { chave: "dias_atraso", rotulo: "Dias em atraso", tipo: "numero" },
  ],
};

function formatarCelula(valor, tipo) {
  if (valor == null || valor === "") return "-";
  switch (tipo) {
    case "moeda": return formatarMoeda(valor);
    case "data": return formatarDataCurta(valor);
    case "dataHora": return formatarDataHora(valor);
    case "bool": return formatarBool(valor);
    case "tipoConta": return valor === "receber" ? "A receber" : valor === "pagar" ? "A pagar" : String(valor);
    case "numero": return Number(valor).toLocaleString("pt-BR");
    default: return String(valor);
  }
}

function linhaCritica(area, row) {
  if (area === "estoque") return Number(row.estoque) < Number(row.estoque_minimo);
  if (area === "inadimplentes") return Number(row.dias_atraso) > 30;
  return false;
}

function renderTabela(area, linhas) {
  if (linhas.length === 0) return `<p class="muted">Nenhum dado disponivel ainda.</p>`;
  const colunas = AREA_COLUMNS[area] || Object.keys(linhas[0]).map((k) => ({ chave: k, rotulo: k, tipo: "texto" }));
  return `<div class="card" style="overflow-x:auto; padding:0">
      <table>
        <thead><tr>${colunas.map((c) => `<th>${escapeHtml(c.rotulo)}</th>`).join("")}</tr></thead>
        <tbody>
          ${linhas.slice(0, 200).map((row) => `<tr${linhaCritica(area, row) ? ' class="linha-critica"' : ""}>${colunas.map((c) =>
            `<td${c.tipo === "moeda" || c.tipo === "numero" ? ' class="numerico"' : ""}>${escapeHtml(formatarCelula(row[c.chave], c.tipo))}</td>`
          ).join("")}</tr>`).join("")}
        </tbody>
      </table>
    </div>`;
}

function paginaArea(sessao, area, dados, atualizado, periodo) {
  const linhas = Array.isArray(dados) ? dados : [];
  const resumo = calcularResumo(area, linhas, periodo);
  const usaPeriodo = AREAS_COM_PERIODO.includes(area);

  const tiles = resumo.tiles.length
    ? `<div class="card">
        ${resumo.tiles.map((t) => `
          <div class="resumo-linha">
            <span class="rotulo">${escapeHtml(t.rotulo)}</span>
            <span class="valor ${t.cor}">${escapeHtml(t.valor)}</span>
          </div>
        `).join("")}
      </div>`
    : "";

  const grafico = resumo.grafico ? svgBarrasAgrupadas(resumo.grafico) : "";
  // "Ver lancamentos" tem que respeitar o mesmo periodo escolhido pro
  // resumo/grafico -- senao a tabela mostraria tudo enquanto o resto da
  // tela mostra só o periodo filtrado, inconsistente.
  const linhasTabela = usaPeriodo ? filtrarPorPeriodo(linhas, AREA_CAMPO_DATA[area], periodo) : linhas;
  const tabela = renderTabela(area, linhasTabela);

  // Anos oferecidos no dropdown = anos com dado de verdade pra esse cliente
  // (cliente com 20 anos de historico ve 20 anos; cliente novo, com so 1 ano
  // sincronizado, ve so 1) -- nao um numero fixo, ja que a sincronizacao traz
  // o historico completo agora, e o alcance varia de cliente pra cliente.
  const anoAtual = new Date().getFullYear();
  const campoDataArea = AREA_CAMPO_DATA[area];
  let anoMinimo = anoAtual;
  if (usaPeriodo && campoDataArea) {
    for (const r of linhas) {
      if (!r[campoDataArea]) continue;
      const ano = new Date(r[campoDataArea]).getFullYear();
      if (!isNaN(ano) && ano < anoMinimo) anoMinimo = ano;
    }
  }
  const anosOpcoes = [];
  for (let a = anoAtual; a >= anoMinimo; a--) anosOpcoes.push(a);

  const mesesResumo = usaPeriodo && periodo.tipo === "ano" ? calcularResumoMensal(area, linhas, periodo.ano) : [];
  const listaMeses = mesesResumo.length ? `
    <div class="card" style="padding:0">
      ${mesesResumo.map((m, i) => `
        <div class="mes-bloco">
          <div class="mes-linha" onclick="toggleMes(${i})">
            <span class="mes-nome">${m.nome}</span>
            <span class="mes-valor ${m.total.cor}">${escapeHtml(m.total.valor)}</span>
            <span class="mes-chevron" id="mesChevron${i}">▾</span>
          </div>
          <div class="mes-detalhes hidden" id="mesDetalhes${i}">
            ${m.tiles.map((t) => `
              <div class="resumo-linha">
                <span class="rotulo">${escapeHtml(t.rotulo)}</span>
                <span class="valor ${t.cor}">${escapeHtml(t.valor)}</span>
              </div>
            `).join("")}
          </div>
        </div>
      `).join("")}
    </div>
    <script>
    function toggleMes(i) {
      var det = document.getElementById("mesDetalhes" + i);
      var chev = document.getElementById("mesChevron" + i);
      var aberto = !det.classList.contains("hidden");
      det.classList.toggle("hidden");
      chev.textContent = aberto ? "▾" : "▴";
    }
    </script>
  ` : "";

  const seletorPeriodo = usaPeriodo ? `
    <div class="periodo-row">
      <button class="periodo-btn${periodo.tipo === "dia" ? " ativo" : ""}" type="button" onclick="togglePeriodoDia()">Dia</button>
      ${["semana", "mes"].map((p) => `
        <a class="periodo-btn${periodo.tipo === p ? " ativo" : ""}" href="?periodo=${p}">${PERIODO_LABEL[p]}</a>
      `).join("")}
      <button class="periodo-btn${periodo.tipo === "ano" ? " ativo" : ""}" type="button" onclick="togglePeriodoAno()">Ano</button>
      <button class="periodo-btn${periodo.tipo === "personalizado" ? " ativo" : ""}" type="button" onclick="togglePeriodoCustom()">Personalizado</button>
    </div>
    <form method="get" id="formPeriodoDia" class="${periodo.tipo === "dia" ? "" : "hidden"}" style="margin-bottom:14px">
      <input type="hidden" name="periodo" value="dia" />
      <label class="muted" style="font-size:0.75rem; display:block; margin-bottom:4px">Escolher dia</label>
      <input class="input" type="date" name="data" value="${formatarDataInput(periodo.tipo === "dia" ? periodo.inicio : new Date())}" onchange="this.form.submit()" />
      <button class="btn-primary" type="button" style="margin-top:10px" onclick="window.location.href='?periodo=dia'">Hoje</button>
    </form>
    <form method="get" id="formPeriodoAno" class="${periodo.tipo === "ano" ? "" : "hidden"}" style="margin-bottom:14px">
      <input type="hidden" name="periodo" value="ano" />
      <label class="muted" style="font-size:0.75rem; display:block; margin-bottom:4px">Escolher ano</label>
      <select class="input" name="ano" onchange="this.form.submit()">
        ${anosOpcoes.map((a) => `<option value="${a}"${periodo.tipo === "ano" && periodo.ano === a ? " selected" : ""}>${a}</option>`).join("")}
      </select>
    </form>
    <form method="get" id="formPeriodoCustom" class="${periodo.tipo === "personalizado" ? "" : "hidden"}" style="margin-bottom:14px">
      <input type="hidden" name="periodo" value="personalizado" />
      <div class="grid2">
        <div>
          <label class="muted" style="font-size:0.75rem; display:block; margin-bottom:4px">De</label>
          <input class="input" style="margin-bottom:0" type="date" name="inicio" value="${formatarDataInput(periodo.inicio)}" />
        </div>
        <div>
          <label class="muted" style="font-size:0.75rem; display:block; margin-bottom:4px">Ate</label>
          <input class="input" style="margin-bottom:0" type="date" name="fim" value="${formatarDataInput(periodo.fim)}" />
        </div>
      </div>
      <button class="btn-primary" type="submit" style="margin-top:10px">Aplicar</button>
    </form>
    <script>
    function togglePeriodoDia() {
      document.getElementById("formPeriodoDia").classList.toggle("hidden");
    }
    function togglePeriodoAno() {
      document.getElementById("formPeriodoAno").classList.toggle("hidden");
    }
    function togglePeriodoCustom() {
      document.getElementById("formPeriodoCustom").classList.toggle("hidden");
    }
    </script>
  ` : "";

  return layout(AREA_LABELS[area], `
    <header>
      <a class="back-btn" href="/${sessao.slug}/painel">←</a>
      <div class="brand">${AREA_ICONS[area]} ${AREA_LABELS[area]}</div>
    </header>
    <main>
      <p class="muted">${atualizado ? "Ultima atualizacao: " + formatarDataHora(atualizado) : "Ainda sem sincronizacao"}</p>
      ${seletorPeriodo}
      ${tiles}
      ${grafico}
      ${listaMeses}
      <button class="btn-secondary" type="button" id="btnLancamentos" onclick="toggleLancamentos()">
        <span id="btnLancamentosTexto">Ver lançamentos ▾</span>
      </button>
      <div id="lancamentosArea" class="hidden" style="margin-top:14px">
        ${tabela}
      </div>
    </main>
    <script>
    function toggleLancamentos() {
      var area = document.getElementById("lancamentosArea");
      var txt = document.getElementById("btnLancamentosTexto");
      var aberto = !area.classList.contains("hidden");
      if (aberto) {
        area.classList.add("hidden");
        txt.textContent = "Ver lançamentos ▾";
      } else {
        area.classList.remove("hidden");
        txt.textContent = "Ocultar lançamentos ▴";
        area.scrollIntoView({ behavior: "smooth", block: "start" });
      }
    }
    </script>
  `, sessao.slug);
}

function paginaAdmin(clientes, ultimaPorCliente) {
  const linhas = clientes.map((c) => `
    <tr>
      <td class="muted">${escapeHtml(c.codigo)}</td>
      <td>${escapeHtml(c.nome)}</td>
      <td class="muted">/${escapeHtml(c.slug)}</td>
      <td>${c.liberado ? "Liberado" : "Bloqueado"}</td>
      <td class="muted">${ultimaPorCliente[c.id] ? formatarDataHora(ultimaPorCliente[c.id]) : "-"}</td>
      <td class="muted" style="font-family:monospace; font-size:0.75rem">${escapeHtml(c.token_agente)}</td>
      <td>
        ${c.liberado
          ? `<form method="post" action="/admin/${c.id}/bloquear" style="display:inline"><button class="btn-danger" style="width:auto; padding:5px 10px; font-size:0.8rem">Bloquear</button></form>`
          : `<form method="post" action="/admin/${c.id}/liberar" style="display:inline"><button class="btn-primary" style="width:auto; padding:5px 10px; font-size:0.8rem">Liberar</button></form>`}
        <form method="post" action="/admin/${c.id}/excluir" style="display:inline" onsubmit="return confirm('Excluir este cliente?')"><button class="btn-secondary" style="width:auto; padding:5px 10px; font-size:0.8rem">Excluir</button></form>
      </td>
    </tr>
  `).join("");

  return layout("Admin", `
    <header><div class="brand">SolusysMobile - Admin</div></header>
    <main style="max-width:900px">
      <div class="card" style="overflow-x:auto">
        <table>
          <thead><tr><th>Codigo</th><th>Nome</th><th>Link</th><th>Status</th><th>Ultima sync</th><th>Token do agente</th><th></th></tr></thead>
          <tbody>${linhas || `<tr><td colspan="7" class="muted">Nenhum cliente cadastrado ainda.</td></tr>`}</tbody>
        </table>
      </div>
      <div class="card">
        <h3 style="margin-top:0">Novo cliente (manual)</h3>
        <p class="muted">O cliente normalmente escolhe o proprio link direto na telinha do agente, na instalacao -- use isso aqui so em caso especial.</p>
        <form method="post" action="/admin/criar">
          <input class="input" name="codigo" placeholder="Codigo do cliente (o mesmo da licenca)" />
          <input class="input" name="slug" placeholder="Link (ex: restaurantedoze)" />
          <input class="input" name="nome" placeholder="Nome (sua referencia)" />
          <button class="btn-primary" type="submit">Criar</button>
        </form>
      </div>
    </main>
  `);
}

function escapeHtml(v) {
  return String(v ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function formatarDataHora(d) {
  const dt = new Date(d);
  return dt.toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" });
}

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

initDb()
  .then(() => {
    app.listen(PORT, "0.0.0.0", () => {
      console.log(`SolusysMobile servidor rodando em http://0.0.0.0:${PORT}`);
    });
  })
  .catch((err) => {
    console.error("Falha ao iniciar (banco):", err);
    process.exit(1);
  });
