import express from 'express';
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
import { join, dirname, basename } from 'path';
import { fileURLToPath } from 'url';
import { readFile, writeFile, stat } from 'fs/promises';
import { EventEmitter } from 'events';
import { readFileSync, writeFileSync } from 'fs';
import {
  setBroadcast, addEvent, getEvents, getAllMemories, getSkillsFromDb, toggleSkill,
  deleteEventsUpTo, getChatCount, deleteMemory, getDailyUsage, getDailyTotalCost, getTasks, markProcessed,
  // v3.0 — KG
  addEntity, getEntity, findEntities, getEntitiesByType, getRelations, getOutgoingRelations, addRelation, getEntityCount, getRelationCount,
  // v3.0 — Workspaces
  addWorkspace, getWorkspace, listWorkspacesFromDb, setActiveWorkspace, getActiveWorkspace, deleteWorkspaceFromDb,
  // v3.0 — Ingestion
  addIngestionItem, getIngestionQueue,
  // v3.0 — Cognitive
  getCognitiveLogs
} from './db.mjs';
import { getConfig, updateConfig, getAgentRoot } from './config.mjs';
import { getWorkspaceRoot, listWorkspaceFiles, writeWorkspaceBuffer } from './workspace.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));

let wss;
const clients = new Set();
export const serverEvents = new EventEmitter();
let runtimeApi = {
  getTaskSnapshot: () => null,
  cancelTask: () => false,
  retryTask: () => null,
  setLoopEnabled: () => false,
  getLoopEnabled: () => true,
  getAgentState: () => ({ conversation_active: false, autonomy_paused: false })
};

export function setRuntimeApi(handlers = {}) {
  runtimeApi = { ...runtimeApi, ...handlers };
}

export function startServer() {
  const app = express();
  app.use(express.json({ limit: '10mb' }));

  // Serve index.html (v4 UI) with auth token injected
  app.get('/', async (req, res) => {
    try {
      const html = await readFile(join(__dirname, 'public', 'index.html'), 'utf-8');
      const { authToken } = getConfig();
      const tokenMeta = authToken ? `<meta name="auth-token" content="${authToken}">` : '';
      res.type('html').send(html.replace('</head>', `${tokenMeta}\n</head>`));
    } catch (err) {
      res.status(500).send('Could not load UI');
    }
  });

  app.use(express.static(join(__dirname, 'public')));
  app.use('/lib', express.static(join(__dirname, 'node_modules/marked/lib')));

  // Auth middleware for API routes
  app.use('/api', (req, res, next) => {
    const { authToken } = getConfig();
    if (!authToken) return next();
    const bearer = req.headers.authorization?.replace('Bearer ', '');
    const queryToken = req.query.token;
    if (bearer === authToken || queryToken === authToken) return next();
    res.status(401).json({ error: 'Unauthorized' });
  });

  // Serve agent workspace files
  app.use('/inspiration', express.static(join(__dirname, 'inspiration')));
  app.get('/workspace-files/:name', (req, res) => {
    const safeName = basename(req.params.name || '');
    if (!safeName) {
      return res.status(400).send('Invalid file name');
    }
    const root = getWorkspaceRoot();
    res.sendFile(join(root, safeName), (err) => {
      if (err) {
        if (!res.headersSent) res.status(404).send('File not found');
      }
    });
  });

  // Events timeline
  app.get('/api/events', (req, res) => {
    const { type, limit = 50, offset = 0, since } = req.query;
    const safeLimit = Math.max(1, Math.min(500, parseInt(limit, 10) || 50));
    const safeOffset = Math.max(0, parseInt(offset, 10) || 0);
    const events = getEvents({
      type: type || undefined,
      limit: safeLimit,
      offset: safeOffset,
      since: since || undefined
    });
    res.json(events);
  });

  // Memories
  app.get('/api/memories', (req, res) => {
    const { category } = req.query;
    res.json(getAllMemories(category || undefined));
  });

  // Skills
  app.get('/api/skills', (req, res) => {
    res.json(getSkillsFromDb());
  });

  app.post('/api/skills/:name/toggle', (req, res) => {
    const { enabled } = req.body;
    toggleSkill(req.params.name, enabled);
    res.json({ ok: true });
  });

  // Chat — user sends a message
  app.post('/api/chat', (req, res) => {
    const { message } = req.body;
    if (!message || !message.trim()) {
      return res.status(400).json({ error: 'Message required' });
    }
    const event = addEvent('chat', 'user', message.trim());
    res.json(event);
    // Notify agent to process immediately
    serverEvents.emit('new-chat');
  });

  // Clear chat messages up to and including a given event ID
  app.post('/api/chat/clear', (req, res) => {
    const { upToId } = req.body;
    if (!upToId) return res.status(400).json({ error: 'upToId required' });
    const deleted = deleteEventsUpTo(upToId);
    // Broadcast so all clients refresh
    broadcastToClients({ type: 'chat_cleared', data: { upToId, deleted } });
    res.json({ ok: true, deleted });
  });

  // Get chat message count
  app.get('/api/chat/count', (req, res) => {
    res.json({ count: getChatCount() });
  });

  // File upload (always to workspace)
  app.post('/api/upload', async (req, res) => {
    try {
      const { filename, content, encoding } = req.body;
      if (!filename || !content) {
        return res.status(400).json({ error: 'filename and content required' });
      }
      const buf = encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf-8');
      const written = await writeWorkspaceBuffer(filename, buf);
      const event = addEvent('file', 'user', `Uploaded file: ${written.name}`, {
        path: written.publicPath,
        size: buf.length,
        destination: 'workspace'
      });
      res.json(event);
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // List workspace files
  app.get('/api/workspace-files', async (req, res) => {
    try {
      const files = await listWorkspaceFiles();
      res.json(files);
    } catch {
      res.json([]);
    }
  });

  app.get('/api/journal', async (req, res) => {
    try {
      const content = await readFile(join(__dirname, 'journal.md'), 'utf-8');
      res.json({ content, exists: true });
    } catch {
      res.json({ content: '', exists: false });
    }
  });

  // Heartbeat status + meta mode
  const metaFlagPath = join(__dirname, 'workspace', '.meta-mode');

  app.get('/api/status', async (req, res) => {
    try {
      const hbPath = join(__dirname, '.heartbeat');
      const hbStat = await stat(hbPath);
      const ageSeconds = (Date.now() - hbStat.mtimeMs) / 1000;
      let metaMode = false;
      try {
        const flag = await readFile(metaFlagPath, 'utf-8');
        metaMode = flag.trim() === 'enabled';
      } catch {}
      const config = getConfig();
      const queue = runtimeApi.getTaskSnapshot ? runtimeApi.getTaskSnapshot() : null;
      const agentState = runtimeApi.getAgentState ? runtimeApi.getAgentState() : {};
      const loopEnabled = runtimeApi.getLoopEnabled ? !!runtimeApi.getLoopEnabled() : true;
      res.json({
        alive: ageSeconds < 600,
        heartbeat_age: Math.round(ageSeconds),
        meta_mode: metaMode,
        agentName: config.agentName,
        conversation_active: !!agentState.conversation_active,
        autonomy_paused: !!agentState.autonomy_paused,
        loop_enabled: loopEnabled,
        queue_depths: queue
      });
    } catch {
      res.json({ alive: false, heartbeat_age: null, meta_mode: false });
    }
  });

  app.get('/api/meta-mode', async (req, res) => {
    try {
      const flag = await readFile(metaFlagPath, 'utf-8');
      res.json({ enabled: flag.trim() === 'enabled' });
    } catch {
      res.json({ enabled: false });
    }
  });

  // Config endpoints
  app.get('/api/config', (req, res) => {
    const config = getConfig();
    const { authToken, ...safe } = config;
    res.json(safe);
  });

  app.put('/api/config', (req, res) => {
    try {
      const updates = req.body;
      const allowed = ['cli', 'port', 'loopMinutes', 'compactThreshold', 'compactKeepRecent', 'agentName', 'models', 'dailyBudget', 'workspaceRoot', 'maxUserTaskConcurrency', 'autonomyActiveChatPauseMs', 'desktopHotkey',
        /* v3.0 */ 'activeWorkspace', 'workspaces', 'ingestion', 'cognitive', 'multimodal', 'kgEnabled',
        /* v3.1 */ 'voice',
        /* v4.0 */ 'persona', 'vector'];
      const filtered = {};
      for (const key of allowed) {
        if (key in updates) filtered[key] = updates[key];
      }
      const newConfig = updateConfig(filtered);
      const { authToken, ...safe } = newConfig;
      res.json({ ok: true, config: safe, restartRequired: false });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // Delete a memory
  app.delete('/api/memories/:key', (req, res) => {
    deleteMemory(decodeURIComponent(req.params.key));
    res.json({ ok: true });
  });

  // Usage tracking
  app.get('/api/usage', (req, res) => {
    const { date } = req.query;
    const usage = getDailyUsage(date);
    const total = getDailyTotalCost(date);
    const { dailyBudget } = getConfig();
    res.json({ usage, total, budget: dailyBudget });
  });

  app.get('/api/tasks', (req, res) => {
    const limit = Math.max(1, Math.min(200, parseInt(req.query.limit || '100', 10) || 100));
    res.json(getTasks({ limit }));
  });

  app.post('/api/tasks/:id/cancel', (req, res) => {
    const taskId = parseInt(req.params.id, 10);
    if (!taskId) return res.status(400).json({ error: 'invalid task id' });
    const ok = runtimeApi.cancelTask ? runtimeApi.cancelTask(taskId) : false;
    if (!ok) return res.status(404).json({ error: 'task not cancelable' });
    res.json({ ok: true });
  });

  app.post('/api/tasks/:id/retry', (req, res) => {
    const taskId = parseInt(req.params.id, 10);
    if (!taskId) return res.status(400).json({ error: 'invalid task id' });
    const newTaskId = runtimeApi.retryTask ? runtimeApi.retryTask(taskId) : null;
    if (!newTaskId) return res.status(404).json({ error: 'task not retriable' });
    res.json({ ok: true, taskId: newTaskId });
  });

  app.get('/api/loop', (req, res) => {
    const enabled = runtimeApi.getLoopEnabled ? !!runtimeApi.getLoopEnabled() : true;
    res.json({ enabled });
  });

  app.post('/api/loop', (req, res) => {
    if (!runtimeApi.setLoopEnabled) {
      return res.status(503).json({ error: 'Loop control unavailable' });
    }
    const enabled = !!req.body?.enabled;
    const updated = runtimeApi.setLoopEnabled(enabled);
    res.json({ ok: true, enabled: !!updated });
  });

  app.post('/api/loop/pause', (req, res) => {
    if (!runtimeApi.setLoopEnabled) {
      return res.status(503).json({ error: 'Loop control unavailable' });
    }
    const updated = runtimeApi.setLoopEnabled(false);
    res.json({ ok: true, enabled: !!updated });
  });

  app.post('/api/loop/resume', (req, res) => {
    if (!runtimeApi.setLoopEnabled) {
      return res.status(503).json({ error: 'Loop control unavailable' });
    }
    const updated = runtimeApi.setLoopEnabled(true);
    res.json({ ok: true, enabled: !!updated });
  });

  // ── v3.0 Workspace endpoints ─────────────────────────────────

  app.get('/api/workspaces', async (req, res) => {
    try {
      const workspaces = listWorkspacesFromDb();
      // Enrich each workspace with a live file count from the filesystem
      const enriched = await Promise.all(workspaces.map(async (ws) => {
        try {
          const files = await listWorkspaceFiles(ws.id);
          return { ...ws, fileCount: files.length };
        } catch {
          return { ...ws, fileCount: 0 };
        }
      }));
      res.json(enriched);
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.post('/api/workspaces', (req, res) => {
    try {
      const { name, displayName, rootPath, description } = req.body;
      if (!name) return res.status(400).json({ error: 'name required' });
      addWorkspace(name, displayName || name, rootPath || '', description || '');
      res.json({ ok: true, workspace: getWorkspace(name) });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.put('/api/workspaces/:id/activate', (req, res) => {
    try {
      const id = req.params.id;
      const ws = getWorkspace(id);
      if (!ws) return res.status(404).json({ error: 'workspace not found' });
      setActiveWorkspace(id);
      updateConfig({ activeWorkspace: id });
      res.json({ ok: true, active: id });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.delete('/api/workspaces/:id', (req, res) => {
    try {
      const id = req.params.id;
      if (id === 'default') return res.status(400).json({ error: 'cannot delete default workspace' });
      deleteWorkspaceFromDb(id);
      res.json({ ok: true });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // ── v3.0 Knowledge Graph endpoints ─────────────────────────

  app.get('/api/kg/entities', (req, res) => {
    try {
      const { q, type, workspace, limit } = req.query;
      const safeLimit = Math.max(1, Math.min(500, parseInt(limit, 10) || 50));
      if (q) {
        res.json(findEntities(q, { workspaceId: workspace, type, limit: safeLimit }));
      } else if (type) {
        res.json(getEntitiesByType(type, workspace));
      } else {
        // Return all entities up to limit
        res.json(findEntities('%', { workspaceId: workspace, limit: safeLimit }));
      }
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.get('/api/kg/entities/:id/relations', (req, res) => {
    try {
      const id = parseInt(req.params.id, 10);
      if (!id) return res.status(400).json({ error: 'invalid entity id' });
      res.json(getRelations(id));
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.post('/api/kg/entities', (req, res) => {
    try {
      const { name, type, workspace, metadata } = req.body;
      if (!name || !type) return res.status(400).json({ error: 'name and type required' });
      const id = addEntity(name, type, workspace || 'default', metadata);
      res.json({ ok: true, id, entity: getEntity(id) });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.get('/api/kg/stats', (req, res) => {
    try {
      res.json({
        entityCount: getEntityCount(),
        relationCount: getRelationCount()
      });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // ── v3.0 Ingestion endpoints ───────────────────────────────

  app.post('/api/ingest', (req, res) => {
    try {
      const { sourceType, sourcePath, workspaceId } = req.body;
      if (!sourceType || !sourcePath) return res.status(400).json({ error: 'sourceType and sourcePath required' });
      const id = addIngestionItem(sourceType, sourcePath, workspaceId || 'default');
      res.json({ ok: true, id });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.get('/api/ingest/queue', (req, res) => {
    try {
      const { state } = req.query;
      res.json(getIngestionQueue(state));
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // ── v3.0 Tools Registry endpoints ──────────────────────────

  app.get('/api/tools', (req, res) => {
    try {
      const toolsPath = join(getAgentRoot(), 'tools.md');
      const content = readFileSync(toolsPath, 'utf-8');
      res.json({ content });
    } catch (err) {
      res.json({ content: '', error: err.message });
    }
  });

  app.put('/api/tools', (req, res) => {
    try {
      const { content } = req.body;
      if (typeof content !== 'string') return res.status(400).json({ error: 'content required' });
      const toolsPath = join(getAgentRoot(), 'tools.md');
      writeFileSync(toolsPath, content, 'utf-8');
      res.json({ ok: true });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // ── v3.0 Cognitive Log endpoint ────────────────────────────

  app.get('/api/cognitive/logs', (req, res) => {
    try {
      const { module: mod, limit } = req.query;
      const safeLimit = Math.max(1, Math.min(200, parseInt(limit, 10) || 50));
      res.json(getCognitiveLogs(mod, safeLimit));
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // ── v3.1 Voice endpoints ────────────────────────────────────

  app.get('/api/voice/status', (req, res) => {
    const state = runtimeApi.getVoiceState ? runtimeApi.getVoiceState() : { active: false, state: 'idle' };
    res.json(state);
  });

  app.post('/api/voice/toggle', async (req, res) => {
    const { enabled } = req.body;
    if (!runtimeApi.toggleVoice) {
      return res.status(503).json({ error: 'Voice control unavailable' });
    }
    try {
      const active = await runtimeApi.toggleVoice(!!enabled);
      res.json({ ok: true, active });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  app.post('/api/voice/listen', (req, res) => {
    if (!runtimeApi.voiceHotkey) {
      return res.status(503).json({ error: 'Voice control unavailable' });
    }
    try {
      runtimeApi.voiceHotkey();
      res.json({ ok: true });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // ── v4.0 SSE Streaming Chat endpoint ─────────────────────────

  app.post('/api/chat/stream', async (req, res) => {
    const { message } = req.body;
    if (!message || !message.trim()) {
      return res.status(400).json({ error: 'Message required' });
    }

    // Record user message as event and immediately mark processed so the
    // agent loop doesn't also pick it up (avoiding duplicate skill execution).
    const userEvent = addEvent('chat', 'user', message.trim());
    markProcessed([userEvent.id]);
    // Note: we do NOT emit 'new-chat' — this endpoint handles the response
    // itself, then kicks off skill detection below to avoid duplication.

    // Set up SSE headers
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'X-Accel-Buffering': 'no'
    });

    try {
      const { streamLLM } = await import('./llm.mjs');
      let getKGContext;
      try { ({ getKGContext } = await import('./knowledge-graph.mjs')); } catch {}

      // Build prompt with KG context
      let kgContext = '';
      if (getKGContext) {
        try { kgContext = await getKGContext(message.trim()); } catch {}
      }
      const prompt = kgContext
        ? `Context:\n${kgContext}\n\nUser: ${message.trim()}`
        : message.trim();

      let lastText = '';
      for await (const chunk of streamLLM(prompt, { model: 'balanced' })) {
        lastText = chunk.text;
        res.write(`data: ${JSON.stringify({ text: chunk.text })}\n\n`);
        broadcastToClients({ type: 'stream', data: { text: chunk.text } });
      }

      // Record agent response
      const agentEvent = addEvent('chat', 'agent', lastText || '[streamed response]', { reply_to: userEvent.id, streamed: true });

      res.write(`data: ${JSON.stringify({ id: agentEvent.id, done: true })}\n\n`);
      res.end();

      // Detect and queue skill in the background (after stream completes)
      // so building/creating happens once, not duplicated by the agent loop.
      if (runtimeApi.detectAndQueueSkill) {
        runtimeApi.detectAndQueueSkill(message.trim(), userEvent.id).catch(err => {
          console.error('[server] Skill detection after stream failed:', err.message);
        });
      }
    } catch (err) {
      console.error('[server] SSE stream error:', err.message);
      res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
      res.end();
    }
  });

  // ── v4.0 Persona endpoints ──────────────────────────────────

  app.get('/api/persona', async (req, res) => {
    try {
      const { getPersonaConfig } = await import('./persona.mjs');
      res.json(getPersonaConfig());
    } catch (err) {
      // Fallback to config defaults
      const config = getConfig();
      res.json(config.persona || { name: 'AI-Do', greeting: 'Hello!', avatarStyle: 'default', traits: {} });
    }
  });

  app.put('/api/persona', async (req, res) => {
    try {
      const { updatePersona } = await import('./persona.mjs');
      const updated = await updatePersona(req.body);
      res.json({ ok: true, persona: updated });
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  // ── v4.0 Skill Palette endpoint ─────────────────────────────

  app.get('/api/skills/palette', (req, res) => {
    try {
      const dbSkills = getSkillsFromDb();
      // Categorize skills for the function palette
      const categoryMap = {
        'research': 'Search', 'analyze': 'Analyze', 'vision': 'Analyze',
        'build-page': 'Create', 'write-doc': 'Create', 'create-file': 'Create',
        'journal': 'Journal', 'ingest': 'Retrieve', 'install-tool': 'Configure',
        'configure-agent': 'Configure', 'publish-site': 'Create',
        'self-improve': 'Evolve', 'meta-develop': 'Evolve', 'summarize': 'Analyze'
      };
      const iconMap = {
        'Search': 'search', 'Analyze': 'analyze', 'Create': 'create',
        'Journal': 'journal', 'Retrieve': 'retrieve', 'Configure': 'configure',
        'Evolve': 'evolve', 'Visualize': 'vision'
      };

      const palette = dbSkills.map(s => ({
        name: s.name,
        description: s.description,
        enabled: !!s.enabled,
        category: categoryMap[s.name] || 'Other',
        icon: iconMap[categoryMap[s.name]] || 'skills',
        displayName: s.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
      }));

      res.json(palette);
    } catch (err) {
      res.status(500).json({ error: err.message });
    }
  });

  const server = createServer(app);

  // WebSocket
  wss = new WebSocketServer({ server, path: '/ws', verifyClient: (info) => {
    const { authToken } = getConfig();
    if (!authToken) return true;
    const url = new URL(info.req.url, `http://${info.req.headers.host}`);
    return url.searchParams.get('token') === authToken;
  }});
  wss.on('connection', (ws) => {
    clients.add(ws);
    ws.on('close', () => clients.delete(ws));
    ws.on('error', () => clients.delete(ws));
  });

  // Wire up broadcast for db.mjs
  setBroadcast(broadcastToClients);

  const { port } = getConfig();
  server.listen(port, () => {
    console.log(`[server] Web UI: http://localhost:${port}`);
  });

  return server;
}

function broadcastToClients(msg) {
  const data = JSON.stringify(msg);
  for (const client of clients) {
    if (client.readyState === 1) {
      client.send(data);
    }
  }
}

/**
 * Broadcast a partial streaming response to all connected WebSocket clients.
 * Used by agent.mjs to push real-time chat updates.
 */
export function broadcastStream(msgId, partialText) {
  broadcastToClients({ type: 'stream', data: { id: msgId, text: partialText } });
}

/**
 * Broadcast agent status updates to all connected WebSocket clients.
 * Used to show real-time activity in the UI.
 */
export function broadcastStatus(status) {
  broadcastToClients({ type: 'status', data: status });
}

/**
 * Broadcast an activity event to all connected WebSocket clients.
 * Used by the v4 UI activity feed.
 */
export function broadcastActivity(kind, source, content, metadata = null) {
  broadcastToClients({
    type: 'activity',
    data: { kind, source, content, metadata, timestamp: Date.now() }
  });
}

/**
 * Broadcast skill lifecycle events to all connected WebSocket clients.
 */
export function broadcastSkillStart(name, args) {
  broadcastToClients({ type: 'skill_start', data: { name, args, timestamp: Date.now() } });
}

export function broadcastSkillComplete(name, result) {
  broadcastToClients({ type: 'skill_complete', data: { name, result, timestamp: Date.now() } });
}

/**
 * Broadcast voice activation state changes to all connected WebSocket clients.
 * Sends { type: 'voice', data: { state, ... } } so the browser can update the voice orb
 * and react to wake word detections.
 */
export function broadcastVoice(state, extra = {}) {
  broadcastToClients({ type: 'voice', data: { state, ...extra, timestamp: Date.now() } });
}
