'use strict';
const fs = require('fs');
const path = require('path');
const brain = require('../brain');

const CLAUDE_DIR = path.join(process.env.HOME, '.claude');
const BATCH_SIZE = 100; // messages per API call
const RATE_LIMIT_MS = 1200; // Slack rate limit: ~1 req/sec

function getOwnerSlackId() {
  return process.env.SLACK_OWNER_USER_ID || null;
}

/**
 * Get fresh Slack token from Claude Code's credentials.
 */
function getSlackToken() {
  try {
    const creds = JSON.parse(fs.readFileSync(path.join(CLAUDE_DIR, '.credentials.json'), 'utf8'));
    const mcpOAuth = creds.mcpOAuth || {};
    // Look for any Slack entry with a token (try regardless of expiry — let API reject if truly dead)
    for (const [key, entry] of Object.entries(mcpOAuth)) {
      if (key.includes('slack') && entry.accessToken) {
        return entry.accessToken;
      }
    }
  } catch {}
  // Fallback: environment variable
  return process.env.SLACK_TOKEN || null;
}

/**
 * Call Slack Web API.
 */
async function slackApi(method, params = {}, token) {
  const url = new URL(`https://slack.com/api/${method}`);

  // For GET methods, add params to URL
  const getMethods = ['conversations.list', 'conversations.history', 'users.info', 'auth.test'];
  const isGet = getMethods.includes(method);

  let res;
  if (isGet) {
    Object.entries(params).forEach(([k, v]) => {
      if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
    });
    res = await fetch(url.toString(), {
      headers: { 'Authorization': `Bearer ${token}` },
      signal: AbortSignal.timeout(15000),
    });
  } else {
    res = await fetch(url.toString(), {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(params),
      signal: AbortSignal.timeout(15000),
    });
  }

  const data = await res.json();
  if (!data.ok) throw new Error(`Slack API ${method}: ${data.error}`);
  return data;
}

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

/**
 * Get or create the ingestion checkpoint.
 */
function getIngestCheckpoint() {
  const cp = brain.getCheckpoint('slack-ingest');
  if (cp && cp.metadata) {
    try { return JSON.parse(cp.metadata); } catch {}
  }
  return {
    phase: 'conversations', // 'conversations' -> 'history' -> 'done'
    conversations: [],      // list of {id, name, type} to process
    current_conv_index: 0,  // which conversation we're on
    current_oldest: null,   // oldest timestamp fetched for current conversation
    total_messages: 0,
    total_conversations: 0,
    started_at: new Date().toISOString(),
  };
}

function saveIngestCheckpoint(state) {
  brain.upsertCheckpoint('slack-ingest', {
    last_memory_at: new Date().toISOString(),
    metadata: JSON.stringify(state),
  });
}

/**
 * Phase 1: Discover all conversations (DMs, group DMs, channels the user is in).
 */
async function discoverConversations(token) {
  const conversations = [];
  let cursor = null;

  // Get DMs and group DMs
  for (const types of ['im', 'mpim']) {
    cursor = null;
    do {
      const params = { types, limit: 200 };
      if (cursor) params.cursor = cursor;

      const data = await slackApi('conversations.list', params, token);
      for (const ch of (data.channels || [])) {
        conversations.push({
          id: ch.id,
          name: ch.name || ch.user || ch.id,
          type: types,
          user: ch.user || null,
        });
      }
      cursor = data.response_metadata?.next_cursor || null;
      await sleep(RATE_LIMIT_MS);
    } while (cursor);
  }

  // Get channels the user is a member of
  cursor = null;
  do {
    const params = { types: 'public_channel,private_channel', limit: 200 };
    if (cursor) params.cursor = cursor;

    const data = await slackApi('conversations.list', params, token);
    for (const ch of (data.channels || [])) {
      if (ch.is_member) {
        conversations.push({
          id: ch.id,
          name: ch.name || ch.id,
          type: ch.is_private ? 'private_channel' : 'public_channel',
        });
      }
    }
    cursor = data.response_metadata?.next_cursor || null;
    await sleep(RATE_LIMIT_MS);
  } while (cursor);

  console.log(`[slack-ingest] Discovered ${conversations.length} conversations`);
  return conversations;
}

/**
 * Phase 2: Fetch history for a single conversation.
 * Returns number of messages ingested.
 */
async function fetchConversationHistory(conv, token, oldestTs) {
  let ingested = 0;
  let cursor = null;
  let hasMore = true;

  // Calculate 4 years ago in Unix timestamp
  const fourYearsAgo = String(Math.floor((Date.now() - 4 * 365.25 * 24 * 60 * 60 * 1000) / 1000));
  const oldest = oldestTs || fourYearsAgo;

  while (hasMore) {
    const params = {
      channel: conv.id,
      limit: BATCH_SIZE,
      oldest: oldest,
    };
    if (cursor) params.cursor = cursor;

    let data;
    try {
      data = await slackApi('conversations.history', params, token);
    } catch (err) {
      // Channel might not be accessible
      if (err.message.includes('channel_not_found') || err.message.includes('not_in_channel')) {
        console.log(`[slack-ingest] Skipping ${conv.name}: ${err.message}`);
        return ingested;
      }
      throw err;
    }

    for (const msg of (data.messages || [])) {
      if (!msg.text && !msg.attachments) continue;

      const isOwner = msg.user === getOwnerSlackId();
      const text = msg.text || '';
      if (!text.trim()) continue;

      // Build descriptive content
      const senderName = isOwner ? (process.env.WALLE_OWNER_NAME || 'Owner') : (msg.user || 'unknown');
      const content = `${senderName}: ${text}`;

      const result = brain.insertMemory({
        source: 'slack',
        source_id: `slack-${conv.id}-${msg.ts}`,
        source_channel: conv.name || conv.id,
        memory_type: isOwner ? 'message_sent' : 'message_received',
        direction: isOwner ? 'outbound' : (msg.user ? 'inbound' : 'observed'),
        participants: JSON.stringify([senderName]),
        content: content,
        timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
        metadata: JSON.stringify({
          source: 'slack_ingest',
          conv_type: conv.type,
          conv_name: conv.name,
          thread_ts: msg.thread_ts || null,
          reactions: msg.reactions ? msg.reactions.map(r => r.name) : [],
        }),
      });

      if (result) ingested++;
    }

    hasMore = data.has_more || false;
    cursor = data.response_metadata?.next_cursor || null;

    if (!cursor && data.messages && data.messages.length > 0) {
      // No cursor but has_more -- Slack pagination can be tricky, stop if no cursor
      hasMore = false;
    }

    await sleep(RATE_LIMIT_MS);
  }

  return ingested;
}

/**
 * Main ingestion loop. Call this from the daemon or manually.
 * Processes one batch (a few conversations) per call, then checkpoints.
 * Returns { done, messagesIngested, conversationsProcessed }
 */
async function runIngestBatch(opts = {}) {
  if (!getOwnerSlackId()) {
    const message = 'SLACK_OWNER_USER_ID env var required';
    console.log(`[slack-ingest] ${message}`);
    return { done: false, messagesIngested: 0, conversationsProcessed: 0, error: 'missing_env', message };
  }

  // Pre-check: try to refresh token if it's about to expire
  try {
    const slackMcp = require('../tools/slack-mcp');
    await slackMcp.ensureValidToken();
  } catch (e) {
    if (e.code !== 'MODULE_NOT_FOUND') console.log('[slack-ingest] Token refresh pre-check failed:', e.message);
  }

  const token = getSlackToken();
  if (!token) {
    console.log('[slack-ingest] No valid Slack token found. Use Slack in Claude Code first to refresh.');
    return { done: false, messagesIngested: 0, conversationsProcessed: 0, error: 'no_token' };
  }

  const state = getIngestCheckpoint();
  const batchSize = opts.conversationsPerBatch || 3; // Process 3 conversations per batch

  // Phase 1: Discover conversations if not done.
  // Wrap in try/catch so an auth-style failure (token_expired,
  // invalid_auth, account_inactive, ratelimited) returns the standard
  // error-shape instead of letting the exception escape — callers
  // (skill-planner, the runIngestBatch unit test) rely on the
  // {done, messagesIngested, conversationsProcessed} contract.
  if (state.phase === 'conversations' || state.conversations.length === 0) {
    console.log('[slack-ingest] Discovering conversations...');
    try {
      state.conversations = await discoverConversations(token);
    } catch (err) {
      const msg = err && err.message ? err.message : String(err);
      const code = /token_expired/i.test(msg) ? 'token_expired'
        : /invalid_auth/i.test(msg) ? 'invalid_auth'
        : /account_inactive/i.test(msg) ? 'account_inactive'
        : /ratelimited|rate_limited/i.test(msg) ? 'ratelimited'
        : 'discovery_failed';
      console.log(`[slack-ingest] Discovery failed (${code}): ${msg}`);
      return { done: false, messagesIngested: 0, conversationsProcessed: 0, error: code, message: msg };
    }
    state.total_conversations = state.conversations.length;
    state.phase = 'history';
    state.current_conv_index = 0;
    saveIngestCheckpoint(state);
    return { done: false, messagesIngested: 0, conversationsProcessed: 0, phase: 'discovered', total: state.conversations.length };
  }

  // Phase 2: Fetch history for conversations
  if (state.phase === 'history') {
    let messagesIngested = 0;
    let processed = 0;

    const startIdx = state.current_conv_index;
    const endIdx = Math.min(startIdx + batchSize, state.conversations.length);

    for (let i = startIdx; i < endIdx; i++) {
      const conv = state.conversations[i];
      console.log(`[slack-ingest] Processing ${conv.type} "${conv.name}" (${i + 1}/${state.conversations.length})`);

      try {
        const count = await fetchConversationHistory(conv, token, state.current_oldest);
        messagesIngested += count;
        console.log(`[slack-ingest] Ingested ${count} messages from "${conv.name}"`);
      } catch (err) {
        console.error(`[slack-ingest] Error on "${conv.name}":`, err.message);
        // Continue to next conversation
      }

      processed++;
      state.current_conv_index = i + 1;
      state.total_messages += messagesIngested;
      saveIngestCheckpoint(state);
    }

    const done = state.current_conv_index >= state.conversations.length;
    if (done) {
      state.phase = 'done';
      state.completed_at = new Date().toISOString();
      saveIngestCheckpoint(state);
      console.log(`[slack-ingest] COMPLETE. Total: ${state.total_messages} messages from ${state.total_conversations} conversations`);
    }

    return { done, messagesIngested, conversationsProcessed: processed, progress: `${state.current_conv_index}/${state.conversations.length}` };
  }

  // Already done
  return { done: true, messagesIngested: 0, conversationsProcessed: 0, phase: 'done' };
}

/**
 * Reset checkpoint to start over.
 */
function resetCheckpoint() {
  brain.upsertCheckpoint('slack-ingest', {
    last_memory_at: null,
    metadata: JSON.stringify({
      phase: 'conversations',
      conversations: [],
      current_conv_index: 0,
      total_messages: 0,
      total_conversations: 0,
      started_at: new Date().toISOString(),
    }),
  });
  console.log('[slack-ingest] Checkpoint reset');
}

/**
 * Get current progress.
 */
function getProgress() {
  const state = getIngestCheckpoint();
  return {
    phase: state.phase,
    conversations_total: state.total_conversations,
    conversations_processed: state.current_conv_index,
    messages_ingested: state.total_messages,
    started_at: state.started_at,
    completed_at: state.completed_at || null,
    percent: state.total_conversations > 0 ? Math.round(state.current_conv_index / state.total_conversations * 100) : 0,
  };
}

module.exports = { runIngestBatch, resetCheckpoint, getProgress, getSlackToken };
