const express = require('express');
const router = express.Router();
const rateLimit = require('express-rate-limit');
const { requireAuth } = require('../middleware/auth');
const { sanitizeError } = require('../utils/security');
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
const {
  buildLlmTransferPrompt,
  parseLlmTransferText,
  importanceForCategory,
} = require('../services/memory/llm_transfer');

router.use(requireAuth);

const apiKeyMutationLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 60,
  message: { error: 'Too many API key update attempts, try again later' },
  standardHeaders: true,
  legacyHeaders: false,
});

const transferImportLimiter = rateLimit({
  windowMs: 10 * 60 * 1000,
  max: 20,
  message: { error: 'Too many memory imports, try again later' },
  standardHeaders: true,
  legacyHeaders: false,
});

const MAX_TRANSFER_TEXT_BYTES = 60 * 1024;

function normalizeMemoryIds(value) {
  return [...new Set(
    (Array.isArray(value) ? value : [])
      .map((id) => String(id || '').trim())
      .filter(Boolean)
  )];
}

function findOwnedMemoryIds(db, userId, agentId, ids) {
  if (!ids.length) {
    return [];
  }
  const placeholders = ids.map(() => '?').join(', ');
  return db.prepare(
    `SELECT id FROM memories WHERE user_id = ? AND agent_id = ? AND id IN (${placeholders})`
  ).all(userId, agentId, ...ids).map((row) => row.id);
}

function parsePlainObject(input, fieldName) {
  if (input == null) return null;
  if (typeof input === 'string') {
    try {
      input = JSON.parse(input);
    } catch {
      throw new Error(`${fieldName} must be valid JSON.`);
    }
  }
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
    throw new Error(`${fieldName} must be an object.`);
  }
  return input;
}

function normalizeOptionalStringField(value, fieldName, maxLength, pattern = null) {
  if (value == null || value === '') return null;
  if (typeof value !== 'string') {
    throw new Error(`${fieldName} must be a string.`);
  }
  const normalized = value.trim().slice(0, maxLength);
  if (!normalized) return null;
  if (pattern && !pattern.test(normalized)) {
    throw new Error(`${fieldName} has an invalid format.`);
  }
  return normalized;
}

function normalizeSourceRef(input) {
  const raw = parsePlainObject(input, 'sourceRef');
  if (!raw) return undefined;
  return {
    sourceType: normalizeOptionalStringField(raw.sourceType ?? raw.type, 'sourceRef.sourceType', 48, /^[a-z0-9_:-]+$/i),
    sourceId: normalizeOptionalStringField(raw.sourceId ?? raw.id, 'sourceRef.sourceId', 128),
    sourceLabel: normalizeOptionalStringField(raw.sourceLabel ?? raw.label, 'sourceRef.sourceLabel', 160),
  };
}

function normalizeScope(input) {
  const raw = parsePlainObject(input, 'scope');
  if (!raw) return undefined;
  const scopeType = normalizeOptionalStringField(raw.scopeType ?? raw.type, 'scope.scopeType', 32, /^(agent|conversation|task|channel|shared)$/i);
  return {
    scopeType: scopeType ? scopeType.toLowerCase() : null,
    scopeId: normalizeOptionalStringField(raw.scopeId ?? raw.id, 'scope.scopeId', 128),
  };
}

function normalizeMetadata(input) {
  const raw = parsePlainObject(input, 'metadata');
  return raw == null ? undefined : raw;
}

// ─────────────────────────────────────────────────────────────────────────────
// Overview (for initial page load)
// ─────────────────────────────────────────────────────────────────────────────

router.get('/', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  const coreMemory = { ...(mm.getCoreMemory(userId, { agentId }) || {}) };
  delete coreMemory.active_context;
  const knowledgeViews = mm.listKnowledgeViews(userId, { agentId, limit: 12 });
  res.json({
    agentId,
    assistantBehaviorNotes: mm.getAssistantBehaviorNotes(userId, { agentId }),
    assistantSelfState: mm.getAssistantSelfState(userId, { agentId }),
    dailyLogs: mm.listDailyLogs(7, userId),
    apiKeys: Object.keys(mm.readApiKeys(userId)),
    coreMemory,
    stats: mm.getMemoryStats(userId, { agentId }),
    entities: mm.listEntities(userId, { agentId, limit: 12 }),
    knowledgeViews,
    ingestionOverview: mm.getIngestionOverview(userId, { agentId }),
    recentKnowledgeChanges: mm.listRecentKnowledgeChanges(userId, { agentId, limit: 8 }),
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// Semantic Memories
// ─────────────────────────────────────────────────────────────────────────────

// List memories (with optional ?category= and ?limit= filters)
router.get('/memories', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  const { category, limit = 50, offset = 0, archived = false } = req.query;
  try {
    const memories = mm.listMemories(userId, {
      category: category || null,
      limit: parseInt(limit),
      offset: parseInt(offset),
      includeArchived: archived === 'true',
      agentId,
    });
    res.json(memories);
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.get('/ingestion/status', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const ingestionService = req.app.locals.memoryIngestionService;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  try {
    res.json({
      agentId,
      overview: mm.getIngestionOverview(userId, { agentId }),
      jobs: mm.listIngestionJobs(userId, { agentId }),
      recentChanges: mm.listRecentKnowledgeChanges(userId, { agentId }),
      connections: ingestionService?.listConnectionStatuses?.(userId, { agentId }) || [],
      service: ingestionService?.getStatus?.() || { state: 'unavailable' },
    });
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.post('/ingestion/documents', async (req, res) => {
  const ingestionService = req.app.locals.memoryIngestionService;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  if (!ingestionService) {
    return res.status(503).json({ error: 'Memory ingestion service is unavailable.' });
  }
  const documents = Array.isArray(req.body?.documents) ? req.body.documents : [];
  if (!documents.length) {
    return res.status(400).json({ error: 'documents must contain at least one item.' });
  }
  try {
    const result = await ingestionService.ingestDocuments(userId, documents, {
      agentId,
      sourceType: req.body.sourceType,
      providerKey: req.body.providerKey,
      connectionId: req.body.connectionId,
      sourceAccount: req.body.sourceAccount,
      metadata: normalizeMetadata(req.body.metadata),
    });
    res.json(result);
  } catch (err) {
    res.status(400).json({ error: sanitizeError(err) });
  }
});

router.get('/knowledge-views', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  try {
    res.json(mm.listKnowledgeViews(userId, {
      agentId,
      viewType: req.query.viewType || null,
      limit: req.query.limit,
    }));
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.get('/entities', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  try {
    res.json(mm.listEntities(userId, {
      agentId,
      query: req.query.query || null,
      limit: req.query.limit,
    }));
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.get('/facts', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  try {
    res.json(mm.listFacts(userId, {
      agentId,
      status: req.query.status || '',
      subject: req.query.subject || '',
      predicate: req.query.predicate || '',
      validAt: req.query.validAt || '',
      knownAt: req.query.knownAt || '',
      limit: req.query.limit,
    }));
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.post('/facts/reconcile', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  try {
    res.json(mm.reconcileFacts(userId, {
      agentId,
      minimumConfidence: req.body?.minimumConfidence,
    }));
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.post('/knowledge-views/materialize', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  try {
    res.json({
      agentId,
      views: mm.materializeKnowledgeViews(userId, { agentId }),
    });
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

// Save a new memory
router.post('/memories', async (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  const { content, category = 'episodic', importance = 5, sourceRef, scope, staleAfterDays, metadata } = req.body;
  if (!content || !content.trim()) return res.status(400).json({ error: 'content is required' });
  try {
    let normalizedStaleAfterDays;
    if (staleAfterDays != null && staleAfterDays !== '') {
      normalizedStaleAfterDays = Number.parseInt(staleAfterDays, 10);
      if (!Number.isInteger(normalizedStaleAfterDays) || normalizedStaleAfterDays <= 0) {
        return res.status(400).json({ error: 'staleAfterDays must be a positive integer.' });
      }
    }

    const id = await mm.saveMemory(userId, content, category, importance, {
      agentId,
      sourceRef: normalizeSourceRef(sourceRef),
      scope: normalizeScope(scope),
      staleAfterDays: normalizedStaleAfterDays,
      metadata: normalizeMetadata(metadata),
    });
    res.json({ success: true, id });
  } catch (err) {
    const message = sanitizeError(err);
    if (/must be valid JSON|must be an object|must be a string|has an invalid format/i.test(message)) {
      return res.status(400).json({ error: message });
    }
    res.status(500).json({ error: message });
  }
});

// Update a memory
router.put('/memories/:id', async (req, res) => {
  const mm = req.app.locals.memoryManager;
  const db = require('../db/database');
  // Verify ownership before updating
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
  const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ? AND agent_id = ?').get(req.params.id, req.session.userId, agentId);
  if (!existing) return res.status(404).json({ error: 'Memory not found' });
  const { content, importance, category } = req.body;
  try {
    const updated = await mm.updateMemory(req.params.id, { content, importance, category });
    if (!updated) return res.status(404).json({ error: 'Memory not found' });
    res.json(updated);
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

// Delete a memory
router.delete('/memories/:id', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const db = require('../db/database');
  // Verify ownership before deleting
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
  const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ? AND agent_id = ?').get(req.params.id, req.session.userId, agentId);
  if (!existing) return res.status(404).json({ error: 'Memory not found' });
  mm.deleteMemories([req.params.id], req.session.userId);
  res.json({ success: true });
});

router.post('/memories/bulk-delete', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const db = require('../db/database');
  const ids = normalizeMemoryIds(req.body?.ids);
  if (!ids.length) {
    return res.status(400).json({ error: 'ids is required' });
  }
  try {
    const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
    const ownedIds = findOwnedMemoryIds(db, req.session.userId, agentId, ids);
    if (!ownedIds.length) {
      return res.status(404).json({ error: 'Memory not found' });
    }
    const deletedCount = mm.deleteMemories(ownedIds, req.session.userId);
    res.json({ success: true, deletedCount });
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.post('/memories/bulk-archive', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const db = require('../db/database');
  const ids = normalizeMemoryIds(req.body?.ids);
  const archived = req.body?.archived !== false;
  if (!ids.length) {
    return res.status(400).json({ error: 'ids is required' });
  }
  try {
    const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
    const ownedIds = findOwnedMemoryIds(db, req.session.userId, agentId, ids);
    if (!ownedIds.length) {
      return res.status(404).json({ error: 'Memory not found' });
    }
    const archivedCount = mm.archiveMemories(ownedIds, archived, req.session.userId);
    res.json({ success: true, archivedCount });
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

// Semantic recall (search)
router.post('/memories/recall', async (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  const { query, limit = 8 } = req.body;
  if (!query) return res.status(400).json({ error: 'query is required' });
  try {
    const results = await mm.recallMemory(userId, query, parseInt(limit), { agentId });
    res.json(results);
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

// ─────────────────────────────────────────────────────────────────────────────
// Core Memory
// ─────────────────────────────────────────────────────────────────────────────

router.get('/core', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  const coreMemory = { ...(mm.getCoreMemory(userId, { agentId }) || {}) };
  delete coreMemory.active_context;
  res.json(coreMemory);
});

// ─────────────────────────────────────────────────────────────────────────────
// LLM Transfer (prompt + import)
// ─────────────────────────────────────────────────────────────────────────────

router.get('/transfer-prompt', (req, res) => {
  const prompt = buildLlmTransferPrompt({ agentLabel: 'NeoAgent' });
  res.json({ prompt });
});

router.post('/transfer-import', transferImportLimiter, async (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  const text = String(req.body?.text || '').trim();
  if (!text) {
    return res.status(400).json({ error: 'text is required' });
  }
  if (Buffer.byteLength(text, 'utf8') > MAX_TRANSFER_TEXT_BYTES) {
    return res.status(413).json({ error: 'text exceeds 60KB limit' });
  }

  const applyBehaviorNotes = req.body?.applyBehaviorNotes !== false;
  const applyCoreMemory = req.body?.applyCoreMemory !== false;

  try {
    const parsed = parseLlmTransferText(text);
    let importedCount = 0;
    let skippedCount = 0;

    for (const memory of parsed.memories) {
      const content = String(memory.content || '').trim();
      if (!content) {
        skippedCount += 1;
        continue;
      }
      await mm.saveMemory(userId, content, memory.category, importanceForCategory(memory.category), {
        agentId,
        sourceRef: {
          sourceType: 'llm_import',
          sourceLabel: 'LLM memory transfer',
        },
        metadata: {
          importedFrom: 'llm_transfer',
        },
      });
      importedCount += 1;
    }

    let coreUpdatedCount = 0;
    if (applyCoreMemory && parsed.coreEntries) {
      for (const [key, value] of Object.entries(parsed.coreEntries)) {
        if (!key || key === 'active_context') continue;
        mm.updateCore(userId, key, value, { agentId, confirmed: true });
        coreUpdatedCount += 1;
      }
    }

    let behaviorNotesUpdated = false;
    if (applyBehaviorNotes && parsed.behaviorNotes) {
      mm.setAssistantBehaviorNotes(userId, parsed.behaviorNotes, { agentId });
      behaviorNotesUpdated = true;
    }

    res.json({
      importedCount,
      skippedCount,
      coreUpdatedCount,
      behaviorNotesUpdated,
      warnings: parsed.warnings || [],
    });
  } catch (err) {
    res.status(500).json({ error: sanitizeError(err) });
  }
});

router.put('/core/:key', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  const { value } = req.body;
  if (value === undefined) return res.status(400).json({ error: 'value is required' });
  if (req.params.key === 'active_context') {
    return res.status(400).json({ error: 'active_context is no longer a supported core memory key' });
  }
  mm.updateCore(userId, req.params.key, value, { agentId, confirmed: true });
  res.json({ success: true });
});

router.delete('/core/:key', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const userId = req.session.userId;
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
  if (req.params.key === 'active_context') {
    return res.status(400).json({ error: 'active_context is no longer a supported core memory key' });
  }
  mm.deleteCore(userId, req.params.key, { agentId });
  res.json({ success: true });
});

// ─────────────────────────────────────────────────────────────────────────────
// Daily Logs
// ─────────────────────────────────────────────────────────────────────────────

router.get('/daily', (req, res) => {
  const limit = parseInt(req.query.limit) || 7;
  res.json(req.app.locals.memoryManager.listDailyLogs(limit, req.session.userId));
});

router.get('/daily/:date', (req, res) => {
  const content = req.app.locals.memoryManager.readDailyLog(new Date(req.params.date), req.session.userId);
  res.json({ date: req.params.date, content });
});

// ─────────────────────────────────────────────────────────────────────────────
// API Keys (agent-managed)
// ─────────────────────────────────────────────────────────────────────────────

router.get('/api-keys', (req, res) => {
  const keys = req.app.locals.memoryManager.readApiKeys(req.session.userId);
  const masked = {};
  for (const [k, v] of Object.entries(keys)) {
    masked[k] = v ? `${v.slice(0, 4)}...${v.slice(-4)}` : null;
  }
  res.json(masked);
});

router.put('/api-keys/:service', apiKeyMutationLimiter, (req, res) => {
  req.app.locals.memoryManager.setApiKey(req.params.service, req.body.key, req.session.userId);
  res.json({ success: true });
});

router.delete('/api-keys/:service', apiKeyMutationLimiter, (req, res) => {
  req.app.locals.memoryManager.deleteApiKey(req.params.service, req.session.userId);
  res.json({ success: true });
});

// ─────────────────────────────────────────────────────────────────────────────
// Conversation History
// ─────────────────────────────────────────────────────────────────────────────

router.get('/conversations', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
  const conversations = mm.getRecentConversations(req.session.userId, parseInt(req.query.limit) || 20, { agentId });
  res.json(conversations);
});

router.post('/conversations/search', (req, res) => {
  const mm = req.app.locals.memoryManager;
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
  const results = mm.searchConversations(req.session.userId, req.body.query, {
    sessions: parseInt(req.body.limit) || 8,
    agentId
  });
  res.json(results);
});

module.exports = router;
