import { getRuntimeUrlResolver } from './runtime-url';
import { runtimeFetch } from './runtime-fetch';

interface TerminalWebSocketDescriptor {
  path: string;
  v?: number;
  enc?: string;
}

interface TerminalTransportCapability {
  preferred?: 'ws' | 'http' | 'sse';
  transports?: Array<'ws' | 'http' | 'sse'>;
  ws?: TerminalWebSocketDescriptor;
}

export interface TerminalSession {
  sessionId: string;
  cols: number;
  rows: number;
  capabilities?: {
    input?: TerminalTransportCapability;
    stream?: TerminalTransportCapability;
  };
}

export interface TerminalStreamEvent {
  type: 'connected' | 'data' | 'exit' | 'reconnecting';
  data?: string;
  exitCode?: number;
  signal?: number | null;
  attempt?: number;
  maxAttempts?: number;
  runtime?: 'node' | 'bun';
  ptyBackend?: string;
}

export interface CreateTerminalOptions {
  cwd: string;
  cols?: number;
  rows?: number;
}

export interface ConnectStreamOptions {
  maxRetries?: number;
  initialRetryDelay?: number;
  maxRetryDelay?: number;
  connectionTimeout?: number;
}

type TerminalControlMessage = {
  t: string;
  s?: string;
  c?: string;
  d?: string;
  f?: boolean;
  i?: number;
  r?: number;
  v?: number;
  exitCode?: number;
  signal?: number | null;
  runtime?: 'node' | 'bun';
  ptyBackend?: string;
};

type StreamSubscription = {
  token: symbol;
  sessionId: string;
  onEvent: (event: TerminalStreamEvent) => void;
  onError?: (error: Error, fatal?: boolean) => void;
  maxRetries: number;
  initialRetryDelay: number;
  maxRetryDelay: number;
  connectionTimeout: number;
  retryCount: number;
  connected: boolean;
  connectionTimeoutId: ReturnType<typeof setTimeout> | null;
};

const CONTROL_TAG_JSON = 0x01;
const WS_READY_STATE_OPEN = 1;
const WS_READY_STATE_CONNECTING = 0;
const DEFAULT_TERMINAL_WS_PATH = '/api/terminal/ws';
const WS_SEND_WAIT_MS = 1200;
const WS_RECONNECT_JITTER_MS = 250;
const WS_KEEPALIVE_INTERVAL_MS = 20000;
const WS_CONNECT_TIMEOUT_MS = 5000;
const GLOBAL_TERMINAL_TRANSPORT_STATE_KEY = '__openchamberTerminalTransportState';

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

const normalizeWebSocketPath = (pathValue: string): string => {
  return getRuntimeUrlResolver().websocket(pathValue);
};

const encodeControlFrame = (payload: TerminalControlMessage): Uint8Array => {
  const jsonBytes = textEncoder.encode(JSON.stringify(payload));
  const bytes = new Uint8Array(jsonBytes.length + 1);
  bytes[0] = CONTROL_TAG_JSON;
  bytes.set(jsonBytes, 1);
  return bytes;
};

const isWsTransportSupported = (capability: TerminalTransportCapability | null | undefined): boolean => {
  if (!capability) return false;
  const transports = capability.transports ?? [];
  const supportsTransport = transports.includes('ws') || capability.preferred === 'ws';
  return supportsTransport && typeof capability.ws?.path === 'string' && capability.ws.path.length > 0;
};

const getPreferredTerminalWsPath = (state: TerminalTransportGlobalState): string => (
  state.streamCapability?.ws?.path
  ?? state.inputCapability?.ws?.path
  ?? DEFAULT_TERMINAL_WS_PATH
);

const createTransportError = (code: string | undefined): Error => {
  switch (code) {
    case 'SESSION_NOT_FOUND':
      return new Error('Terminal session not found');
    case 'NOT_BOUND':
      return new Error('Terminal session is not bound');
    case 'WRITE_FAIL':
      return new Error('Failed to write to terminal');
    case 'RATE_LIMIT':
      return new Error('Terminal websocket is rate limited');
    case 'BAD_FRAME':
      return new Error('Terminal websocket protocol violation');
    default:
      return new Error('Terminal websocket error');
  }
};

class TerminalTransportManager {
  private socket: WebSocket | null = null;
  private socketUrl = '';
  private boundSessionId: string | null = null;
  private requestedSessionId: string | null = null;
  private openPromise: Promise<WebSocket | null> | null = null;
  private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
  private keepaliveInterval: ReturnType<typeof setInterval> | null = null;
  private closed = false;
  private subscriptions = new Map<symbol, StreamSubscription>();
  private activeSubscriptionToken: symbol | null = null;
  private replayCursorBySession = new Map<string, number>();

  configure(socketUrl: string): void {
    if (!socketUrl) {
      return;
    }

    if (this.socketUrl === socketUrl) {
      this.closed = false;
      if (!this.isConnectedOrConnecting()) {
        this.ensureConnected();
      }
      return;
    }

    this.socketUrl = socketUrl;
    this.closed = false;
    this.resetConnection();
    this.ensureConnected();
  }

  subscribe(
    sessionId: string,
    onEvent: (event: TerminalStreamEvent) => void,
    onError?: (error: Error, fatal?: boolean) => void,
    options?: ConnectStreamOptions
  ): () => void {
    const token = Symbol(sessionId);
    const subscription: StreamSubscription = {
      token,
      sessionId,
      onEvent,
      onError,
      maxRetries: options?.maxRetries ?? 3,
      initialRetryDelay: options?.initialRetryDelay ?? 1000,
      maxRetryDelay: options?.maxRetryDelay ?? 8000,
      connectionTimeout: options?.connectionTimeout ?? 10000,
      retryCount: 0,
      connected: false,
      connectionTimeoutId: null,
    };

    this.subscriptions.set(token, subscription);
    this.activeSubscriptionToken = token;
    this.boundSessionId = null;
    this.requestedSessionId = sessionId;
    this.ensureConnected();
    this.startConnectionTimeout(subscription);
    this.bindActiveSession();

    return () => {
      this.clearConnectionTimeout(subscription);
      this.subscriptions.delete(token);
      if (this.activeSubscriptionToken === token) {
        this.activeSubscriptionToken = null;
      }
      if (this.boundSessionId === sessionId) {
        this.boundSessionId = null;
      }
      if (this.requestedSessionId === sessionId) {
        this.requestedSessionId = null;
      }
      if (this.subscriptions.size === 0) {
        this.clearReconnectTimeout();
        this.resetConnection();
      }
    };
  }

  async sendInput(sessionId: string, data: string): Promise<boolean> {
    if (!sessionId || !data || this.closed || !this.socketUrl) {
      return false;
    }

    const socket = await this.getOpenSocket(WS_SEND_WAIT_MS);
    if (!socket || socket.readyState !== WS_READY_STATE_OPEN) {
      return false;
    }

    try {
      if (this.boundSessionId !== sessionId) {
        this.requestedSessionId = sessionId;
        socket.send(encodeControlFrame({ t: 'b', s: sessionId, r: this.replayCursorBySession.get(sessionId) ?? 0, v: 2 }));
      }
      socket.send(data);
      return true;
    } catch {
      this.handleSocketFailure(new Error('Terminal websocket send failed'));
      return false;
    }
  }

  unbindSession(sessionId: string): void {
    if (!sessionId) {
      return;
    }
    if (this.boundSessionId === sessionId) {
      this.boundSessionId = null;
    }
    if (this.requestedSessionId === sessionId) {
      this.requestedSessionId = null;
    }
  }

  close(): void {
    this.closed = true;
    this.clearReconnectTimeout();
    for (const subscription of this.subscriptions.values()) {
      this.clearConnectionTimeout(subscription);
    }
    this.resetConnection();
    this.socketUrl = '';
    this.subscriptions.clear();
    this.activeSubscriptionToken = null;
    this.replayCursorBySession.clear();
  }

  prime(): void {
    if (this.closed || !this.socketUrl || this.isConnectedOrConnecting()) {
      return;
    }

    this.ensureConnected();
  }

  isConnectedOrConnecting(socketUrl?: string): boolean {
    if (this.closed) {
      return false;
    }

    if (socketUrl && this.socketUrl !== socketUrl) {
      return false;
    }

    if (this.socket && (this.socket.readyState === WS_READY_STATE_OPEN || this.socket.readyState === WS_READY_STATE_CONNECTING)) {
      return true;
    }

    return this.openPromise !== null;
  }

  private getActiveSubscription(): StreamSubscription | null {
    if (!this.activeSubscriptionToken) {
      return null;
    }

    return this.subscriptions.get(this.activeSubscriptionToken) ?? null;
  }

  private startConnectionTimeout(subscription: StreamSubscription): void {
    this.clearConnectionTimeout(subscription);
    subscription.connectionTimeoutId = setTimeout(() => {
      if (this.getActiveSubscription()?.token !== subscription.token || subscription.connected) {
        return;
      }

      this.handleSocketFailure(new Error('Connection timeout'));
    }, subscription.connectionTimeout);
  }

  private clearConnectionTimeout(subscription: StreamSubscription): void {
    if (!subscription.connectionTimeoutId) {
      return;
    }

    clearTimeout(subscription.connectionTimeoutId);
    subscription.connectionTimeoutId = null;
  }

  private async getOpenSocket(waitMs: number): Promise<WebSocket | null> {
    if (this.socket && this.socket.readyState === WS_READY_STATE_OPEN) {
      return this.socket;
    }

    this.ensureConnected();

    if (this.socket && this.socket.readyState === WS_READY_STATE_OPEN) {
      return this.socket;
    }

    const opened = await Promise.race([
      this.openPromise ?? Promise.resolve(null),
      new Promise<null>((resolve) => {
        setTimeout(() => resolve(null), waitMs);
      }),
    ]);

    if (opened && opened.readyState === WS_READY_STATE_OPEN) {
      return opened;
    }

    if (this.socket && this.socket.readyState === WS_READY_STATE_OPEN) {
      return this.socket;
    }

    return null;
  }

  private ensureConnected(): void {
    if (this.closed || !this.socketUrl) {
      return;
    }

    if (this.socket && (this.socket.readyState === WS_READY_STATE_OPEN || this.socket.readyState === WS_READY_STATE_CONNECTING)) {
      return;
    }

    if (this.openPromise) {
      return;
    }

    this.clearReconnectTimeout();

    this.openPromise = new Promise<WebSocket | null>((resolve) => {
      let settled = false;
      let connectTimeout: ReturnType<typeof setTimeout> | null = null;

      const settle = (value: WebSocket | null) => {
        if (settled) {
          return;
        }
        settled = true;
        if (connectTimeout) {
          clearTimeout(connectTimeout);
          connectTimeout = null;
        }
        this.openPromise = null;
        resolve(value);
      };

      try {
        const socket = new WebSocket(this.socketUrl);
        socket.binaryType = 'arraybuffer';

        socket.onopen = () => {
          this.socket = socket;
          this.startKeepalive();
          settle(socket);
        };

        socket.onmessage = (event) => {
          void this.handleSocketMessage(event.data);
        };

        socket.onerror = () => {
          if (!this.closed && !this.getActiveSubscription()) {
            this.scheduleReconnect(new Error('Terminal websocket error'));
          }
        };

        socket.onclose = () => {
          if (this.socket === socket) {
            this.socket = null;
            this.boundSessionId = null;
            this.stopKeepalive();
            if (!this.closed) {
              this.scheduleReconnect(new Error('Terminal stream connection error'));
            }
          }
          settle(null);
        };

        this.socket = socket;

        connectTimeout = setTimeout(() => {
          if (socket.readyState === WS_READY_STATE_CONNECTING) {
            socket.close();
            settle(null);
          }
        }, WS_CONNECT_TIMEOUT_MS);
      } catch {
        settle(null);
        if (!this.closed) {
          this.scheduleReconnect(new Error('Terminal websocket open failed'));
        }
      }
    });
  }

  private bindActiveSession(): void {
    const activeSubscription = this.getActiveSubscription();
    if (!activeSubscription || !this.socket || this.socket.readyState !== WS_READY_STATE_OPEN) {
      return;
    }

    this.requestedSessionId = activeSubscription.sessionId;

    try {
      this.socket.send(encodeControlFrame({ t: 'b', s: activeSubscription.sessionId, r: this.replayCursorBySession.get(activeSubscription.sessionId) ?? 0, v: 2 }));
    } catch {
      this.handleSocketFailure(new Error('Terminal websocket bind failed'));
    }
  }

  private scheduleReconnect(error: Error): void {
    if (this.closed || !this.socketUrl || this.reconnectTimeout) {
      return;
    }

    const activeSubscription = this.getActiveSubscription();
    if (!activeSubscription) {
      return;
    }

    const attempt = activeSubscription.retryCount + 1;
    const initialDelay = activeSubscription.initialRetryDelay;
    const maxDelay = activeSubscription.maxRetryDelay;
    const maxRetries = activeSubscription.maxRetries;

    if (attempt > maxRetries) {
      this.clearConnectionTimeout(activeSubscription);
      activeSubscription.onError?.(error, true);
      return;
    }

    activeSubscription.retryCount = attempt;
    activeSubscription.connected = false;
    activeSubscription.onEvent({
      type: 'reconnecting',
      attempt,
      maxAttempts: maxRetries,
    });
    this.startConnectionTimeout(activeSubscription);

    const baseDelay = Math.min(initialDelay * Math.pow(2, Math.max(attempt - 1, 0)), maxDelay);
    const jitter = Math.floor(Math.random() * WS_RECONNECT_JITTER_MS);
    const delay = baseDelay + jitter;

    this.reconnectTimeout = setTimeout(() => {
      this.reconnectTimeout = null;
      this.ensureConnected();
      this.bindActiveSession();
    }, delay);
  }

  private clearReconnectTimeout(): void {
    if (!this.reconnectTimeout) {
      return;
    }

    clearTimeout(this.reconnectTimeout);
    this.reconnectTimeout = null;
  }

  private sendControl(payload: TerminalControlMessage): boolean {
    if (!this.socket || this.socket.readyState !== WS_READY_STATE_OPEN) {
      return false;
    }

    try {
      this.socket.send(encodeControlFrame(payload));
      return true;
    } catch {
      this.handleSocketFailure(new Error('Terminal websocket control send failed'));
      return false;
    }
  }

  private startKeepalive(): void {
    this.stopKeepalive();
    this.keepaliveInterval = setInterval(() => {
      if (this.closed) {
        return;
      }

      this.sendControl({ t: 'p', v: 2 });
    }, WS_KEEPALIVE_INTERVAL_MS);
  }

  private stopKeepalive(): void {
    if (!this.keepaliveInterval) {
      return;
    }

    clearInterval(this.keepaliveInterval);
    this.keepaliveInterval = null;
  }

  private async handleSocketMessage(messageData: unknown): Promise<void> {
    const bytes = await this.asUint8Array(messageData);
    if (bytes && bytes.length > 0 && bytes[0] === CONTROL_TAG_JSON) {
      this.handleControlMessage(bytes);
      return;
    }

    const text = await this.asText(messageData);
    if (!text) {
      return;
    }

    const activeSubscription = this.getActiveSubscription();
    if (!activeSubscription) {
      return;
    }

    activeSubscription.onEvent({ type: 'data', data: text });
  }

  private handleControlMessage(bytes: Uint8Array): void {
    if (bytes.length < 2) {
      return;
    }

    let payload: TerminalControlMessage;
    try {
      payload = JSON.parse(textDecoder.decode(bytes.subarray(1))) as TerminalControlMessage;
    } catch {
      this.handleSocketFailure(new Error('Terminal websocket control parse failed'));
      return;
    }

    const activeSubscription = this.getActiveSubscription();

    switch (payload.t) {
      case 'ok':
        this.bindActiveSession();
        return;
      case 'po':
        return;
      case 'd': {
        const sessionId = payload.s ?? this.boundSessionId ?? this.requestedSessionId;
        if (!activeSubscription || !sessionId || sessionId !== activeSubscription.sessionId) {
          return;
        }

        if (typeof payload.i === 'number' && Number.isFinite(payload.i)) {
          this.replayCursorBySession.set(sessionId, Math.max(this.replayCursorBySession.get(sessionId) ?? 0, Math.trunc(payload.i)));
        }

        if (typeof payload.d === 'string' && payload.d.length > 0) {
          activeSubscription.onEvent({ type: 'data', data: payload.d });
        }
        return;
      }
      case 'bok': {
        this.boundSessionId = payload.s ?? this.requestedSessionId;
        if (!activeSubscription) {
          return;
        }
        activeSubscription.retryCount = 0;
        activeSubscription.connected = true;
        this.clearConnectionTimeout(activeSubscription);
        activeSubscription.onEvent({
          type: 'connected',
          runtime: payload.runtime,
          ptyBackend: payload.ptyBackend,
        });
        return;
      }
      case 'x': {
        if (!activeSubscription) {
          this.boundSessionId = null;
          return;
        }

        if (payload.s && payload.s !== activeSubscription.sessionId) {
          return;
        }

        activeSubscription.connected = false;
        this.clearConnectionTimeout(activeSubscription);
        this.boundSessionId = null;
        this.requestedSessionId = null;
        this.replayCursorBySession.delete(activeSubscription.sessionId);
        activeSubscription.onEvent({
          type: 'exit',
          exitCode: payload.exitCode,
          signal: payload.signal ?? null,
        });
        return;
      }
      case 'e': {
        const error = createTransportError(payload.c);
        const isFatal = payload.f === true || payload.c === 'SESSION_NOT_FOUND';

        if (payload.c === 'NOT_BOUND' || payload.c === 'SESSION_NOT_FOUND') {
          this.boundSessionId = null;
        }

        if (activeSubscription) {
          activeSubscription.connected = false;
          if (isFatal) {
            this.clearConnectionTimeout(activeSubscription);
          }
          activeSubscription.onError?.(error, isFatal);
        }

        if (payload.f === true) {
          this.handleSocketFailure(error);
        }
        return;
      }
      default:
        return;
    }
  }

  private async asUint8Array(messageData: unknown): Promise<Uint8Array | null> {
    if (messageData instanceof ArrayBuffer) {
      return new Uint8Array(messageData);
    }

    if (messageData instanceof Uint8Array) {
      return messageData;
    }

    if (typeof Blob !== 'undefined' && messageData instanceof Blob) {
      const buffer = await messageData.arrayBuffer();
      return new Uint8Array(buffer);
    }

    return null;
  }

  private async asText(messageData: unknown): Promise<string> {
    if (typeof messageData === 'string') {
      return messageData;
    }

    const bytes = await this.asUint8Array(messageData);
    if (!bytes) {
      return '';
    }

    return textDecoder.decode(bytes);
  }

  private handleSocketFailure(error: Error): void {
    this.boundSessionId = null;
    this.requestedSessionId = null;
    this.resetConnection();
    this.scheduleReconnect(error);
  }

  private resetConnection(): void {
    this.openPromise = null;
    this.stopKeepalive();
    if (this.socket) {
      const socket = this.socket;
      this.socket = null;
      socket.onopen = null;
      socket.onmessage = null;
      socket.onerror = null;
      socket.onclose = null;
      if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CONNECTING) {
        socket.close();
      }
    }
    this.boundSessionId = null;
  }
}

type TerminalTransportGlobalState = {
  inputCapability: TerminalTransportCapability | null;
  streamCapability: TerminalTransportCapability | null;
  manager: TerminalTransportManager | null;
};

const getTerminalTransportGlobalState = (): TerminalTransportGlobalState => {
  const globalScope = globalThis as typeof globalThis & {
    [GLOBAL_TERMINAL_TRANSPORT_STATE_KEY]?: TerminalTransportGlobalState;
  };

  if (!globalScope[GLOBAL_TERMINAL_TRANSPORT_STATE_KEY]) {
    globalScope[GLOBAL_TERMINAL_TRANSPORT_STATE_KEY] = {
      inputCapability: null,
      streamCapability: null,
      manager: null,
    };
  }

  return globalScope[GLOBAL_TERMINAL_TRANSPORT_STATE_KEY];
};

const ensureTerminalTransportManager = (): TerminalTransportManager => {
  const globalState = getTerminalTransportGlobalState();
  if (!globalState.manager) {
    globalState.manager = new TerminalTransportManager();
  }
  return globalState.manager;
};

const applyTerminalTransportCapabilities = (capabilities: TerminalSession['capabilities'] | undefined): void => {
  const globalState = getTerminalTransportGlobalState();
  globalState.inputCapability = capabilities?.input ?? null;
  globalState.streamCapability = capabilities?.stream ?? null;

  if (!isWsTransportSupported(globalState.inputCapability) && !isWsTransportSupported(globalState.streamCapability)) {
    globalState.manager?.close();
    globalState.manager = null;
    return;
  }

  const socketUrl = normalizeWebSocketPath(getPreferredTerminalWsPath(globalState));
  if (!socketUrl) {
    return;
  }

  const manager = ensureTerminalTransportManager();
  manager.configure(socketUrl);
};

const sendTerminalInputHttp = async (sessionId: string, data: string): Promise<void> => {
  const response = await runtimeFetch(`/api/terminal/${sessionId}/input`, {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain' },
    body: data,
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({ error: 'Failed to send terminal input' }));
    throw new Error(error.error || 'Failed to send terminal input');
  }
};

export async function createTerminalSession(options: CreateTerminalOptions): Promise<TerminalSession> {
  const response = await runtimeFetch('/api/terminal/create', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      cwd: options.cwd,
      cols: options.cols ?? 80,
      rows: options.rows ?? 24,
    }),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({ error: 'Failed to create terminal' }));
    throw new Error(error.error || 'Failed to create terminal session');
  }

  const session = await response.json() as TerminalSession;
  applyTerminalTransportCapabilities(session.capabilities);
  return session;
}

const connectTerminalStreamViaSse = (
  sessionId: string,
  onEvent: (event: TerminalStreamEvent) => void,
  onError?: (error: Error, fatal?: boolean) => void,
  options: ConnectStreamOptions = {}
): (() => void) => {
  const maxRetries = options.maxRetries ?? 3;
  const initialRetryDelay = options.initialRetryDelay ?? 1000;
  const maxRetryDelay = options.maxRetryDelay ?? 8000;
  const connectionTimeout = options.connectionTimeout ?? 10000;

  let eventSource: EventSource | null = null;
  let retryCount = 0;
  let retryTimeout: ReturnType<typeof setTimeout> | null = null;
  let connectionTimeoutId: ReturnType<typeof setTimeout> | null = null;
  let isClosed = false;
  let terminalExited = false;

  const clearTimeouts = () => {
    if (retryTimeout) {
      clearTimeout(retryTimeout);
      retryTimeout = null;
    }
    if (connectionTimeoutId) {
      clearTimeout(connectionTimeoutId);
      connectionTimeoutId = null;
    }
  };

  const cleanup = () => {
    if (isClosed) {
      return;
    }

    isClosed = true;
    clearTimeouts();
    if (eventSource) {
      eventSource.close();
      eventSource = null;
    }
  };

  const handleError = (error: Error, isFatal: boolean) => {
    if (isClosed || terminalExited) {
      return;
    }

    if (retryCount < maxRetries && !isFatal) {
      retryCount += 1;
      const delay = Math.min(initialRetryDelay * Math.pow(2, retryCount - 1), maxRetryDelay);

      onEvent({
        type: 'reconnecting',
        attempt: retryCount,
        maxAttempts: maxRetries,
      });

      retryTimeout = setTimeout(() => {
        if (!isClosed && !terminalExited) {
          connect();
        }
      }, delay);
      return;
    }

    onError?.(error, true);
    cleanup();
  };

  const connect = () => {
    if (isClosed || terminalExited) {
      return;
    }

    if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
      return;
    }

    eventSource = new EventSource(getRuntimeUrlResolver().sse(`/api/terminal/${sessionId}/stream`));
    let opened = false;

    connectionTimeoutId = setTimeout(() => {
      if (!opened && eventSource?.readyState !== EventSource.OPEN) {
        eventSource?.close();
        handleError(new Error('Connection timeout'), false);
      }
    }, connectionTimeout);

    eventSource.onopen = () => {
      if (opened) {
        return;
      }

      opened = true;
      retryCount = 0;
      clearTimeouts();
      onEvent({ type: 'connected' });
    };

    eventSource.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data) as TerminalStreamEvent;

        if (data.type === 'exit') {
          getTerminalTransportGlobalState().manager?.unbindSession(sessionId);
          terminalExited = true;
          cleanup();
        }

        onEvent(data);
      } catch (error) {
        onError?.(error as Error, false);
      }
    };

    eventSource.onerror = () => {
      clearTimeouts();
      const isFatalError = terminalExited || eventSource?.readyState === EventSource.CLOSED;
      eventSource?.close();
      eventSource = null;

      if (!terminalExited) {
        handleError(new Error('Terminal stream connection error'), isFatalError);
      }
    };
  };

  connect();
  return cleanup;
};

export function connectTerminalStream(
  sessionId: string,
  onEvent: (event: TerminalStreamEvent) => void,
  onError?: (error: Error, fatal?: boolean) => void,
  options: ConnectStreamOptions = {}
): () => void {
  const globalState = getTerminalTransportGlobalState();
  if (!isWsTransportSupported(globalState.streamCapability)) {
    return connectTerminalStreamViaSse(sessionId, onEvent, onError, options);
  }

  const manager = ensureTerminalTransportManager();
  const socketUrl = normalizeWebSocketPath(getPreferredTerminalWsPath(globalState));
  if (!socketUrl) {
    return connectTerminalStreamViaSse(sessionId, onEvent, onError, options);
  }

  manager.configure(socketUrl);
  return manager.subscribe(sessionId, onEvent, onError, options);
}

export async function sendTerminalInput(
  sessionId: string,
  data: string
): Promise<void> {
  const globalState = getTerminalTransportGlobalState();
  if (globalState.manager && await globalState.manager.sendInput(sessionId, data)) {
    return;
  }

  await sendTerminalInputHttp(sessionId, data);
}

export async function resizeTerminal(
  sessionId: string,
  cols: number,
  rows: number
): Promise<void> {
  const response = await runtimeFetch(`/api/terminal/${sessionId}/resize`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ cols, rows }),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({ error: 'Failed to resize terminal' }));
    throw new Error(error.error || 'Failed to resize terminal');
  }
}

export async function closeTerminal(sessionId: string): Promise<void> {
  getTerminalTransportGlobalState().manager?.unbindSession(sessionId);

  const response = await runtimeFetch(`/api/terminal/${sessionId}`, {
    method: 'DELETE',
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({ error: 'Failed to close terminal' }));
    throw new Error(error.error || 'Failed to close terminal');
  }
}

export async function restartTerminalSession(
  currentSessionId: string,
  options: { cwd: string; cols?: number; rows?: number }
): Promise<TerminalSession> {
  getTerminalTransportGlobalState().manager?.unbindSession(currentSessionId);

  const response = await runtimeFetch(`/api/terminal/${currentSessionId}/restart`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      cwd: options.cwd,
      cols: options.cols ?? 80,
      rows: options.rows ?? 24,
    }),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({ error: 'Failed to restart terminal' }));
    throw new Error(error.error || 'Failed to restart terminal');
  }

  const session = await response.json() as TerminalSession;
  applyTerminalTransportCapabilities(session.capabilities);
  return session;
}

export async function forceKillTerminal(options: {
  sessionId?: string;
  cwd?: string;
}): Promise<void> {
  const response = await runtimeFetch('/api/terminal/force-kill', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(options),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({ error: 'Failed to force kill terminal' }));
    throw new Error(error.error || 'Failed to force kill terminal');
  }

  if (options.sessionId) {
    getTerminalTransportGlobalState().manager?.unbindSession(options.sessionId);
  }
}

export function disposeTerminalInputTransport(): void {
  const globalState = getTerminalTransportGlobalState();
  globalState.manager?.close();
  globalState.manager = null;
  globalState.inputCapability = null;
  globalState.streamCapability = null;
}

export function primeTerminalInputTransport(): void {
  const globalState = getTerminalTransportGlobalState();
  if (
    globalState.inputCapability &&
    globalState.streamCapability &&
    !isWsTransportSupported(globalState.inputCapability) &&
    !isWsTransportSupported(globalState.streamCapability)
  ) {
    return;
  }

  const preferredPath = getPreferredTerminalWsPath(globalState) || DEFAULT_TERMINAL_WS_PATH;
  const socketUrl = normalizeWebSocketPath(preferredPath);
  if (!socketUrl) {
    return;
  }

  const manager = ensureTerminalTransportManager();
  if (manager.isConnectedOrConnecting(socketUrl)) {
    return;
  }

  manager.configure(socketUrl);
  manager.prime();
}

const hotModule = (import.meta as ImportMeta & {
  hot?: {
    dispose: (callback: () => void) => void;
  };
}).hot;

if (hotModule) {
  hotModule.dispose(() => {
    disposeTerminalInputTransport();
  });
}
