// index.js
const path = require('path');
const fs   = require('fs');
const express = require('express');
const crypto  = require('crypto');
const { machineIdSync } = require('node-machine-id');
const msal    = require('@azure/msal-node');
const { Client } = require('@microsoft/microsoft-graph-client');
require('isomorphic-fetch');

const app = express();

// Middleware
app.use(express.json());
// Sirve primero la carpeta public
app.use(express.static(path.join(__dirname, 'public')));
// Y luego el resto (si necesitas otros assets junto al index.js)
app.use(express.static(__dirname));


app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'index.html'));
});

// Load .env
let envPath = path.join(__dirname, '..', '.env');
if (!fs.existsSync(envPath)) {
  envPath = path.join(__dirname, '.env');
}
if (fs.existsSync(envPath)) {
  require('dotenv').config({ path: envPath });
} else {
  console.warn(`.env no encontrado en ${envPath}`);
}

const {
  CLIENT_ID,
  CLIENT_SECRET,
  TENANT_ID    = 'consumers',
  REDIRECT_URI = 'http://localhost:3000/redirect',
  PORT         = process.env.PORT || 3000,
  TRUSTED_LICENSE_KEY
} = process.env;

// Utility to broadcast SSE
// sessions now tracks accessToken, licenseMode, demoStart, demoDuration
const sessions = {};         // sessionId -> { accessToken, licenseMode, demoStart, demoDuration }
const clientsBySession = {}; // sessionId -> [res,...]
function broadcast(sessionId, event, data) {
  const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
  (clientsBySession[sessionId] || []).forEach(res => res.write(payload));
}

// Machine ID endpoint
app.get('/machine-id', (req, res) => {
  try {
    res.json({ machineId: machineIdSync() });
  } catch (err) {
    console.error('Machine ID error', err);
    res.status(500).json({ error: 'No se pudo obtener Machine ID' });
  }
});

// License validation: sets licenseMode and demo timer
app.post('/validate-license', (req, res) => {
  const { licenseKey, sessionId } = req.body;
  if (!licenseKey) {
    return res.json({ valid: false, msg: 'Debes enviar una clave.' });
  }
  if (!sessionId) {
    return res.status(400).json({ valid: false, msg: 'sessionId requerido.' });
  }
  try {
    const id = machineIdSync();
    // Clave secreta fija para el hashing HMAC
    const secret = TRUSTED_LICENSE_KEY || 'REDROOM_SECRET_KEY_2026';
    const expected = crypto.createHmac('sha256', secret)
                           .update(id)
                           .digest('hex');
    
    // Validar clave de licencia ingresada contra la calculada para este HWID
    const valid = (licenseKey.trim() === expected);
    const mode  = valid ? 'pro' : 'demo';

    // initialize or update session record
    sessions[sessionId] = sessions[sessionId] || {};
    sessions[sessionId].licenseMode  = mode;
    sessions[sessionId].licensed = valid;

    if (valid) {
      return res.json({ valid: true, mode: 'pro', msg: 'Licencia PRO activada correctamente.' });
    } else {
      return res.json({ valid: false, mode: 'demo', msg: 'Clave de licencia inválida para este equipo.' });
    }
  } catch (err) {
    console.error('Licencia error', err);
    res.status(500).json({ valid: false, msg: 'Error interno al validar licencia.' });
  }
});

// SSE endpoint
app.get('/events', (req, res) => {
  const sessionId = req.query.sessionId;
  if (!sessionId) return res.status(400).end();

  res.writeHead(200, {
    'Content-Type':  'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection:      'keep-alive'
  });
  clientsBySession[sessionId] = clientsBySession[sessionId] || [];
  clientsBySession[sessionId].push(res);
  req.on('close', () => {
    clientsBySession[sessionId] = clientsBySession[sessionId]
      .filter(r => r !== res);
  });
});

// MSAL setup
const pca = new msal.ConfidentialClientApplication({
  auth: {
    clientId: CLIENT_ID,
    authority: `https://login.microsoftonline.com/${TENANT_ID}`,
    clientSecret: CLIENT_SECRET
  }
});

// OAuth routes
app.get('/start-auth', async (req, res) => {
  const sessionId = req.query.sessionId;
  if (!sessionId) return res.status(400).json({ error: 'sessionId requerido' });
  try {
    const url = await pca.getAuthCodeUrl({
      scopes: ['User.Read','Mail.Read','Contacts.Read','offline_access'],
      redirectUri: REDIRECT_URI,
      state: sessionId
    });
    res.json({ url });
  } catch (err) {
    console.error('start-auth error', err);
    res.status(500).json({ error: err.message });
  }
});

app.get('/redirect', async (req, res) => {
  const sessionId = req.query.state;
  if (!sessionId) return res.status(400).send('state faltante');
  try {
    const tokenResponse = await pca.acquireTokenByCode({
      code: req.query.code,
      scopes: ['User.Read','Mail.Read','Contacts.Read','offline_access'],
      redirectUri: REDIRECT_URI
    });
    // ensure session record exists
    sessions[sessionId] = sessions[sessionId] || {};
    sessions[sessionId].accessToken = tokenResponse.accessToken;
    // if demo not yet started, initialize demo
    if (!sessions[sessionId].licenseMode) {
      sessions[sessionId].licenseMode   = 'pro';
      sessions[sessionId].demoDuration  = 999999999;
      sessions[sessionId].demoStart     = Date.now();
    }
    res.send('<h2>¡Autenticación completada!</h2><script>window.close()</script>');
  } catch (err) {
    console.error('redirect error', err);
    res.status(500).send('Falló la autenticación');
  }
});

app.get('/auth-status', (req, res) => {
  const sessionId = req.query.sessionId;
  const s = sessions[sessionId]||{};
  res.json({
    authenticated: !!s.accessToken,
    mode:          s.licenseMode || 'pro'
  });
});

// Start extraction with demo-time check
app.post('/start-extract', (req, res) => {
  const sessionId = req.query.sessionId;
  const s = sessions[sessionId]||{};
  const token = s.accessToken;

  if (!sessionId) return res.status(400).json({ error: 'sessionId requerido' });
  if (!token)     return res.status(401).json({ error: 'No autenticado' });

  // if in demo mode, enforce 30-minute limit
  if (s.licenseMode === 'demo') {
    // Límite del demo deshabilitado
  }

  res.json({ ok: true });
  extractAll(sessionId, token).catch(err => {
    console.error('Error general en extracción', err);
    broadcast(sessionId, 'progress', {
      current:0, total:0,
      msg: 'Error interno en extracción'
    });
  });
});

// Helper: recursively get all mail folders
async function getAllFolders(graph) {
  const all = [];
  async function recurse(parentId = null) {
    try {
      const resp = parentId
        ? await graph.api(`/me/mailFolders/${parentId}/childFolders`).get()
        : await graph.api('/me/mailFolders').get();
      for (const f of resp.value) {
        all.push(f);
        await recurse(f.id);
      }
    } catch (err) {
      console.warn(`Error al listar childFolders de ${parentId}`, err.statusCode||err.message);
    }
  }
  await recurse();
  return all;
}

// Main extraction flow
async function extractAll(sessionId, token) {
  const graph = Client.init({ authProvider: cb => cb(null, token) });
  const start = Date.now();

  // 1) Get folders
  broadcast(sessionId,'progress',{ current:0, total:0, msg:'Recuperando carpetas…' });
  let folders = [];
  try { folders = await getAllFolders(graph); }
  catch (err) { console.warn('getAllFolders falló:', err); }
  broadcast(sessionId,'progress',{ current:0, total:folders.length, msg:`Carpetas: ${folders.length}` });

  // 2) Count total messages
  let total = 0;
  for (const f of folders) {
    try {
      const cnt = await graph
        .api(`/me/mailFolders/${f.id}/messages?$count=true`)
        .header('ConsistencyLevel','eventual')
        .top(1)
        .get();
      total += cnt['@odata.count']||0;
    } catch (err) {
      console.warn(`No pude contar ${f.displayName}`, err.statusCode||err.message);
    }
  }
  broadcast(sessionId,'progress',{ current:0, total, msg:`Total mensajes: ${total}` });

  // 3) Extract messages sequentially
  let current = 0;
  const seenEmails = new Set();
  for (const folder of folders) {
    try {
      let resp = await graph
        .api(`/me/mailFolders/${folder.id}/messages`)
        .select('receivedDateTime,from,subject,toRecipients,ccRecipients,bccRecipients')
        .top(200)
        .get();
      do {
        for (const m of resp.value) {
          current++;
          
          const fromAddress = m.from?.emailAddress.address || '';
          const toAddresses = m.toRecipients?.map(r => r.emailAddress.address).filter(a => a) || [];
          const ccAddresses = m.ccRecipients?.map(r => r.emailAddress.address).filter(a => a) || [];
          const bccAddresses = m.bccRecipients?.map(r => r.emailAddress.address).filter(a => a) || [];
          
          // Filtrar correos destinatarios únicos que no hayamos visto
          const newTo = [];
          const newCc = [];
          const newBcc = [];
          
          if (fromAddress && !seenEmails.has(fromAddress.toLowerCase())) {
            seenEmails.add(fromAddress.toLowerCase());
          }
          
          for (const addr of toAddresses) {
            const lower = addr.toLowerCase();
            if (!seenEmails.has(lower)) {
              seenEmails.add(lower);
              newTo.push(addr);
            }
          }
          for (const addr of ccAddresses) {
            const lower = addr.toLowerCase();
            if (!seenEmails.has(lower)) {
              seenEmails.add(lower);
              newCc.push(addr);
            }
          }
          for (const addr of bccAddresses) {
            const lower = addr.toLowerCase();
            if (!seenEmails.has(lower)) {
              seenEmails.add(lower);
              newBcc.push(addr);
            }
          }
          
          // Solo enviar si aportó al menos un correo nuevo (emisor o destinatarios)
          if (fromAddress || newTo.length > 0 || newCc.length > 0 || newBcc.length > 0) {
            broadcast(sessionId, 'email', { 
              folder: folder.displayName,
              receivedDateTime: m.receivedDateTime,
              from: fromAddress,
              to:   newTo.join('; '),
              cc:   newCc.join('; '),
              bcc:  newBcc.join('; '),
              subject: m.subject||''
            });
          }
          broadcast(sessionId,'progress',{ current, total, msg:`Extrayendo ${current}/${total}` });
          if (current % 200 === 0) await new Promise(r=>setImmediate(r));
        }
        if (!resp['@odata.nextLink']) break;
        resp = await graph.api(resp['@odata.nextLink']).get();
      } while (true);
    } catch (err) {
      console.warn(`Saltando ${folder.displayName} por error`, err.statusCode||err.message);
      broadcast(sessionId,'progress',{ current, total, msg:`Saltando ${folder.displayName}` });
    }
  }

  // 4) Extract contacts
  let contacts = [];
  try {
    let cResp = await graph.api('/me/contacts').select('displayName,emailAddresses').top(100).get();
    contacts.push(...cResp.value);
    while (cResp['@odata.nextLink']) {
      cResp = await graph.api(cResp['@odata.nextLink']).get();
      contacts.push(...cResp.value);
    }
  } catch (err) {
    console.warn('Error recuperando contactos', err.statusCode||err.message);
  }
  contacts.forEach(c =>
    broadcast(sessionId,'contact',{ displayName: c.displayName||'', email: c.emailAddresses?.[0]?.address||'' })
  );

  // 5) Done
  const elapsed = Math.round((Date.now() - start)/1000);
  broadcast(sessionId,'done',{ executionTime: elapsed });
  broadcast(sessionId,'progress',{ current: total, total, msg:'Extracción completada' });
}

if (require.main === module) {
  app.listen(PORT, ()=> console.log(`Servidor listo en http://localhost:${PORT}`));
} else {
  module.exports = app;
}
