/**
 * Session command handlers.
 *
 * These operate on sessions that already exist — listing them, showing server details,
 * closing, and restarting — plus the shared status/display helpers used across the CLI.
 * Creating sessions (single, config-file, and discovery connects) lives in connect.ts.
 */

import {
  OutputMode,
  isProcessAlive,
  getServerHost,
  redactHeaders,
  ClientError,
} from '../../lib/index.js';
import { DISCONNECTED_THRESHOLD_MS } from '../../lib/types.js';
import type { ServerConfig, ConnectionMode } from '../../lib/types.js';
import {
  formatOutput,
  formatSuccess,
  formatError,
  formatSessionLine,
  formatServerDetails,
  theme,
} from '../output.js';
import { withMcpClient, resolveAuthProfile } from '../helpers.js';
import { listAuthProfiles } from '../../lib/auth/profiles.js';
import {
  sessionExists,
  deleteSession,
  updateSession,
  consolidateSessions,
  getSession,
} from '../../lib/sessions.js';
import {
  startBridge,
  StartBridgeOptions,
  stopBridge,
  reconnectCrashedSessions,
} from '../../lib/bridge-manager.js';
import chalk from 'chalk';
import { createLogger } from '../../lib/logger.js';
import { getBridgeLogPath } from '../../lib/log-reader.js';

const logger = createLogger('sessions');

/**
 * Map the internal `connectionMode` enum to the public `--json` `stateless` field:
 * `true` for stateless connections, `false` for stateful ones, and `null` when the mode
 * is unknown / not yet determined. The field is always present, so consumers see a stable
 * schema (`stateless` is `true | false | null`, never absent on connected targets).
 */
export function statelessField(connectionMode: ConnectionMode | undefined): {
  stateless: boolean | null;
} {
  if (connectionMode === 'stateless') return { stateless: true };
  if (connectionMode === 'stateful') return { stateless: false };
  return { stateless: null };
}

export type DisplayStatus =
  | 'live'
  | 'connecting'
  | 'reconnecting'
  | 'disconnected'
  | 'crashed'
  | 'unauthorized'
  | 'expired';

/**
 * Determine bridge status for a session
 */
export function getBridgeStatus(session: {
  status?: string;
  pid?: number;
  lastSeenAt?: string;
}): DisplayStatus {
  if (session.status === 'unauthorized') {
    return 'unauthorized';
  }
  if (session.status === 'expired') {
    return 'expired';
  }
  // Transient states: connecting (initial) or reconnecting (after crash)
  if (session.status === 'connecting' || session.status === 'reconnecting') {
    return session.status;
  }
  if (!session.pid || !isProcessAlive(session.pid)) {
    return 'crashed';
  }
  // Bridge is alive — check if server is actually responding
  if (session.lastSeenAt) {
    const lastSeenMs = Date.now() - new Date(session.lastSeenAt).getTime();
    if (lastSeenMs > DISCONNECTED_THRESHOLD_MS) {
      return 'disconnected';
    }
  }
  return 'live';
}

/**
 * Format bridge status for display with dot indicator
 */
export function formatBridgeStatus(status: DisplayStatus): { dot: string; text: string } {
  switch (status) {
    case 'live':
      return { dot: theme.green('●'), text: theme.green('live') };
    case 'connecting':
      return { dot: theme.yellow('●'), text: theme.yellow('connecting') };
    case 'reconnecting':
      return { dot: theme.yellow('●'), text: theme.yellow('reconnecting') };
    case 'disconnected':
      return { dot: theme.yellow('●'), text: theme.yellow('disconnected') };
    case 'crashed':
      return { dot: theme.yellow('○'), text: theme.yellow('crashed') };
    case 'unauthorized':
      return { dot: theme.red('○'), text: theme.red('unauthorized') };
    case 'expired':
      return { dot: theme.red('○'), text: theme.red('expired') };
  }
}

/**
 * Format time ago in human-friendly way
 */
export function formatTimeAgo(isoDate: string | undefined): string {
  if (!isoDate) return '';

  const date = new Date(isoDate);
  const now = new Date();
  const diffMs = now.getTime() - date.getTime();
  const diffSecs = Math.floor(diffMs / 1000);
  const diffMins = Math.floor(diffSecs / 60);
  const diffHours = Math.floor(diffMins / 60);
  const diffDays = Math.floor(diffHours / 24);

  if (diffSecs < 60) return 'just now';
  if (diffMins < 60) return `${diffMins}m ago`;
  if (diffHours < 24) return `${diffHours}h ago`;
  if (diffDays === 1) return 'yesterday';
  if (diffDays < 7) return `${diffDays} days ago`;
  if (diffDays < 30) {
    const weeks = Math.floor(diffDays / 7);
    return `${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`;
  }
  const months = Math.floor(diffDays / 30);
  return `${months} ${months === 1 ? 'month' : 'months'} ago`;
}

/**
 * List active sessions and authentication profiles
 * Consolidates session state first (cleans up crashed bridges, removes expired sessions)
 */
export async function listSessionsAndAuthProfiles(options: {
  outputMode: OutputMode;
}): Promise<{ hasSessions: boolean }> {
  // Consolidate sessions first (cleans up crashed bridges, removes expired sessions)
  const consolidateResult = await consolidateSessions(false);
  const sessions = Object.values(consolidateResult.sessions);

  // Auto-restart crashed bridges in the background (fire-and-forget)
  reconnectCrashedSessions(consolidateResult.sessionsToRestart);

  // Load auth profiles from disk
  const profiles = await listAuthProfiles();

  if (options.outputMode === 'json') {
    // Add bridge status to JSON output. The persisted `connectionMode` enum (stored in
    // sessions.json) is mapped to the public `stateless` field here so the list output
    // matches `mcpc @<session>` and `mcpc connect` (null until the mode is known).
    const sessionsWithStatus = sessions.map(({ connectionMode, ...session }) => ({
      ...session,
      status: getBridgeStatus(session),
      ...statelessField(connectionMode),
    }));
    console.log(
      formatOutput(
        {
          sessions: sessionsWithStatus,
          profiles,
        },
        'json'
      )
    );
  } else {
    // Display sessions
    if (sessions.length === 0) {
      console.log(chalk.bold('No active MCP sessions.'));
      console.log(chalk.dim('  ↳ run: mcpc connect mcp.example.com @test'));
    } else {
      console.log(chalk.bold('MCP sessions:'));
      for (const session of sessions) {
        const status = getBridgeStatus(session);
        const { dot, text } = formatBridgeStatus(status);

        // Format status with time ago info (show for non-live states and stale live sessions)
        let statusStr = `${dot} ${text}`;
        if (session.lastSeenAt) {
          const lastSeenMs = Date.now() - new Date(session.lastSeenAt).getTime();
          const isStale = lastSeenMs > 5 * 60 * 1000; // 5 minutes
          if (status !== 'live' || isStale) {
            const timeAgo = formatTimeAgo(session.lastSeenAt);
            if (timeAgo) {
              statusStr += chalk.dim(`, ${timeAgo}`);
            }
          }
        }

        console.log(`  ${formatSessionLine(session)} ${statusStr}`);

        // Show recovery hints for non-live sessions
        if (status === 'unauthorized') {
          console.log(chalk.dim(`    ↳ run: mcpc ${session.name} restart`));
        } else if (status === 'crashed') {
          console.log(chalk.dim(`    ↳ run: mcpc ${session.name}`));
        } else if (status === 'expired') {
          console.log(chalk.dim(`    ↳ run: mcpc ${session.name} restart`));
        }
      }
    }

    // Display auth profiles
    console.log('');
    if (profiles.length === 0) {
      console.log(chalk.bold('No OAuth profiles.'));
      console.log(chalk.dim('  ↳ run: mcpc login mcp.example.com'));
    } else {
      console.log(chalk.bold('Saved OAuth profiles:'));
      for (const profile of profiles) {
        const hostStr = getServerHost(profile.serverUrl);
        const nameStr = theme.magenta(profile.name);
        const userStr = profile.userEmail || profile.userName || '';
        // Show refreshedAt if available, otherwise createdAt
        const timeAgo = formatTimeAgo(profile.refreshedAt || profile.createdAt);
        const timeLabel = profile.refreshedAt ? 'refreshed' : 'created';

        let line = `  ${hostStr} / ${nameStr}`;
        if (userStr) {
          line += chalk.dim(` (${userStr})`);
        }
        if (timeAgo) {
          line += chalk.dim(`, ${timeLabel} ${timeAgo}`);
        }
        console.log(line);
      }
    }
  }

  return { hasSessions: sessions.length > 0 };
}

/**
 * Close a session
 */
export async function closeSession(
  name: string,
  options: { outputMode: OutputMode }
): Promise<void> {
  try {
    // Check if session exists
    if (!(await sessionExists(name))) {
      throw new ClientError(`Session not found: ${name}`);
    }

    // Stop the bridge process (graceful: send IPC shutdown on Windows so
    // the bridge can send HTTP DELETE to the server before exiting)
    await stopBridge(name, { graceful: true });

    // Delete session record from storage
    await deleteSession(name);

    // Success!
    if (options.outputMode === 'human') {
      console.log(formatSuccess(`Session ${name} closed successfully\n`));
    } else {
      console.log(
        formatOutput(
          {
            sessionName: name,
            closed: true,
          },
          'json'
        )
      );
    }
  } catch (error) {
    if (options.outputMode === 'human') {
      console.error(formatError((error as Error).message));
    } else {
      console.error(
        formatOutput(
          {
            sessionName: name,
            closed: false,
            error: (error as Error).message,
          },
          'json'
        )
      );
    }
    throw error;
  }
}

/**
 * Get server instructions and capabilities (also used for help command)
 */
export async function showServerDetails(
  target: string,
  options: {
    outputMode: OutputMode;
    config?: string;
    headers?: string[];
    timeout?: number;
    verbose?: boolean;
    hideTarget?: boolean;
  }
): Promise<void> {
  await withMcpClient(target, options, async (client, context) => {
    const serverDetails = await client.getServerDetails();
    const { serverInfo, capabilities, instructions, protocolVersion, connectionMode } =
      serverDetails;

    // Get tools list (uses bridge cache when available, no extra server call)
    const cachedToolsResult = await client.listAllTools();
    const tools = cachedToolsResult.tools;

    if (options.outputMode === 'human') {
      console.log(formatServerDetails(serverDetails, target, tools));
    } else {
      // JSON output MUST match MCP InitializeResult structure!
      // See https://modelcontextprotocol.io/specification/2025-11-25/schema#initializeresult
      // Build _mcpc.server with redacted headers for security
      const server: ServerConfig = {
        ...context.serverConfig,
        ...(context.serverConfig?.headers && {
          headers: redactHeaders(context.serverConfig.headers),
        }),
      };

      // The bridge log path is useful debug context for callers — only meaningful for
      // session targets (those starting with "@"); ad-hoc URL/config targets have no
      // persistent bridge log. The size is deliberately not emitted: it's a snapshot
      // that goes stale as the bridge keeps writing, and callers can stat `logPath`
      // (or run `mcpc @<session> logs`) for a fresh value when they actually need it.
      let logPath: string | undefined;
      if (target.startsWith('@')) {
        logPath = getBridgeLogPath(target);
      }

      console.log(
        formatOutput(
          {
            _mcpc: {
              sessionName: context.sessionName,
              profileName: context.profileName,
              server,
              ...statelessField(connectionMode),
              ...(logPath && { logPath }),
            },
            protocolVersion,
            capabilities,
            serverInfo,
            instructions,
            ...(tools.length > 0 && { toolNames: tools.map((t) => t.name) }),
          },
          'json'
        )
      );
    }
  });
}

/**
 * Restart a session by stopping and restarting the bridge process
 */
export async function restartSession(
  name: string,
  options: { outputMode: OutputMode; verbose?: boolean }
): Promise<void> {
  try {
    // Get existing session
    const session = await getSession(name);

    if (!session) {
      throw new ClientError(`Session not found: ${name}`);
    }

    if (options.outputMode === 'human') {
      console.log(theme.yellow(`Restarting session ${name}...`));
    }

    // Stop the bridge (even if it's alive)
    try {
      await stopBridge(name);
    } catch {
      // Bridge may already be stopped
    }

    // Get server config from session
    const serverConfig = session.server;
    if (!serverConfig) {
      throw new ClientError(`Session ${name} has no server configuration`);
    }

    // Load headers from keychain if present
    const { readKeychainSessionHeaders } = await import('../../lib/auth/keychain.js');
    const headers = await readKeychainSessionHeaders(name);

    // Resolve auth profile: use stored profile, or auto-detect a "default" profile.
    // This handles the case where user creates a session without auth, then later runs
    // `mcpc login <server>` to create a default profile, and restarts the session.
    const hasExplicitAuthHeader = headers?.Authorization !== undefined;
    let profileName = session.profileName;
    if (!profileName && serverConfig.url && !hasExplicitAuthHeader && !session.x402) {
      profileName = await resolveAuthProfile(serverConfig.url, serverConfig.url, undefined, {
        sessionName: name,
      });
      if (profileName) {
        logger.debug(`Discovered auth profile "${profileName}" for session ${name}`);
        await updateSession(name, { profileName });
      }
    }

    // Start bridge process.
    // NOTE: Do NOT pass mcpSessionId on explicit restart — a restart starts a fresh session
    // rather than resuming the old one. Session resumption is only attempted on automatic
    // bridge restart (when the bridge crashes and the CLI detects it); if the server rejects
    // the session ID, the session is marked as expired.
    const bridgeOptions: StartBridgeOptions = {
      sessionName: name,
      serverConfig: { ...serverConfig, ...(headers && { headers }) },
      verbose: options.verbose || false,
      ...(headers && { headers }),
      ...(profileName && { profileName }),
      ...(session.proxy && { proxyConfig: session.proxy }),
      ...(session.x402 && { x402: session.x402 }),
      ...(session.insecure && { insecure: session.insecure }),
    };

    const { pid } = await startBridge(bridgeOptions);

    // Update session with new bridge PID and clear any expired/crashed status
    await updateSession(name, { pid, status: 'active' });
    logger.debug(`Session ${name} restarted with bridge PID: ${pid}`);

    // Success message
    if (options.outputMode === 'human') {
      console.log(formatSuccess(`Session ${name} restarted`));
      console.log(
        chalk.dim(
          'Note: previous session state was lost (e.g. added tools, resource subscriptions, async tasks)'
        )
      );
    }

    // Show server details (like when creating a session)
    await showServerDetails(name, {
      ...options,
      hideTarget: false,
    });
  } catch (error) {
    if (options.outputMode === 'human') {
      console.error(formatError((error as Error).message));
    } else {
      console.error(
        formatOutput(
          {
            sessionName: name,
            restarted: false,
            error: (error as Error).message,
          },
          'json'
        )
      );
    }
    throw error;
  }
}
