// Agente local do SolusysMobile -- roda no PC do cliente, ao lado do Genius.
// A cada 10 minutos sincroniza (de forma incremental -- so o que mudou desde
// o ultimo sync, servidor-autoritativo via /ultima-sincronizacao) as tabelas
// espelhadas + credenciais pro servidor central. A cada poucos segundos
// confere se teve pedido de "Sincronizar agora". Ver queries.js pra lista de
// tabelas e MEMORIA.md pra decisao de arquitetura (banco `{codigo}_genius`
// por cliente, tabelas identicas as do Genius).
// (O listener de binlog para sincronizar na hora que algo muda fica em
// binlog-listener.js -- ainda NAO totalmente testado de ponta a ponta,
// ver aviso la dentro.)
require("dotenv").config();
const fs = require("fs");
const path = require("path");
const mysql = require("mysql2/promise");
const bcrypt = require("bcryptjs");
const cron = require("node-cron");
const queries = require("./queries");

// Valores padrao: o Genius instala sempre com o mesmo usuario/senha de banco
// em todo cliente, e todo agente fala com o mesmo servidor central -- por
// isso ficam fixos aqui. Um .env do lado do cliente so e necessario nos
// casos raros em que algo diverge do padrao (senha do banco diferente, etc).
const PADRAO = {
  DB_HOST: "127.0.0.1",
  DB_PORT: 3306,
  DB_USER: "root",
  DB_PASSWORD: "lisaeu",
  DB_NAME: "genius",
  // IP publico fixo do servidor central -- sem dominio por enquanto (Rodrigo
  // ainda vai apontar um dominio pra esse IP depois, pros clientes acessarem
  // o painel; o agente nao depende de dominio nenhum, so precisa alcancar o
  // servidor, entao aponta direto pro IP mesmo mudando o dominio no futuro).
  SERVIDOR_URL: "http://45.173.233.129:4000",
  REGISTRO_SECRET: "aaf915450af29e0f2fb4dd05a11e8f1d081680f976a6e22c",
};

const DB_CONFIG = {
  host: process.env.DB_HOST || PADRAO.DB_HOST,
  port: process.env.DB_PORT || PADRAO.DB_PORT,
  user: process.env.DB_USER || PADRAO.DB_USER,
  password: process.env.DB_PASSWORD || PADRAO.DB_PASSWORD,
  database: process.env.DB_NAME || PADRAO.DB_NAME,
};

const SERVIDOR_URL = process.env.SERVIDOR_URL || PADRAO.SERVIDOR_URL;
const REGISTRO_SECRET = process.env.REGISTRO_SECRET || PADRAO.REGISTRO_SECRET;
// ID_EMPRESA so serve de fallback pro auto-registro (que precisa de 1 valor
// so pra achar o nome da filial se "parametros" estiver vazio) -- a
// sincronizacao de verdade descobre TODAS as empresas sozinha (buscarEmpresas)
const ID_EMPRESA = process.env.ID_EMPRESA || 1;
const POLL_INTERVAL_MS = Number(process.env.POLL_INTERVAL_MS || 7000);

// Depois que o admin aprova, o proprio agente descobre o token sozinho (ver
// verificarAprovacao) e grava aqui do lado -- assim sobrevive a reinicios
// sem precisar editar .env nenhum na maquina do cliente.
const CONFIG_LOCAL_PATH = path.join(__dirname, "agente.local.json");

function carregarConfigLocal() {
  try {
    return JSON.parse(fs.readFileSync(CONFIG_LOCAL_PATH, "utf8"));
  } catch {
    return null;
  }
}

function salvarConfigLocal(codigo, token, slug) {
  fs.writeFileSync(CONFIG_LOCAL_PATH, JSON.stringify({ codigo, token_agente: token, slug }, null, 2));
}

const configLocal = carregarConfigLocal();
let CODIGO_CLIENTE = process.env.CODIGO_CLIENTE || configLocal?.codigo || "";
let TOKEN_AGENTE = process.env.TOKEN_AGENTE || configLocal?.token_agente || "";
let SLUG_CLIENTE = configLocal?.slug || "";

// ---------- Fase 1: telinha local de configuracao -- so roda enquanto o
// agente ainda nao tiver CODIGO_CLIENTE/TOKEN_AGENTE. O proprio cliente
// escolhe o link (slug) dele aqui, na propria maquina -- sem precisar de
// nenhuma aprovacao manual do lado do servidor central. ----------

async function descobrirDados() {
  let conn;
  try {
    conn = await mysql.createConnection(DB_CONFIG);
    return await queries.buscarDadosRegistro(conn, ID_EMPRESA);
  } finally {
    if (conn) await conn.end().catch(() => {});
  }
}

async function registrarComSlug(dados, slug) {
  const resp = await fetch(`${SERVIDOR_URL}/api/agente/auto-registrar`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Registro-Secret": REGISTRO_SECRET },
    body: JSON.stringify({ ...dados, slug }),
  });
  const data = await resp.json().catch(() => ({}));
  if (!resp.ok) return { ok: false, motivo: data.motivo || `erro_http_${resp.status}` };
  return data;
}

// ---------- Fase 2: sincronizacao de verdade (so roda depois que CODIGO_CLIENTE
// e TOKEN_AGENTE forem descobertos -- por .env, por agente.local.json de uma
// execucao anterior, ou pela telinha de configuracao acima) ----------

let sincronizando = false;

// Janela de seguranca pra tabelas sem coluna de "ultima alteracao" no Genius
// (vendas/compras) -- mesmo com sync recente, sempre revisita pelo menos os
// ultimos N dias, pra pegar edicao/cancelamento que a incremental por data de
// CRIACAO sozinha nao detectaria. ON DUPLICATE KEY UPDATE (pela Id de origem)
// no servidor corrige a linha certa em vez de duplicar.
const JANELA_SEGURANCA_MS = 3 * 24 * 60 * 60 * 1000;

async function postJson(caminho, corpo) {
  const resp = await fetch(`${SERVIDOR_URL}${caminho}`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Agente-Token": TOKEN_AGENTE },
    body: JSON.stringify(corpo),
  });
  if (!resp.ok) {
    const texto = await resp.text().catch(() => "");
    throw new Error(`${caminho} respondeu ${resp.status}: ${texto}`);
  }
  return resp.json();
}

async function sincronizarTudo(motivo = "agendada") {
  if (sincronizando) {
    console.log(`[${new Date().toLocaleTimeString()}] sincronizacao (${motivo}) ignorada, ja tem uma em andamento`);
    return;
  }
  sincronizando = true;
  const inicio = Date.now();
  let conn;
  try {
    conn = await mysql.createConnection(DB_CONFIG);

    const listaEmpresas = await queries.buscarEmpresas(conn);
    const credenciais = await queries.buscarCredenciais(conn, bcrypt);

    await postJson(`/api/agente/${CODIGO_CLIENTE}/sync/empresas`, {
      empresas: listaEmpresas.map((e) => ({ id_empresa: e.id_empresa, nome: e.nome })),
      credenciais,
    });

    // Server-autoritativo: pergunta ate onde cada tabela ja foi sincronizada
    // (por id_empresa) em vez de guardar isso do lado do agente -- assim
    // funciona igual mesmo se o agente.local.json for perdido/reinstalado.
    const estado = await (async () => {
      const resp = await fetch(`${SERVIDOR_URL}/api/agente/${CODIGO_CLIENTE}/ultima-sincronizacao`, {
        headers: { "X-Agente-Token": TOKEN_AGENTE },
      });
      if (!resp.ok) throw new Error(`ultima-sincronizacao respondeu ${resp.status}`);
      return resp.json();
    })();

    // Cada tabela/empresa e paginada por cursor de Id (queries.TAMANHO_PAGINA
    // por pagina) -- uma requisicao por pagina, ate a tabela se esgotar. Uma
    // tabela larga (ex: caixa com Descricao_caixa, produtos com ~115 colunas)
    // e/ou um cliente grande pode ter mais linhas do que cabe numa unica
    // requisicao; paginar evita depender de um limite fixo (que ja truncou
    // dado de verdade) ou de um limite de tamanho de body generoso demais.
    let linhasEnviadas = 0;
    for (const tabela of Object.keys(queries.TABELAS)) {
      const cfg = queries.TABELAS[tabela];
      const infoTabela = estado.tabelas?.[tabela] || { existe: false, porEmpresa: {} };
      let precisaSchema = !infoTabela.existe;

      for (const emp of listaEmpresas) {
        const idEmpresa = emp.id_empresa;
        let desde = infoTabela.porEmpresa ? infoTabela.porEmpresa[idEmpresa] : null;
        if (desde && cfg.colunaCriacao) {
          // tabela sem coluna de alteracao (vendas/compras): aplica a janela
          // de seguranca de 3 dias por cima do "desde" que o servidor mandou.
          const limite = Date.now() - JANELA_SEGURANCA_MS;
          desde = new Date(Math.min(new Date(desde).getTime(), limite));
        }

        let cursorId = 0;
        while (true) {
          const linhas = await queries.buscarTabela(conn, tabela, idEmpresa, desde, cursorId);
          if (linhas.length === 0) break;

          const schema = precisaSchema ? await queries.capturarSchema(conn, tabela) : undefined;
          await postJson(`/api/agente/${CODIGO_CLIENTE}/sync/tabela/${tabela}`, {
            schema,
            porEmpresa: { [idEmpresa]: linhas },
          });
          precisaSchema = false; // so a 1a pagina desta tabela (de qualquer empresa) manda o schema
          linhasEnviadas += linhas.length;

          cursorId = linhas[linhas.length - 1].Id;
          if (linhas.length < queries.TAMANHO_PAGINA) break; // ultima pagina
        }
      }
    }

    await postJson(`/api/agente/${CODIGO_CLIENTE}/sync/finalizar`, {});

    const ms = Date.now() - inicio;
    console.log(`[${new Date().toLocaleTimeString()}] sincronizacao (${motivo}) OK em ${ms}ms (${listaEmpresas.length} empresa${listaEmpresas.length === 1 ? "" : "s"}, ${linhasEnviadas} linha${linhasEnviadas === 1 ? "" : "s"} enviada${linhasEnviadas === 1 ? "" : "s"})`);
  } catch (err) {
    console.error(`[${new Date().toLocaleTimeString()}] ERRO na sincronizacao (${motivo}):`, err.message);
  } finally {
    if (conn) await conn.end().catch(() => {});
    sincronizando = false;
  }
}

async function checarPedidoDeSincronizacao() {
  try {
    const resp = await fetch(`${SERVIDOR_URL}/api/agente/${CODIGO_CLIENTE}/pendente`, {
      headers: { "X-Agente-Token": TOKEN_AGENTE },
    });
    if (!resp.ok) return;
    const data = await resp.json();
    if (data.pendente) {
      await sincronizarTudo("sincronizar agora");
    }
  } catch (err) {
    // silencioso -- sem internet/servidor fora do ar e normal acontecer,
    // nao precisa logar toda hora, a proxima tentativa (poucos segundos
    // depois) tenta de novo sozinha
  }
}

// ---------- Telinha local de configuracao (porta fixa na maquina do cliente,
// so acessivel de dentro da propria rede dele -- nunca exposta pra fora).
// Fica no ar o tempo todo (nao so na 1a instalacao): antes de configurado
// mostra o formulario de escolher o link; depois de configurado mostra o
// link ja escolhido, pra sempre poder abrir localhost:PORTA_SETUP sem medo
// de dar erro de conexao. ----------

const PORTA_SETUP = Number(process.env.PORTA_SETUP || 4321);

function paginaBase(corpo) {
  return `<!doctype html><html lang="pt-br"><head><meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SolusysMobile - Configuracao</title>
<style>
  body{font-family:system-ui,sans-serif;background:#f5f6f8;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0;padding:20px;box-sizing:border-box}
  .card{background:#fff;border-radius:14px;box-shadow:0 4px 16px rgba(0,0,0,.08);padding:28px;max-width:420px;width:100%}
  h1{font-size:1.2rem;margin:0 0 10px}
  p{color:#555;font-size:.92rem;line-height:1.4}
  .muted{color:#999;font-size:.85rem}
  input{width:100%;padding:12px;border:1px solid #ccc;border-radius:8px;font-size:1rem;margin:6px 0 14px;box-sizing:border-box}
  button{width:100%;padding:12px;border:none;border-radius:8px;background:#2670c7;color:#fff;font-size:1rem;font-weight:600;cursor:pointer}
  .erro{background:#fdecea;color:#c0392b;border-radius:8px;padding:10px 14px;font-size:.88rem;margin-bottom:14px}
  .ok{background:#eafaf1;color:#1e8449;border-radius:8px;padding:14px;font-size:.95rem}
  .link{font-weight:600;word-break:break-all}
</style></head><body><div class="card">${corpo}</div></body></html>`;
}

function paginaFormulario({ dados, erro } = {}) {
  if (!dados) {
    return paginaBase(`
      <h1>Nao encontrei os dados do Genius</h1>
      <p>Confirme que o Genius esta instalado e configurado nesta maquina (campo <b>codigo_cliente</b> preenchido) e recarregue esta pagina.</p>
    `);
  }
  return paginaBase(`
    <h1>Configurar acesso</h1>
    <p>Empresa detectada: <b>${dados.nome || "(sem nome)"}</b> &middot; codigo <b>${dados.codigo}</b></p>
    <p>Escolha o link que voce vai usar pra acessar pelo celular (so letras minusculas, numeros e hifen):</p>
    ${erro ? `<div class="erro">${erro}</div>` : ""}
    <form method="post" action="/registrar">
      <input name="slug" placeholder="ex: restaurantedoze" required pattern="[a-z0-9-]{3,40}" title="letras minusculas, numeros e hifen, 3 a 40 caracteres" autofocus />
      <button type="submit">Confirmar</button>
    </form>
  `);
}

function paginaConfigurado(slug) {
  return paginaBase(`
    <h1>Configurado!</h1>
    <div class="ok">
      Acesse pelo celular em:<br />
      <span class="link">${SERVIDOR_URL}/${slug}</span>
    </div>
    <p class="muted">A sincronizacao com o servidor ja esta rodando sozinha em segundo plano.</p>
  `);
}

function lerCorpoFormulario(req) {
  return new Promise((resolve) => {
    let corpo = "";
    req.on("data", (pedaco) => (corpo += pedaco));
    req.on("end", () => resolve(Object.fromEntries(new URLSearchParams(corpo))));
  });
}

function iniciarServidorSetup() {
  const http = require("http");
  const servidorSetup = http.createServer(async (req, res) => {
    if (req.method === "GET" && req.url === "/") {
      if (CODIGO_CLIENTE && TOKEN_AGENTE) {
        res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
        return res.end(paginaConfigurado(SLUG_CLIENTE));
      }
      const dados = await descobrirDados().catch(() => null);
      res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
      return res.end(paginaFormulario({ dados }));
    }

    if (req.method === "POST" && req.url === "/registrar") {
      if (CODIGO_CLIENTE && TOKEN_AGENTE) {
        res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
        return res.end(paginaConfigurado(SLUG_CLIENTE));
      }
      const { slug } = await lerCorpoFormulario(req);
      const dados = await descobrirDados().catch(() => null);
      if (!dados) {
        res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
        return res.end(paginaFormulario({ dados: null }));
      }

      const MENSAGENS = {
        slug_em_uso: "Esse link ja esta em uso por outro cliente -- escolha outro.",
        slug_invalido: "Link invalido -- use so letras minusculas, numeros e hifen (3 a 40 caracteres).",
      };
      try {
        const resultado = await registrarComSlug(dados, slug);
        if (!resultado.ok) {
          res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
          return res.end(paginaFormulario({ dados, erro: MENSAGENS[resultado.motivo] || "Nao foi possivel confirmar -- tente novamente." }));
        }
        CODIGO_CLIENTE = dados.codigo;
        TOKEN_AGENTE = resultado.token_agente;
        SLUG_CLIENTE = resultado.slug;
        salvarConfigLocal(CODIGO_CLIENTE, TOKEN_AGENTE, SLUG_CLIENTE);
        console.log(`Configurado! Cliente ${CODIGO_CLIENTE} -> link /${SLUG_CLIENTE} -- comecando a sincronizar de verdade.`);
        res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
        res.end(paginaConfigurado(SLUG_CLIENTE));
        comecarSincronizacaoReal();
      } catch (err) {
        res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
        res.end(paginaFormulario({ dados, erro: "Nao foi possivel falar com o servidor -- verifique a internet e tente de novo." }));
      }
      return;
    }

    res.writeHead(404);
    res.end();
  });
  servidorSetup.listen(PORTA_SETUP, () => {
    console.log(`Telinha de configuracao em http://localhost:${PORTA_SETUP}/`);
  });
}

// ---------- Start ----------

let syncIniciado = false;

function comecarSincronizacaoReal() {
  if (syncIniciado) return;
  syncIniciado = true;
  console.log(`Agente SolusysMobile iniciado -- cliente ${CODIGO_CLIENTE}, servidor ${SERVIDOR_URL}`);
  cron.schedule("*/10 * * * *", () => sincronizarTudo("agendada 10min"));
  setInterval(checarPedidoDeSincronizacao, POLL_INTERVAL_MS);
  sincronizarTudo("inicializacao");
}

// Cobre o caso de um agente configurado pelo jeito antigo (.env com
// CODIGO_CLIENTE/TOKEN_AGENTE direto, sem passar pela telinha nova) -- busca
// o slug dele no servidor uma vez e guarda local, so pra telinha de "ja
// configurado" mostrar o link certo em vez de aparecer em branco.
async function garantirSlug() {
  if (SLUG_CLIENTE) return;
  try {
    const dados = await descobrirDados();
    if (!dados) return;
    const resultado = await registrarComSlug(dados, "");
    if (resultado.ok && resultado.slug) {
      SLUG_CLIENTE = resultado.slug;
      salvarConfigLocal(CODIGO_CLIENTE, TOKEN_AGENTE, SLUG_CLIENTE);
    }
  } catch {
    // sem internet agora -- tenta de novo no proximo restart do servico, nao afeta a sincronizacao
  }
}

function iniciar() {
  iniciarServidorSetup();
  if (CODIGO_CLIENTE && TOKEN_AGENTE) {
    garantirSlug();
    comecarSincronizacaoReal();
  } else {
    console.log(`Aguardando configuracao -- abra http://localhost:${PORTA_SETUP}/ nesta maquina e escolha o link.`);
  }
}

iniciar();
