import express from 'express';
import http from 'node:http';
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';
import { initRealtime, broadcastStateChange, getOnlineCount } from './realtime.js';
import * as gcal from './google-calendar.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);
});

// Diagnostic log endpoint — recebe log do BLE pra debugar
app.post('/api/bms/diag-log', requireAuth, (req, res) => {
  const { log } = req.body || {};
  if (!log || typeof log !== 'string') return res.status(400).json({ error: 'log string required' });
  const dir = path.join(db.dataDir, 'diag-logs');
  try { fs.mkdirSync(dir, { recursive: true }); } catch {}
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
  const file = path.join(dir, `${req.user.id}-${ts}.txt`);
  try {
    fs.writeFileSync(file, log.slice(0, 50000));
    db.audit(req.user.id, 'bms_diag_log', 'bluetooth', null, { bytes: log.length, file: path.basename(file) }, req.ip);
    res.json({ ok: true, file: path.basename(file) });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// ADMIN: lista TODOS os logs (BOAT_TOKEN apenas)
app.get('/api/bms/diag-log/_all', requireAuth, (req, res) => {
  if (!req.user.viaBoatToken) return res.status(403).json({ error: 'admin only' });
  const dir = path.join(db.dataDir, 'diag-logs');
  try {
    if (!fs.existsSync(dir)) return res.json({ files: [] });
    const files = fs.readdirSync(dir).map(f => {
      const stat = fs.statSync(path.join(dir, f));
      return { name: f, size: stat.size, mtime: stat.mtime };
    }).sort((a, b) => b.mtime - a.mtime);
    res.json({ files });
  } catch (e) { res.status(500).json({ error: e.message }) }
});

// Lista logs disponíveis (debug)
app.get('/api/bms/diag-log', requireAuth, (req, res) => {
  const dir = path.join(db.dataDir, 'diag-logs');
  try {
    if (!fs.existsSync(dir)) return res.json({ files: [] });
    const files = fs.readdirSync(dir)
      .filter(f => f.startsWith(`${req.user.id}-`))
      .map(f => {
        const stat = fs.statSync(path.join(dir, f));
        return { name: f, size: stat.size, mtime: stat.mtime };
      })
      .sort((a, b) => b.mtime - a.mtime);
    res.json({ files });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// Lê conteúdo de um log específico
app.get('/api/bms/diag-log/:file', requireAuth, (req, res) => {
  const file = req.params.file.replace(/[^a-zA-Z0-9._-]/g, '');
  // Admin (BOAT_TOKEN) lê qualquer; user normal só os próprios
  if (!req.user.viaBoatToken && !file.startsWith(`${req.user.id}-`)) {
    return res.status(403).json({ error: 'forbidden' });
  }
  const fullPath = path.join(db.dataDir, 'diag-logs', file);
  try {
    if (!fs.existsSync(fullPath)) return res.status(404).json({ error: 'not found' });
    const content = fs.readFileSync(fullPath, 'utf8');
    res.type('text/plain').send(content);
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// ===== Google Login via OAuth redirect (pra apps Capacitor onde GSI popup não funciona) =====
// In-memory store: session_id → { tokens, createdAt }. Auto-expira em 10min.
const pendingGoogleSessions = new Map();
setInterval(() => {
  const now = Date.now();
  for (const [k, v] of pendingGoogleSessions.entries()) {
    if (now - v.createdAt > 10 * 60 * 1000) pendingGoogleSessions.delete(k);
  }
}, 60000);

// App chama isso pra obter URL pro browser externo
app.get('/api/auth/google/start', (req, res) => {
  if (!gcal.isEnabled()) return gcal.disabledResponse(res);
  const sessionId = req.query.session || ('s_' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2));
  // Reusa buildAuthUrl mas com flow=login e session encoded em state
  // Truque: passa userId=0 (não-autenticado) + flow no returnTo
  const stateRaw = Buffer.from(JSON.stringify({ uid: 0, flow: 'login', session: sessionId, n: Math.random().toString(36).slice(2) })).toString('base64url');
  const u = new URL('https://accounts.google.com/o/oauth2/v2/auth');
  u.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID);
  u.searchParams.set('redirect_uri', process.env.GOOGLE_REDIRECT_URI);
  u.searchParams.set('response_type', 'code');
  u.searchParams.set('scope', 'openid email profile');
  u.searchParams.set('access_type', 'online');
  u.searchParams.set('prompt', 'select_account');
  u.searchParams.set('state', stateRaw);
  res.json({ url: u.toString(), session: sessionId });
});

// App polling: retorna 204 se ainda esperando, 200 com tokens se Google completou
app.get('/api/auth/google/poll', (req, res) => {
  const session = req.query.session;
  if (!session) return res.status(400).json({ error: 'session required' });
  const data = pendingGoogleSessions.get(session);
  if (!data) return res.status(204).send();
  pendingGoogleSessions.delete(session); // one-shot
  res.json(data.tokens);
});

// Login com Google (Sign-In via popup do GSI no web) — recebe ID token, valida no Google, cria/loga user
app.post('/api/auth/google', async (req, res) => {
  const { credential } = req.body || {};
  if (!credential) return res.status(400).json({ error: 'credential (Google ID token) required' });
  try {
    // Valida o ID token via tokeninfo endpoint do Google (sem dependência adicional)
    const r = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(credential)}`);
    if (!r.ok) return res.status(401).json({ error: 'invalid_google_token' });
    const info = await r.json();
    // Confere que aud = nosso CLIENT_ID
    const expectedAud = process.env.GOOGLE_CLIENT_ID || '';
    if (expectedAud && info.aud !== expectedAud) {
      return res.status(401).json({ error: 'token_audience_mismatch' });
    }
    if (!info.email_verified || info.email_verified === 'false') {
      return res.status(403).json({ error: 'email_not_verified' });
    }
    const email = info.email;
    const name = info.name || email.split('@')[0];
    let user = db.findUserByEmail(email);
    if (!user) {
      // Auto-cria com senha aleatória inutilizável (login só via Google daqui pra frente)
      const randomPwd = 'google-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
      const hash = await hashPassword(randomPwd);
      const id = db.createUser(email, hash, name);
      db.audit(id, 'user_signup_google', 'user', String(id), { email }, req.ip);
      user = db.findUserById(id);
    }
    db.updateLastLogin(user.id);
    db.audit(user.id, 'user_login_google', 'user', String(user.id), {}, req.ip);
    const safe = db.findUserById(user.id);
    res.json({
      user: safe,
      accessToken: signAccessToken(safe),
      refreshToken: signRefreshToken(safe),
    });
  } catch (e) {
    console.warn('[auth/google]', e.message);
    res.status(500).json({ error: e.message });
  }
});

// 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'
      ]
    }
  }]);
});

// Atalho: /apk redireciona pra última APK release no Forgejo
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.10.11/Shivao-v1.10.11.apk';
app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL));

// Página A4 imprimível com QR Code + instruções (cola no barco/marina)
app.get('/imprimir', (req, res) => {
  const url = `https://${req.headers.host || 'shivao.pontualtech.work'}/apk`;
  const qrApi = `https://api.qrserver.com/v1/create-qr-code/?size=480x480&data=${encodeURIComponent(url)}&color=0e2a3d&bgcolor=ffffff&qzone=2&format=png`;
  res.type('html').send(`<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><title>Imprimir · QR Code Shivao</title><style>
@page{size:A4;margin:0}
*{box-sizing:border-box;-webkit-print-color-adjust:exact;print-color-adjust:exact}
body{margin:0;padding:0;background:#efe5cd;font-family:Georgia,serif;color:#0e2a3d}
.page{width:210mm;min-height:297mm;padding:25mm 20mm;margin:0 auto;background:linear-gradient(180deg,#fbf5e2 0%,#efe5cd 100%);position:relative;overflow:hidden}
.page::before{content:'';position:absolute;top:0;left:0;right:0;height:8mm;background:linear-gradient(90deg,#0e2a3d,#a07832,#0e2a3d)}
.page::after{content:'';position:absolute;bottom:0;left:0;right:0;height:8mm;background:linear-gradient(90deg,#0e2a3d,#a07832,#0e2a3d)}
.head{text-align:center;margin-bottom:8mm}
.brand{display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:6mm}
.compass{width:42px;height:42px;border:2.5px solid #a07832;border-radius:50%;position:relative;flex-shrink:0}
.compass::before,.compass::after{content:'';position:absolute;background:#a07832}
.compass::before{top:50%;left:-3px;right:-3px;height:2px;transform:translateY(-50%)}
.compass::after{top:-3px;bottom:-3px;left:50%;width:2px;transform:translateX(-50%)}
h1{font-style:italic;font-size:34pt;color:#a07832;margin:0;letter-spacing:-0.5px}
.subtitle{font-style:italic;font-size:13pt;color:#5d7186;margin-top:1mm}
.tagline{font-family:'Courier New',monospace;font-size:9pt;letter-spacing:3px;text-transform:uppercase;color:#7d6943;margin-top:2mm}
.qr-section{background:#fff;padding:10mm;border-radius:6mm;box-shadow:0 4px 16px rgba(14,42,61,.12);margin:8mm auto;width:fit-content;text-align:center;border:1px solid rgba(184,156,108,.3)}
.qr-section img{display:block;margin:0 auto;width:140mm;height:140mm;max-width:100%}
.qr-label{font-family:'Courier New',monospace;font-size:11pt;color:#0e2a3d;margin-top:5mm;letter-spacing:1px}
.steps{margin:8mm 0;display:grid;grid-template-columns:repeat(2,1fr);gap:5mm}
.step{background:rgba(255,255,255,.55);padding:5mm 6mm;border-radius:4mm;border-left:3px solid #a07832}
.step-num{font-family:'Courier New',monospace;font-size:9pt;color:#a07832;letter-spacing:2px;font-weight:600}
.step-text{font-size:11pt;line-height:1.5;margin-top:2mm;color:#0e2a3d}
.step-text strong{color:#0e2a3d}
.url-box{background:#0e2a3d;color:#efe5cd;padding:5mm 8mm;text-align:center;border-radius:4mm;margin-top:4mm}
.url-box .label{font-family:'Courier New',monospace;font-size:8pt;letter-spacing:3px;text-transform:uppercase;opacity:.7;margin-bottom:2mm}
.url-box .url{font-family:'Courier New',monospace;font-size:14pt;letter-spacing:1px;color:#c89f54;font-weight:600}
.foot{position:absolute;bottom:14mm;left:20mm;right:20mm;text-align:center;font-size:8pt;color:#5d7186;font-style:italic;line-height:1.6}
.foot a{color:#a07832;text-decoration:none}
.print-btn{position:fixed;bottom:20px;right:20px;background:#a07832;color:#fff;padding:14px 24px;border-radius:8px;border:none;font-family:Georgia,serif;font-size:14pt;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.2);z-index:1000}
@media print{.print-btn{display:none}}
</style></head><body>
<div class="page">
  <div class="head">
    <div class="brand"><div class="compass"></div><h1>Shivao</h1></div>
    <div class="subtitle">Diário de Bordo · Aplicativo Náutico</div>
    <div class="tagline">GPS · Vigia de Fundeio · Meteorologia</div>
  </div>
  <div class="qr-section">
    <img src="${qrApi}" alt="QR Code">
    <div class="qr-label">aponte a câmera do celular Android aqui ↑</div>
  </div>
  <div class="steps">
    <div class="step"><div class="step-num">PASSO 1</div><div class="step-text">Abra a <strong>câmera</strong> do seu Android e aponte pro QR Code acima</div></div>
    <div class="step"><div class="step-num">PASSO 2</div><div class="step-text">Toque na notificação que aparece pra abrir o link no <strong>Chrome</strong></div></div>
    <div class="step"><div class="step-num">PASSO 3</div><div class="step-text">O Chrome baixa o <strong>APK</strong> (3,4 MB). Toque no download → <strong>Instalar</strong></div></div>
    <div class="step"><div class="step-num">PASSO 4</div><div class="step-text">Aceite "fontes desconhecidas" se pedir. <strong>Pronto</strong> — abra o ícone Shivao</div></div>
  </div>
  <div class="url-box">
    <div class="label">Não tem leitor de QR? Digite no Chrome:</div>
    <div class="url">shivao.pontualtech.work/apk</div>
  </div>
  <div class="foot">
    Shivao · PontualTech · CNPJ 32.772.178/0001-47<br>
    <a href="https://shivao.pontualtech.work/politica">shivao.pontualtech.work/politica</a> · <a href="https://shivao.pontualtech.work/termos">/termos</a>
  </div>
</div>
<button class="print-btn" onclick="window.print()">🖨️ Imprimir / Salvar PDF</button>
</body></html>`);
});

// QR Code da URL /apk pra facilitar instalação no celular
app.get('/qr', (req, res) => {
  const url = `https://${req.headers.host || 'shivao.pontualtech.work'}/apk`;
  const qrApi = `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(url)}&color=0e2a3d&bgcolor=efe5cd&qzone=2`;
  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>Baixar app · Shivao</title><style>body{font-family:Georgia,serif;background:#efe5cd;color:#0e2a3d;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:20px}h1{font-style:italic;color:#a07832;margin:0 0 8px}p{margin:0 0 24px;color:#5d7186}.qr{background:#fff;padding:24px;border-radius:16px;box-shadow:0 8px 32px rgba(14,42,61,.15);margin:8px 0}img{display:block}.url{font-family:'Courier New',monospace;font-size:13px;background:#fff;padding:10px 14px;border-radius:8px;margin-top:14px;border:1px solid rgba(184,156,108,.3)}.btn{display:inline-block;background:linear-gradient(180deg,#143a52,#0e2a3d);color:#efe5cd;padding:14px 24px;border-radius:8px;text-decoration:none;margin-top:20px;font-family:Georgia,serif;font-style:italic;letter-spacing:.5px;box-shadow:0 4px 12px rgba(14,42,61,.2)}.btn:hover{transform:translateY(-1px)}small{opacity:.6;margin-top:24px;text-align:center;max-width:340px;line-height:1.5}</style></head><body>
<h1>Shivao · Diário de Bordo</h1>
<p>Aponte a câmera do celular pro QR Code:</p>
<div class="qr"><img src="${qrApi}" alt="QR Code APK Shivao" width="400" height="400"></div>
<div class="url">${url}</div>
<a class="btn" href="/apk">Baixar APK direto</a>
<small>Android: o Chrome vai baixar o APK. Toque na notificação de download e siga as instruções pra instalar.</small>
</body></html>`);
});

// Termos de Uso
app.get('/termos', (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>Termos de Uso · 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}.warn{background:#8c343422;border-left:4px solid #8c3434;padding:12px 16px;margin:20px 0}a{color:#3f7768}small{opacity:.7}</style></head><body>
<h1>Termos de Uso — Shivao</h1>
<p><small>Última atualização: 27 de abril de 2026 · Versão 1.0</small></p>

<p>Ao usar o aplicativo <strong>Shivao</strong> (operado por <strong>PontualTech</strong>, CNPJ 32.772.178/0001-47), você concorda com estes termos. Leia com atenção.</p>

<h2>1. Aceitação</h2>
<p>O uso do app implica aceitação completa destes termos. Se não concorda, não use.</p>

<h2>2. Cadastro e conta</h2>
<ul>
<li>Você precisa ter ≥18 anos OU autorização do responsável legal.</li>
<li>Informações verdadeiras e atualizadas. Senha é responsabilidade sua.</li>
<li>1 conta por usuário. Compartilhamento de credenciais é proibido.</li>
<li>Podemos suspender contas com violação destes termos.</li>
</ul>

<h2>3. Planos, pagamentos e renovação</h2>
<ul>
<li><strong>Free:</strong> grátis, recursos limitados (vigia local + diário 10 últimas).</li>
<li><strong>Pro/Captain:</strong> assinatura mensal ou anual via Asaas (PIX, cartão, boleto).</li>
<li><strong>Renovação:</strong> ao fim do ciclo, você renova manualmente. Sem cobrança automática surpresa.</li>
<li><strong>Reembolso:</strong> 7 dias de arrependimento (CDC art. 49) — devolução integral via mesmo método. Após 7 dias: pro-rata do tempo restante.</li>
</ul>

<h2>4. Uso permitido</h2>
<ul>
<li>Uso pessoal ou profissional náutico (lazer, trabalho, charters).</li>
<li>1 usuário = 1 ou múltiplos barcos (no plano Captain).</li>
<li>Compartilhamento público de posição é OK pra tripulação/familia (links temporários).</li>
</ul>

<h2>5. Uso PROIBIDO</h2>
<ul>
<li>❌ Engenharia reversa do app ou backend (exceto pra interoperar legalmente).</li>
<li>❌ Revender o serviço como white-label sem licença comercial.</li>
<li>❌ Atacar a infraestrutura (DDoS, brute-force, exploit).</li>
<li>❌ Cadastrar bots ou contas falsas em massa.</li>
<li>❌ Usar pra atividade ilegal (pesca em área proibida, navegação clandestina, etc).</li>
</ul>

<h2>6. Limitação de responsabilidade — IMPORTANTE</h2>
<div class="warn"><strong>⚠️ AVISO CRÍTICO PARA NAVEGAÇÃO:</strong>
<p>O Shivao é uma <strong>FERRAMENTA AUXILIAR</strong> de navegação e segurança. <strong>NÃO substitui</strong>:</p>
<ul>
<li>Equipamentos náuticos certificados (chartplotter, AIS, VHF, balsas).</li>
<li>Cartas náuticas oficiais (Marinha do Brasil, NOAA).</li>
<li>Atenção do skipper.</li>
<li>Comunicação com a Capitania dos Portos.</li>
</ul>
<p>Não nos responsabilizamos por:</p>
<ul>
<li>Decisões tomadas com base no app (rota, fundeio, meteorologia).</li>
<li>Falha de GPS, internet, sensores ou notificações.</li>
<li>Danos materiais, pessoais ou ambientais decorrentes do uso.</li>
<li>Perda de dados (faça backups regulares).</li>
</ul>
<p><strong>O comandante da embarcação é o único responsável pela segurança a bordo.</strong></p></div>

<h2>7. Propriedade intelectual</h2>
<ul>
<li>Código, logo, nome "Shivao" pertencem à PontualTech.</li>
<li>Seus dados (viagens, mídia) pertencem a VOCÊ — exporte quando quiser, exclua a qualquer momento.</li>
<li>Bibliotecas open source: Leaflet (BSD-2), OpenStreetMap (ODbL), express-rate-limit (MIT), bcryptjs (MIT), jsonwebtoken (MIT).</li>
</ul>

<h2>8. Suspensão e cancelamento</h2>
<ul>
<li>Você pode cancelar a qualquer momento via app (Aba Conta → Sair → Excluir conta).</li>
<li>Podemos suspender se houver violação destes termos, com aviso por e-mail (exceto urgências de segurança).</li>
<li>Após cancelamento: 30 dias pra reativar, depois exclusão permanente.</li>
</ul>

<h2>9. Mudanças nos termos</h2>
<p>Notificaremos por e-mail mudanças materiais com 30 dias de antecedência. Versão atual em <a href="https://shivao.pontualtech.work/termos">shivao.pontualtech.work/termos</a>.</p>

<h2>10. Lei aplicável e foro</h2>
<p>Estes termos seguem a lei brasileira. Foro: comarca de São Paulo/SP. Disputas de consumo podem usar <a href="https://www.consumidor.gov.br">consumidor.gov.br</a> antes de judicializar.</p>

<h2>11. Contato</h2>
<p>Suporte: <a href="mailto:contato@pontualtech.com.br">contato@pontualtech.com.br</a><br>
Privacidade/LGPD: <a href="mailto:dpo@pontualtech.com.br">dpo@pontualtech.com.br</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>`);
});

// 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,
    googleCalendar: gcal.isEnabled(),
    version: '1.0'
  });
});

// ===== Google Calendar OAuth + sync =====
app.get('/api/google/status', requireAuth, (req, res) => {
  if (!gcal.isEnabled()) return res.json({ enabled: false });
  const conn = db.getGoogleConnection(req.user.id);
  res.json({
    enabled: true,
    connected: !!conn,
    email: conn?.email || null,
    last_sync_at: conn?.last_sync_at || null,
  });
});

app.get('/api/google/auth-url', requireAuth, (req, res) => {
  if (!gcal.isEnabled()) return gcal.disabledResponse(res);
  const returnTo = (req.query.return_to || '/').toString().slice(0, 200);
  const url = gcal.buildAuthUrl(req.user.id, returnTo);
  res.json({ url });
});

app.get('/api/google/callback', async (req, res) => {
  if (!gcal.isEnabled()) return gcal.disabledResponse(res);
  const { code, state, error } = req.query;
  if (error) return res.status(400).send(`Erro do Google: ${error}`);
  if (!code || !state) return res.status(400).send('Faltam parâmetros code/state.');
  let parsed;
  try { parsed = JSON.parse(Buffer.from(state, 'base64url').toString('utf8')); }
  catch (e) { return res.status(400).send('state inválido'); }
  try {
    const tokens = await gcal.exchangeCodeForTokens(code);
    const userInfo = await gcal.getUserInfo(tokens.access_token);

    // === Flow LOGIN (do app via /api/auth/google/start) ===
    if (parsed.flow === 'login' && parsed.session) {
      let user = db.findUserByEmail(userInfo.email);
      if (!user) {
        const randomPwd = 'google-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
        const hash = await hashPassword(randomPwd);
        const id = db.createUser(userInfo.email, hash, userInfo.name || userInfo.email.split('@')[0]);
        db.audit(id, 'user_signup_google', 'user', String(id), { email: userInfo.email }, req.ip);
        user = db.findUserById(id);
      }
      db.updateLastLogin(user.id);
      db.audit(user.id, 'user_login_google', 'user', String(user.id), {}, req.ip);
      const safe = db.findUserById(user.id);
      // Salva tokens em memória pra app coletar via /poll
      pendingGoogleSessions.set(parsed.session, {
        tokens: {
          user: safe,
          accessToken: signAccessToken(safe),
          refreshToken: signRefreshToken(safe),
        },
        createdAt: Date.now(),
      });
      return res.send(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Login OK</title>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <style>body{font-family:system-ui;background:#0e2a3d;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;text-align:center;padding:20px}h1{color:#d4a04a;margin:0 0 16px}p{margin:8px 0;line-height:1.5}.big{font-size:64px;margin-bottom:16px}</style></head>
        <body><div><div class="big">⚓</div><h1>Logado com sucesso</h1>
        <p>Você está conectado como <strong>${userInfo.email}</strong></p>
        <p style="opacity:.7">Volte pro app — ele vai detectar o login automaticamente em alguns segundos.</p>
        <script>setTimeout(()=>{try{window.close()}catch(e){}},3000)</script>
        </div></body></html>`);
    }

    // === Flow CALENDAR (conectar Google Calendar pra user já logado) ===
    const userId = parsed.uid;
    if (!userId) return res.status(400).send('state sem uid');
    db.saveGoogleConnection(userId, {
      access_token: tokens.access_token,
      refresh_token: tokens.refresh_token,
      expires_at: Date.now() + (tokens.expires_in * 1000),
      email: userInfo.email,
    });
    db.audit(userId, 'google_connected', 'google_calendar', null, { email: userInfo.email }, req.ip);
    const returnTo = parsed.rt || '/';
    res.send(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Google conectado</title>
      <meta name="viewport" content="width=device-width,initial-scale=1">
      <style>body{font-family:system-ui;background:#0e2a3d;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;text-align:center;padding:20px}h1{color:#d4a04a}</style></head>
      <body><div><h1>✓ Google Agenda conectado</h1>
      <p>Conectado como <strong>${userInfo.email}</strong></p>
      <p>Pode fechar esta janela e voltar pro app.</p>
      <script>setTimeout(()=>{try{window.close()}catch(e){};location.href=${JSON.stringify(returnTo)}},2000)</script>
      </div></body></html>`);
  } catch (e) {
    console.warn('[google] callback failed', e.message);
    res.status(500).send('Erro: ' + e.message);
  }
});

app.post('/api/google/disconnect', requireAuth, (req, res) => {
  if (!gcal.isEnabled()) return gcal.disabledResponse(res);
  db.deleteGoogleConnection(req.user.id);
  db.audit(req.user.id, 'google_disconnected', 'google_calendar', null, null, req.ip);
  res.json({ ok: true });
});

// Push: envia/atualiza um evento no Google a partir de uma pendência local
app.post('/api/google/sync-pending', requireAuth, async (req, res) => {
  if (!gcal.isEnabled()) return gcal.disabledResponse(res);
  const { pending } = req.body || {};
  if (!pending || !pending.id) return res.status(400).json({ error: 'pending.id required' });
  try {
    if (pending.deleted) {
      if (pending.googleEventId) await gcal.deleteEvent(req.user.id, pending.googleEventId);
      return res.json({ ok: true, deleted: true });
    }
    const ev = await gcal.upsertEventForPending(req.user.id, pending);
    res.json({ ok: true, event: { id: ev.id, htmlLink: ev.htmlLink, updated: ev.updated } });
  } catch (e) {
    if (String(e.message).includes('not_connected')) {
      return res.status(409).json({ error: 'not_connected' });
    }
    res.status(500).json({ error: e.message });
  }
});

// Pull: lista eventos modificados no Google (cliente faz merge)
app.get('/api/google/pull', requireAuth, async (req, res) => {
  if (!gcal.isEnabled()) return gcal.disabledResponse(res);
  try {
    const conn = db.getGoogleConnection(req.user.id);
    if (!conn) return res.status(409).json({ error: 'not_connected' });
    const result = await gcal.listChangedEvents(req.user.id, conn.sync_token);
    if (result.nextSyncToken) {
      db.setGoogleSyncToken(req.user.id, result.nextSyncToken, Date.now());
    }
    const items = (result.items || []).map(gcal.eventToPending);
    res.json({ items });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

// --- 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);
  // Notifica outros devices do mesmo user em tempo real (não bloqueia resposta)
  const originDeviceId = req.headers['x-device-id'] || req.query.device || null;
  broadcastStateChange(req.user.id, { kind: 'state', updated_at: ts, originDeviceId });
  res.json({ ok: true, updated_at: ts, online_devices: getOnlineCount(req.user.id) });
});

// --- 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 ====
const httpServer = http.createServer(app);
initRealtime(httpServer);

httpServer.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));
