import type { IncomingMessage } from 'http';
import type { Duplex } from 'stream';
import WebSocket, { WebSocketServer } from 'ws';
import { FileSystemService } from '../services/FileSystemService';
import { HostTerminalService } from '../services/HostTerminalService';
import { ROLE_PERMISSIONS } from '../middleware/permissions';
import type { UserRole } from '../services/DatabaseService';
import { getErrorMessage } from '../utils/errors';
import { rejectUpgrade as reject } from './reject';
import { isConsoleSessionScope } from '../helpers/consoleSession';

interface HostConsoleContext {
  nodeId: number;
  decoded: { scope?: string; username?: string; acting_as?: string };
  isProxyToken: boolean;
  wsResolvedUser: { username: string; role: UserRole; token_version: number } | undefined;
  stackParam: string | null;
}

/**
 * Handle `/api/system/host-console` WebSocket upgrades.
 *
 * Enforces two gates before spawning the host PTY:
 *  1. Machine-credential rejection: node_proxy tokens cannot reach an
 *     interactive host shell directly (remote forwarding mints a
 *     console_session via POST /console-token instead).
 *  2. RBAC: user session tokens require the `system:console` permission.
 *     console_session tokens are pre-gated at issuance (see
 *     `routes/console.ts`) and skip this check.
 *
 * Path scoping and one-time jti consumption are enforced in upgradeHandler
 * before this handler runs.
 */
export function handleHostConsoleWs(
  req: IncomingMessage,
  socket: Duplex,
  head: Buffer,
  ctx: HostConsoleContext,
): void {
  const { nodeId, decoded, isProxyToken, wsResolvedUser, stackParam } = ctx;

  if (isProxyToken) return reject(socket, 403, 'Forbidden');

  const isConsoleSession = isConsoleSessionScope(decoded.scope);
  if (!isConsoleSession) {
    const userRole = wsResolvedUser?.role;
    if (!userRole || !ROLE_PERMISSIONS[userRole]?.includes('system:console')) {
      console.log('[HostConsole] Access denied: insufficient permissions', {
        username: wsResolvedUser?.username || decoded.username,
        role: userRole,
      });
      return reject(socket, 403, 'Forbidden');
    }
  }

  // Option B: console_session principal stays "console_session"; hub operator
  // is recorded separately as acting_as. Browser sessions use the real user.
  const consoleUsername = isConsoleSession
    ? 'console_session'
    : (wsResolvedUser?.username || decoded.username || 'unknown');
  const actingAs = isConsoleSession && typeof decoded.acting_as === 'string' && decoded.acting_as
    ? decoded.acting_as
    : null;

  console.log('[HostConsole] WebSocket upgrade accepted', {
    username: consoleUsername,
    actingAs,
    nodeId,
    stack: stackParam || '(root)',
  });

  // Client IP for the audit trail. Express's req.ip is unavailable on a raw
  // upgrade socket, so take the first x-forwarded-for hop and fall back to the
  // socket address.
  const forwarded = req.headers['x-forwarded-for'];
  const xff = typeof forwarded === 'string' ? forwarded.split(',')[0].trim() : '';
  const ipAddress = xff || req.socket.remoteAddress || '';

  const hostConsoleWss = new WebSocketServer({ noServer: true });
  hostConsoleWss.handleUpgrade(req, socket, head, (ws) => {
    hostConsoleWss.close();
    let targetDirectory: string;
    try {
      const baseDir = FileSystemService.getInstance(nodeId).getBaseDir();
      const resolved = HostTerminalService.resolveConsoleDirectory(baseDir, stackParam);
      if (resolved === null) {
        ws.send('Error: Invalid stack path\r\n');
        ws.close();
        return;
      }
      targetDirectory = resolved;
    } catch (error) {
      console.error('[HostConsole] Failed to resolve console directory', {
        user: consoleUsername,
        actingAs,
        nodeId,
        stack: stackParam || '(root)',
        error: getErrorMessage(error, 'unknown'),
      });
      if (ws.readyState === WebSocket.OPEN) {
        ws.send('Error: Failed to resolve console directory.\r\n');
        ws.close();
      }
      return;
    }
    const auditCtx = { username: consoleUsername, actingAs, nodeId, ipAddress };
    try {
      HostTerminalService.spawnTerminal(ws, targetDirectory, auditCtx);
    } catch (error) {
      console.error('[HostConsole] Unhandled spawn error:', {
        user: consoleUsername,
        actingAs,
        error: getErrorMessage(error, 'unknown'),
      });
      if (ws.readyState === WebSocket.OPEN) {
        ws.send('Error: Failed to start terminal session.\r\n');
        ws.close();
      }
    }
  });
}
