// PRE Web GUI — Express + WebSocket server

const express = require('express');
const http = require('http');
const { WebSocketServer } = require('ws');
const path = require('path');
const os = require('os');
const fs = require('fs');

// ── Core modules (used directly by WebSocket handler, cron loop, or startup) ──
const { routeAndRespond } = require('./src/tools');
const routerConfig = require('./src/router');
const bgAgents = require('./src/background-agents');
const {
  listSessions, getSession, appendMessage, createSession,
} = require('./src/sessions');
const { MODEL_CTX, ARTIFACTS_DIR } = require('./src/constants');
const cronSystem = require('./src/tools/cron');
const { executeCronJob, checkMissedJobs } = require('./src/cron-runner');
const { runDeepResearch } = require('./src/research');
const delegate = require('./src/tools/delegate');
const mcp = require('./src/mcp');
const triggerSystem = require('./src/triggers');
const telegramReceiver = require('./src/telegram-receiver');
const argusSystem = require('./src/argus');
const telemetry = require('./src/telemetry');
const { gpuDetector } = require('./src/gpu-detect');
const tinygradServer = require('./src/tinygrad');
const tinygradTrain = require('./src/tinygrad-train');
const escalation = require('./src/escalation');

// ── Route modules ──
const sessionRoutes = require('./src/routes/sessions');
const providerRoutes = require('./src/routes/providers');
const intelligenceRoutes = require('./src/routes/intelligence');
const { apiRouter: connectionRoutes, oauthRouter } = require('./src/routes/connections');
const systemRoutes = require('./src/routes/system');
const dashboardRoutes = require('./src/routes/dashboard');
const memoryRoutes = require('./src/routes/memory');
const createCronRoutes = require('./src/routes/cron');
const triggerRoutes = require('./src/routes/triggers');
const mediaRoutes = require('./src/routes/media');
const liveDataRoutes = require('./src/routes/live-data');
const createIntegrationRoutes = require('./src/routes/integrations');
const { apiRouter: mcpApiRoutes, mcpTransportRouter } = require('./src/routes/mcp-api');

const PORT = parseInt(process.env.PRE_WEB_PORT || '7749', 10);
const CWD = process.env.PRE_CWD || os.homedir();

const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server, maxPayload: 50 * 1024 * 1024 }); // 50MB for image uploads

// Serve static files (no caching in dev to avoid stale JS/CSS)
app.use(express.static(path.join(__dirname, 'public'), {
  etag: false,
  maxAge: 0,
  setHeaders: (res) => {
    res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
  },
}));
app.use(express.json({
  limit: '10mb',
  // Capture raw body for webhook HMAC verification
  verify: (req, _res, buf) => {
    if (req.url && req.url.startsWith('/api/triggers/webhook/')) {
      req.rawBody = buf;
    }
  },
}));

// Serve artifacts — inject base stylesheet into HTML files for consistent readability
app.use('/artifacts', (req, res, next) => {
  const filePath = path.join(ARTIFACTS_DIR, decodeURIComponent(req.path));
  const ext = path.extname(filePath).toLowerCase();

  // Force download for document types
  if (['.docx', '.xlsx', '.pdf', '.txt', '.xml'].includes(ext)) {
    res.set('Content-Disposition', `attachment; filename="${path.basename(filePath)}"`);
    return res.sendFile(filePath);
  }

  // Inject light-theme base stylesheet into HTML artifacts
  if (ext === '.html' || ext === '.htm') {
    try {
      let html = fs.readFileSync(filePath, 'utf-8');
      const baseCSS = `<style id="pre-artifact-base">
  /* PRE artifact base — enforce readable light theme */
  :root {
    color-scheme: light only !important;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --bg: #fafafa !important;
      --fg: #1a1a2e !important;
      --text: #1a1a2e !important;
      --muted: #555 !important;
      --border: #e2e8f0 !important;
      --surface: #ffffff !important;
      --callout-bg: #f8fafc !important;
      --table-stripe: #f8fafc !important;
      --table-header: #334155 !important;
      --code-bg: #f4f4f8 !important;
      --shadow: rgba(0,0,0,0.06) !important;
    }
    body {
      background: #fafafa !important;
      color: #1a1a2e !important;
    }
    table, th, td { color: inherit !important; }
    thead, thead th { background: #334155 !important; color: #fff !important; }
    tbody tr:nth-child(even) { background: #f8fafc !important; }
    tbody tr:hover { background: #eef2ff !important; }
    .callout, blockquote, .toc, nav { background: #f8fafc !important; color: inherit !important; }
    pre, code { background: #f4f4f8 !important; color: #1a1a2e !important; }
    a { color: #2563eb !important; }
    h1, h2, h3, h4, h5, h6 { color: #111827 !important; }
    .footnotes, .meta, .subtitle { color: #555 !important; }
  }
</style>`;
      // Inject right after <head> or at the start of the document
      if (html.includes('</head>')) {
        html = html.replace('</head>', baseCSS + '\n</head>');
      } else if (html.includes('<html')) {
        html = html.replace(/<html[^>]*>/, '$&\n<head>' + baseCSS + '</head>');
      } else {
        html = baseCSS + '\n' + html;
      }
      res.type('html').send(html);
    } catch (err) {
      if (err.code === 'ENOENT') return res.status(404).send('Not found');
      return next(err);
    }
    return;
  }

  // All other files — static serving
  next();
}, express.static(ARTIFACTS_DIR));

// ── REST API (route modules) ──

// broadcastWS is defined below — forward-declare for route factories
let broadcastWS;

app.use('/api', sessionRoutes);
app.use('/api', providerRoutes);
app.use('/api', intelligenceRoutes);
app.use('/api', connectionRoutes);
app.use('/oauth', oauthRouter);
app.use('/api', systemRoutes);
app.use('/api', dashboardRoutes);
app.use('/api', memoryRoutes);
app.use('/api', createCronRoutes((...args) => broadcastWS(...args)));
app.use('/api', triggerRoutes);
app.use('/api', mediaRoutes);
app.use('/api', liveDataRoutes);
app.use('/api', createIntegrationRoutes((...args) => broadcastWS(...args)));
app.use('/api', mcpApiRoutes);
app.use('/', mcpTransportRouter);

// Broadcast a WS event to all connected clients
broadcastWS = (event) => {
  const msg = JSON.stringify(event);
  wss.clients.forEach((client) => {
    if (client.readyState === 1) client.send(msg);
  });
};


// ── WebSocket handler ──

wss.on('connection', (ws) => {
  let sessionId = 'web:general';
  let abortController = null;
  // Pending confirmation requests
  const pendingConfirms = new Map();

  // Ensure session exists
  createSession('web', 'general');

  ws.on('message', async (raw) => {
    let msg;
    try { msg = JSON.parse(raw.toString()); } catch { return; }

    if (msg.type === 'switch_session') {
      sessionId = msg.sessionId || 'web:general';
      const history = getSession(sessionId);
      ws.send(JSON.stringify({ type: 'session_history', messages: history, sessionId }));
      return;
    }

    if (msg.type === 'cancel') {
      console.log(`[ws] Cancel requested — abortController ${abortController ? 'exists, aborting' : 'is null (nothing to abort)'}`);
      if (abortController) abortController.abort();
      return;
    }

    // Handle /tier command — per-session tier pinning
    if (msg.type === 'tier') {
      const validTiers = ['fast', 'standard', 'frontier', 'auto'];
      const requested = (msg.tier || '').toLowerCase().trim();
      if (!validTiers.includes(requested)) {
        ws.send(JSON.stringify({ type: 'tier_result', success: false, message: `Invalid tier "${requested}". Valid: ${validTiers.join(', ')}` }));
      } else if (requested === 'auto') {
        // Remove pin — let the router classify normally
        routerConfig.saveConfig({ ...routerConfig.getConfig(), _sessionPin: null });
        ws.send(JSON.stringify({ type: 'tier_result', success: true, tier: 'auto', message: 'Router will classify messages automatically.' }));
        console.log(`[tier] Session pin cleared — auto routing`);
      } else {
        // Pin to a specific tier for this session
        routerConfig.saveConfig({ ...routerConfig.getConfig(), _sessionPin: requested });
        ws.send(JSON.stringify({ type: 'tier_result', success: true, tier: requested, message: `All messages will route to ${requested} tier until you send /tier auto.` }));
        console.log(`[tier] Session pinned to ${requested}`);
      }
      return;
    }

    // Handle tool confirmation responses
    if (msg.type === 'confirm') {
      const resolver = pendingConfirms.get(msg.id);
      if (resolver) {
        resolver(msg.approved);
        pendingConfirms.delete(msg.id);
      }
      return;
    }

    // ── Delegate to frontier AI ──
    if (msg.type === 'delegate') {
      const target = msg.target; // 'claude', 'codex', 'gemini'
      const content = msg.content;
      if (!target || !content?.trim()) return;

      console.log(`[ws] Delegating to ${target}: ${content.slice(0, 80)}...`);

      // Save user message to session
      appendMessage(sessionId, { role: 'user', content, delegate: target });
      ws.send(JSON.stringify({ type: 'delegate_start', target }));

      try {
        const result = await delegate.execute(target, content, {
          signal: abortController?.signal,
          onToken: (chunk) => {
            if (ws.readyState === 1) {
              ws.send(JSON.stringify({ type: 'delegate_token', target, content: chunk }));
            }
          },
        });

        // Save assistant response to session
        appendMessage(sessionId, { role: 'assistant', content: result.response, delegate: target });
        if (ws.readyState === 1) {
          ws.send(JSON.stringify({
            type: 'delegate_done',
            target,
            response: result.response,
            duration: result.duration,
          }));
        }
      } catch (err) {
        if (ws.readyState === 1) {
          ws.send(JSON.stringify({ type: 'delegate_error', target, message: err.message }));
        }
      }
      return;
    }

    if (msg.type === 'message') {
      console.log(`[ws] Received message: ${msg.content?.slice(0, 80)}...`);
      let userContent = msg.content;
      if (!userContent || !userContent.trim()) return;

      // Per-message escalation: either the UI toggle (msg.escalate) or a leading
      // /escalate prefix in the content. Strip the prefix before persisting so
      // history stays clean.
      let escalateFlag = !!msg.escalate;
      const escMatch = userContent.match(/^\/escalate\b\s*([\s\S]*)$/);
      if (escMatch) {
        escalateFlag = true;
        userContent = escMatch[1].trim();
      }

      // Check if this session needs an auto-generated title
      const history = getSession(sessionId);
      const userMsgCount = history.filter(m => m.role === 'user').length;
      const sessions = listSessions();
      const sessionMeta = sessions.find(s => s.id === sessionId);
      // Generate a title if the session has no display name yet — not just on the first message.
      // Previous logic (userMsgCount === 0) meant sessions that hit max tool turns or got
      // aborted on the first message would never get titled.
      const needsTitle = !sessionMeta?.displayName;
      console.log(`[ws] needsTitle=${needsTitle} userMsgCount=${userMsgCount} displayName=${sessionMeta?.displayName}`);

      // Save user message to session
      const userMsg = { role: 'user', content: userContent };
      if (escalateFlag) userMsg.escalated = true;
      // Support single image (legacy) or multiple images array
      if (msg.images && msg.images.length > 0) {
        userMsg.images = msg.images;
      } else if (msg.image) {
        userMsg.images = [msg.image];
      }
      // Auto-save uploaded images to artifacts so they can be referenced in HTML artifacts.
      // Stores paths in userMsg.imagePaths for the model to use.
      if (userMsg.images && userMsg.images.length > 0) {
        const uploadsDir = path.join(ARTIFACTS_DIR, 'uploads');
        if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
        userMsg.imagePaths = [];
        for (let i = 0; i < userMsg.images.length; i++) {
          try {
            const buf = Buffer.from(userMsg.images[i], 'base64');
            // Detect format from magic bytes
            const ext = (buf[0] === 0x89 && buf[1] === 0x50) ? 'png'
              : (buf[0] === 0xFF && buf[1] === 0xD8) ? 'jpg'
              : (buf[0] === 0x47 && buf[1] === 0x49) ? 'gif' : 'png';
            const fname = `upload-${Date.now()}-${i}.${ext}`;
            fs.writeFileSync(path.join(uploadsDir, fname), buf);
            userMsg.imagePaths.push(`/artifacts/uploads/${fname}`);
          } catch {}
        }
        if (userMsg.imagePaths.length > 0) {
          // Append path info to message so the model knows where to find the images
          userMsg.content += `\n[Uploaded image(s) saved to: ${userMsg.imagePaths.join(', ')}. Use these paths in HTML artifacts with <img src="..."> to embed them.]`;
        }
      }
      appendMessage(sessionId, userMsg);

      // Helper to send WS events safely
      const wsSend = (event) => {
        if (ws.readyState === 1) ws.send(JSON.stringify(event));
      };

      const localAbortController = new AbortController();
      abortController = localAbortController;

      // ── Deep Research mode ──
      if (msg.researchMode) {
        const frontier = msg.useFrontier || false;
        console.log(`[ws] Deep Research mode for: ${userContent.slice(0, 80)}... (frontier: ${frontier || 'off'})`);
        try {
          await runDeepResearch({
            sessionId,
            query: userContent,
            cwd: CWD,
            send: wsSend,
            signal: localAbortController.signal,
            useFrontier: frontier,
          });
        } catch (err) {
          if (err.message !== 'Research cancelled' && ws.readyState === 1) {
            ws.send(JSON.stringify({ type: 'error', message: err.message }));
          }
          wsSend({ type: 'done', stats: {}, context: { used: 0, max: 0, pct: 0 } });
        }
        // Only clear if this handler's controller is still the active one
        if (abortController === localAbortController) abortController = null;
        return;
      }

      // ── Normal chat — routed through multi-model router ──
      try {
        await routeAndRespond({
          sessionId,
          cwd: CWD,
          signal: localAbortController.signal,
          userMessage: userContent,
          needsTitle,
          send: wsSend,
          _escalate: escalateFlag,
          onConfirmRequest: (id, toolName, toolArgs) => {
            return new Promise((resolve) => {
              pendingConfirms.set(id, resolve);
              if (ws.readyState === 1) {
                ws.send(JSON.stringify({
                  type: 'confirm_request',
                  id,
                  tool: toolName,
                  args: toolArgs,
                }));
              }
              // Auto-timeout after 60s
              setTimeout(() => {
                if (pendingConfirms.has(id)) {
                  pendingConfirms.delete(id);
                  resolve(false);
                }
              }, 60000);
            });
          },
        });
      } catch (err) {
        console.log(`[ws] routeAndRespond error: "${err.message}" (ws=${ws.readyState === 1 ? 'open' : 'closed'})`);
        if (ws.readyState === 1) {
          if (err.message === 'Request aborted') {
            // User cancelled — send done so frontend resets streaming state
            ws.send(JSON.stringify({ type: 'done' }));
          } else {
            ws.send(JSON.stringify({ type: 'error', message: err.message }));
          }
        }
      }
      // Only clear if this handler's controller is still the active one
      if (abortController === localAbortController) abortController = null;
    }
  });
});

// ── Graceful shutdown ──

function shutdown() {
  console.log('\n  PRE Web GUI shutting down...');
  bgAgents.shutdown();
  triggerSystem.shutdown();
  telegramReceiver.shutdown();
  telemetry.shutdown();
  gpuDetector.stop();
  escalation.stopProbing();
  tinygradServer.shutdown();
  tinygradTrain.shutdown();
  wss.clients.forEach(ws => ws.close());
  server.close();
  process.exit(0);
}

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

// ── Cron background loop ──
// Check every 30 seconds. Jobs fire at most once per minute.

let lastCronMinute = -1;

setInterval(() => {
  const now = new Date();
  const currentMinute = now.getHours() * 60 + now.getMinutes();
  // Only check once per minute
  if (currentMinute === lastCronMinute) return;
  lastCronMinute = currentMinute;

  const jobs = cronSystem.loadJobs();
  let changed = false;

  for (const job of jobs) {
    if (!job.enabled) continue;
    if (!cronSystem.matchesNow(job.schedule)) continue;

    // Prevent double-fire within the same minute
    if (job.last_run_at) {
      const lastRun = new Date(job.last_run_at);
      if (lastRun.getHours() === now.getHours() && lastRun.getMinutes() === now.getMinutes()
          && lastRun.getDate() === now.getDate()) continue;
    }

    console.log(`[cron] Firing job ${job.id}: "${job.description}"`);
    job.last_run_at = Date.now();
    job.run_count = (job.run_count || 0) + 1;
    changed = true;

    // Execute server-side — runs headlessly, stores in own session, sends notifications
    executeCronJob(job, { broadcastWS }).catch(err => {
      console.error(`[cron] Execution error for job ${job.id}: ${err.message}`);
    });
  }

  if (changed) cronSystem.saveJobs(jobs);
}, 30000);

// ── Start ──

server.listen(PORT, async () => {
  const jobs = cronSystem.loadJobs();
  const enabledCount = jobs.filter(j => j.enabled).length;
  console.log(`\n  PRE Web GUI running at http://localhost:${PORT}`);
  console.log(`  Working directory: ${CWD}`);
  console.log(`  Model context: ${MODEL_CTX} tokens`);
  console.log(`  Cron jobs: ${enabledCount} active / ${jobs.length} total`);

  // Initialize event-driven triggers (file watchers, webhooks)
  const triggerStats = triggerSystem.init(async (job) => {
    return executeCronJob(job, { broadcastWS });
  });
  const triggers = triggerSystem.loadTriggers();
  if (triggers.length > 0) {
    console.log(`  Triggers: ${triggerStats.watcherCount} active / ${triggers.length} total`);
  }

  // Initialize background agents: broadcast, state recovery, sleep detection
  bgAgents.setBroadcast(broadcastWS);
  const bgRecovery = bgAgents.init();
  if (bgRecovery.recovered > 0) {
    console.log(`  Background agents: ${bgRecovery.recovered} interrupted agent(s) recovered`);
  }

  // Rotate telemetry log if oversized or stale
  telemetry.rotate();

  // Start GPU hot-plug detection and tinygrad auto-start
  gpuDetector.start();
  gpuDetector.on('gpu-available', (state) => broadcastWS({ type: 'gpu_status', ...state }));
  gpuDetector.on('gpu-unavailable', (state) => broadcastWS({ type: 'gpu_status', ...state }));
  gpuDetector.on('gpu-changed', (state) => broadcastWS({ type: 'gpu_status', ...state }));
  tinygradServer.enableAutoStart(() => require('./src/connections').getProvider());
  tinygradTrain.init();
  tinygradTrain.setBroadcast(broadcastWS);
  const gpuState = gpuDetector.getState();
  if (gpuState?.available) {
    console.log(`  GPU: ${gpuState.name} (${gpuState.vram_gb}GB VRAM, ${gpuState.backend})`);
  }

  // Escalation availability: probe runtime + model, broadcast on change
  escalation.events.on('status', (s) => broadcastWS({ type: 'escalation_status', ...s }));
  escalation.startProbing(30000);
  escalation.probe().then((s) => {
    if (s.available) console.log(`  Escalation: ${s.label} via ${s.runtime}`);
    else if (s.runtime) console.log(`  Escalation: not ready — ${s.reason}`);
  }).catch(() => {});

  // Initialize Argus companion
  argusSystem.init(broadcastWS);
  const buddyCfg = argusSystem.getConfig();
  console.log(`  Argus: ${buddyCfg.enabled ? 'enabled' : 'disabled'} (${buddyCfg.name})`);

  console.log(`  MCP server: HTTP at /mcp | stdio via mcp-stdio.js`);

  // Initialize Telegram background receiver (incoming message polling)
  // Telegram receiver now maintains its own persistent sessions per chat
  // and calls runToolLoop directly — no longer routes through executeCronJob.
  telegramReceiver.init(broadcastWS).then(tg => {
    if (tg.active) {
      console.log(`  Telegram: receiver active (@${tg.username})`);
    }
  }).catch(err => {
    console.error(`  Telegram: receiver failed — ${err.message}`);
  });

  // Check for cron jobs that were missed while the system was down
  if (enabledCount > 0) {
    checkMissedJobs({ broadcastWS }).then(fired => {
      if (fired) console.log('  [cron] Missed jobs queued for execution');
    }).catch(err => {
      console.error(`  [cron] Missed job check failed: ${err.message}`);
    });
  }

  // Auto-connect MCP servers
  const mcpConfig = mcp.loadConfig();
  const mcpCount = Object.keys(mcpConfig.servers || {}).length;
  if (mcpCount > 0) {
    const results = await mcp.connectAll();
    const connected = Object.values(results).filter(r => r.connected).length;
    const totalTools = Object.values(results).reduce((sum, r) => sum + (r.tools || 0), 0);
    console.log(`  MCP servers: ${connected}/${mcpCount} connected (${totalTools} tools)`);
  }
  console.log('');
});
