import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { LgSoundbarMapper } from './lg_soundbar.mapper.js';
import type { ILgSoundbarConfig, ILgSoundbarSnapshot } from './lg_soundbar.types.js';
import { lgSoundbarProfile } from './lg_soundbar.types.js';

export class LgSoundbarClient extends SimpleLocalClient<ILgSoundbarConfig> {
  private liveSnapshot?: ILgSoundbarSnapshot;

  constructor(private readonly configArg: ILgSoundbarConfig) {
    super(lgSoundbarProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ILgSoundbarSnapshot> {
    if (!forceRefreshArg && this.liveSnapshot) {
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    if (this.shouldUseNativeTcp(forceRefreshArg)) {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.liveSnapshot = LgSoundbarMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.liveSnapshot = LgSoundbarMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData ?? snapshot, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return SimpleLocalMapper.clone(this.liveSnapshot);
  }

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

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.endpoint()) {
      return super.execute(requestArg);
    }

    try {
      const packet = await this.packetForService(requestArg);
      await this.exchange([packet], { minResponses: 0, idleMs: 40 });
      this.liveSnapshot = undefined;
      return { success: true, data: packet };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private shouldUseNativeTcp(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    return true;
  }

  private async fetchSnapshot(): Promise<ILgSoundbarSnapshot> {
    const responses = await this.exchange([
      { cmd: 'get', msg: 'PRODUCT_INFO' },
      { cmd: 'get', msg: 'MAC_INFO_DEV' },
      { cmd: 'get', msg: 'EQ_VIEW_INFO' },
      { cmd: 'get', msg: 'SPK_LIST_VIEW_INFO' },
      { cmd: 'get', msg: 'FUNC_VIEW_INFO' },
      { cmd: 'get', msg: 'SETTING_VIEW_INFO' },
      { cmd: 'get', msg: 'PLAY_INFO' },
    ], { minResponses: 1, idleMs: 80 });

    return this.snapshotFromResponses(responses);
  }

  private snapshotFromResponses(responsesArg: Record<string, unknown>[]): ILgSoundbarSnapshot {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('LG Soundbar TCP snapshots require config.host or config.url.');
    }

    const byMessage: Record<string, Record<string, unknown>> = {};
    for (const response of responsesArg) {
      const message = stringValue(response.msg);
      if (!message) {
        continue;
      }
      byMessage[message] = { ...(byMessage[message] || {}), ...recordValue(response.data) };
    }

    const speaker = byMessage.SPK_LIST_VIEW_INFO || {};
    const func = byMessage.FUNC_VIEW_INFO || {};
    const setting = byMessage.SETTING_VIEW_INFO || {};
    const eq = byMessage.EQ_VIEW_INFO || {};
    const play = byMessage.PLAY_INFO || {};
    const product = byMessage.PRODUCT_INFO || {};
    const mac = byMessage.MAC_INFO_DEV || {};

    const volume = numberValue(speaker.i_vol);
    const volumeMax = numberValue(speaker.i_vol_max);
    const currentFunction = numberValue(func.i_curr_func ?? speaker.i_curr_func);
    const currentEq = numberValue(setting.i_curr_eq ?? eq.i_curr_eq);
    const streamType = numberValue(play.i_stream_type);
    const playControl = numberValue(play.i_play_ctrl);
    const powerStatus = boolValue(speaker.b_powerstatus);
    const supportPlayControl = boolValue(play.b_support_play_ctrl);
    const source = currentFunction === undefined ? undefined : lgSoundbarFunctions[currentFunction];
    const soundMode = currentEq === undefined ? undefined : lgSoundbarEqualisers[currentEq];
    const sourceList = numberArray(func.ai_func_list).map((indexArg) => lgSoundbarFunctions[indexArg]).filter((valueArg): valueArg is string => Boolean(valueArg)).sort();
    const soundModeList = numberArray(eq.ai_eq_list).map((indexArg) => lgSoundbarEqualisers[indexArg]).filter((valueArg): valueArg is string => Boolean(valueArg)).sort();
    const state = mediaState(powerStatus, streamType, playControl);
    const uuid = stringValue(mac.s_uuid) || stringValue(product.s_uuid);
    const name = this.configArg.name || stringValue(setting.s_user_name) || stringValue(speaker.s_user_name) || lgSoundbarProfile.defaultName || lgSoundbarProfile.displayName;

    const entities: ISimpleLocalEntitySnapshot[] = [{
      id: 'media_player',
      uniqueId: `${lgSoundbarProfile.domain}_${SimpleLocalMapper.slug(this.configArg.uniqueId || uuid || endpoint.host)}_media_player`,
      name,
      platform: 'media_player',
      state,
      available: true,
      writable: true,
      attributes: cleanAttributes({
        volume,
        volumeMin: numberValue(speaker.i_vol_min),
        volumeMax,
        volumeLevel: volume !== undefined && volumeMax ? volume / volumeMax : undefined,
        isVolumeMuted: boolValue(speaker.b_mute),
        source,
        sourceList: sourceList.length ? sourceList : undefined,
        soundMode,
        soundModeList: soundModeList.length ? soundModeList : undefined,
        rearVolume: numberValue(setting.i_rear_level),
        rearVolumeMin: numberValue(setting.i_rear_min),
        rearVolumeMax: numberValue(setting.i_rear_max),
        wooferVolume: numberValue(setting.i_woofer_level),
        wooferVolumeMin: numberValue(setting.i_woofer_min),
        wooferVolumeMax: numberValue(setting.i_woofer_max),
        bass: numberValue(eq.i_bass),
        treble: numberValue(eq.i_treble),
        mediaArtist: stringValue(play.s_artist),
        mediaTitle: stringValue(play.s_title),
        mediaImageUrl: stringValue(play.s_albumart),
        streamType,
        supportPlayControl,
      }),
    }];

    return {
      device: {
        id: this.configArg.uniqueId || this.configArg.deviceId || uuid || `${endpoint.host}:${endpoint.port}`,
        name,
        manufacturer: 'LG',
        model: stringValue(product.s_model_name) || stringValue(product.s_product_model) || 'LG Soundbar',
        serialNumber: uuid,
        host: endpoint.host,
        port: endpoint.port,
        protocol: 'tcp',
        attributes: cleanAttributes({
          uuid,
          macAddress: stringValue(mac.s_mac_addr) || stringValue(mac.s_mac_address),
          rawProductInfo: Object.keys(product).length ? product : undefined,
        }),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'tcp',
      rawData: {
        responses: responsesArg,
        byMessage,
      },
      capabilities: {
        localRead: true,
        localControl: true,
        encryptedTcp: true,
      },
    };
  }

  private async packetForService(requestArg: IServiceCallRequest): Promise<Record<string, unknown>> {
    const data = requestArg.data || {};
    if (data.packet && typeof data.packet === 'object' && !Array.isArray(data.packet)) {
      return data.packet as Record<string, unknown>;
    }

    if (requestArg.service === 'turn_on') {
      return { cmd: 'set', data: { b_powerkey: true }, msg: 'SPK_LIST_VIEW_INFO' };
    }
    if (requestArg.service === 'turn_off') {
      return { cmd: 'set', data: { b_powerkey: false }, msg: 'SPK_LIST_VIEW_INFO' };
    }
    if (requestArg.service === 'media_play') {
      await this.assertPlayControlSupported();
      return { cmd: 'set', data: { i_play_ctrl: 0 }, msg: 'PLAY_INFO' };
    }
    if (requestArg.service === 'media_pause') {
      await this.assertPlayControlSupported();
      return { cmd: 'set', data: { i_play_ctrl: 1 }, msg: 'PLAY_INFO' };
    }
    if (requestArg.service === 'media_play_pause') {
      const snapshot = await this.getSnapshot();
      return snapshot.entities[0]?.state === 'playing'
        ? { cmd: 'set', data: { i_play_ctrl: 1 }, msg: 'PLAY_INFO' }
        : { cmd: 'set', data: { i_play_ctrl: 0 }, msg: 'PLAY_INFO' };
    }
    if (requestArg.service === 'volume_mute') {
      return { cmd: 'set', data: { b_mute: data.mute ?? data.is_volume_muted ?? true }, msg: 'SPK_LIST_VIEW_INFO' };
    }
    if (requestArg.service === 'volume_up' || requestArg.service === 'volume_down') {
      const snapshot = await this.getSnapshot();
      const currentVolume = numberValue(snapshot.entities[0]?.attributes?.volume) || 0;
      const nextVolume = Math.max(0, currentVolume + (requestArg.service === 'volume_up' ? 1 : -1));
      return { cmd: 'set', data: { i_vol: nextVolume }, msg: 'SPK_LIST_VIEW_INFO' };
    }
    if (requestArg.service === 'set_volume_level' || requestArg.service === 'volume_set' || requestArg.service === 'set_level') {
      const snapshot = await this.getSnapshot();
      const volumeMax = numberValue(snapshot.entities[0]?.attributes?.volumeMax) || 100;
      const requested = numberValue(data.volume_level ?? data.volume ?? data.level ?? data.percentage);
      if (requested === undefined) {
        throw new Error('LG Soundbar volume_set requires data.volume_level, data.volume, data.level, or data.percentage.');
      }
      const value = requested <= 1 ? Math.round(requested * volumeMax) : Math.round(requested);
      return { cmd: 'set', data: { i_vol: Math.max(0, Math.min(volumeMax, value)) }, msg: 'SPK_LIST_VIEW_INFO' };
    }
    if (requestArg.service === 'select_source') {
      const functionIndex = indexValue(data.source ?? data.sourceIndex ?? data.function, lgSoundbarFunctions);
      if (functionIndex === undefined) {
        throw new Error('LG Soundbar select_source requires data.source matching a documented LG function.');
      }
      return { cmd: 'set', data: { i_curr_func: functionIndex }, msg: 'FUNC_VIEW_INFO' };
    }
    if (requestArg.service === 'select_sound_mode') {
      const eqIndex = indexValue(data.sound_mode ?? data.soundMode ?? data.equaliser, lgSoundbarEqualisers);
      if (eqIndex === undefined) {
        throw new Error('LG Soundbar select_sound_mode requires data.sound_mode matching a documented LG equaliser.');
      }
      return { cmd: 'set', data: { i_curr_eq: eqIndex }, msg: 'EQ_VIEW_INFO' };
    }

    throw new Error(`Unsupported LG Soundbar service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async assertPlayControlSupported(): Promise<void> {
    const snapshot = await this.getSnapshot();
    if (snapshot.entities[0]?.attributes?.supportPlayControl === false) {
      throw new Error('LG Soundbar reported that PLAY_INFO play control is not supported for the current stream.');
    }
  }

  private async exchange(packetsArg: Record<string, unknown>[], optionsArg: ILgSoundbarExchangeOptions): Promise<Record<string, unknown>[]> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('LG Soundbar TCP exchange requires config.host or config.url.');
    }
    const timeoutMs = this.configArg.timeoutMs || 3000;
    const idleMs = optionsArg.idleMs || 80;
    const minResponses = optionsArg.minResponses ?? 1;

    return new Promise<Record<string, unknown>[]>((resolve, reject) => {
      const responses: Record<string, unknown>[] = [];
      let buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
      let settled = false;
      let idleTimer: NodeJS.Timeout | undefined;
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        if (idleTimer) {
          clearTimeout(idleTimer);
        }
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(responses);
      };
      const armIdle = () => {
        if (idleTimer) {
          clearTimeout(idleTimer);
        }
        if (responses.length >= minResponses) {
          idleTimer = setTimeout(() => finish(), idleMs);
        }
      };

      socket.setTimeout(timeoutMs, () => responses.length >= minResponses ? finish() : finish(new Error(`LG Soundbar TCP request to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => {
        for (const packet of packetsArg) {
          socket.write(encryptPacket(packet));
        }
        armIdle();
      });
      socket.on('data', (chunkArg) => {
        buffer = Buffer.concat([buffer, Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)]);
        const parsed = parseFrames(buffer);
        buffer = parsed.rest;
        responses.push(...parsed.packets);
        armIdle();
      });
      socket.on('end', () => responses.length >= minResponses ? finish() : finish(new Error('LG Soundbar closed the TCP connection without a complete response.')));
      socket.on('close', () => responses.length >= minResponses ? finish() : finish(new Error('LG Soundbar closed the TCP connection before responding.')));
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private endpoint(): ILgSoundbarEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    if (rawUrl) {
      const parsed = parseUrl(rawUrl);
      if (parsed) {
        return { host: parsed.hostname, port: this.configArg.port || numberValue(parsed.port) || lgSoundbarDefaultPort };
      }
    }
    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const parsedHost = parseUrl(host);
    if (parsedHost) {
      return { host: parsedHost.hostname, port: this.configArg.port || numberValue(parsedHost.port) || lgSoundbarDefaultPort };
    }
    return { host, port: this.configArg.port || lgSoundbarDefaultPort };
  }
}

interface ILgSoundbarEndpoint {
  host: string;
  port: number;
}

interface ILgSoundbarExchangeOptions {
  minResponses?: number;
  idleMs?: number;
}

const lgSoundbarDefaultPort = 9741;
const soundbarKey = Buffer.from('T^&*J%^7tr~4^%^&I(o%^!jIJ__+a0 k', 'utf8');
const soundbarIv = Buffer.from("'%^Ur7gy$~t+f)%@", 'utf8');

const lgSoundbarEqualisers = ['Standard', 'Bass', 'Flat', 'Boost', 'Treble and Bass', 'User', 'Music', 'Cinema', 'Night', 'News', 'Voice', 'ia_sound', 'Adaptive Sound Control', 'Movie', 'Bass Blast', 'Dolby Atmos', 'DTS Virtual X', 'Bass Boost Plus', 'DTS X', 'AI Sound Pro', 'Clear Voice', 'Sports', 'Game'];
const lgSoundbarFunctions = ['Wi-Fi', 'Bluetooth', 'Portable', 'Aux', 'Optical', 'CP', 'HDMI', 'ARC', 'Spotify', 'Optical2', 'HDMI2', 'HDMI3', 'LG TV', 'Mic', 'Chromecast', 'Optical/HDMI ARC', 'LG Optical', 'FM', 'USB', 'USB2', 'E-ARC'];

const encryptPacket = (packetArg: Record<string, unknown>): Buffer<ArrayBufferLike> => {
  const cipher = plugins.crypto.createCipheriv('aes-256-cbc', soundbarKey, soundbarIv);
  const encrypted = Buffer.concat([cipher.update(JSON.stringify(packetArg), 'utf8'), cipher.final()]);
  const header = Buffer.alloc(5);
  header[0] = 0x10;
  header.writeUInt32BE(encrypted.length, 1);
  return Buffer.concat([header, encrypted]);
};

const decryptPacket = (dataArg: Buffer<ArrayBufferLike>): Record<string, unknown> => {
  const decipher = plugins.crypto.createDecipheriv('aes-256-cbc', soundbarKey, soundbarIv);
  const decrypted = Buffer.concat([decipher.update(dataArg), decipher.final()]).toString('utf8');
  const parsed = JSON.parse(decrypted) as unknown;
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error('LG Soundbar decrypted packet was not a JSON object.');
  }
  return parsed as Record<string, unknown>;
};

const parseFrames = (bufferArg: Buffer<ArrayBufferLike>): { packets: Record<string, unknown>[]; rest: Buffer<ArrayBufferLike> } => {
  const packets: Record<string, unknown>[] = [];
  let offset = 0;
  while (offset < bufferArg.length && bufferArg[offset] !== 0x10) {
    offset++;
  }
  while (bufferArg.length - offset >= 5) {
    if (bufferArg[offset] !== 0x10) {
      offset++;
      continue;
    }
    const length = bufferArg.readUInt32BE(offset + 1);
    if (bufferArg.length - offset - 5 < length) {
      break;
    }
    packets.push(decryptPacket(bufferArg.subarray(offset + 5, offset + 5 + length)));
    offset += 5 + length;
  }
  return { packets, rest: bufferArg.subarray(offset) };
};

const mediaState = (powerStatusArg: boolean | undefined, streamTypeArg: number | undefined, playControlArg: number | undefined): string => {
  if (powerStatusArg === false) {
    return 'off';
  }
  if (streamTypeArg !== undefined && streamTypeArg !== 0 && playControlArg !== undefined) {
    return playControlArg === 0 ? 'playing' : 'paused';
  }
  return 'on';
};

const indexValue = (valueArg: unknown, valuesArg: string[]): number | undefined => {
  const numeric = numberValue(valueArg);
  if (numeric !== undefined && numeric >= 0 && numeric < valuesArg.length) {
    return numeric;
  }
  const text = stringValue(valueArg)?.toLowerCase();
  return text ? valuesArg.findIndex((value) => value.toLowerCase() === text) : undefined;
};

const recordValue = (valueArg: unknown): Record<string, unknown> => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : {};
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : stringValue(valueArg) && Number.isFinite(Number(valueArg)) ? Number(valueArg) : undefined;
const boolValue = (valueArg: unknown): boolean | undefined => typeof valueArg === 'boolean' ? valueArg : typeof valueArg === 'number' ? valueArg !== 0 : stringValue(valueArg) ? ['true', '1', 'yes', 'on'].includes(stringValue(valueArg)!.toLowerCase()) : undefined;
const numberArray = (valueArg: unknown): number[] => Array.isArray(valueArg) ? valueArg.map(numberValue).filter((value): value is number => value !== undefined) : [];
const parseUrl = (valueArg: string): URL | undefined => {
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
const cleanAttributes = (attributesArg: Record<string, unknown>): Record<string, unknown> | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(clean).length ? clean : undefined;
};
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
