/**
 * Slack OAuth for the native MCP server (https://mcp.slack.com/mcp).
 *
 * Slack's native MCP authenticates with a confidential OAuth 2.0 *user* token
 * (xoxp-), minted from a registered app's client_id/client_secret. This is a
 * different credential from the xoxc- browser session tokens nomos captures for
 * the in-process Slack tools, so it lives in its own integration row.
 *
 * Mirrors the Google OAuth token lifecycle in `src/auth/google-integration.ts`,
 * minus refresh — Slack user tokens are long-lived and have no refresh endpoint.
 *
 * Token resolution (getSlackMcpToken): integration `slack-oauth:<userId>` →
 * env `SLACK_MCP_USER_TOKEN` (a paste-a-token fast path for testing).
 */

import { getIntegration, upsertIntegration } from "../db/integrations.ts";
import { createLogger } from "../lib/logger.ts";

const log = createLogger("slack-oauth");

/** Slack's OAuth endpoints for a user-token grant. */
export const SLACK_AUTHORIZE_URL = "https://slack.com/oauth/v2/authorize";

/**
 * OAuth **user** scopes the native MCP tools need (superset). Requested as
 * `user_scope` — the native MCP acts as the user, not a bot.
 * @see https://docs.slack.dev/ai/slack-mcp-server/
 */
export const SLACK_MCP_USER_SCOPES = [
  "search:read.public",
  "search:read.private",
  "search:read.im",
  "search:read.mpim",
  "search:read.users",
  "search:read.files",
  "chat:write",
  "channels:history",
  "channels:read",
  "groups:history",
  "groups:read",
  "im:history",
  "mpim:history",
  "users:read",
  "users:read.email",
  "reactions:write",
  "emoji:read",
  "files:read",
  "canvases:read",
  "canvases:write",
];

/** Integration row name holding a user's native-MCP OAuth token. */
function integrationName(userId: string): string {
  return `slack-oauth:${userId}`;
}

/**
 * The app's OAuth client credentials. Read from the `slack-oauth` integration
 * secrets first (Settings UI / OAuth deposit), then env. Throws if unset — the
 * connect flow can't run without them.
 */
export async function slackOAuthClientCreds(): Promise<{
  clientId: string;
  clientSecret: string;
}> {
  // Prefer the DB integration, then the MCP-specific env vars, then the generic
  // Slack app creds (so an existing SLACK_CLIENT_ID/SECRET app can be reused).
  let clientId = process.env.SLACK_OAUTH_CLIENT_ID || process.env.SLACK_CLIENT_ID;
  let clientSecret = process.env.SLACK_OAUTH_CLIENT_SECRET || process.env.SLACK_CLIENT_SECRET;
  try {
    const integ = await getIntegration("slack-oauth");
    clientId = integ?.secrets.client_id || clientId;
    clientSecret = integ?.secrets.client_secret || clientSecret;
  } catch {
    // DB not available — env only
  }
  if (!clientId || !clientSecret) {
    throw new Error(
      "Slack native MCP requires an app client_id/client_secret " +
        "(SLACK_OAUTH_CLIENT_ID / SLACK_OAUTH_CLIENT_SECRET, or the slack-oauth integration).",
    );
  }
  return { clientId, clientSecret };
}

/** True if OAuth client creds are configured (sync/env-only fast check). */
export function isSlackOAuthConfiguredSync(): boolean {
  return Boolean(process.env.SLACK_OAUTH_CLIENT_ID && process.env.SLACK_OAUTH_CLIENT_SECRET);
}

/** Build the Slack consent URL for a native-MCP connect. */
export function buildSlackAuthorizeUrl(opts: {
  clientId: string;
  redirectUri: string;
  state: string;
  scopes?: string[];
}): string {
  const params = new URLSearchParams({
    client_id: opts.clientId,
    // Native MCP acts as the user, so we request user_scope (not bot scope).
    user_scope: (opts.scopes ?? SLACK_MCP_USER_SCOPES).join(","),
    redirect_uri: opts.redirectUri,
    state: opts.state,
  });
  return `${SLACK_AUTHORIZE_URL}?${params.toString()}`;
}

export interface SlackOAuthResult {
  accessToken: string;
  scopes: string;
  teamId: string;
  teamName: string;
  userId: string;
}

/**
 * Exchange an authorization `code` for a user access token via Slack's
 * `oauth.v2.access` endpoint. Returns the `authed_user` token (xoxp-).
 */
export async function exchangeSlackCode(opts: {
  code: string;
  redirectUri: string;
}): Promise<SlackOAuthResult> {
  const { clientId, clientSecret } = await slackOAuthClientCreds();
  const { WebClient } = await import("@slack/web-api");
  const client = new WebClient();
  const result = await client.oauth.v2.access({
    client_id: clientId,
    client_secret: clientSecret,
    code: opts.code,
    redirect_uri: opts.redirectUri,
  });
  const authedUser = result.authed_user as
    | { access_token?: string; id?: string; scope?: string }
    | undefined;
  const team = result.team as { id?: string; name?: string } | undefined;
  if (!authedUser?.access_token) {
    throw new Error("Slack OAuth succeeded but no user access token was returned");
  }
  return {
    accessToken: authedUser.access_token,
    scopes: authedUser.scope ?? "",
    teamId: team?.id ?? "unknown",
    teamName: team?.name ?? "unknown",
    userId: authedUser.id ?? "unknown",
  };
}

/** Persist a user's native-MCP OAuth token (encrypted). */
export async function storeSlackOAuth(userId: string, r: SlackOAuthResult): Promise<void> {
  await upsertIntegration(integrationName(userId), {
    enabled: true,
    config: {
      provider: "slack-oauth",
      user_id: userId,
      team_id: r.teamId,
      team_name: r.teamName,
      slack_user_id: r.userId,
      scopes: r.scopes,
    },
    secrets: { access_token: r.accessToken },
    metadata: { connected_at: new Date().toISOString() },
  });
  log.info({ userId, teamId: r.teamId }, "stored Slack native-MCP OAuth token");
}

/**
 * The user token to send as `Authorization: Bearer` to mcp.slack.com. Reads the
 * per-user integration first, then the `SLACK_MCP_USER_TOKEN` env fast path.
 */
export async function getSlackMcpToken(userId: string): Promise<string | null> {
  // Explicit env injection wins — this is the `SLACK_ACCESS_TOKEN` bearer
  // convention (per-run injection, like the `${SLACK_ACCESS_TOKEN}` in a
  // .mcp.json). Env-authoritative on purpose: a freshly-minted token injected
  // here overrides a stale/revoked token still sitting in the DB.
  if (process.env.SLACK_ACCESS_TOKEN) return process.env.SLACK_ACCESS_TOKEN;
  if (process.env.SLACK_MCP_USER_TOKEN) return process.env.SLACK_MCP_USER_TOKEN;
  // DB reads. Transient failures deliberately PROPAGATE (we do NOT swallow to null)
  // so callers can distinguish a DB blip from a genuine "not connected". Swallowing
  // conflated the two, flipping the agent's Slack status between turns — which busts
  // the system-prompt cache and momentarily tells the agent it has no Slack tools.
  // Per-user token from `nomos slack connect-mcp` / OAuth deposit.
  const perUser = await getIntegration(integrationName(userId));
  if (perUser?.enabled && perUser.secrets.access_token) return perUser.secrets.access_token;
  // The user token minted when the app was authorized on the workspace — this IS the
  // SLACK_ACCESS_TOKEN and mcp.slack.com accepts it. Preferred over the manual-paste
  // global row so a live authorization always wins.
  const wsToken = await connectedWorkspaceUserToken();
  if (wsToken) return wsToken;
  // Global token pasted via the Settings UI (`slack-oauth` integration).
  const global = await getIntegration("slack-oauth");
  if (global?.enabled && global.secrets.access_token) return global.secrets.access_token;
  return null;
}

/**
 * The OAuth user token from a connected workspace (the token the "Authorize
 * Workspace" flow stores in `slack-ws:<teamId>`). Prefers the default-channel
 * workspace, else the first. Only returns an `xoxp-` OAuth token — browser
 * `xoxc-` session tokens are not accepted by mcp.slack.com.
 */
async function connectedWorkspaceUserToken(): Promise<string | null> {
  const { listWorkspaces } = await import("../db/slack-workspaces.ts");
  const workspaces = await listWorkspaces();
  if (workspaces.length === 0) return null;
  let chosen = workspaces[0];
  try {
    const { getNotificationDefault } = await import("../db/notification-defaults.ts");
    const nd = await getNotificationDefault();
    const prefix = "slack-user:";
    if (nd?.platform?.startsWith(prefix)) {
      const teamId = nd.platform.slice(prefix.length);
      chosen = workspaces.find((w) => w.team_id === teamId) ?? chosen;
    }
  } catch {
    // notification default not available — keep the first workspace
  }
  const token = chosen.access_token;
  return token && token.startsWith("xoxp-") ? token : null;
}

/** Remove a user's stored native-MCP OAuth token and close its warm proxy socket. */
export async function disconnectSlackMcp(userId: string): Promise<void> {
  // Capture the token before we clear it, so we can close the proxy connection.
  const existing = await getIntegration(integrationName(userId)).catch(() => null);
  const token = existing?.secrets.access_token;
  await upsertIntegration(integrationName(userId), { enabled: false, secrets: {} });
  if (token) {
    try {
      const { closeSlackMcp } = await import("../sdk/slack-mcp-proxy.ts");
      await closeSlackMcp(token);
    } catch {
      // proxy not loaded / already closed — non-fatal
    }
  }
}
