'use strict';
const http = require('http');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const SLACK_MCP_URL = 'https://mcp.slack.com/mcp';
const CLIENT_ID = '1601185624273.8899143856786';
const CALLBACK_PORT = 3118; // Must match Slack's registered redirect_uri for this client_id
const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
const TOKEN_FILE = path.join(process.env.HOME, '.claude', 'wall-e-slack-token.json');

// ── Token persistence ──

function loadToken() {
  try {
    const data = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
    if (data.access_token) return data;
  } catch {}
  return null;
}

function saveToken(tokenData) {
  const dir = path.dirname(TOKEN_FILE);
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokenData, null, 2), { mode: 0o600 });
}

function clearToken() {
  try { fs.unlinkSync(TOKEN_FILE); } catch {}
}

// ── Token auto-refresh ──

/**
 * Refresh the Slack OAuth token using the stored refresh_token.
 * No user interaction needed — pure server-to-server HTTP call.
 * Returns the new token data or null on failure.
 */
async function refreshToken() {
  const existing = loadToken();
  if (!existing || !existing.refresh_token) {
    console.error('[slack-mcp] No refresh_token stored — cannot auto-refresh. Re-authenticate manually.');
    return null;
  }

  try {
    const resp = await fetch('https://slack.com/api/oauth.v2.user.access', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        client_id: CLIENT_ID,
        grant_type: 'refresh_token',
        refresh_token: existing.refresh_token,
      }),
    });
    const data = await resp.json();
    if (!data.ok) {
      console.error('[slack-mcp] Token refresh failed:', data.error);
      return null;
    }

    const expiresInSec = data.expires_in || 43200;
    const refreshed = {
      access_token: data.access_token,
      refresh_token: data.refresh_token || existing.refresh_token, // Slack may rotate refresh tokens
      team_id: existing.team_id,
      team_name: existing.team_name,
      user_id: existing.user_id,
      scope: data.scope || existing.scope,
      expiresAt: Date.now() + expiresInSec * 1000, // absolute epoch ms
    };
    saveToken(refreshed);
    console.log('[slack-mcp] Token refreshed (expires at', new Date(refreshed.expiresAt).toISOString(), ')');
    return refreshed;
  } catch (err) {
    console.error('[slack-mcp] Token refresh error:', err.message);
    return null;
  }
}

/**
 * Check if the token needs refreshing (within 2 hours of expiry).
 * Returns true if token is still valid (possibly after refresh).
 */
async function ensureValidToken() {
  const token = loadToken();
  if (!token || !token.access_token) return false;

  // Compute expiresAt — prefer the field directly, fall back to legacy format
  let expiresAt = token.expiresAt || 0;
  if (!expiresAt && token.obtained_at) {
    expiresAt = new Date(token.obtained_at).getTime() + (token.expires_in || 43200) * 1000;
  }

  const now = Date.now();
  const TWO_HOURS = 2 * 60 * 60 * 1000;

  if (now >= expiresAt) {
    console.log('[slack-mcp] Token expired — attempting auto-refresh...');
    const refreshed = await refreshToken();
    return !!refreshed;
  }

  if (now >= expiresAt - TWO_HOURS) {
    console.log('[slack-mcp] Token expiring soon — proactive refresh...');
    const refreshed = await refreshToken();
    // If refresh fails, current token may still work (not yet expired)
    return true;
  }

  return true;
}

// ── OAuth PKCE flow ──

function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
  return { verifier, challenge };
}

/**
 * Start OAuth flow: spins up a temporary callback server on port 3118
 * (the registered redirect_uri for this Slack client_id), opens browser,
 * and waits for Slack to redirect back with the auth code.
 *
 * The server runs inside the same Node process as CTM, so EDR/antivirus
 * won't flag it as a new suspicious process.
 */
async function authenticate(opts = {}) {
  const existing = loadToken();
  if (!opts.force && existing && existing.access_token && await isAuthenticated()) {
    return existing.access_token;
  }

  const { verifier, challenge } = generatePKCE();
  const state = crypto.randomBytes(16).toString('hex');

  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      server.close();
      reject(new Error('OAuth flow timed out (5min). Click Connect again.'));
    }, 300000);

    const server = http.createServer(async (req, res) => {
      const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
      if (url.pathname !== '/callback') { res.writeHead(404); res.end(); return; }

      const code = url.searchParams.get('code');
      const returnedState = url.searchParams.get('state');

      if (returnedState !== state) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(errorPage('OAuth state mismatch. Please try again.'));
        clearTimeout(timeout); server.close(); reject(new Error('State mismatch')); return;
      }
      if (!code) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(errorPage('No authorization code received.'));
        clearTimeout(timeout); server.close(); reject(new Error('No code')); return;
      }

      try {
        const tokenResp = await fetch('https://slack.com/api/oauth.v2.user.access', {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({ client_id: CLIENT_ID, code, code_verifier: verifier, redirect_uri: CALLBACK_URL }),
        });
        const tokenData = await tokenResp.json();
        if (!tokenData.ok) throw new Error(tokenData.error);

        const tokenInfo = {
          access_token: tokenData.access_token || tokenData.authed_user?.access_token,
          refresh_token: tokenData.refresh_token,
          team_id: tokenData.team?.id || tokenData.team_id,
          team_name: tokenData.team?.name,
          user_id: tokenData.user_id || tokenData.authed_user?.id,
          scope: tokenData.scope || tokenData.authed_user?.scope,
          obtained_at: new Date().toISOString(),
        };
        saveToken(tokenInfo);

        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(successPage());
        clearTimeout(timeout); server.close();
        console.log('[slack-mcp] OAuth completed — team:', tokenInfo.team_name);
        resolve(tokenInfo.access_token);
      } catch (err) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(errorPage('Token exchange failed: ' + err.message));
        clearTimeout(timeout); server.close(); reject(err);
      }
    });

    server.listen(CALLBACK_PORT, () => {
      const scopes = 'search:read.public,search:read.private,search:read.mpim,search:read.im,search:read.files,search:read.users,chat:write,channels:history,groups:history,mpim:history,im:history,canvases:read,users:read,users:read.email';
      const authUrl = `https://slack.com/oauth/v2_user/authorize?client_id=${CLIENT_ID}&scope=${encodeURIComponent(scopes)}&redirect_uri=${encodeURIComponent(CALLBACK_URL)}&state=${state}&code_challenge=${challenge}&code_challenge_method=S256`;
      console.log('[slack-mcp] OAuth callback listening on port', CALLBACK_PORT);

      const { execFile } = require('child_process');
      execFile('open', [authUrl], (err) => {
        if (err) console.error('[slack-mcp] Failed to open browser:', err.message);
      });
    });

    server.on('error', (err) => {
      clearTimeout(timeout);
      if (err.code === 'EADDRINUSE') {
        reject(new Error(`Port ${CALLBACK_PORT} is in use. Close any other OAuth flows and try again.`));
      } else {
        reject(new Error('OAuth callback server failed: ' + err.message));
      }
    });
  });
}

function successPage() {
  return '<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0d1117;color:#e6edf3"><h2 style="color:#3fb950">Wall-E connected to Slack!</h2><p style="color:#8b949e">You can close this tab.</p></body></html>';
}

function errorPage(msg) {
  return '<html><body style="font-family:system-ui;text-align:center;padding:60px;background:#0d1117;color:#e6edf3"><h2 style="color:#f85149">Slack Connection Failed</h2><p style="color:#8b949e">' + msg + '</p></body></html>';
}

// ── MCP HTTP client ──

/**
 * Call a Slack MCP tool via the HTTP MCP protocol.
 * Handles authentication automatically.
 */
async function callSlackMcp(toolName, args = {}) {
  // Proactively refresh if token is expiring soon
  await ensureValidToken();

  let token = loadToken()?.access_token;
  if (!token) {
    throw new Error('Not authenticated with Slack. Run: node tools/slack-mcp.js auth');
  }

  const body = {
    jsonrpc: '2.0',
    id: crypto.randomUUID(),
    method: 'tools/call',
    params: { name: toolName, arguments: args },
  };

  let resp = await fetch(SLACK_MCP_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify(body),
  });

  if (resp.status === 401) {
    // Token expired mid-flight — try auto-refresh once
    console.log('[slack-mcp] Got 401 — attempting auto-refresh...');
    const refreshed = await refreshToken();
    if (refreshed) {
      token = refreshed.access_token;
      resp = await fetch(SLACK_MCP_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
        body: JSON.stringify(body),
      });
    }
    if (resp.status === 401) {
      clearToken();
      throw new Error('Slack token expired and auto-refresh failed. Reconnect Slack from Settings.');
    }
  }

  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(`Slack MCP error (${resp.status}): ${text.slice(0, 500)}`);
  }

  const result = await resp.json();
  if (result.error) {
    throw new Error(`MCP error: ${JSON.stringify(result.error)}`);
  }

  return result.result;
}

/**
 * List available tools on the Slack MCP server.
 */
async function listSlackTools() {
  let token = loadToken()?.access_token;
  if (!token) {
    throw new Error('Not authenticated with Slack');
  }

  const body = {
    jsonrpc: '2.0',
    id: crypto.randomUUID(),
    method: 'tools/list',
    params: {},
  };

  const resp = await fetch(SLACK_MCP_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
    },
    body: JSON.stringify(body),
  });

  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(`Slack MCP error (${resp.status}): ${text.slice(0, 500)}`);
  }

  const result = await resp.json();
  return result.result?.tools || [];
}

/**
 * Check if we have a valid Slack token.
 * Also attempts auto-refresh if the token is expired but refresh_token is available.
 */
async function isAuthenticated() {
  const token = loadToken();
  if (!token || !token.access_token) return false;

  // Compute expiresAt — prefer the field directly, fall back to legacy format
  let expiresAt = token.expiresAt || 0;
  if (!expiresAt && token.obtained_at) {
    expiresAt = new Date(token.obtained_at).getTime() + (token.expires_in || 43200) * 1000;
  }

  if (Date.now() >= expiresAt) {
    if (token.refresh_token) {
      const refreshed = await refreshToken();
      return !!refreshed;
    }
    return false;
  }
  return true;
}

/** Sync check — use when async isn't possible */
function isAuthenticatedSync() {
  const token = loadToken();
  return !!(token && token.access_token);
}

// ── CLI ──

if (require.main === module) {
  const cmd = process.argv[2];
  if (cmd === 'auth') {
    authenticate().then(token => {
      console.log('[slack-mcp] Authenticated successfully!');
      console.log('[slack-mcp] Token saved to:', TOKEN_FILE);
    }).catch(err => {
      console.error('[slack-mcp] Auth failed:', err.message);
      process.exit(1);
    });
  } else if (cmd === 'tools') {
    listSlackTools().then(tools => {
      tools.forEach(t => console.log(`- ${t.name}: ${(t.description || '').slice(0, 100)}`));
    }).catch(err => {
      console.error('Error:', err.message);
    });
  } else if (cmd === 'call') {
    const tool = process.argv[3];
    const args = process.argv[4] ? JSON.parse(process.argv[4]) : {};
    callSlackMcp(tool, args).then(result => {
      console.log(JSON.stringify(result, null, 2));
    }).catch(err => {
      console.error('Error:', err.message);
    });
  } else if (cmd === 'status') {
    const token = loadToken();
    if (token) {
      console.log('Authenticated:', true);
      console.log('Team:', token.team_name || 'unknown');
      console.log('User:', token.user_id || 'unknown');
      console.log('Obtained:', token.obtained_at || 'unknown');
    } else {
      console.log('Authenticated:', false);
      console.log('Run: node tools/slack-mcp.js auth');
    }
  } else {
    console.log('Usage:');
    console.log('  node tools/slack-mcp.js auth     — Authenticate with Slack');
    console.log('  node tools/slack-mcp.js status   — Check auth status');
    console.log('  node tools/slack-mcp.js tools    — List available tools');
    console.log('  node tools/slack-mcp.js call <tool> [args-json]  — Call a tool');
  }
}

module.exports = { authenticate, callSlackMcp, listSlackTools, isAuthenticated, isAuthenticatedSync, loadToken, clearToken, refreshToken, ensureValidToken };
