import express from 'express';
import multer from 'multer';
import rateLimit from 'express-rate-limit';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as db from './db.js';
import { dispatchAlarm, dispatchTest, listConfiguredChannels } from './notifications.js';
import { validate, setStateSchema, updateZonesSchema, signupSchema, loginSchema, checkoutSchema } from './schemas/index.js';
import { hashPassword, verifyPassword, signAccessToken, signRefreshToken, verifyToken, PLANS, planFeatures } from './auth.js';
import * as billing from './billing.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = parseInt(process.env.PORT || '3000');
const TOKEN = process.env.BOAT_TOKEN;
const HEARTBEAT_TIMEOUT = parseInt(process.env.HEARTBEAT_TIMEOUT_SEC || '300') * 1000;

if (!TOKEN || TOKEN.length < 16) {
  console.error('ERRO: BOAT_TOKEN não configurado ou muito curto. Defina no .env (mínimo 16 chars).');
  process.exit(1);
}

const app = express();
app.use(express.json({ limit: '10mb' }));
app.disable('x-powered-by');
app.set('trust proxy', 1);

// CORS — single owner app, allow all origins so PWA on any device works
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS,PATCH');
  res.setHeader('Access-Control-Allow-Headers', 'Authorization,Content-Type');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

// Rate limit para endpoints PÚBLICOS de share.
// 60 req/min/IP cobre auto-refresh do frontend público (4/min/usuário) com margem ~15× pra
// tripulação compartilhar IP NAT (família/marina). Atacante real precisaria 1000+ IPs distintos.
// Confiamos no `app.set('trust proxy', 1)` acima pra extrair o IP real atrás do Coolify/nginx.
const publicShareLimiter = rateLimit({
  windowMs: 60 * 1000,
  limit: 60,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests, slow down.' },
});

// Auth middleware: aceita JWT (multi-tenant) OU BOAT_TOKEN legado (mapeia ao user 1)
function requireAuth(req, res, next) {
  const auth = req.headers.authorization || '';
  const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
  if (!token) return res.status(401).json({ error: 'Missing token' });

  // Fallback BOAT_TOKEN: backwards-compat com app legado do dono (Karlão), mapeia pro user default id=1
  if (token === TOKEN) {
    req.user = { id: 1, email: 'karlao@outlook.com', viaBoatToken: true };
    return next();
  }

  // JWT
  const payload = verifyToken(token);
  if (!payload || payload.type !== 'access' || !payload.uid) {
    return res.status(401).json({ error: 'Invalid token' });
  }
  // Carrega user fresh do DB pra confirmar que ainda existe
  const user = db.findUserById(payload.uid);
  if (!user) return res.status(401).json({ error: 'User not found' });
  req.user = user;
  next();
}

// ==== Public endpoints ====
app.get('/api/health', (req, res) => res.json({ ok: true, ts: Date.now() }));

// ===== Auth endpoints (multi-tenant SaaS) =====
app.post('/api/auth/signup', validate(signupSchema), async (req, res) => {
  const { email, password, name } = req.body;
  if (db.findUserByEmail(email)) return res.status(409).json({ error: 'Email já cadastrado' });
  try {
    const hash = await hashPassword(password);
    const id = db.createUser(email, hash, name);
    db.audit(id, 'user_signup', 'user', String(id), { email }, req.ip);
    const user = db.findUserById(id);
    db.updateLastLogin(id);
    res.json({
      user,
      accessToken: signAccessToken(user),
      refreshToken: signRefreshToken(user),
    });
  } catch (e) {
    res.status(400).json({ error: e.message });
  }
});

app.post('/api/auth/login', validate(loginSchema), async (req, res) => {
  const { email, password } = req.body;
  const user = db.findUserByEmail(email);
  if (!user) return res.status(401).json({ error: 'Credenciais inválidas' });
  const ok = await verifyPassword(password, user.password_hash);
  if (!ok) return res.status(401).json({ error: 'Credenciais inválidas' });
  db.updateLastLogin(user.id);
  db.audit(user.id, 'user_login', 'user', String(user.id), {}, req.ip);
  const safe = db.findUserById(user.id);
  res.json({
    user: safe,
    accessToken: signAccessToken(safe),
    refreshToken: signRefreshToken(safe),
  });
});

app.post('/api/auth/refresh', (req, res) => {
  const { refreshToken } = req.body || {};
  if (!refreshToken) return res.status(400).json({ error: 'refreshToken required' });
  const payload = verifyToken(refreshToken);
  if (!payload || payload.type !== 'refresh') return res.status(401).json({ error: 'Invalid refresh' });
  const user = db.findUserById(payload.uid);
  if (!user) return res.status(401).json({ error: 'User not found' });
  res.json({ accessToken: signAccessToken(user) });
});

app.get('/api/auth/me', requireAuth, (req, res) => {
  res.json(req.user);
});

// Plans + license info
app.get('/api/license', requireAuth, (req, res) => {
  const lic = db.getActiveLicense(req.user.id) || { plan: 'free', status: 'active', expires_at: null };
  res.json({
    plan: lic.plan,
    status: lic.status,
    expires_at: lic.expires_at,
    features: planFeatures(lic.plan),
    plans: PLANS,
    billingEnabled: billing.isAsaasConfigured(),
  });
});

// ===== Billing endpoints (Asaas) =====
app.get('/api/billing/status', (req, res) => {
  res.json({ configured: billing.isAsaasConfigured() });
});

app.post('/api/billing/checkout', requireAuth, validate(checkoutSchema), async (req, res) => {
  if (!billing.isAsaasConfigured()) return res.status(503).json({ error: 'Billing não configurado no servidor (ASAAS_API_KEY ausente)' });
  if (req.user.viaBoatToken) return res.status(403).json({ error: 'Use uma conta com login (não BOAT_TOKEN) pra fazer upgrade' });

  const { plan, cycle, billingType } = req.body;
  try {
    const value = billing.priceFor(plan, cycle);
    let customerId = db.getUserAsaasCustomerId(req.user.id);
    if (!customerId) {
      customerId = await billing.getOrCreateCustomer(req.user);
      db.setUserAsaasCustomerId(req.user.id, customerId);
    }
    const payment = await billing.createPayment({
      customerId, plan, cycle, value, billingType,
      description: `Shivao ${PLANS[plan].name} (${cycle === 'monthly' ? 'mensal' : 'anual'})`,
    });
    db.createPayment({
      user_id: req.user.id,
      asaas_payment_id: payment.id,
      asaas_customer_id: customerId,
      plan, cycle, value, billing_type: billingType,
      status: payment.status,
      invoice_url: payment.invoiceUrl,
      due_date: new Date(payment.dueDate).getTime(),
    });
    db.audit(req.user.id, 'checkout_created', 'payment', payment.id, { plan, cycle, value, billingType }, req.ip);

    // Pra PIX, busca QR code
    let pix = null;
    if (billingType === 'PIX') {
      try { pix = await billing.getPixQrCode(payment.id); } catch (e) { console.warn('[pix qrcode]', e.message); }
    }
    res.json({
      paymentId: payment.id,
      invoiceUrl: payment.invoiceUrl,
      bankSlipUrl: payment.bankSlipUrl,
      status: payment.status,
      value: payment.value,
      dueDate: payment.dueDate,
      pix: pix ? { qrCode: pix.encodedImage, payload: pix.payload, expiresAt: pix.expirationDate } : null,
    });
  } catch (e) {
    console.error('[checkout]', e);
    res.status(500).json({ error: e.message });
  }
});

app.get('/api/billing/payment/:id', requireAuth, async (req, res) => {
  const local = db.findPaymentByAsaasId(req.params.id);
  if (!local || local.user_id !== req.user.id) return res.status(404).json({ error: 'not found' });
  // Reconcilia com Asaas se ainda PENDING (pra caso webhook ter falhado)
  if (local.status === 'PENDING' && billing.isAsaasConfigured()) {
    try {
      const fresh = await billing.getPaymentStatus(req.params.id);
      if (fresh.status !== local.status) {
        db.updatePaymentStatus(req.params.id, fresh.status, billing.isPaidStatus(fresh.status) ? Date.now() : null);
        if (billing.isPaidStatus(fresh.status)) {
          db.setLicense(local.user_id, local.plan, billing.computeExpiresAt(local.cycle), local.asaas_payment_id);
          db.audit(local.user_id, 'license_activated', 'payment', req.params.id, { plan: local.plan, cycle: local.cycle, source: 'reconcile' }, req.ip);
        }
      }
      return res.json({ ...local, status: fresh.status });
    } catch (e) { /* fallthrough returns cached */ }
  }
  res.json(local);
});

app.get('/api/billing/payments', requireAuth, (req, res) => {
  res.json(db.listUserPayments(req.user.id));
});

// Webhook Asaas: chama isso quando status muda. Precisa ser configurado no painel Asaas → Integrações
// URL: https://shivao.pontualtech.work/api/billing/asaas-webhook
// Header asaas-access-token: ASAAS_WEBHOOK_TOKEN (defina o mesmo no Coolify env)
app.post('/api/billing/asaas-webhook', (req, res) => {
  const headerToken = req.headers['asaas-access-token'];
  if (!billing.verifyWebhookToken(headerToken)) {
    console.warn('[asaas-webhook] invalid token');
    return res.status(401).json({ error: 'Invalid webhook token' });
  }
  const event = req.body;
  if (!event || !event.event || !event.payment) {
    return res.status(400).json({ error: 'Invalid payload' });
  }
  const p = event.payment;
  const local = db.findPaymentByAsaasId(p.id);
  if (!local) {
    console.warn('[asaas-webhook] payment not found:', p.id);
    // Aceitar 200 mesmo assim — Asaas não retentar
    return res.json({ ok: true, ignored: true });
  }
  db.updatePaymentStatus(p.id, p.status, billing.isPaidStatus(p.status) ? Date.now() : null);
  // Ativar licença se pago
  if (billing.isPaidStatus(p.status)) {
    db.setLicense(local.user_id, local.plan, billing.computeExpiresAt(local.cycle), p.id);
    db.audit(local.user_id, 'license_activated', 'payment', p.id, { plan: local.plan, cycle: local.cycle, event: event.event }, req.ip);
    console.log(`[asaas-webhook] license activated user=${local.user_id} plan=${local.plan}`);
  } else if (billing.isFailedStatus(p.status)) {
    // Pagamento estornado: revogar se a licença vinculada a esse payment
    const lic = db.getActiveLicense(local.user_id);
    if (lic && lic.asaas_subscription_id === p.id) {
      db.setLicense(local.user_id, 'free', null, null);
      db.audit(local.user_id, 'license_revoked', 'payment', p.id, { reason: p.status }, req.ip);
    }
  }
  res.json({ ok: true });
});

// Digital Asset Links — verifica TWA do APK Android e remove a barra de URL do Chrome
app.get('/.well-known/assetlinks.json', (req, res) => {
  res.json([{
    relation: ['delegate_permission/common.handle_all_urls'],
    target: {
      namespace: 'android_app',
      package_name: 'br.com.pontualtech.shivao',
      sha256_cert_fingerprints: [
        'CA:BE:35:59:92:BA:3D:69:7C:38:0A:8A:E0:20:DE:2A:78:29:08:1C:93:F4:62:D5:6E:3F:04:E0:F5:26:23:09'
      ]
    }
  }]);
});

// Política de Privacidade (URL pública obrigatória pra Play Store + LGPD)
app.get('/politica', (req, res) => {
  res.type('html').send(`<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Política de Privacidade · Shivao</title><style>body{font-family:Georgia,serif;max-width:780px;margin:40px auto;padding:0 20px;color:#0e2a3d;background:#efe5cd;line-height:1.7}h1,h2{font-style:italic;color:#a07832}h1{border-bottom:2px solid #a07832;padding-bottom:8px}h2{margin-top:32px}code{background:#fff;padding:2px 6px;border-radius:3px;font-size:.9em}a{color:#3f7768}small{opacity:.7}</style></head><body>
<h1>Política de Privacidade — Shivao</h1>
<p><small>Última atualização: 27 de abril de 2026 · Versão 1.0</small></p>

<p>O <strong>Shivao</strong> é um aplicativo de diário de bordo náutico operado por <strong>PontualTech</strong> (CNPJ 32.772.178/0001-47, São Paulo/SP, Brasil). Esta política descreve como coletamos, usamos e protegemos seus dados pessoais, em conformidade com a <strong>LGPD (Lei 13.709/2018)</strong> e o <strong>GDPR (Regulamento UE 2016/679)</strong>.</p>

<h2>1. Quais dados coletamos</h2>
<ul>
<li><strong>Conta:</strong> e-mail, senha (hash bcrypt), nome opcional.</li>
<li><strong>Dados de bordo:</strong> registros de viagens, manutenções, fotos/áudios/vídeos que VOCÊ adicionar, posições GPS quando você ativa rastreio ou vigia.</li>
<li><strong>Pagamentos:</strong> processados pela Asaas (parceiro PCI-DSS). Não armazenamos número de cartão.</li>
<li><strong>Logs técnicos:</strong> IP, timestamps, ações sensíveis (criar/revogar share, sync de estado) — guardados por 90 dias para auditoria de segurança.</li>
</ul>

<h2>2. O que NÃO coletamos</h2>
<ul>
<li>❌ Analytics de comportamento (Google Analytics, Facebook Pixel, etc).</li>
<li>❌ Tracking entre apps/sites.</li>
<li>❌ Anúncios de terceiros.</li>
<li>❌ Compartilhamento com brokers de dados.</li>
</ul>

<h2>3. Para que usamos seus dados</h2>
<ul>
<li>Operar o serviço (sync, vigia de fundeio, alarme remoto).</li>
<li>Processar pagamentos (apenas Asaas).</li>
<li>Enviar e-mails operacionais (recuperação de senha, confirmação de pagamento, alerta de fundeio).</li>
<li>Cumprir obrigações legais (notas fiscais, intimações judiciais quando aplicável).</li>
</ul>

<h2>4. Onde seus dados ficam</h2>
<p>Servidores próprios em data center na Alemanha (Hetzner Online GmbH, certificado ISO 27001), gerenciados pela PontualTech. Backups criptografados.</p>

<h2>5. Permissões do app Android</h2>
<ul>
<li><strong>Localização (incluindo background):</strong> imprescindível pra GPS de viagens, vigia de fundeio com alarme de drift e compartilhamento ao vivo.</li>
<li><strong>Câmera/Microfone/Galeria:</strong> apenas quando você anexar mídia a um registro.</li>
<li><strong>Notificações:</strong> alarme local de fundeio (toca som + vibra mesmo com tela apagada).</li>
<li><strong>Sensores (bússola, barômetro):</strong> usados localmente no celular, não transmitidos.</li>
</ul>

<h2>6. Compartilhamento de posição ao vivo</h2>
<p>Quando você cria um link público de compartilhamento, qualquer pessoa com o link vê a posição do barco em tempo real. Você controla a duração e pode revogar a qualquer momento. Os links usam tokens randômicos de 96 bits (impossíveis de adivinhar).</p>

<h2>7. Seus direitos (LGPD/GDPR)</h2>
<p>Você pode, a qualquer momento, solicitar:</p>
<ul>
<li>Acesso aos seus dados (exportar tudo via app).</li>
<li>Correção de dados incorretos.</li>
<li>Exclusão da conta e todos os dados (delete em até 30 dias).</li>
<li>Portabilidade (exportar GPX/CSV/JSON).</li>
<li>Revogar consentimento (cancelar assinatura).</li>
</ul>
<p>Solicitações por <a href="mailto:dpo@pontualtech.com.br">dpo@pontualtech.com.br</a>.</p>

<h2>8. Retenção de dados</h2>
<ul>
<li>Conta ativa: enquanto você usar.</li>
<li>Conta cancelada: 30 dias (período de arrependimento), depois exclusão permanente.</li>
<li>Logs de auditoria: 90 dias.</li>
<li>Notas fiscais: 5 anos (exigência legal).</li>
</ul>

<h2>9. Cookies</h2>
<p>O app usa apenas <code>localStorage</code> e <code>IndexedDB</code> locais (não são cookies HTTP). Sem cookies de tracking de terceiros.</p>

<h2>10. Crianças</h2>
<p>O Shivao não é destinado a menores de 13 anos. Não coletamos dados de menores intencionalmente.</p>

<h2>11. Mudanças nesta política</h2>
<p>Notificaremos por e-mail mudanças materiais com 30 dias de antecedência. Versão atual e histórico em <a href="https://shivao.pontualtech.work/politica">shivao.pontualtech.work/politica</a>.</p>

<h2>12. Contato e DPO</h2>
<p><strong>Encarregado de Dados (DPO):</strong> Karlão · <a href="mailto:dpo@pontualtech.com.br">dpo@pontualtech.com.br</a></p>
<p><strong>Suporte:</strong> <a href="mailto:contato@pontualtech.com.br">contato@pontualtech.com.br</a></p>
<p><strong>ANPD (autoridade brasileira):</strong> <a href="https://www.gov.br/anpd">gov.br/anpd</a></p>

<hr style="margin:40px 0;border:none;border-top:1px solid #a07832">
<p style="text-align:center"><small>Shivao · Diário de Bordo · PontualTech · ${new Date().getFullYear()}</small></p>
</body></html>`);
});

// PWA manifest (necessário pra "Add to Home Screen" + APK via PWABuilder)
app.get('/manifest.json', (req, res) => {
  res.json({
    name: 'Shivao · Diário de Bordo',
    short_name: 'Shivao',
    description: 'Diário de bordo do veleiro Shivao — viagens, manutenções, GPS, fundeio com alarme remoto',
    start_url: '/',
    display: 'standalone',
    orientation: 'any',
    background_color: '#0e2a3d',
    theme_color: '#0e2a3d',
    lang: 'pt-BR',
    icons: [
      { src: '/icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' },
      { src: '/icon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'maskable' }
    ],
    categories: ['navigation', 'travel', 'productivity']
  });
});

// ==== Static frontend ====
const publicDir = path.join(__dirname, '..', 'public');
app.use(express.static(publicDir));

// ==== Authenticated API ====

// Server info (channels configured, version)
app.get('/api/info', requireAuth, (req, res) => {
  res.json({
    channels: listConfiguredChannels(),
    heartbeatTimeoutSec: HEARTBEAT_TIMEOUT / 1000,
    version: '1.0'
  });
});

// --- State sync (whole JSON blob, per-user) ---
app.get('/api/data', requireAuth, (req, res) => {
  const s = db.getState(req.user.id);
  res.json(s);
});

app.post('/api/data', requireAuth, validate(setStateSchema), (req, res) => {
  const { data } = req.body;
  const ts = db.setState(req.user.id, data);
  db.audit(req.user.id, 'state_set', 'state', null, { bytes: JSON.stringify(data).length }, req.ip);
  res.json({ ok: true, updated_at: ts });
});

// --- Media ---
const mediaDir = path.join(db.dataDir, 'media');
const upload = multer({
  storage: multer.diskStorage({
    destination: mediaDir,
    filename: (req, file, cb) => {
      const id = req.body.id || ('m_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7));
      const ext = (file.mimetype.split('/')[1] || 'bin').replace(/[^a-z0-9]/gi, '');
      cb(null, `${id}.${ext}`);
    }
  }),
  limits: { fileSize: 50 * 1024 * 1024 }
});

app.post('/api/media', requireAuth, upload.single('file'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'no file' });
  const id = req.body.id || path.parse(req.file.filename).name;
  const meta = {
    id,
    parent_id: req.body.parent_id || null,
    parent_type: req.body.parent_type || null,
    kind: req.body.kind || 'photo',
    mime: req.file.mimetype,
    size: req.file.size,
    filename: req.file.filename,
    created_at: parseInt(req.body.created_at) || Date.now()
  };
  // remove existing if any (overwrite — escopo do user)
  const ex = db.getMedia(req.user.id, id);
  if (ex && ex.filename !== meta.filename) {
    try { fs.unlinkSync(path.join(mediaDir, ex.filename)); } catch (e) {}
    db.deleteMedia(req.user.id, id);
  } else if (ex) {
    db.deleteMedia(req.user.id, id);
  }
  db.insertMedia(req.user.id, meta);
  db.audit(req.user.id, 'media_insert', 'media', id, { kind: meta.kind, mime: meta.mime, size: meta.size }, req.ip);
  res.json({ ok: true, id, url: `/api/media/${id}` });
});

app.get('/api/media/list', requireAuth, (req, res) => {
  res.json(db.listMedia(req.user.id).map(m => ({
    id: m.id, parent_id: m.parent_id, parent_type: m.parent_type,
    kind: m.kind, mime: m.mime, size: m.size, created_at: m.created_at
  })));
});

app.get('/api/media/:id', requireAuth, (req, res) => {
  const m = db.getMedia(req.user.id, req.params.id);
  if (!m) return res.status(404).json({ error: 'not found' });
  const filepath = path.join(mediaDir, m.filename);
  if (!fs.existsSync(filepath)) return res.status(404).json({ error: 'file missing' });
  res.setHeader('Content-Type', m.mime);
  res.setHeader('Cache-Control', 'private, max-age=31536000');
  fs.createReadStream(filepath).pipe(res);
});

app.delete('/api/media/:id', requireAuth, (req, res) => {
  const m = db.getMedia(req.user.id, req.params.id);
  if (!m) return res.status(404).json({ error: 'not found' });
  try { fs.unlinkSync(path.join(mediaDir, m.filename)); } catch (e) {}
  db.deleteMedia(req.user.id, req.params.id);
  db.audit(req.user.id, 'media_delete', 'media', req.params.id, { kind: m.kind, size: m.size }, req.ip);
  res.json({ ok: true });
});

// --- Anchor watch (with dead-man-switch) ---
app.post('/api/anchor/start', requireAuth, (req, res) => {
  const { boat_name, anchor_lat, anchor_lng, radius } = req.body;
  if (typeof anchor_lat !== 'number' || typeof anchor_lng !== 'number')
    return res.status(400).json({ error: 'lat/lng required' });
  db.setAnchor(req.user.id, {
    active: true,
    boat_name: boat_name || 'Veleiro',
    anchor_lat, anchor_lng,
    radius: radius || 50,
    started_at: Date.now(),
    last_heartbeat: Date.now(),
    last_lat: anchor_lat,
    last_lng: anchor_lng,
    last_distance: 0,
    alarm_fired: false
  });
  res.json({ ok: true });
});

app.post('/api/anchor/heartbeat', requireAuth, (req, res) => {
  const { lat, lng, distance } = req.body;
  db.updateHeartbeat(req.user.id, lat, lng, distance || 0);
  const a = db.getAnchor(req.user.id);
  res.json({ ok: true, active: !!a?.active });
});

app.post('/api/anchor/alarm', requireAuth, async (req, res) => {
  const a = db.getAnchor(req.user.id);
  const payload = {
    boat: req.body.boat_name || a?.boat_name || 'Veleiro',
    lat: req.body.lat ?? a?.last_lat,
    lng: req.body.lng ?? a?.last_lng,
    distance: req.body.distance ?? a?.last_distance,
    radius: req.body.radius ?? a?.radius,
    reason: req.body.reason || 'drift',
    ts: Date.now()
  };
  db.setAlarmFired(req.user.id, true);
  const result = await dispatchAlarm(payload);
  db.logAlarm(req.user.id, 'drift', payload, result.sent, result.failed);
  res.json(result);
});

app.post('/api/anchor/stop', requireAuth, (req, res) => {
  db.clearAnchor(req.user.id);
  res.json({ ok: true });
});

app.get('/api/anchor/status', requireAuth, (req, res) => {
  res.json(db.getAnchor(req.user.id) || { active: 0 });
});

// --- Test endpoint ---
app.post('/api/test', requireAuth, async (req, res) => {
  const result = await dispatchTest();
  db.logAlarm(req.user.id, 'test', {}, result.sent, result.failed);
  res.json(result);
});

app.get('/api/alarms', requireAuth, (req, res) => {
  res.json(db.recentAlarms(req.user.id, 50));
});

// Auditoria (consulta autenticada — quem fez o quê e quando nos endpoints sensíveis do user)
app.get('/api/audit', requireAuth, (req, res) => {
  const limit = Math.min(parseInt(req.query.limit) || 100, 500);
  res.json(db.recentAudit(req.user.id, limit));
});

// ==== LIVE SHARE ====
import crypto from 'node:crypto';

app.post('/api/share/create', requireAuth, (req, res) => {
  const { durationMinutes, boatName, zones } = req.body;
  if (!durationMinutes || durationMinutes < 1 || durationMinutes > 30 * 24 * 60)
    return res.status(400).json({ error: 'invalid duration' });
  const token = crypto.randomBytes(12).toString('base64url');
  const expiresAt = Date.now() + durationMinutes * 60 * 1000;
  db.createShare(req.user.id, token, boatName || 'Shivao', expiresAt, zones);
  db.audit(req.user.id, 'share_create', 'share', token, { boatName: boatName || 'Shivao', expiresAt, zonesCount: Array.isArray(zones) ? zones.length : 0 }, req.ip);
  const proto = req.headers['x-forwarded-proto'] || req.protocol;
  const host = req.headers['x-forwarded-host'] || req.headers.host;
  const url = `${proto}://${host}/share/${token}`;
  res.json({ token, expiresAt, url });
});

app.get('/api/share/list', requireAuth, (req, res) => {
  res.json(db.listActiveShares(req.user.id).map(s => ({
    token: s.token, boatName: s.boat_name, expiresAt: s.expires_at, createdAt: s.created_at
  })));
});

app.delete('/api/share/:token', requireAuth, (req, res) => {
  db.revokeShare(req.user.id, req.params.token);
  db.audit(req.user.id, 'share_revoke', 'share', req.params.token, {}, req.ip);
  res.json({ ok: true });
});

app.post('/api/share/:token/zones', requireAuth, validate(updateZonesSchema), (req, res) => {
  const { zones } = req.body;
  db.updateShareZones(req.user.id, req.params.token, zones);
  db.audit(req.user.id, 'share_zones_update', 'share', req.params.token, { zonesCount: zones.length }, req.ip);
  res.json({ ok: true });
});

app.post('/api/share/position', requireAuth, (req, res) => {
  const { lat, lng, speed, boatName } = req.body;
  if (typeof lat !== 'number' || typeof lng !== 'number')
    return res.status(400).json({ error: 'lat/lng required' });
  // posta apenas para shares do user logado
  const active = db.listActiveShares(req.user.id);
  let posted = 0;
  for (const s of active) {
    if (boatName && s.boat_name && s.boat_name !== boatName) continue;
    db.addSharePosition(s.token, lat, lng, speed);
    posted++;
  }
  res.json({ ok: true, posted });
});

// ==== PUBLIC share endpoints (no auth) ====
app.get('/api/share/:token/info', publicShareLimiter, (req, res) => {
  const s = db.getShare(req.params.token);
  if (!s || s.revoked || s.expires_at < Date.now())
    return res.status(404).json({ error: 'not found or expired' });
  let zones = null;
  if (s.zones) { try { zones = JSON.parse(s.zones); } catch (e) {} }
  res.json({ boatName: s.boat_name, expiresAt: s.expires_at, zones });
});

app.get('/api/share/:token/positions', publicShareLimiter, (req, res) => {
  const s = db.getShare(req.params.token);
  if (!s || s.revoked || s.expires_at < Date.now())
    return res.status(404).json({ error: 'not found or expired' });
  const positions = db.getSharePositions(req.params.token, 500);
  res.json(positions);
});

app.get('/share/:token', publicShareLimiter, (req, res) => {
  const s = db.getShare(req.params.token);
  if (!s || s.revoked) return res.status(404).type('html').send(sharePage(null, 'Link inválido ou revogado'));
  if (s.expires_at < Date.now()) return res.status(410).type('html').send(sharePage(null, 'Link expirado'));
  res.type('html').send(sharePage(s, null));
});

function sharePage(share, errorMsg) {
  if (errorMsg) {
    return `<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Indisponível</title><style>body{font-family:system-ui,sans-serif;background:#0e2a3d;color:#faf2dd;margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}.box{text-align:center;max-width:380px}.box h1{font-family:Georgia,serif;font-style:italic;font-size:32px;margin:0 0 8px;color:#c89f54}.box p{opacity:.85;line-height:1.6}</style></head><body><div class="box"><h1>Shivao</h1><p>${errorMsg}</p></div></body></html>`;
  }
  return `<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#0e2a3d">
<title>${escapeHtml(share.boat_name || 'Shivao')} ao vivo</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0e2a3d;color:#faf2dd}
.bar{background:#0e2a3d;color:#faf2dd;padding:14px 16px;border-bottom:1px solid #6f5217;box-shadow:0 1px 0 #a07832 inset;display:flex;justify-content:space-between;align-items:center;gap:10px}
.bar-left h1{font-family:'Cormorant Garamond',Georgia,serif;font-style:italic;font-weight:500;font-size:22px;color:#c89f54;line-height:1}
.bar-left .sub{font-family:'JetBrains Mono','Courier New',monospace;font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:#a07832;margin-top:3px}
.bar-right{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.04em;text-align:right;color:#a07832}
.bar-right .live::before{content:'';display:inline-block;width:7px;height:7px;border-radius:50%;background:#ff4444;margin-right:5px;animation:p 1.4s infinite}
@keyframes p{50%{opacity:.3}}
#map{height:calc(100vh - 56px);width:100%;background:#1a2733}
.empty-msg{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(14,42,61,.9);color:#c89f54;padding:18px 24px;font-family:Georgia,serif;font-style:italic;text-align:center;border:1px solid #a07832}
</style>
</head>
<body>
<div class="bar">
<div class="bar-left"><h1>${escapeHtml(share.boat_name || 'Shivao')}</h1><div class="sub">posição ao vivo</div></div>
<div class="bar-right"><div class="live">AO VIVO</div><div id="last-update">aguardando…</div></div>
</div>
<div id="map"></div>
<script>
const TOKEN=${JSON.stringify(req.params.token)};
const map=L.map('map',{zoomControl:true});
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19,attribution:'© OSM'}).addTo(map);
let trail=null,marker=null,fitDone=false;
const boatIcon=L.divIcon({html:'<svg viewBox="0 0 24 24" width="28" height="28" fill="#c89f54" stroke="#0e2a3d" stroke-width="1.5"><path d="M3 18h18l-2-6H5z"/><path d="M12 3v9" stroke-width="1.5"/></svg>',iconSize:[28,28],iconAnchor:[14,14],className:''});
async function refresh(){
  try{
    // info (com zonas) na primeira vez
    if(!window._zonesLoaded){
      try{
        const i=await fetch('/api/share/'+TOKEN+'/info');
        if(i.ok){
          const info=await i.json();
          if(info.zones&&Array.isArray(info.zones)){
            info.zones.forEach(z=>{
              const color=z.type==='forbidden'?'#8c3434':'#b67025';
              L.circle([z.center.lat,z.center.lng],{radius:z.radius,color,fillColor:color,fillOpacity:.15,weight:1.5,opacity:.6}).addTo(map).bindPopup(z.name+' · '+(z.type==='forbidden'?'Proibida':'Atenção'));
            });
            if(info.zones.length&&!fitDone){
              const bounds=L.latLngBounds(info.zones.map(z=>[z.center.lat,z.center.lng]));
              map.fitBounds(bounds,{padding:[40,40],maxZoom:13});
            }
          }
          window._zonesLoaded=true;
        }
      }catch(e){}
    }
    const r=await fetch('/api/share/'+TOKEN+'/positions');
    if(!r.ok){
      if(r.status===404||r.status===410){
        document.body.innerHTML='<div class="empty-msg">Link expirado ou revogado</div>';
        return;
      }
      throw new Error('HTTP '+r.status);
    }
    const ps=await r.json();
    if(!ps.length){
      document.getElementById('last-update').textContent='sem posição';
      if(!document.querySelector('.empty-msg')){
        const e=document.createElement('div');e.className='empty-msg';e.textContent='Aguardando primeira posição…';document.body.appendChild(e);
      }
      return;
    }
    document.querySelectorAll('.empty-msg').forEach(e=>e.remove());
    const last=ps[ps.length-1];
    const ll=ps.map(p=>[p.lat,p.lng]);
    if(trail)trail.remove();
    if(ps.length>1)trail=L.polyline(ll,{color:'#a07832',weight:3,opacity:.6}).addTo(map);
    if(!marker)marker=L.marker([last.lat,last.lng],{icon:boatIcon}).addTo(map);
    else marker.setLatLng([last.lat,last.lng]);
    if(!fitDone){fitDone=true;if(ps.length>1)map.fitBounds(ll,{padding:[40,40]});else map.setView([last.lat,last.lng],14)}
    const ago=Math.round((Date.now()-last.ts)/1000);
    const agoTxt=ago<60?ago+'s':ago<3600?Math.round(ago/60)+'min':Math.round(ago/3600)+'h';
    const spd=last.speed?(last.speed*1.94384).toFixed(1)+' kn · ':'';
    document.getElementById('last-update').textContent=spd+'há '+agoTxt;
  }catch(e){console.error(e)}
}
refresh();
setInterval(refresh,15000);
</script>
</body>
</html>`;
}

function escapeHtml(s){return String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c])}

// cleanup expired shares once a day
setInterval(() => {
  try {
    const n = db.cleanupExpiredShares();
    if (n) console.log(`[cleanup] removidos ${n} shares expirados`);
  } catch (e) { console.warn(e); }
}, 24 * 3600 * 1000);

// ==== Dead-man switch background check (multi-user) ====
const lastDeadmanFire = new Map(); // user_id -> ts
async function checkDeadman() {
  const now = Date.now();
  const sessions = db.listActiveAnchors();
  for (const a of sessions) {
    if (!a.active) continue;
    const since = now - (a.last_heartbeat || a.started_at);
    if (since < HEARTBEAT_TIMEOUT) continue;
    const last = lastDeadmanFire.get(a.user_id) || 0;
    if (now - last < HEARTBEAT_TIMEOUT) continue;
    lastDeadmanFire.set(a.user_id, now);
    console.log(`[deadman] user=${a.user_id} no heartbeat in ${Math.round(since/1000)}s — firing alarm`);
    const payload = {
      boat: a.boat_name || 'Veleiro',
      lat: a.last_lat, lng: a.last_lng,
      distance: a.last_distance,
      radius: a.radius,
      reason: 'heartbeat_lost',
      ts: now,
      minutes_lost: Math.round(since / 60000)
    };
    try {
      const result = await dispatchAlarm(payload);
      db.logAlarm(a.user_id, 'heartbeat_lost', payload, result.sent, result.failed);
    } catch (e) {
      console.warn(`[deadman] dispatch failed user=${a.user_id}:`, e.message);
    }
  }
}
setInterval(checkDeadman, 30000); // check every 30s

// ==== Start ====
app.listen(PORT, () => {
  console.log(`Shivao Cloud rodando em :${PORT}`);
  console.log(`Canais configurados: ${listConfiguredChannels().join(', ') || '(nenhum!)'}`);
  console.log(`Dead-man switch: ${HEARTBEAT_TIMEOUT/1000}s`);
});

process.on('SIGTERM', () => process.exit(0));
process.on('SIGINT', () => process.exit(0));
