import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { MycroftMapper } from './mycroft.mapper.js';
import type { IMycroftConfig } from './mycroft.types.js';
import { mycroftDefaultPath, mycroftDefaultPort, mycroftDefaultTimeoutMs, mycroftProfile } from './mycroft.types.js';

interface IMycroftEndpoint {
  host: string;
  port: number;
  path: string;
  useTls: boolean;
}

const websocketGuid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

export class MycroftClient extends SimpleLocalClient<IMycroftConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IMycroftConfig) {
    super(mycroftProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.cachedSnapshot) {
      return clone(this.cachedSnapshot);
    }
    if (this.configArg.client || this.configArg.snapshotProvider || (!forceRefreshArg && this.configArg.snapshot)) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }
    if (!this.endpoint()) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }

    try {
      await this.withWebSocketConnection(async () => undefined);
      this.cachedSnapshot = MycroftMapper.toSnapshot({
        config: this.configArg,
        rawData: this.rawStatusData(true),
        online: true,
        source: 'tcp',
      });
    } catch (errorArg) {
      this.cachedSnapshot = MycroftMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
    }
    return clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor) {
      return super.execute(requestArg);
    }
    if (!this.endpoint()) {
      return { success: false, error: 'Mycroft local notify/speak control requires config.host/config.url, an injected client.execute(), or commandExecutor.' };
    }
    if (!['mycroft', 'notify', 'send_message', 'speak'].includes(requestArg.service)) {
      return { success: false, error: `Unsupported Mycroft service: ${requestArg.domain}.${requestArg.service}` };
    }

    const message = stringValue(requestArg.data?.message) || stringValue(requestArg.data?.text) || stringValue(requestArg.data?.utterance);
    if (!message) {
      return { success: false, error: 'Mycroft notify/speak requires data.message, data.text, or data.utterance.' };
    }

    try {
      const payload = {
        type: 'speak',
        data: {
          expect_response: false,
          utterance: message,
        },
        context: null,
      };
      await this.sendWebSocketText(JSON.stringify(payload));
      this.cachedSnapshot = undefined;
      return { success: true, data: { type: payload.type, utterance: message } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  private rawStatusData(onlineArg: boolean): Record<string, unknown> {
    const endpoint = this.endpoint();
    const id = this.configArg.uniqueId || this.configArg.deviceId || (endpoint ? `${endpoint.host}:${endpoint.port}` : mycroftProfile.domain);
    return {
      device: {
        id,
        name: this.configArg.name || mycroftProfile.defaultName,
        manufacturer: mycroftProfile.manufacturer,
        model: mycroftProfile.model,
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: 'websocket',
        configurationUrl: endpoint ? `${endpoint.useTls ? 'wss' : 'ws'}://${endpoint.host}:${endpoint.port}${endpoint.path}` : undefined,
      },
      entities: [
        {
          id: 'websocket',
          uniqueId: `${id}_websocket`,
          name: 'WebSocket',
          platform: 'binary_sensor' as const,
          state: onlineArg,
          available: true,
          writable: false,
          attributes: { path: endpoint?.path },
        },
      ],
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: 'tcp',
    };
  }

  private async sendWebSocketText(textArg: string): Promise<void> {
    await this.withWebSocketConnection(async (socketArg) => {
      await writeBuffer(socketArg, webSocketTextFrame(textArg));
      await writeBuffer(socketArg, Buffer.from([0x88, 0x80, 0, 0, 0, 0]));
    });
  }

  private async withWebSocketConnection(callbackArg: (socketArg: plugins.net.Socket) => Promise<void> | void): Promise<void> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Mycroft WebSocket requires config.host or config.url.');
    }
    if (endpoint.useTls) {
      throw new Error('Mycroft mycroftapi 2.0 documents an unencrypted ws:// endpoint; wss:// is not implemented here without protocol evidence.');
    }

    const timeoutMs = this.configArg.timeoutMs || mycroftDefaultTimeoutMs;
    const key = plugins.crypto.randomBytes(16).toString('base64');
    const expectedAccept = plugins.crypto.createHash('sha1').update(`${key}${websocketGuid}`).digest('base64');

    await new Promise<void>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });
      const chunks: Buffer[] = [];
      let settled = false;

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.end();
        socket.destroy();
        errorArg ? reject(errorArg) : resolve();
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`Mycroft WebSocket connection to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => {
        socket.write([
          `GET ${endpoint.path} HTTP/1.1`,
          `Host: ${endpoint.host}:${endpoint.port}`,
          'Upgrade: websocket',
          'Connection: Upgrade',
          `Sec-WebSocket-Key: ${key}`,
          'Sec-WebSocket-Version: 13',
          '\r\n',
        ].join('\r\n'));
      });
      socket.on('data', async (chunkArg) => {
        chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg));
        const text = Buffer.concat(chunks).toString('utf8');
        const headerEnd = text.indexOf('\r\n\r\n');
        if (headerEnd === -1) {
          return;
        }
        try {
          validateHandshake(text.slice(0, headerEnd), expectedAccept);
          await callbackArg(socket);
          finish();
        } catch (errorArg) {
          finish(errorArg instanceof Error ? errorArg : new Error(String(errorArg)));
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('end', () => finish(new Error('Mycroft WebSocket closed before the handshake completed.')));
      socket.on('close', () => finish(new Error('Mycroft WebSocket closed before the handshake completed.')));
    });
  }

  private endpoint(): IMycroftEndpoint | undefined {
    const parsed = parseEndpoint(this.configArg.url);
    if (parsed) {
      return parsed;
    }
    if (!this.configArg.host) {
      return undefined;
    }
    return {
      host: this.configArg.host,
      port: this.configArg.port || mycroftDefaultPort,
      path: this.configArg.path || mycroftDefaultPath,
      useTls: Boolean(this.configArg.useTls),
    };
  }
}

function webSocketTextFrame(textArg: string): Buffer {
  const payload = Buffer.from(textArg, 'utf8');
  const mask = plugins.crypto.randomBytes(4);
  const header = payload.length < 126
    ? Buffer.from([0x81, 0x80 | payload.length])
    : Buffer.from([0x81, 0x80 | 126, payload.length >> 8, payload.length & 0xff]);
  const masked = Buffer.alloc(payload.length);
  for (let i = 0; i < payload.length; i++) {
    masked[i] = payload[i] ^ mask[i % 4];
  }
  return Buffer.concat([header, mask, masked]);
}

function validateHandshake(headerArg: string, expectedAcceptArg: string): void {
  const lines = headerArg.split('\r\n');
  if (!/^HTTP\/1\.1 101\b/.test(lines[0] || '')) {
    throw new Error(`Mycroft WebSocket handshake failed: ${lines[0] || 'missing status line'}`);
  }
  const headers = Object.fromEntries(lines.slice(1).map((lineArg) => {
    const index = lineArg.indexOf(':');
    return index === -1 ? ['', ''] : [lineArg.slice(0, index).trim().toLowerCase(), lineArg.slice(index + 1).trim()];
  }));
  if (headers['sec-websocket-accept'] !== expectedAcceptArg) {
    throw new Error('Mycroft WebSocket handshake returned an invalid Sec-WebSocket-Accept header.');
  }
}

function writeBuffer(socketArg: plugins.net.Socket, bufferArg: Buffer): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    socketArg.write(bufferArg, (errorArg) => errorArg ? reject(errorArg) : resolve());
  });
}

function parseEndpoint(valueArg: unknown): IMycroftEndpoint | undefined {
  if (typeof valueArg !== 'string' || !valueArg.trim()) {
    return undefined;
  }
  try {
    const parsed = new URL(valueArg.includes('://') ? valueArg : `ws://${valueArg}`);
    return {
      host: parsed.hostname,
      port: parsed.port ? Number(parsed.port) : mycroftDefaultPort,
      path: parsed.pathname && parsed.pathname !== '/' ? parsed.pathname : mycroftDefaultPath,
      useTls: parsed.protocol === 'wss:' || parsed.protocol === 'https:',
    };
  } catch {
    return undefined;
  }
}

function stringValue(valueArg: unknown): string | undefined {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
}

function clone<TValue>(valueArg: TValue): TValue {
  return JSON.parse(JSON.stringify(valueArg)) as TValue;
}

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