const brain = require('../brain');
const eventBus = require('../events/event-bus');

function checkpointName(adapter) {
  const name = adapter?.name || adapter?.constructor?.name || 'unknown';
  return `ingest:${name}`;
}

function newerTimestamp(a, b) {
  if (!a) return b || null;
  if (!b) return a;
  return b > a ? b : a;
}

async function runOnce(adapters) {
  const legacyCheckpoint = brain.getCheckpoint('ingest');
  let memoriesIngested = 0;
  let latestTimestamp = legacyCheckpoint?.last_memory_at || null;
  const adapterResults = {};

  for (const adapter of adapters) {
    const adapterCheckpointName = checkpointName(adapter);
    const adapterCheckpoint = brain.getCheckpoint(adapterCheckpointName);
    const since = adapterCheckpoint?.last_memory_at || null;
    let adapterIngested = 0;
    let adapterLatestTimestamp = since;

    try {
      const memories = await adapter.poll(since);
      // Insert in chunked transactions instead of N individually-locked INSERTs. One
      // INSERT per message means N cross-process write-lock acquisitions per poll;
      // wrapping ALL of them in a single transaction would swing the other way and hold
      // the lock for the whole batch (the harvest lock-hold class). Chunking bounds the
      // hold while still collapsing N lock acquisitions into N/CHUNK. High-importance
      // events are emitted AFTER each chunk commits, never from inside the transaction.
      const INSERT_CHUNK = 200;
      for (let i = 0; i < memories.length; i += INSERT_CHUNK) {
        const slice = memories.slice(i, i + INSERT_CHUNK);
        const highImportance = [];
        const insertChunk = brain.getDb().transaction(() => {
          for (const mem of slice) {
            adapterLatestTimestamp = newerTimestamp(adapterLatestTimestamp, mem.timestamp);
            latestTimestamp = newerTimestamp(latestTimestamp, mem.timestamp);
            const result = brain.insertMemory(mem);
            if (result) {
              memoriesIngested++;
              adapterIngested++;
              if (mem.importance >= 0.8 || mem.memory_type === 'message_received') {
                highImportance.push({ id: result?.id, content: (mem.content || '').slice(0, 200) });
              }
            }
          }
        });
        insertChunk();
        for (const h of highImportance) eventBus.emitHighImportance(h.id, h.content);
      }
      // After inserting individual memories, create exchange pairs for conversational sources
      if (memories.length >= 2) {
        try {
          const { chunkExchanges } = require('../extraction/exchange-chunker');
          const byChannel = {};
          for (const mem of memories) {
            // Only chunk conversational sources (slack, chat, email)
            if (!mem.direction || mem.direction === 'none') continue;
            const ch = mem.source_channel || mem.source || 'default';
            if (!byChannel[ch]) byChannel[ch] = [];
            byChannel[ch].push(mem);
          }
          for (const [channel, msgs] of Object.entries(byChannel)) {
            if (msgs.length < 2) continue;
            const formatted = msgs.map(m => ({
              role: m.direction === 'outbound' ? 'user' : 'assistant',
              content: m.content,
              timestamp: m.timestamp,
            }));
            const exchanges = chunkExchanges(formatted);
            for (let i = 0; i < exchanges.length; i++) {
              brain.insertMemory({
                source: msgs[0].source || 'exchange',
                source_id: `exchange-${channel}-${Date.now()}-${i}`,
                source_channel: channel,
                memory_type: 'exchange',
                direction: 'exchange',
                content: exchanges[i].content,
                timestamp: exchanges[i].timestamp,
                importance: 0.6,
              });
            }
          }
        } catch (e) {
          console.error('[ingest] Exchange chunking failed:', e.message);
        }
      }
      brain.upsertCheckpoint(adapterCheckpointName, {
        last_memory_at: adapterLatestTimestamp,
        metadata: JSON.stringify({
          adapter: adapter.name || adapter.constructor?.name || 'unknown',
          memories_ingested: adapterIngested,
          memories_seen: memories.length,
        }),
      });
      adapterResults[adapter.name || adapter.constructor?.name || 'unknown'] = {
        memoriesIngested: adapterIngested,
        memoriesSeen: memories.length,
        lastMemoryAt: adapterLatestTimestamp,
      };
    } catch (err) {
      console.error(`[ingest] Adapter ${adapter.name} failed:`, err.message);
      adapterResults[adapter.name || adapter.constructor?.name || 'unknown'] = {
        error: err.message,
        lastMemoryAt: adapterLatestTimestamp,
      };
    }
  }

  brain.upsertCheckpoint('ingest', {
    last_memory_at: latestTimestamp,
    metadata: JSON.stringify({ memories_ingested: memoriesIngested, adapters: adapterResults }),
  });

  return { memoriesIngested };
}

module.exports = { checkpointName, runOnce };
