import { WebSocket } from 'ws';

interface PendingRuntimeCommand {
  resolve: (value: string) => void;
  reject: (error: Error) => void;
  timeout: NodeJS.Timeout;
}

interface StartedRuntime {
  shutdown: () => Promise<void>;
}

export interface RuntimeCommandClient {
  connect: () => Promise<void>;
  disconnect: () => void;
  executeRuntimeCommand: (input?: string) => Promise<string>;
}

export interface GatewayRuntimeCommandClientOptions {
  url: string;
  token?: string;
  requestTimeoutMs?: number;
  websocketFactory?: (url: string) => WebSocket;
}

export class GatewayRuntimeCommandClient implements RuntimeCommandClient {
  private readonly url: string;
  private readonly token?: string;
  private readonly requestTimeoutMs: number;
  private readonly websocketFactory: (url: string) => WebSocket;

  private ws: WebSocket | null = null;
  private connectPromise: Promise<void> | null = null;
  private nextId = 1;
  private pending = new Map<number, PendingRuntimeCommand>();

  constructor(options: GatewayRuntimeCommandClientOptions) {
    const timeoutMs = options.requestTimeoutMs ?? 15_000;
    if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
      throw new Error('requestTimeoutMs must be a positive number');
    }
    this.url = options.url;
    this.token = options.token;
    this.requestTimeoutMs = timeoutMs;
    this.websocketFactory = options.websocketFactory ?? ((url) => new WebSocket(url));
  }

  get connected(): boolean {
    return this.ws?.readyState === WebSocket.OPEN;
  }

  async connect(): Promise<void> {
    if (this.connected) {
      return;
    }

    if (this.connectPromise) {
      return this.connectPromise;
    }

    this.connectPromise = this.openConnection();
    try {
      await this.connectPromise;
    } finally {
      this.connectPromise = null;
    }
  }

  disconnect(): void {
    const ws = this.ws;
    this.ws = null;
    this.rejectAllPending(new Error('Disconnected from gateway runtime command service'));
    if (ws) {
      ws.close(1000, 'TUI runtime bridge shutting down');
    }
  }

  async executeRuntimeCommand(input?: string): Promise<string> {
    if (!this.connected) {
      await this.connect();
    }

    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      throw new Error('Gateway connection is not open');
    }

    const id = this.nextId++;
    const commandArgs = (input ?? '').trim();
    const message = commandArgs ? `/runtime ${commandArgs}` : '/runtime';

    return new Promise<string>((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pending.delete(id);
        reject(new Error('Timed out waiting for /runtime command response'));
      }, this.requestTimeoutMs);

      this.pending.set(id, { resolve, reject, timeout });

      const payload: Record<string, unknown> = {
        id,
        method: 'agent.send',
        params: {
          message,
          metadata: {
            isCommand: true,
            command: 'runtime',
            ...(commandArgs ? { commandArgs } : {}),
          },
        },
      };

      this.ws?.send(JSON.stringify(payload), (error) => {
        if (!error) {
          return;
        }
        const pending = this.pending.get(id);
        if (!pending) {
          return;
        }
        clearTimeout(pending.timeout);
        this.pending.delete(id);
        pending.reject(error);
      });
    });
  }

  private async openConnection(): Promise<void> {
    const ws = this.websocketFactory(withToken(this.url, this.token));

    await new Promise<void>((resolve, reject) => {
      let settled = false;

      const onOpen = () => {
        cleanup();
        settled = true;
        this.ws = ws;
        this.ws.on('message', (raw) => this.handleMessage(raw.toString()));
        this.ws.on('close', () => {
          if (this.ws === ws) {
            this.ws = null;
            this.rejectAllPending(new Error('Gateway connection closed'));
          }
        });
        this.ws.on('error', () => {
          // close event handles pending rejection
        });
        resolve();
      };

      const onError = (error: Error) => {
        cleanup();
        settled = true;
        reject(error);
      };

      const onClose = (code: number, reason?: Buffer) => {
        cleanup();
        if (settled) {
          return;
        }
        settled = true;
        const reasonText = reason?.toString().trim();
        if (code === 4003) {
          reject(new Error('Gateway is locked by another client (code 4003)'));
          return;
        }
        reject(new Error(reasonText ? `Gateway closed connection: ${reasonText}` : 'Gateway closed before connection was established'));
      };

      const cleanup = () => {
        ws.off('open', onOpen);
        ws.off('error', onError);
        ws.off('close', onClose);
      };

      ws.once('open', onOpen);
      ws.once('error', onError);
      ws.once('close', onClose);
    });
  }

  private handleMessage(raw: string): void {
    let parsed: unknown;
    try {
      parsed = JSON.parse(raw);
    } catch {
      return;
    }

    if (!parsed || typeof parsed !== 'object') {
      return;
    }
    const message = parsed as {
      id?: unknown;
      event?: unknown;
      data?: unknown;
      error?: { message?: unknown };
      result?: unknown;
    };

    if (typeof message.id !== 'number') {
      return;
    }

    const pending = this.pending.get(message.id);
    if (!pending) {
      return;
    }

    if (typeof message.event === 'string') {
      if (message.event === 'done') {
        this.resolvePending(message.id, getDoneContent(message.data));
        return;
      }
      if (message.event === 'error') {
        this.rejectPending(message.id, new Error(getErrorMessage(message.data)));
      }
      return;
    }

    if (message.error && typeof message.error.message === 'string') {
      this.rejectPending(message.id, new Error(message.error.message));
      return;
    }

    if (message.result !== undefined) {
      const text = typeof message.result === 'string'
        ? message.result
        : JSON.stringify(message.result, null, 2);
      this.resolvePending(message.id, text);
    }
  }

  private resolvePending(id: number, value: string): void {
    const pending = this.pending.get(id);
    if (!pending) {
      return;
    }
    clearTimeout(pending.timeout);
    this.pending.delete(id);
    pending.resolve(value);
  }

  private rejectPending(id: number, error: Error): void {
    const pending = this.pending.get(id);
    if (!pending) {
      return;
    }
    clearTimeout(pending.timeout);
    this.pending.delete(id);
    pending.reject(error);
  }

  private rejectAllPending(error: Error): void {
    for (const [id, pending] of this.pending.entries()) {
      clearTimeout(pending.timeout);
      this.pending.delete(id);
      pending.reject(error);
    }
  }
}

export interface TuiRuntimeGatewayBridgeOptions {
  client: RuntimeCommandClient;
  startDaemon?: () => Promise<StartedRuntime>;
  startupTimeoutMs?: number;
  retryIntervalMs?: number;
  sleep?: (ms: number) => Promise<void>;
}

export class TuiRuntimeGatewayBridge {
  private readonly client: RuntimeCommandClient;
  private readonly startDaemon?: () => Promise<StartedRuntime>;
  private readonly startupTimeoutMs: number;
  private readonly retryIntervalMs: number;
  private readonly sleep: (ms: number) => Promise<void>;

  private startedRuntime: StartedRuntime | null = null;
  private ensurePromise: Promise<void> | null = null;

  constructor(options: TuiRuntimeGatewayBridgeOptions) {
    this.client = options.client;
    this.startDaemon = options.startDaemon;
    this.startupTimeoutMs = options.startupTimeoutMs ?? 10_000;
    this.retryIntervalMs = options.retryIntervalMs ?? 250;
    this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
  }

  async ensureReady(): Promise<void> {
    if (this.ensurePromise) {
      return this.ensurePromise;
    }
    this.ensurePromise = this.ensureReadyInner().finally(() => {
      this.ensurePromise = null;
    });
    return this.ensurePromise;
  }

  async executeRuntimeCommand(input?: string): Promise<string> {
    await this.ensureReady();
    return this.client.executeRuntimeCommand(input);
  }

  async shutdown(): Promise<void> {
    this.client.disconnect();
    if (this.startedRuntime) {
      await this.startedRuntime.shutdown();
      this.startedRuntime = null;
    }
  }

  private async ensureReadyInner(): Promise<void> {
    try {
      await this.client.connect();
      return;
    } catch (error) {
      const firstMessage = error instanceof Error ? error.message : String(error);

      if (!this.startDaemon) {
        throw new Error(`Gateway connection failed: ${firstMessage}`);
      }

      if (!this.startedRuntime) {
        try {
          this.startedRuntime = await this.startDaemon();
        } catch (startError) {
          if (!isAddressInUseError(startError)) {
            throw startError;
          }
          // Another process won the race to bind the gateway port.
          // Treat as attach race and continue with connect retries.
        }
      }

      const deadline = Date.now() + this.startupTimeoutMs;
      let lastMessage = firstMessage;
      while (Date.now() <= deadline) {
        try {
          await this.client.connect();
          return;
        } catch (retryError) {
          lastMessage = retryError instanceof Error ? retryError.message : String(retryError);
          await this.sleep(this.retryIntervalMs);
        }
      }

      throw new Error(`Gateway did not become ready after auto-start (${this.startupTimeoutMs}ms timeout). Last error: ${lastMessage}`);
    }
  }
}

function withToken(url: string, token?: string): string {
  if (!token) {
    return url;
  }

  const parsed = new URL(url);
  parsed.searchParams.set('token', token);
  return parsed.toString();
}

function getDoneContent(data: unknown): string {
  if (typeof data === 'string') {
    return data;
  }
  if (data && typeof data === 'object' && 'content' in data && typeof (data as { content?: unknown }).content === 'string') {
    return (data as { content: string }).content;
  }
  if (data === undefined || data === null) {
    return '';
  }
  return JSON.stringify(data, null, 2);
}

function getErrorMessage(data: unknown): string {
  if (typeof data === 'string') {
    return data;
  }
  if (data && typeof data === 'object' && 'message' in data && typeof (data as { message?: unknown }).message === 'string') {
    return (data as { message: string }).message;
  }
  return 'Runtime command failed';
}

function isAddressInUseError(error: unknown): boolean {
  if (!error || typeof error !== 'object') {
    return false;
  }
  if ('code' in error && (error as { code?: unknown }).code === 'EADDRINUSE') {
    return true;
  }
  if ('message' in error && typeof (error as { message?: unknown }).message === 'string') {
    return (error as { message: string }).message.includes('EADDRINUSE');
  }
  return false;
}
