import * as plugins from '../../plugins.js';
import { HyperionMapper } from './hyperion.mapper.js';
import type {
  IHyperionCommandRequest,
  IHyperionConfig,
  IHyperionImageSnapshot,
  IHyperionJsonRpcRequest,
  IHyperionJsonRpcResponse,
  IHyperionRawData,
  IHyperionRefreshResult,
  IHyperionServerInfo,
  IHyperionSnapshot,
  IHyperionSystemInfo,
  THyperionImageFormat,
  THyperionSnapshotSource,
  THyperionTransport,
} from './hyperion.types.js';
import {
  hyperionDefaultJsonPort,
  hyperionDefaultOrigin,
  hyperionDefaultPriority,
  hyperionDefaultSslWebPort,
  hyperionDefaultTimeoutMs,
  hyperionDefaultWebPort,
  hyperionSolidEffect,
} from './hyperion.types.js';

interface IRequestOptions {
  allowError?: boolean;
  skipAuth?: boolean;
}

type TWebSocketEvent = { data?: unknown; message?: unknown; error?: unknown };
type TWebSocketHandler = (eventArg: TWebSocketEvent) => void;
type TWebSocketLike = {
  send(dataArg: string): void;
  close(): void;
  addEventListener?: (eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler) => void;
  removeEventListener?: (eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler) => void;
  onopen?: TWebSocketHandler | null;
  onmessage?: TWebSocketHandler | null;
  onerror?: TWebSocketHandler | null;
  onclose?: TWebSocketHandler | null;
};
type TWebSocketConstructor = new (urlArg: string) => TWebSocketLike;

export class HyperionApiError extends Error {}
export class HyperionApiConnectionError extends HyperionApiError {}
export class HyperionApiAuthorizationError extends HyperionApiError {}
export class HyperionUnsupportedCommandError extends HyperionApiError {}

export class HyperionClient {
  private currentSnapshot?: IHyperionSnapshot;
  private restorePoint?: IHyperionSnapshot;
  private tan = 1;

  constructor(private readonly config: IHyperionConfig) {
    this.currentSnapshot = config.snapshot ? HyperionMapper.toSnapshot({ config, source: 'snapshot', online: config.snapshot.online }) : undefined;
  }

  public async getSnapshot(forceRefreshArg = false): Promise<IHyperionSnapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return HyperionMapper.clone(this.currentSnapshot);
    }
    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = HyperionMapper.toSnapshot({ config: this.config, source: 'snapshot', online: this.config.snapshot.online });
      return HyperionMapper.clone(this.currentSnapshot);
    }
    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return HyperionMapper.clone(this.currentSnapshot);
    }
    if (!forceRefreshArg && this.config.rawData) {
      this.currentSnapshot = HyperionMapper.toSnapshot({ config: this.config, rawData: this.config.rawData, online: this.config.online ?? true, source: 'manual' });
      return HyperionMapper.clone(this.currentSnapshot);
    }
    if (this.hasEndpoint()) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return HyperionMapper.clone(this.currentSnapshot);
    }
    this.currentSnapshot = this.offlineSnapshot('Hyperion snapshots require config.host/config.url, an injected client, or static snapshot/rawData.');
    return HyperionMapper.clone(this.currentSnapshot);
  }

  public async refresh(): Promise<IHyperionRefreshResult> {
    try {
      this.currentSnapshot = undefined;
      const snapshot = await this.getSnapshot(this.hasEndpoint() || Boolean(this.config.client));
      const success = snapshot.online && !snapshot.error;
      return { success, snapshot, error: success ? undefined : snapshot.error, data: { source: snapshot.source } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: HyperionMapper.clone(snapshot), error };
    }
  }

  public async validateConnection(): Promise<IHyperionSnapshot> {
    const snapshot = await this.fetchSnapshot();
    if (!snapshot.device.id) {
      throw new HyperionApiConnectionError('Hyperion server did not provide a stable identifier.');
    }
    return snapshot;
  }

  public async request(payloadArg: IHyperionJsonRpcRequest, optionsArg: IRequestOptions = {}): Promise<IHyperionJsonRpcResponse> {
    if (this.config.client?.request && !this.hasEndpoint()) {
      const response = await this.config.client.request(this.withTan(payloadArg));
      return this.assertResponse(response, optionsArg);
    }
    const payload = this.withTan(payloadArg);
    const transport = this.transport();
    const response = transport === 'http'
      ? await this.requestHttp(payload, optionsArg)
      : transport === 'websocket'
        ? await this.requestWebSocket(payload, optionsArg)
        : await this.requestTcp(payload, optionsArg);
    return this.assertResponse(response, optionsArg);
  }

  public async execute(commandArg: IHyperionCommandRequest): Promise<unknown> {
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }
    if (this.config.client?.execute) {
      return this.config.client.execute(commandArg);
    }
    if (commandArg.action === 'refresh') {
      return (await this.refresh()).snapshot;
    }
    if (commandArg.action === 'restore') {
      await this.restore(commandArg.snapshot);
      return { restored: true };
    }
    if (!this.hasEndpoint()) {
      throw new HyperionApiConnectionError('Hyperion commands require config.host/config.url, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }
    const payload = this.payloadForCommand(commandArg);
    const response = await this.request(payload);
    if (commandArg.action === 'get_image_snapshot') {
      return response.info as IHyperionImageSnapshot | undefined;
    }
    return response.info ?? response;
  }

  public async snapshot(): Promise<IHyperionSnapshot> {
    this.restorePoint = await this.getSnapshot();
    return HyperionMapper.clone(this.restorePoint);
  }

  public async restore(snapshotArg = this.restorePoint): Promise<void> {
    if (!snapshotArg) {
      throw new HyperionUnsupportedCommandError('Hyperion restore requires a prior snapshot.');
    }
    for (const instance of snapshotArg.instances) {
      const priority = instance.priorities.find((priorityArg) => priorityArg.priority === snapshotArg.settings.priority) || instance.visiblePriority;
      if (!priority) {
        await this.execute({ action: 'clear', instance: instance.instance, priority: snapshotArg.settings.priority });
        continue;
      }
      if (priority.componentId === 'COLOR' && Array.isArray(priority.value?.RGB)) {
        await this.execute({ action: 'set_color', instance: instance.instance, color: priority.value.RGB, priority: priority.priority || snapshotArg.settings.priority, origin: priority.origin || snapshotArg.settings.origin });
      } else if (priority.componentId === 'EFFECT' && priority.owner) {
        await this.execute({ action: 'set_effect', instance: instance.instance, effect: priority.owner, priority: priority.priority || snapshotArg.settings.priority, origin: priority.origin || snapshotArg.settings.origin });
      }
    }
  }

  public async destroy(): Promise<void> {
    await this.config.client?.destroy?.();
  }

  private async snapshotFromClient(): Promise<IHyperionSnapshot> {
    const client = this.config.client;
    if (!client) {
      throw new HyperionApiConnectionError('No Hyperion client configured.');
    }
    if (client.getSnapshot) {
      const snapshot = await client.getSnapshot();
      if (this.isSnapshot(snapshot)) {
        return HyperionMapper.toSnapshot({ config: { ...this.config, snapshot }, source: 'client', online: snapshot.online });
      }
      return HyperionMapper.toSnapshot({ config: this.config, rawData: snapshot, online: true, source: 'client' });
    }
    if (client.getRawData) {
      return HyperionMapper.toSnapshot({ config: this.config, rawData: await client.getRawData(), online: true, source: 'client' });
    }
    throw new HyperionApiConnectionError('Hyperion client must expose getSnapshot() or getRawData().');
  }

  private async fetchSnapshot(): Promise<IHyperionSnapshot> {
    await this.assertAuthenticationConfigured();
    const source = this.transport() as THyperionSnapshotSource;
    const sysInfo = await this.request({ command: 'sysinfo' }).then((responseArg) => this.infoObject<IHyperionSystemInfo>(responseArg.info)).catch(() => undefined);
    const rootInfo = await this.request({ command: 'serverinfo', subcommand: 'getInfo', instance: this.config.instance }).then((responseArg) => this.infoObject<IHyperionServerInfo>(responseArg.info));
    const instanceServerInfo: Record<number, IHyperionServerInfo> = {};
    const instances = Array.isArray(rootInfo.instance) && rootInfo.instance.length ? rootInfo.instance : [{ instance: this.config.instance ?? 0, running: true }];
    for (const instance of instances) {
      if (typeof instance.instance !== 'number' || instance.running === false) {
        continue;
      }
      if (this.config.instance === instance.instance) {
        instanceServerInfo[instance.instance] = rootInfo;
        continue;
      }
      instanceServerInfo[instance.instance] = await this.request({ command: 'serverinfo', subcommand: 'getInfo', instance: instance.instance }).then((responseArg) => this.infoObject<IHyperionServerInfo>(responseArg.info)).catch(() => rootInfo);
    }
    const rawData: IHyperionRawData = {
      sysInfo,
      serverInfo: rootInfo,
      instanceServerInfo,
      fetchedAt: new Date().toISOString(),
    };
    return HyperionMapper.toSnapshot({ config: this.config, rawData, online: true, source });
  }

  private async assertAuthenticationConfigured(): Promise<void> {
    const response = await this.request({ command: 'authorize', subcommand: 'tokenRequired' }, { allowError: true, skipAuth: true }).catch(() => undefined);
    const info = this.infoObject<{ required?: boolean }>(response?.info);
    if (info.required && !this.config.token) {
      throw new HyperionApiAuthorizationError('Hyperion requires an authorization token.');
    }
  }

  private payloadForCommand(commandArg: IHyperionCommandRequest): IHyperionJsonRpcRequest {
    const instance = this.commandInstance(commandArg);
    const priority = this.priority(commandArg.priority);
    const origin = commandArg.origin || this.config.origin || hyperionDefaultOrigin;
    if (commandArg.action === 'set_color') {
      const color = commandArg.color?.slice(0, 3).map((valueArg) => this.colorByte(valueArg));
      if (!color || color.length < 3) {
        throw new HyperionUnsupportedCommandError('Hyperion set_color requires an RGB color array.');
      }
      return this.cleanPayload({ command: 'color', instance, color, priority, origin, duration: commandArg.duration });
    }
    if (commandArg.action === 'set_effect') {
      if (!commandArg.effect || commandArg.effect === hyperionSolidEffect) {
        return this.cleanPayload({ command: 'color', instance, color: [255, 255, 255], priority, origin, duration: commandArg.duration });
      }
      return this.cleanPayload({ command: 'effect', instance, effect: { name: commandArg.effect, args: commandArg.effectArgs }, priority, origin, duration: commandArg.duration });
    }
    if (commandArg.action === 'clear') {
      return this.cleanPayload({ command: 'clear', instance, priority });
    }
    if (commandArg.action === 'set_component') {
      if (!commandArg.component || typeof commandArg.state !== 'boolean') {
        throw new HyperionUnsupportedCommandError('Hyperion set_component requires component and boolean state.');
      }
      return this.cleanPayload({ command: 'componentstate', instance, componentstate: { component: commandArg.component, state: commandArg.state } });
    }
    if (commandArg.action === 'adjust_brightness') {
      if (typeof commandArg.brightness !== 'number') {
        throw new HyperionUnsupportedCommandError('Hyperion adjust_brightness requires brightness.');
      }
      return this.cleanPayload({ command: 'adjustment', instance, adjustment: { id: commandArg.adjustmentId || 'default', brightness: Math.max(0, Math.min(100, Math.round((commandArg.brightness * 100) / 255))) } });
    }
    if (commandArg.action === 'get_image_snapshot') {
      return this.cleanPayload({ command: 'instance-data', subcommand: 'getImageSnapshot', instance: this.singleInstance(commandArg), format: this.format(commandArg.format) });
    }
    if (commandArg.action === 'image_stream_start' || commandArg.action === 'image_stream_stop') {
      if (this.transport() === 'http') {
        throw new HyperionUnsupportedCommandError('Hyperion image streaming requires the TCP JSON server or WebSocket transport; HTTP JSON-RPC only supports still image snapshots.');
      }
      return this.cleanPayload({ command: 'ledcolors', subcommand: commandArg.action === 'image_stream_start' ? 'imagestream-start' : 'imagestream-stop', instance: this.singleInstance(commandArg) });
    }
    if (commandArg.action === 'raw_command') {
      if (!commandArg.payload) {
        throw new HyperionUnsupportedCommandError('Hyperion raw_command requires payload.');
      }
      return commandArg.payload;
    }
    throw new HyperionUnsupportedCommandError(`Unsupported Hyperion command: ${commandArg.action}`);
  }

  private async requestHttp(payloadArg: IHyperionJsonRpcRequest, optionsArg: IRequestOptions): Promise<IHyperionJsonRpcResponse> {
    const url = `${this.baseUrl('http')}/json-rpc`;
    let response: Response;
    try {
      response = await globalThis.fetch(url, {
        method: 'POST',
        headers: this.headers(optionsArg),
        body: JSON.stringify(payloadArg),
        signal: AbortSignal.timeout(this.timeoutMs()),
      });
    } catch (errorArg) {
      throw new HyperionApiConnectionError(`Connection to ${url} failed: ${this.errorMessage(errorArg)}`);
    }
    if (response.status === 401 || response.status === 403) {
      throw new HyperionApiAuthorizationError(`Hyperion authorization failed for ${url}: HTTP ${response.status}`);
    }
    if (!response.ok) {
      throw new HyperionApiConnectionError(`Hyperion request to ${url} failed: HTTP ${response.status}`);
    }
    try {
      return await response.json() as IHyperionJsonRpcResponse;
    } catch (errorArg) {
      throw new HyperionApiConnectionError(`Hyperion response from ${url} was not JSON: ${this.errorMessage(errorArg)}`);
    }
  }

  private async requestTcp(payloadArg: IHyperionJsonRpcRequest, optionsArg: IRequestOptions): Promise<IHyperionJsonRpcResponse> {
    const host = this.host();
    if (!host) {
      throw new HyperionApiConnectionError('Hyperion TCP transport requires host.');
    }
    const port = this.port('tcp');
    const timeoutMs = this.timeoutMs();
    const loginTan = this.nextTan();
    const loginPayload = { command: 'authorize', subcommand: 'login', token: this.config.token, tan: loginTan };
    const needsLogin = Boolean(this.config.token && !optionsArg.skipAuth);

    return new Promise<IHyperionJsonRpcResponse>((resolve, reject) => {
      let buffer = '';
      let loggedIn = !needsLogin;
      let settled = false;
      const socket = plugins.net.createConnection({ host, port });
      const timer = setTimeout(() => fail(new HyperionApiConnectionError(`Hyperion TCP request to ${host}:${port} timed out.`)), timeoutMs);
      const cleanup = () => {
        clearTimeout(timer);
        socket.removeAllListeners();
        socket.end();
        socket.destroy();
      };
      const fail = (errorArg: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        reject(errorArg);
      };
      const finish = (responseArg: IHyperionJsonRpcResponse) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        resolve(responseArg);
      };
      const send = (payloadToSendArg: IHyperionJsonRpcRequest) => socket.write(`${JSON.stringify(payloadToSendArg)}\n`);
      const handleResponse = (responseArg: IHyperionJsonRpcResponse) => {
        if (!loggedIn && responseArg.tan === loginTan) {
          if (responseArg.success === false) {
            fail(new HyperionApiAuthorizationError(this.responseErrorMessage(responseArg)));
            return;
          }
          loggedIn = true;
          send(payloadArg);
          return;
        }
        if (responseArg.tan === payloadArg.tan || responseArg.command === payloadArg.command || `${responseArg.command}` === `${payloadArg.command}-${payloadArg.subcommand}`) {
          finish(responseArg);
        }
      };
      socket.on('connect', () => send(needsLogin ? loginPayload : payloadArg));
      socket.on('data', (chunkArg) => {
        buffer += chunkArg.toString('utf8');
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';
        for (const line of lines) {
          if (!line.trim()) {
            continue;
          }
          try {
            handleResponse(JSON.parse(line) as IHyperionJsonRpcResponse);
          } catch (errorArg) {
            fail(new HyperionApiConnectionError(`Hyperion TCP response was not JSON: ${this.errorMessage(errorArg)}`));
          }
        }
      });
      socket.on('error', (errorArg) => fail(new HyperionApiConnectionError(`Hyperion TCP connection to ${host}:${port} failed: ${this.errorMessage(errorArg)}`)));
      socket.on('close', () => {
        if (!settled) {
          fail(new HyperionApiConnectionError(`Hyperion TCP connection to ${host}:${port} closed before response.`));
        }
      });
    });
  }

  private async requestWebSocket(payloadArg: IHyperionJsonRpcRequest, optionsArg: IRequestOptions): Promise<IHyperionJsonRpcResponse> {
    const WebSocketCtor = (globalThis as unknown as { WebSocket?: TWebSocketConstructor }).WebSocket;
    if (!WebSocketCtor) {
      throw new HyperionApiConnectionError('Global WebSocket is not available for Hyperion WebSocket transport.');
    }
    const url = `${this.baseUrl('websocket')}/json-rpc`;
    const loginTan = this.nextTan();
    const loginPayload = { command: 'authorize', subcommand: 'login', token: this.config.token, tan: loginTan };
    const needsLogin = Boolean(this.config.token && !optionsArg.skipAuth);
    return new Promise<IHyperionJsonRpcResponse>((resolve, reject) => {
      let loggedIn = !needsLogin;
      let settled = false;
      const socket: TWebSocketLike = new WebSocketCtor(url);
      const timer = setTimeout(() => fail(new HyperionApiConnectionError(`Hyperion WebSocket request to ${url} timed out.`)), this.timeoutMs());
      const cleanup = () => {
        clearTimeout(timer);
        this.removeWebSocketListener(socket, 'open');
        this.removeWebSocketListener(socket, 'message');
        this.removeWebSocketListener(socket, 'error');
        this.removeWebSocketListener(socket, 'close');
        try {
          socket.close();
        } catch {
          // Socket may already be closed by the runtime.
        }
      };
      const fail = (errorArg: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        reject(errorArg);
      };
      const finish = (responseArg: IHyperionJsonRpcResponse) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        resolve(responseArg);
      };
      const send = (payloadToSendArg: IHyperionJsonRpcRequest) => socket.send(JSON.stringify(payloadToSendArg));
      this.addWebSocketListener(socket, 'open', () => send(needsLogin ? loginPayload : payloadArg));
      this.addWebSocketListener(socket, 'message', (eventArg) => {
        const text = this.websocketData(eventArg.data ?? eventArg.message);
        if (!text) {
          return;
        }
        let response: IHyperionJsonRpcResponse;
        try {
          response = JSON.parse(text) as IHyperionJsonRpcResponse;
        } catch (errorArg) {
          fail(new HyperionApiConnectionError(`Hyperion WebSocket response was not JSON: ${this.errorMessage(errorArg)}`));
          return;
        }
        if (!loggedIn && response.tan === loginTan) {
          if (response.success === false) {
            fail(new HyperionApiAuthorizationError(this.responseErrorMessage(response)));
            return;
          }
          loggedIn = true;
          send(payloadArg);
          return;
        }
        if (response.tan === payloadArg.tan || response.command === payloadArg.command || `${response.command}` === `${payloadArg.command}-${payloadArg.subcommand}`) {
          finish(response);
        }
      });
      this.addWebSocketListener(socket, 'error', (eventArg) => fail(new HyperionApiConnectionError(`Hyperion WebSocket error: ${this.errorMessage(eventArg.error || eventArg)}`)));
      this.addWebSocketListener(socket, 'close', () => {
        if (!settled) {
          fail(new HyperionApiConnectionError(`Hyperion WebSocket ${url} closed before response.`));
        }
      });
    });
  }

  private assertResponse(responseArg: IHyperionJsonRpcResponse, optionsArg: IRequestOptions): IHyperionJsonRpcResponse {
    if (!optionsArg.allowError && responseArg.success === false) {
      const message = this.responseErrorMessage(responseArg);
      if (/authorization|auth|token/i.test(message)) {
        throw new HyperionApiAuthorizationError(message);
      }
      throw new HyperionApiError(message);
    }
    return responseArg;
  }

  private responseErrorMessage(responseArg: IHyperionJsonRpcResponse): string {
    const details = Array.isArray(responseArg.errorData) ? responseArg.errorData.map((errorArg) => errorArg.description).filter(Boolean).join('; ') : '';
    return [responseArg.error || 'Hyperion JSON-RPC request failed', details].filter(Boolean).join(': ');
  }

  private withTan(payloadArg: IHyperionJsonRpcRequest): IHyperionJsonRpcRequest {
    return { ...payloadArg, tan: typeof payloadArg.tan === 'number' ? payloadArg.tan : this.nextTan() };
  }

  private nextTan(): number {
    const tan = this.tan;
    this.tan = this.tan >= 999999 ? 1 : this.tan + 1;
    return tan;
  }

  private headers(optionsArg: IRequestOptions): Record<string, string> {
    const headers: Record<string, string> = { 'content-type': 'application/json', accept: 'application/json' };
    if (this.config.token && !optionsArg.skipAuth) {
      headers.authorization = `Bearer ${this.config.token}`;
    }
    return headers;
  }

  private cleanPayload(payloadArg: IHyperionJsonRpcRequest): IHyperionJsonRpcRequest {
    const clean: IHyperionJsonRpcRequest = { command: payloadArg.command };
    for (const [key, value] of Object.entries(payloadArg)) {
      if (value !== undefined) {
        clean[key] = value;
      }
    }
    return clean;
  }

  private commandInstance(commandArg: IHyperionCommandRequest): number[] | undefined {
    if (Array.isArray(commandArg.instance)) {
      return commandArg.instance;
    }
    if (typeof commandArg.instance === 'number') {
      return [commandArg.instance];
    }
    if (typeof this.config.instance === 'number') {
      return [this.config.instance];
    }
    return undefined;
  }

  private singleInstance(commandArg: IHyperionCommandRequest): number {
    if (typeof commandArg.instance === 'number') {
      return commandArg.instance;
    }
    if (Array.isArray(commandArg.instance) && typeof commandArg.instance[0] === 'number') {
      return commandArg.instance[0];
    }
    return this.config.instance ?? 0;
  }

  private priority(valueArg: unknown): number {
    const value = typeof valueArg === 'number' && Number.isFinite(valueArg) ? Math.round(valueArg) : this.config.priority ?? hyperionDefaultPriority;
    return Math.max(1, Math.min(253, value));
  }

  private colorByte(valueArg: unknown): number {
    const value = typeof valueArg === 'number' ? valueArg : Number(valueArg);
    return Math.max(0, Math.min(255, Math.round(Number.isFinite(value) ? value : 0)));
  }

  private format(valueArg: THyperionImageFormat | undefined): THyperionImageFormat {
    return valueArg === 'BMP' || valueArg === 'JPG' || valueArg === 'PNG' ? valueArg : 'PNG';
  }

  private infoObject<TInfo>(valueArg: unknown): TInfo {
    return valueArg && typeof valueArg === 'object' ? valueArg as TInfo : {} as TInfo;
  }

  private isSnapshot(valueArg: unknown): valueArg is IHyperionSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'instances' in valueArg);
  }

  private offlineSnapshot(errorArg: string): IHyperionSnapshot {
    return HyperionMapper.toSnapshot({ config: this.config, online: false, source: 'runtime', error: errorArg });
  }

  private hasEndpoint(): boolean {
    return Boolean(this.config.host || this.config.url);
  }

  private transport(): THyperionTransport {
    if (this.config.transport) {
      return this.config.transport;
    }
    const endpoint = this.endpoint();
    if (endpoint?.transport) {
      return endpoint.transport;
    }
    return 'tcp';
  }

  private host(): string | undefined {
    return this.config.host || this.endpoint()?.host;
  }

  private port(transportArg = this.transport()): number {
    const endpoint = this.endpoint();
    if (this.config.port) {
      return this.config.port;
    }
    if (endpoint?.port) {
      return endpoint.port;
    }
    if (transportArg === 'tcp') {
      return this.config.jsonPort || hyperionDefaultJsonPort;
    }
    return this.config.webPort || (this.ssl() ? hyperionDefaultSslWebPort : hyperionDefaultWebPort);
  }

  private ssl(): boolean {
    return this.config.ssl ?? this.config.tls ?? this.endpoint()?.ssl ?? false;
  }

  private baseUrl(transportArg: 'http' | 'websocket'): string {
    const host = this.host();
    if (!host) {
      throw new HyperionApiConnectionError(`Hyperion ${transportArg} transport requires host.`);
    }
    const scheme = transportArg === 'websocket' ? this.ssl() ? 'wss' : 'ws' : this.ssl() ? 'https' : 'http';
    return `${scheme}://${host}:${this.port(transportArg === 'websocket' ? 'websocket' : 'http')}`;
  }

  private endpoint(): { host: string; port?: number; ssl: boolean; transport: THyperionTransport } | undefined {
    if (!this.config.url || !/^[a-z][a-z0-9+.-]*:\/\//i.test(this.config.url)) {
      return undefined;
    }
    try {
      const url = new URL(this.config.url);
      const protocol = url.protocol.replace(':', '').toLowerCase();
      const transport: THyperionTransport = protocol === 'ws' || protocol === 'wss' ? 'websocket' : protocol === 'http' || protocol === 'https' ? 'http' : 'tcp';
      return { host: url.hostname, port: url.port ? Number(url.port) : undefined, ssl: protocol === 'https' || protocol === 'wss', transport };
    } catch {
      return undefined;
    }
  }

  private timeoutMs(): number {
    return this.config.timeoutMs || hyperionDefaultTimeoutMs;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }

  private addWebSocketListener(socketArg: TWebSocketLike, eventArg: 'open' | 'message' | 'error' | 'close', handlerArg: TWebSocketHandler): void {
    if (socketArg.addEventListener) {
      socketArg.addEventListener(eventArg, handlerArg);
      return;
    }
    socketArg[`on${eventArg}` as 'onopen'] = handlerArg;
  }

  private removeWebSocketListener(socketArg: TWebSocketLike, eventArg: 'open' | 'message' | 'error' | 'close'): void {
    if (socketArg.removeEventListener) {
      return;
    }
    socketArg[`on${eventArg}` as 'onopen'] = null;
  }

  private websocketData(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'string') {
      return valueArg;
    }
    if (Buffer.isBuffer(valueArg)) {
      return valueArg.toString('utf8');
    }
    return undefined;
  }
}
