// Ponte HTTP entre o Rails e o WhatsApp, via Baileys (sessao pareada por QR).
//
// ⚠️ NAO E API OFICIAL. Baileys fala o protocolo do WhatsApp Web por engenharia
//    reversa. Funciona e e gratuito, mas esta fora dos Termos do WhatsApp: a
//    Meta pode banir o numero. Use um chip dedicado, nao o numero principal da
//    operacao, e respeite o intervalo entre mensagens (o Rails controla isso;
//    aqui a fila serializa os envios para nunca sair rajada em paralelo).
//
// Superficie HTTP (toda rota, menos /health, exige Authorization: Bearer TOKEN):
//   GET  /health  -> { ok: true }                      (sem auth, para o compose)
//   GET  /status  -> { conectado, numero, qr, desde }
//   POST /enviar  -> { para, texto } => { ok, id }
//   POST /logout  -> encerra a sessao e apaga as credenciais
//
// A porta NAO e publicada no docker-compose: so o container do Rails alcanca.

import { createServer } from 'node:http';
import { readFile, rm, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import makeWASocket, {
  DisconnectReason,
  useMultiFileAuthState,
  fetchLatestBaileysVersion,
} from '@whiskeysockets/baileys';
import pino from 'pino';
import QRCode from 'qrcode';

const PORTA    = Number(process.env.PORT || 3001);
const TOKEN    = process.env.WHATSAPP_TOKEN || '';
const DATA_DIR = process.env.WHATSAPP_DATA_DIR || '/data';
const AUTH_DIR = join(DATA_DIR, 'auth');

const log = pino({ level: process.env.LOG_LEVEL || 'info' });

// Estado observavel pela tela do admin.
const estado = {
  conectado: false,
  numero: null,
  qr: null,        // data URL do QR enquanto nao pareado
  desde: null,
  ultimoErro: null,
};

let sock = null;
let parando = false;

// ── Fila serial de envios ───────────────────────────────────────────────────
// Disparo em paralelo para dezenas de contatos e a forma mais rapida de perder
// o numero. Uma promise encadeada garante um envio por vez, na ordem.
let fila = Promise.resolve();
function enfileirar(tarefa) {
  const resultado = fila.then(tarefa, tarefa);
  fila = resultado.catch(() => {});
  return resultado;
}

async function conectar() {
  await mkdir(AUTH_DIR, { recursive: true });
  const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
  const { version } = await fetchLatestBaileysVersion();

  sock = makeWASocket({
    version,
    auth: state,
    printQRInTerminal: false,
    logger: pino({ level: 'silent' }),
    browser: ['Reem Notas', 'Chrome', '1.0.0'],
    markOnlineOnConnect: false,
  });

  sock.ev.on('creds.update', saveCreds);

  sock.ev.on('connection.update', async (update) => {
    const { connection, lastDisconnect, qr } = update;

    if (qr) {
      estado.qr = await QRCode.toDataURL(qr, { margin: 1, width: 320 });
      estado.conectado = false;
      log.info('QR novo gerado — aguardando leitura');
    }

    if (connection === 'open') {
      estado.conectado = true;
      estado.qr = null;
      estado.desde = new Date().toISOString();
      estado.ultimoErro = null;
      estado.numero = (sock.user?.id || '').split(':')[0] || null;
      log.info({ numero: estado.numero }, 'WhatsApp conectado');
    }

    if (connection === 'close') {
      estado.conectado = false;
      const codigo = lastDisconnect?.error?.output?.statusCode;
      estado.ultimoErro = lastDisconnect?.error?.message || null;

      // loggedOut = a sessao foi encerrada no celular. Reconectar em loop so
      // gera trafego suspeito; as credenciais precisam ser apagadas e o QR
      // lido de novo.
      if (codigo === DisconnectReason.loggedOut) {
        log.warn('sessao encerrada no aparelho — apagando credenciais');
        await rm(AUTH_DIR, { recursive: true, force: true }).catch(() => {});
        estado.numero = null;
      }

      if (!parando) {
        log.info({ codigo }, 'reconectando em 5s');
        setTimeout(() => conectar().catch((e) => log.error(e)), 5000);
      }
    }
  });
}

// ── HTTP ────────────────────────────────────────────────────────────────────
function json(res, status, corpo) {
  const texto = JSON.stringify(corpo);
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
  res.end(texto);
}

function autorizado(req) {
  if (!TOKEN) return false; // sem token configurado, nada passa
  const header = req.headers.authorization || '';
  const enviado = header.startsWith('Bearer ') ? header.slice(7) : '';
  // Comparacao de tamanho fixo evita vazar o token por tempo de resposta.
  if (enviado.length !== TOKEN.length) return false;
  let diff = 0;
  for (let i = 0; i < TOKEN.length; i += 1) diff |= enviado.charCodeAt(i) ^ TOKEN.charCodeAt(i);
  return diff === 0;
}

async function lerCorpo(req) {
  const partes = [];
  let total = 0;
  for await (const parte of req) {
    total += parte.length;
    if (total > 64 * 1024) throw new Error('corpo grande demais');
    partes.push(parte);
  }
  return partes.length ? JSON.parse(Buffer.concat(partes).toString('utf8')) : {};
}

// Destino -> JID. Duas formas:
//   "+55 11 92005-1157" / "5511920051157"  -> "5511920051157@s.whatsapp.net"
//   "120363012345678901@g.us"              -> passa direto (grupo)
// O grupo NAO e telefone: e um identificador proprio do WhatsApp, entao vem
// pronto do cadastro e so precisa ser validado no formato.
function jidDe(destino) {
  const valor = String(destino || '').trim();
  if (/^\d{5,}@g\.us$/.test(valor)) return valor;
  if (/^\d{10,15}@s\.whatsapp\.net$/.test(valor)) return valor;

  const digitos = valor.replace(/\D/g, '');
  if (digitos.length < 12 || digitos.length > 13) return null;
  return `${digitos}@s.whatsapp.net`;
}

function ehGrupo(jid) {
  return String(jid).endsWith('@g.us');
}

const servidor = createServer(async (req, res) => {
  try {
    const url = new URL(req.url, 'http://local');

    if (url.pathname === '/health') return json(res, 200, { ok: true });

    if (!autorizado(req)) return json(res, 401, { erro: 'nao autorizado' });

    if (url.pathname === '/status' && req.method === 'GET') {
      return json(res, 200, {
        conectado: estado.conectado,
        numero: estado.numero,
        qr: estado.qr,
        desde: estado.desde,
        ultimo_erro: estado.ultimoErro,
      });
    }

    if (url.pathname === '/enviar' && req.method === 'POST') {
      const { para, texto } = await lerCorpo(req);
      const jid = jidDe(para);
      if (!jid) return json(res, 422, { erro: 'numero invalido' });
      if (!texto || !String(texto).trim()) return json(res, 422, { erro: 'texto vazio' });
      if (!estado.conectado || !sock) return json(res, 503, { erro: 'whatsapp desconectado' });

      const resultado = await enfileirar(async () => {
        // Mandar para numero que nao tem WhatsApp e um dos sinais que a Meta usa
        // para marcar conta de spam — conferir antes vale o round-trip.
        //
        // A checagem NAO vale para grupo: `onWhatsApp` responde sobre numeros e
        // devolveria vazio para um JID de grupo, derrubando um envio valido.
        if (!ehGrupo(jid)) {
          const [existe] = await sock.onWhatsApp(jid.split('@')[0]);
          if (!existe?.exists) throw new Error('numero sem WhatsApp');
        }
        return sock.sendMessage(jid, { text: String(texto) });
      });

      return json(res, 200, { ok: true, id: resultado?.key?.id || null });
    }

    // Grupos de que o numero conectado PARTICIPA. Existe porque o id de um grupo
    // e opaco (`120363...@g.us`) — digitar na mao e pedir erro. O admin escolhe
    // pelo nome na tela e o id vai junto.
    if (url.pathname === '/grupos' && req.method === 'GET') {
      if (!estado.conectado || !sock) return json(res, 503, { erro: 'whatsapp desconectado' });

      const meu = (sock.user?.id || '').split(':')[0];
      const todos = await sock.groupFetchAllParticipating();

      const grupos = Object.values(todos || {}).map((g) => {
        const eu = (g.participants || []).find((p) => String(p.id).startsWith(meu));
        return {
          id: g.id,
          nome: g.subject || g.id,
          participantes: (g.participants || []).length,
          // `announce` = so admin escreve. Sem ser admin, o envio seria recusado
          // e o erro so apareceria no log — melhor avisar antes, na tela.
          somente_admin: Boolean(g.announce),
          sou_admin: Boolean(eu && eu.admin),
        };
      }).sort((a, b) => a.nome.localeCompare(b.nome));

      return json(res, 200, { grupos });
    }

    if (url.pathname === '/logout' && req.method === 'POST') {
      if (sock) await sock.logout().catch(() => {});
      await rm(AUTH_DIR, { recursive: true, force: true }).catch(() => {});
      estado.conectado = false;
      estado.numero = null;
      estado.qr = null;
      setTimeout(() => conectar().catch((e) => log.error(e)), 1000);
      return json(res, 200, { ok: true });
    }

    return json(res, 404, { erro: 'rota desconhecida' });
  } catch (e) {
    log.error({ err: e }, 'falha na requisicao');
    return json(res, 500, { erro: e.message || 'erro interno' });
  }
});

process.on('SIGTERM', () => { parando = true; servidor.close(() => process.exit(0)); });

servidor.listen(PORTA, '0.0.0.0', () => log.info(`ponte WhatsApp na porta ${PORTA}`));
conectar().catch((e) => log.error({ err: e }, 'falha ao conectar'));
