#!/usr/bin/env node
'use strict';
/**
 * Pull full conversation history from Slack channels/DMs using channel IDs + pagination.
 * Gets EVERY message including context (what others said that led to your replies).
 *
 * Zero Claude tokens — direct Slack MCP calls + brain DB writes.
 *
 * Usage:
 *   node scripts/slack-channel-history.js                     # auto-discover top channels
 *   node scripts/slack-channel-history.js "Jane Smith"         # specific DM by name
 *   node scripts/slack-channel-history.js --channel C0123ABC  # specific channel by ID
 *   node scripts/slack-channel-history.js --list              # list your channels
 */

const brain = require('../brain');
const slackMcp = require('../tools/slack-mcp');

const SLACK_OWNER_USER_ID = process.env.SLACK_OWNER_USER_ID;
if (!SLACK_OWNER_USER_ID) throw new Error('SLACK_OWNER_USER_ID env var required');

const PAUSE_MS = 1200;
const PAGE_LIMIT = 100; // max per API call
const MAX_PAGES = 20;   // per channel (2000 messages max)
const SLACK_OWNER_HANDLE = process.env.SLACK_OWNER_HANDLE || 'owner';
const WALLE_OWNER_NAME = process.env.WALLE_OWNER_NAME || 'Owner';

async function main() {
  brain.initDb();
  const db = brain.getDb();

  if (!(await slackMcp.isAuthenticated())) {
    console.error('Slack token expired! Run: node tools/slack-mcp.js auth');
    process.exit(1);
  }

  const arg = process.argv.slice(2).join(' ').trim();
  const totalBefore = db.prepare("SELECT count(*) as c FROM memories WHERE source = 'slack'").get().c;
  let stats = { new_messages: 0, skipped: 0, channels_done: 0, pages: 0 };

  if (arg === '--list') {
    await listMyChannels();
    return;
  }

  if (arg.startsWith('--channel ')) {
    const channelId = arg.replace('--channel ', '').trim();
    console.log(`Pulling channel: ${channelId}\n`);
    await pullChannelById(channelId, channelId, db, stats);
  } else if (arg === '--sync' || arg === '--incremental') {
    // Incremental sync: only pull channels with recent activity
    // Step 1: search Slack for YOUR recent messages to find active channels
    console.log('Incremental sync: discovering recently active channels...\n');
    const activeChannels = new Map(); // id → name
    const searchQueries = [`from:${SLACK_OWNER_HANDLE} after:` + daysAgo(7), `to:${SLACK_OWNER_HANDLE} after:` + daysAgo(7)];
    for (const q of searchQueries) {
      try {
        const result = await slackMcp.callSlackMcp('slack_search_public_and_private', {
          query: q, sort: 'timestamp', sort_dir: 'desc', limit: 20, response_format: 'detailed',
        });
        const raw = extractText(result);
        let text = raw;
        try { const p = JSON.parse(raw); text = p.results || raw; } catch {}
        // Extract channel IDs from results
        const channelMatches = text.matchAll(/Channel:.*?\(ID:\s*(\w+)\)/g);
        for (const m of channelMatches) {
          if (!activeChannels.has(m[1])) activeChannels.set(m[1], m[1]);
        }
        // Extract DM participant IDs
        const dmMatches = text.matchAll(/(?:DM|Group DM).*?\(ID:\s*(\w+)\)/g);
        for (const m of dmMatches) {
          if (!activeChannels.has(m[1])) activeChannels.set(m[1], 'DM:' + m[1]);
        }
      } catch (e) {
        if (e.message.includes('401') || e.message.includes('expired')) {
          console.error('Slack token expired!');
          process.exit(1);
        }
      }
      await sleep(PAUSE_MS);
    }

    // Also add channels with very recent messages in brain (last 3 days)
    try {
      const recentChannels = db.prepare(`
        SELECT DISTINCT json_extract(metadata, '$.channel_id') as cid
        FROM memories WHERE source='slack' AND timestamp > datetime('now', '-3 days')
        AND json_extract(metadata, '$.channel_id') IS NOT NULL
      `).all();
      for (const r of recentChannels) {
        if (r.cid && !activeChannels.has(r.cid)) activeChannels.set(r.cid, r.cid);
      }
    } catch {}

    const targets = [...activeChannels.entries()].map(([id, name]) => ({ id, name }));
    console.log(`Found ${targets.length} recently active channels (vs 784 total)\n`);

    for (let i = 0; i < targets.length; i++) {
      const t = targets[i];
      const before = stats.new_messages;
      process.stdout.write(`[${i + 1}/${targets.length}] ${t.name} (${t.id})... `);
      await pullChannelById(t.id, t.name, db, stats);
      console.log(`+${stats.new_messages - before} new (${stats.pages} pages)`);
      console.log(`CHECKPOINT:${t.id}`);
      stats.channels_done++;
      stats.pages = 0;
      await sleep(PAUSE_MS);
    }
  } else if (arg) {
    // Look up user/channel by name
    const id = await resolveTarget(arg);
    if (!id) { console.error(`Could not find: ${arg}`); process.exit(1); }
    console.log(`Resolved "${arg}" → ${id}\n`);
    await pullChannelById(id, arg, db, stats);
  } else {
    // Full pull: all targets
    const targets = await discoverTargets(db);
    const checkpoint = process.env.WALL_E_CHECKPOINT || '';
    let startIdx = 0;
    if (checkpoint) {
      const idx = targets.findIndex(t => t.id === checkpoint || t.name === checkpoint);
      if (idx >= 0) { startIdx = idx; console.log(`Resuming from checkpoint: ${checkpoint}\n`); }
    }
    console.log(`Pulling ${targets.length} channels/DMs (starting from #${startIdx + 1})\n`);

    for (let i = startIdx; i < targets.length; i++) {
      const t = targets[i];
      const before = stats.new_messages;
      process.stdout.write(`[${i + 1}/${targets.length}] ${t.name} (${t.id})... `);
      await pullChannelById(t.id, t.name, db, stats);
      console.log(`+${stats.new_messages - before} new (${stats.pages} pages)`);
      console.log(`CHECKPOINT:${t.id}`);
      stats.channels_done++;
      stats.pages = 0;
      await sleep(PAUSE_MS);
    }
  }

  const totalAfter = db.prepare("SELECT count(*) as c FROM memories WHERE source = 'slack'").get().c;
  console.log(`\nDone! New: ${stats.new_messages}, Skipped: ${stats.skipped}, Channels: ${stats.channels_done}`);
  console.log(`Total: ${totalBefore} → ${totalAfter} (+${totalAfter - totalBefore})`);
}

// ── Resolve a name to a Slack channel/user ID ──

async function resolveTarget(name) {
  // Try user search first (for DMs)
  try {
    const userResult = await slackMcp.callSlackMcp('slack_search_users', { query: name });
    const text = extractText(userResult);
    // Look for user_id pattern
    const idMatch = text.match(/(?:user_id|ID)[:\s]*([UW][A-Z0-9]+)/i);
    if (idMatch) return idMatch[1];
    // Look for ID in structured output
    const blockMatch = text.match(/([UW][A-Z0-9]{8,})/);
    if (blockMatch) return blockMatch[1];
  } catch {}

  // Try channel search
  try {
    const chanResult = await slackMcp.callSlackMcp('slack_search_channels', { query: name });
    const text = extractText(chanResult);
    const idMatch = text.match(/([CG][A-Z0-9]{8,})/);
    if (idMatch) return idMatch[1];
  } catch {}

  return null;
}

// ── Pull full history for a channel/DM with pagination ──

async function pullChannelById(channelId, label, db, stats) {
  let cursor = null;
  let page = 0;

  while (page < MAX_PAGES) {
    try {
      const params = { channel_id: channelId, limit: PAGE_LIMIT };
      if (cursor) params.cursor = cursor;

      const result = await slackMcp.callSlackMcp('slack_read_channel', params);
      const rawText = extractText(result);

      if (!rawText || rawText.length < 10) break;

      // Response may be JSON with {messages, pagination_info} or plain text
      let msgText = rawText;
      let nextCursor = null;
      try {
        const parsed = JSON.parse(rawText);
        msgText = parsed.messages || rawText;
        // Extract cursor from pagination_info: "use cursor: `abc123`"
        if (parsed.pagination_info) {
          const cursorMatch = parsed.pagination_info.match(/cursor:\s*`([^`]+)`/);
          if (cursorMatch) nextCursor = cursorMatch[1];
        }
      } catch { /* not JSON, use as-is */ }

      const messages = parseMessages(msgText, label);

      let newInPage = 0;
      for (const msg of messages) {
        const sourceId = `slack-ch-${msg.timestamp}-${(msg.sender || '').slice(0, 5)}`;
        const exists = db.prepare("SELECT 1 FROM memories WHERE source = 'slack' AND source_id = ?").get(sourceId);
        if (exists) { stats.skipped++; continue; }

        const meta = { channel_id: channelId, channel: label, sender: msg.sender, context: 'channel_history' };
        if (msg.message_ts) meta.message_ts = msg.message_ts;
        if (msg.thread_ts) meta.thread_ts = msg.thread_ts;
        brain.insertMemory({
          source: 'slack',
          source_id: sourceId,
          memory_type: msg.isOwner ? 'message_sent' : 'message_received',
          direction: msg.isOwner ? 'outbound' : 'inbound',
          content: msg.content,
          source_channel: label,
          participants: msg.sender,
          timestamp: msg.timestamp,
          metadata: JSON.stringify(meta),
        });
        newInPage++;
        stats.new_messages++;
      }

      page++;
      stats.pages = page;

      // Pagination
      if (nextCursor) {
        cursor = nextCursor;
      } else {
        break; // no more pages
      }

      // If all messages in this page were dupes, we've caught up
      if (newInPage === 0 && messages.length > 0) break;

      await sleep(PAUSE_MS);
    } catch (err) {
      if (err.message.includes('401') || err.message.includes('expired') || err.message.includes('invalid_auth')) {
        console.error('\nSlack token expired! Run: node tools/slack-mcp.js auth');
        process.exit(1);
      }
      if (err.message.includes('channel_not_found')) {
        process.stdout.write('[not found] ');
      } else {
        process.stdout.write(`[error: ${err.message.slice(0, 50)}] `);
      }
      break;
    }
  }
}

// ── Auto-discover top targets ──

async function discoverTargets(db) {
  const seen = new Set();
  const targets = [];

  function add(id, name) {
    if (!id || seen.has(id)) return;
    seen.add(id);
    targets.push({ id, name });
  }

  // === Phase 1: Discover from Slack directly (catches ad-hoc channels & new 1:1s) ===
  console.log('Phase 1: Discovering channels from Slack search...');

  // Search for your messages across different time periods to discover channel IDs
  const searchQueries = [
    `from:${SLACK_OWNER_HANDLE}`,                           // recent messages (any channel)
    `from:${SLACK_OWNER_HANDLE} before:2025-07-01`,         // older messages
    `from:${SLACK_OWNER_HANDLE} before:2025-01-01`,         // even older
    `from:${SLACK_OWNER_HANDLE} before:2024-07-01`,         // 2024 H1
    `to:${SLACK_OWNER_HANDLE}`,                             // DMs sent to you
  ];

  for (const q of searchQueries) {
    try {
      const result = await slackMcp.callSlackMcp('slack_search_public_and_private', {
        query: q, sort: 'timestamp', sort_dir: 'desc', limit: 20, response_format: 'detailed',
      });
      const text = extractText(result);
      // Try JSON parse first
      let content = text;
      try { content = JSON.parse(text).results || text; } catch {}

      // Extract channel IDs and names from "Channel: #name (ID: CXXX)" or "DM (ID: DXXX)"
      const channelMatches = [...content.matchAll(/Channel:\s*(?:#?(\S+)|DM|Group DM)\s*\(ID:\s*(\w+)\)/g)];
      for (const m of channelMatches) {
        const name = m[1] || (content.includes('Group DM') ? 'Group DM' : 'DM');
        add(m[2], name === 'DM' || name === 'Group DM' ? m[2] : `#${name}`);
      }
      // Also extract from "From: Name (ID: UXXX)" for DMs — the user ID IS the channel ID for DMs
      // We collect participant user IDs to read their DM history
      const fromMatches = [...content.matchAll(/(?:From|Participants):\s*([^\n]+)/g)];
      for (const fm of fromMatches) {
        const userIds = [...fm[1].matchAll(/\(ID:\s*(U[A-Z0-9]+)\)/g)];
        for (const uid of userIds) {
          if (uid[1] !== SLACK_OWNER_USER_ID) { // skip self
            const nameMatch = fm[1].match(new RegExp(`(\\S[^(]+?)\\s*\\(ID:\\s*${uid[1]}`));
            add(uid[1], `DM: ${nameMatch ? nameMatch[1].trim() : uid[1]}`);
          }
        }
      }
      await sleep(PAUSE_MS);
    } catch (err) {
      console.log(`  Search error: ${err.message.slice(0, 60)}`);
    }
  }
  console.log(`  Found ${targets.length} channels from Slack search`);

  // === Phase 2: Add any from brain that Slack search missed ===
  console.log('Phase 2: Adding channels from brain...');
  const brainBefore = targets.length;

  // DM partners from brain
  const dmPartners = db.prepare(`
    SELECT source_channel, participants, count(*) as c FROM memories
    WHERE source='slack' AND (source_channel LIKE 'DM with %' OR source_channel LIKE 'Group DM%')
    GROUP BY source_channel ORDER BY c DESC LIMIT 30
  `).all();

  for (const row of dmPartners) {
    const parts = (row.participants || '').split(',').map(s => s.trim());
    const other = parts.find(p => !isOwner(p));
    if (other && !targets.some(t => t.name.includes(other))) {
      const id = await resolveTarget(other);
      if (id) { add(id, `DM: ${other}`); await sleep(500); }
    }
  }

  // Named channels from brain
  const brainChannels = db.prepare(`
    SELECT DISTINCT source_channel FROM memories
    WHERE source='slack' AND source_channel LIKE '#%'
    ORDER BY source_channel
  `).all();

  for (const row of brainChannels) {
    if (!targets.some(t => t.name === row.source_channel)) {
      const name = row.source_channel.replace('#', '');
      const id = await resolveTarget(name);
      if (id) { add(id, row.source_channel); await sleep(500); }
    }
  }

  // Check metadata for channel IDs already stored
  try {
    const metaChannels = db.prepare(`
      SELECT DISTINCT json_extract(metadata, '$.channel_id') as cid,
             json_extract(metadata, '$.channel') as cname
      FROM memories WHERE source='slack' AND json_extract(metadata, '$.channel_id') IS NOT NULL
    `).all();
    for (const row of metaChannels) {
      if (row.cid) add(row.cid, row.cname || row.cid);
    }
  } catch {}

  console.log(`  Added ${targets.length - brainBefore} from brain (total: ${targets.length})`);

  // Sort by most recent activity — pull active conversations first, stale ones last
  const activityMap = {};
  try {
    const rows = db.prepare(`
      SELECT source_channel, json_extract(metadata, '$.channel_id') as cid, max(timestamp) as latest
      FROM memories WHERE source='slack'
      GROUP BY coalesce(json_extract(metadata, '$.channel_id'), source_channel)
    `).all();
    for (const r of rows) {
      if (r.cid) activityMap[r.cid] = r.latest;
      if (r.source_channel) activityMap[r.source_channel] = r.latest;
    }
  } catch {}
  targets.sort((a, b) => {
    const aTime = activityMap[a.id] || activityMap[a.name] || '2000-01-01';
    const bTime = activityMap[b.id] || activityMap[b.name] || '2000-01-01';
    return bTime.localeCompare(aTime); // most recent first
  });
  console.log(`  Sorted by recent activity (most active first)`);

  return targets;
}

async function listMyChannels() {
  try {
    const result = await slackMcp.callSlackMcp('slack_search_channels', { query: 'growth' });
    console.log(extractText(result));
  } catch (e) { console.error('Error:', e.message); }
}

// ── Parsing ──

function parseMessages(text, defaultChannel) {
  const messages = [];
  const lines = text.split('\n');
  let current = null;

  for (const line of lines) {
    // Primary format: "=== Message from Sender Name (UserID) at YYYY-MM-DD HH:MM:SS TZ ==="
    const mcpMatch = line.match(/^=== Message from (.+?) \([A-Z0-9]+\) at (.+?) ===/);
    if (mcpMatch) {
      if (current && current.content) messages.push(current);
      const sender = mcpMatch[1].trim();
      current = { sender, channel: defaultChannel, timestamp: parseTs(mcpMatch[2]) || new Date().toISOString(), content: '', isOwner: isOwner(sender) };
      continue;
    }

    // Fallback: "**Sender Name** (timestamp)"
    const headerMatch = line.match(/^\*\*(.+?)\*\*\s*[\(-–]\s*(.+?)[\)]*$/);
    if (headerMatch) {
      if (current && current.content) messages.push(current);
      const sender = headerMatch[1].trim();
      current = { sender, channel: defaultChannel, timestamp: parseTs(headerMatch[2]) || new Date().toISOString(), content: '', isOwner: isOwner(sender) };
      continue;
    }

    // Extract message_ts and thread_ts from metadata lines
    if (current) {
      const msgTsMatch = line.match(/^Message[ _]TS:\s*(\d+\.\d+)/);
      if (msgTsMatch) { current.message_ts = msgTsMatch[1]; continue; }
      const threadTsMatch = line.match(/^Thread[ _]TS:\s*(\d+\.\d+)/i);
      if (threadTsMatch) { current.thread_ts = threadTsMatch[1]; continue; }
    }

    // Skip other metadata lines
    if (line.startsWith('Reactions:') || line.startsWith('Channel:') || line.startsWith('---')) continue;

    // Content line
    if (current && line.trim()) {
      current.content += (current.content ? '\n' : '') + line.trim();
    }
  }
  if (current && current.content) messages.push(current);
  return messages;
}

function extractText(result) {
  return (result?.content || []).filter(c => c.type === 'text').map(c => c.text).join('\n');
}

function isOwner(s) {
  if (!s) return false;
  const l = s.toLowerCase();
  const ownerHandle = SLACK_OWNER_HANDLE.split('.')[0].toLowerCase();
  const ownerName = WALLE_OWNER_NAME.toLowerCase();
  return l.includes(ownerHandle) || l.includes(ownerName) || l === 'you' || l === 'me';
}

function parseTs(ts) {
  if (!ts) return null;
  const cleaned = ts.replace(/[()]/g, '').trim();
  try { const d = new Date(cleaned); if (!isNaN(d.getTime())) return d.toISOString(); } catch {}
  return null;
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

function daysAgo(n) {
  const d = new Date();
  d.setDate(d.getDate() - n);
  return d.toISOString().slice(0, 10);
}

main().catch(err => { console.error('Fatal:', err.message); process.exit(1); });
