import * as plugins from '../../plugins.js';
import type {
  IArcamFmjCommandRequest,
  IArcamFmjCommandResult,
  IArcamFmjConfig,
  IArcamFmjDeviceInfo,
  IArcamFmjIncomingAudioInfo,
  IArcamFmjIncomingVideoParameters,
  IArcamFmjModeledCommand,
  IArcamFmjResponsePacket,
  IArcamFmjSnapshot,
  IArcamFmjZoneState,
  TArcamFmjApiModel,
  TArcamFmjSnapshotSource,
  TArcamFmjSource,
} from './arcam_fmj.types.js';
import { arcamFmjDefaultPort } from './arcam_fmj.types.js';

const defaultTimeoutMs = 3000;
const protocolStart = 0x21;
const protocolEnd = 0x0d;
const queryByte = 0xf0;

const commandCodes = {
  POWER: 0x00,
  SIMULATE_RC5_IR_COMMAND: 0x08,
  VOLUME: 0x0d,
  MUTE: 0x0e,
  DECODE_MODE_STATUS_2CH: 0x10,
  DECODE_MODE_STATUS_MCH: 0x11,
  RDS_INFORMATION: 0x12,
  MENU: 0x14,
  TUNER_PRESET: 0x15,
  DAB_STATION: 0x18,
  DLS_PDT_INFO: 0x1a,
  PRESET_DETAIL: 0x1b,
  CURRENT_SOURCE: 0x1d,
  INCOMING_VIDEO_PARAMETERS: 0x42,
  INCOMING_AUDIO_FORMAT: 0x43,
  INCOMING_AUDIO_SAMPLE_RATE: 0x44,
} as const;

const commandNames = Object.fromEntries(
  Object.entries(commandCodes).map(([key, value]) => [value, key])
) as Record<number, string>;

const directPowerWriteSupported = new Set<TArcamFmjApiModel>(['APISA_SERIES', 'APIPA_SERIES', 'APIST_SERIES']);
const directMuteWriteSupported = directPowerWriteSupported;
const directSourceWriteSupported = new Set<TArcamFmjApiModel>(['APISA_SERIES']);
const volumeStepSupported = new Set<TArcamFmjApiModel>(['APIST_SERIES']);

const modelSeries: Array<{ apiModel: TArcamFmjApiModel; models: string[] }> = [
  { apiModel: 'API450_SERIES', models: ['AVR380', 'AVR450', 'AVR750'] },
  { apiModel: 'API860_SERIES', models: ['AV860', 'AVR850', 'AVR550', 'AVR390', 'SR250', 'RV-6', 'RV-9', 'MC-10'] },
  { apiModel: 'APIHDA_SERIES', models: ['AVR5', 'AVR10', 'AVR20', 'AVR30', 'AV40', 'AVR11', 'AVR21', 'AVR31', 'AV41', 'SDP-55', 'SDP-58'] },
  { apiModel: 'APISA_SERIES', models: ['SA10', 'SA20', 'SA30', 'SA750'] },
  { apiModel: 'APIPA_SERIES', models: ['PA720', 'PA240', 'PA410'] },
  { apiModel: 'APIST_SERIES', models: ['ST60'] },
];

const statusSourceMaps: Record<TArcamFmjApiModel, Partial<Record<number, Record<string, number>>>> = {
  API450_SERIES: {
    1: { FOLLOW_ZONE_1: 0x00, CD: 0x01, BD: 0x02, AV: 0x03, SAT: 0x04, PVR: 0x05, VCR: 0x06, AUX: 0x08, DISPLAY: 0x09, FM: 0x0b, DAB: 0x0c, NET: 0x0e, USB: 0x0f, STB: 0x10, GAME: 0x11, PHONO: 0x12, ARC_ERC: 0x13 },
    2: { FOLLOW_ZONE_1: 0x00, CD: 0x01, BD: 0x02, AV: 0x03, SAT: 0x04, PVR: 0x05, VCR: 0x06, AUX: 0x08, DISPLAY: 0x09, FM: 0x0b, DAB: 0x0c, NET: 0x0e, USB: 0x0f, STB: 0x10, GAME: 0x11, PHONO: 0x12, ARC_ERC: 0x13 },
  },
  API860_SERIES: {
    1: { FOLLOW_ZONE_1: 0x00, CD: 0x01, BD: 0x02, AV: 0x03, SAT: 0x04, PVR: 0x05, VCR: 0x06, AUX: 0x08, DISPLAY: 0x09, FM: 0x0b, DAB: 0x0c, NET: 0x0e, USB: 0x0f, STB: 0x10, GAME: 0x11, PHONO: 0x12, ARC_ERC: 0x13 },
    2: { FOLLOW_ZONE_1: 0x00, CD: 0x01, BD: 0x02, AV: 0x03, SAT: 0x04, PVR: 0x05, VCR: 0x06, AUX: 0x08, DISPLAY: 0x09, FM: 0x0b, DAB: 0x0c, NET: 0x0e, USB: 0x0f, STB: 0x10, GAME: 0x11, PHONO: 0x12, ARC_ERC: 0x13 },
  },
  APIHDA_SERIES: {
    1: { FOLLOW_ZONE_1: 0x00, CD: 0x01, BD: 0x02, AV: 0x03, SAT: 0x04, PVR: 0x05, UHD: 0x06, AUX: 0x08, DISPLAY: 0x09, FM: 0x0b, DAB: 0x0c, NET: 0x0e, USB: 0x0f, STB: 0x10, GAME: 0x11, BT: 0x12 },
    2: { FOLLOW_ZONE_1: 0x00, CD: 0x01, BD: 0x02, AV: 0x03, SAT: 0x04, PVR: 0x05, UHD: 0x06, AUX: 0x08, DISPLAY: 0x09, FM: 0x0b, DAB: 0x0c, NET: 0x0e, USB: 0x0f, STB: 0x10, GAME: 0x11, BT: 0x12 },
  },
  APISA_SERIES: {
    1: { PHONO: 0x01, AUX: 0x02, PVR: 0x03, AV: 0x04, STB: 0x05, CD: 0x06, BD: 0x07, SAT: 0x08, GAME: 0x09, NET: 0x0b, USB: 0x0b, ARC_ERC: 0x0d },
    2: { PHONO: 0x01, AUX: 0x02, PVR: 0x03, AV: 0x04, STB: 0x05, CD: 0x06, BD: 0x07, SAT: 0x08, GAME: 0x09, NET: 0x0b, USB: 0x0b, ARC_ERC: 0x0d },
  },
  APIPA_SERIES: {},
  APIST_SERIES: {
    1: { DIG1: 0x01, DIG2: 0x02, DIG3: 0x03, DIG4: 0x04, NET_USB: 0x05 },
  },
};

const rc5SourceMaps: Record<TArcamFmjApiModel, Partial<Record<number, Record<string, number[]>>>> = {
  API450_SERIES: {
    1: { STB: [16, 1], AV: [16, 2], DAB: [16, 72], FM: [16, 54], BD: [16, 4], GAME: [16, 5], VCR: [16, 6], CD: [16, 7], AUX: [16, 8], DISPLAY: [16, 9], SAT: [16, 0], PVR: [16, 34], USB: [16, 18], NET: [16, 11] },
    2: { STB: [23, 8], AV: [23, 9], DAB: [23, 16], FM: [23, 14], BD: [23, 7], GAME: [23, 11], CD: [23, 6], AUX: [23, 13], PVR: [23, 15], USB: [23, 18], NET: [23, 19], FOLLOW_ZONE_1: [16, 20] },
  },
  API860_SERIES: {
    1: { STB: [16, 100], AV: [16, 94], DAB: [16, 72], FM: [16, 28], BD: [16, 98], GAME: [16, 97], VCR: [16, 119], CD: [16, 118], AUX: [16, 99], DISPLAY: [16, 58], SAT: [16, 27], PVR: [16, 96], USB: [16, 93], NET: [16, 92] },
    2: { STB: [23, 8], AV: [23, 9], DAB: [23, 16], FM: [23, 14], BD: [23, 7], GAME: [23, 11], CD: [23, 6], AUX: [23, 13], PVR: [23, 15], USB: [23, 18], NET: [23, 19], SAT: [23, 20], VCR: [23, 21], FOLLOW_ZONE_1: [16, 20] },
  },
  APIHDA_SERIES: {
    1: { STB: [16, 100], AV: [16, 94], DAB: [16, 72], FM: [16, 28], BD: [16, 98], GAME: [16, 97], UHD: [16, 125], CD: [16, 118], AUX: [16, 99], DISPLAY: [16, 58], SAT: [16, 27], PVR: [16, 96], NET: [16, 92], BT: [16, 122] },
    2: { STB: [23, 8], AV: [23, 9], DAB: [23, 16], FM: [23, 14], BD: [23, 7], GAME: [23, 11], CD: [23, 6], AUX: [23, 13], PVR: [23, 15], USB: [23, 18], NET: [23, 19], SAT: [23, 20], UHD: [23, 23], BT: [23, 22], FOLLOW_ZONE_1: [16, 20] },
  },
  APISA_SERIES: {
    1: { PHONO: [16, 117], CD: [16, 118], BD: [16, 98], SAT: [16, 27], PVR: [16, 96], AV: [16, 94], AUX: [16, 99], STB: [16, 100], NET: [16, 92], USB: [16, 93], GAME: [16, 97], ARC_ERC: [16, 125] },
    2: { PHONO: [16, 117], CD: [16, 118], BD: [16, 98], SAT: [16, 27], PVR: [16, 96], AV: [16, 94], AUX: [16, 99], STB: [16, 100], NET: [16, 92], USB: [16, 93], GAME: [16, 97], ARC_ERC: [16, 125] },
  },
  APIPA_SERIES: {},
  APIST_SERIES: {
    1: { DIG1: [21, 94], DIG2: [21, 98], DIG3: [21, 27], DIG4: [21, 97], USB: [21, 93], NET: [21, 92] },
  },
};

const rc5PowerMaps: Record<TArcamFmjApiModel, Partial<Record<number, Record<'true' | 'false', number[]>>>> = {
  API450_SERIES: { 1: { true: [16, 123], false: [16, 124] }, 2: { true: [23, 123], false: [23, 124] } },
  API860_SERIES: { 1: { true: [16, 123], false: [16, 124] }, 2: { true: [23, 123], false: [23, 124] } },
  APIHDA_SERIES: { 1: { true: [16, 123], false: [16, 124] }, 2: { true: [23, 123], false: [23, 124] } },
  APISA_SERIES: { 1: { true: [16, 123], false: [16, 124] }, 2: { true: [16, 123], false: [16, 124] } },
  APIPA_SERIES: {},
  APIST_SERIES: {},
};

const rc5MuteMaps: Record<TArcamFmjApiModel, Partial<Record<number, Record<'true' | 'false', number[]>>>> = {
  API450_SERIES: { 1: { true: [16, 119], false: [16, 120] }, 2: { true: [23, 4], false: [23, 5] } },
  API860_SERIES: { 1: { true: [16, 26], false: [16, 120] }, 2: { true: [23, 4], false: [23, 5] } },
  APIHDA_SERIES: { 1: { true: [16, 26], false: [16, 120] }, 2: { true: [23, 4], false: [23, 5] } },
  APISA_SERIES: { 1: { true: [16, 26], false: [16, 120] }, 2: { true: [16, 26], false: [16, 120] } },
  APIPA_SERIES: {},
  APIST_SERIES: {},
};

const rc5VolumeMaps: Record<TArcamFmjApiModel, Partial<Record<number, Record<'true' | 'false', number[]>>>> = {
  API450_SERIES: { 1: { true: [16, 16], false: [16, 17] }, 2: { true: [23, 1], false: [23, 2] } },
  API860_SERIES: { 1: { true: [16, 16], false: [16, 17] }, 2: { true: [23, 1], false: [23, 2] } },
  APIHDA_SERIES: { 1: { true: [16, 16], false: [16, 17] }, 2: { true: [23, 1], false: [23, 2] } },
  APISA_SERIES: { 1: { true: [16, 16], false: [16, 17] }, 2: { true: [16, 16], false: [16, 17] } },
  APIPA_SERIES: {},
  APIST_SERIES: { 1: { true: [21, 86], false: [21, 85] } },
};

const incomingAudioFormats: Record<number, string> = {
  0x00: 'PCM',
  0x01: 'ANALOGUE_DIRECT',
  0x02: 'DOLBY_DIGITAL',
  0x03: 'DOLBY_DIGITAL_EX',
  0x04: 'DOLBY_DIGITAL_SURROUND',
  0x05: 'DOLBY_DIGITAL_PLUS',
  0x06: 'DOLBY_DIGITAL_TRUE_HD',
  0x07: 'DTS',
  0x08: 'DTS_96_24',
  0x0d: 'DTS_HD_MASTER_AUDIO',
  0x0e: 'DTS_HD_HIGH_RES_AUDIO',
  0x14: 'UNSUPPORTED',
  0x15: 'UNDETECTED',
  0x16: 'DOLBY_ATMOS',
  0x17: 'DTS_X',
  0x18: 'IMAX_ENHANCED',
  0x19: 'AURO_3D',
};

const incomingAudioConfigs: Record<number, string> = {
  0x00: 'DUAL_MONO',
  0x01: 'MONO',
  0x02: 'STEREO_ONLY',
  0x08: 'STEREO_CENTER',
  0x0e: 'STEREO_DOWNMIX',
  0x20: 'UNKNOWN',
  0x21: 'UNDETECTED',
};

const incomingAudioSampleRates: Record<number, number> = {
  0x00: 32000,
  0x01: 44100,
  0x02: 48000,
  0x03: 88200,
  0x04: 96000,
  0x05: 176400,
  0x06: 192000,
};

const videoAspectRatios: Record<number, string> = {
  0x00: 'UNDEFINED',
  0x01: 'ASPECT_4_3',
  0x02: 'ASPECT_16_9',
};

const videoColorspaces: Record<number, string> = {
  0x00: 'NORMAL',
  0x01: 'HDR10',
  0x02: 'DOLBY_VISION',
  0x03: 'HLG',
  0x04: 'HDR10_PLUS',
};

export class ArcamFmjClient {
  constructor(private readonly config: IArcamFmjConfig) {}

  public async getSnapshot(): Promise<IArcamFmjSnapshot> {
    if (this.config.snapshot) {
      return this.normalizeSnapshot(this.cloneValue(this.config.snapshot), 'snapshot');
    }

    if (!this.config.host) {
      return this.normalizeSnapshot({
        deviceInfo: this.deviceInfoFromConfig(),
        zones: this.config.zones?.length ? this.config.zones : [this.manualZone(1, false)],
        online: false,
        source: 'manual',
        lastUpdated: new Date().toISOString(),
      }, 'manual');
    }

    const amx = await this.requestAmx().catch(() => undefined);
    const deviceInfo = this.deviceInfoFromConfig(amx);
    const zoneNumbers = this.config.zones?.length ? this.config.zones.map((zoneArg) => zoneArg.zone) : [1, 2];
    const zones = await Promise.all(zoneNumbers.map((zoneArg) => this.getZoneState(zoneArg, deviceInfo).catch(() => this.manualZone(zoneArg, false))));

    return this.normalizeSnapshot({
      deviceInfo,
      zones,
      online: zones.some((zoneArg) => zoneArg.available !== false),
      source: 'tcp',
      lastUpdated: new Date().toISOString(),
    }, 'tcp');
  }

  public async execute(requestArg: IArcamFmjCommandRequest): Promise<IArcamFmjCommandResult> {
    const modeledCommand = this.modelCommand(requestArg);
    if (this.config.commandExecutor) {
      return {
        transport: 'executor',
        modeledCommand,
        executorResult: await this.config.commandExecutor.execute(modeledCommand),
      };
    }

    if (!this.config.host) {
      throw new Error('Arcam FMJ commands require config.host or commandExecutor.');
    }

    const response = modeledCommand.responseExpected ? await this.requestPacket(modeledCommand) : undefined;
    if (response && response.answerCode !== 0x00) {
      throw new Error(`Arcam FMJ command ${modeledCommand.commandCodeName} failed with answer code 0x${response.answerCode.toString(16)}.`);
    }
    if (!modeledCommand.responseExpected) {
      await this.sendPacket(modeledCommand);
    }
    return { transport: 'tcp', modeledCommand, response };
  }

  public modelCommand(requestArg: IArcamFmjCommandRequest): IArcamFmjModeledCommand {
    const zone = this.normalizeZone(requestArg.zone);
    const apiModel = this.apiModel();

    if (requestArg.command === 'raw_command') {
      if (typeof requestArg.commandCode !== 'number') {
        throw new Error('Arcam FMJ raw_command requires commandCode.');
      }
      return this.modeled(requestArg, zone, apiModel, requestArg.commandCode, requestArg.data || [], Boolean(requestArg.sendOnly), false);
    }

    if (requestArg.command === 'turn_on' || requestArg.command === 'turn_off') {
      const power = requestArg.command === 'turn_on';
      if (directPowerWriteSupported.has(apiModel)) {
        return this.modeled(requestArg, zone, apiModel, commandCodes.POWER, [power ? 0x01 : 0x00], false, false);
      }
      return this.modeled(requestArg, zone, apiModel, commandCodes.SIMULATE_RC5_IR_COMMAND, this.lookupRc5(rc5PowerMaps, apiModel, zone, power), !power, true);
    }

    if (requestArg.command === 'volume_up' || requestArg.command === 'volume_down') {
      const up = requestArg.command === 'volume_up';
      if (volumeStepSupported.has(apiModel)) {
        return this.modeled(requestArg, zone, apiModel, commandCodes.VOLUME, [up ? 0xf1 : 0xf2], false, false);
      }
      return this.modeled(requestArg, zone, apiModel, commandCodes.SIMULATE_RC5_IR_COMMAND, this.lookupRc5(rc5VolumeMaps, apiModel, zone, up), false, true);
    }

    if (requestArg.command === 'set_volume') {
      const rawVolume = typeof requestArg.volume === 'number' ? requestArg.volume : Math.round((requestArg.volumeLevel ?? 0) * 99);
      return this.modeled(requestArg, zone, apiModel, commandCodes.VOLUME, [clamp(Math.round(rawVolume), 0, 99)], false, false);
    }

    if (requestArg.command === 'mute') {
      const muted = Boolean(requestArg.muted);
      if (directMuteWriteSupported.has(apiModel)) {
        return this.modeled(requestArg, zone, apiModel, commandCodes.MUTE, [muted ? 0x00 : 0x01], false, false);
      }
      return this.modeled(requestArg, zone, apiModel, commandCodes.SIMULATE_RC5_IR_COMMAND, this.lookupRc5(rc5MuteMaps, apiModel, zone, muted), false, true);
    }

    if (requestArg.command === 'select_source') {
      if (!requestArg.source) {
        throw new Error('Arcam FMJ select_source requires source.');
      }
      const source = normalizeSource(requestArg.source);
      if (directSourceWriteSupported.has(apiModel)) {
        return this.modeled(requestArg, zone, apiModel, commandCodes.CURRENT_SOURCE, [this.lookupStatusSource(apiModel, zone, source)], false, false);
      }
      return this.modeled({ ...requestArg, source }, zone, apiModel, commandCodes.SIMULATE_RC5_IR_COMMAND, this.lookupRc5Source(apiModel, zone, source), false, true);
    }

    throw new Error(`Unsupported Arcam FMJ command: ${requestArg.command}`);
  }

  public async destroy(): Promise<void> {}

  private async getZoneState(zoneArg: number, deviceInfoArg: IArcamFmjDeviceInfo): Promise<IArcamFmjZoneState> {
    const apiModel = deviceInfoArg.apiModel || this.apiModel();
    const [power, volume, mute, source, menu, decode2ch, decodeMch, incomingVideo, incomingAudio, audioSampleRate, dabStation, dlsPdt, rds, tunerPreset] = await Promise.all([
      this.optionalStatus(zoneArg, commandCodes.POWER),
      this.optionalStatus(zoneArg, commandCodes.VOLUME),
      this.optionalStatus(zoneArg, commandCodes.MUTE),
      this.optionalStatus(zoneArg, commandCodes.CURRENT_SOURCE),
      this.optionalStatus(zoneArg, commandCodes.MENU),
      this.optionalStatus(zoneArg, commandCodes.DECODE_MODE_STATUS_2CH),
      this.optionalStatus(zoneArg, commandCodes.DECODE_MODE_STATUS_MCH),
      this.optionalStatus(zoneArg, commandCodes.INCOMING_VIDEO_PARAMETERS),
      this.optionalStatus(zoneArg, commandCodes.INCOMING_AUDIO_FORMAT),
      this.optionalStatus(zoneArg, commandCodes.INCOMING_AUDIO_SAMPLE_RATE),
      this.optionalStatus(zoneArg, commandCodes.DAB_STATION),
      this.optionalStatus(zoneArg, commandCodes.DLS_PDT_INFO),
      this.optionalStatus(zoneArg, commandCodes.RDS_INFORMATION),
      this.optionalStatus(zoneArg, commandCodes.TUNER_PRESET),
    ]);
    const sourceName = source?.length ? this.sourceFromStatusByte(apiModel, zoneArg, source[0]) : undefined;
    const zone: IArcamFmjZoneState = {
      zone: zoneArg,
      name: this.zoneName(zoneArg),
      power: power?.[0] === 0x01,
      state: power?.[0] === 0x01 ? 'on' : 'off',
      volume: volume?.[0],
      muted: mute?.length ? mute[0] === 0x00 : undefined,
      source: sourceName,
      sourceList: this.sourceList(apiModel, zoneArg),
      soundMode: decodeMch?.length ? `CODE_${decodeMch[0]}` : decode2ch?.length ? `CODE_${decode2ch[0]}` : undefined,
      incomingVideo: incomingVideo ? parseIncomingVideo(incomingVideo) : undefined,
      incomingAudio: parseIncomingAudio(incomingAudio, audioSampleRate),
      dabStation: textValue(dabStation),
      dlsPdt: textValue(dlsPdt),
      rdsInformation: textValue(rds),
      tunerPreset: tunerPreset && tunerPreset[0] !== 0xff ? tunerPreset[0] : undefined,
      available: Boolean(power || volume || mute || source || menu),
    };
    zone.volumeLevel = typeof zone.volume === 'number' ? zone.volume / 99 : undefined;
    zone.media = this.mediaInfo(zone);
    return zone;
  }

  private async optionalStatus(zoneArg: number, commandCodeArg: number): Promise<number[] | undefined> {
    try {
      const response = await this.requestPacket(this.modeled({ command: 'raw_command' }, zoneArg, this.apiModel(), commandCodeArg, [queryByte], false, false));
      return response.answerCode === 0x00 ? response.data : undefined;
    } catch {
      return undefined;
    }
  }

  private async requestPacket(commandArg: IArcamFmjModeledCommand): Promise<IArcamFmjResponsePacket> {
    const response = await this.exchange(this.commandPacketBytes(commandArg), (packetArg) => {
      return packetArg.type === 'response' && packetArg.packet.zone === commandArg.zone && packetArg.packet.commandCode === commandArg.commandCode;
    });
    if (response.type !== 'response') {
      throw new Error('Arcam FMJ command returned non-command response.');
    }
    return response.packet;
  }

  private async sendPacket(commandArg: IArcamFmjModeledCommand): Promise<void> {
    await this.exchange(this.commandPacketBytes(commandArg), () => false, false);
  }

  private async requestAmx(): Promise<Record<string, string>> {
    const response = await this.exchange(Buffer.from('AMX\r', 'ascii'), (packetArg) => packetArg.type === 'amx');
    if (response.type !== 'amx') {
      throw new Error('Arcam FMJ AMX request returned command response.');
    }
    return response.values;
  }

  private async exchange(
    requestArg: Buffer,
    matchesArg: (packetArg: TParsedPacket) => boolean,
    expectResponseArg = true
  ): Promise<TParsedPacket> {
    if (!this.config.host) {
      throw new Error('Arcam FMJ TCP exchange requires config.host.');
    }
    const host = this.config.host;
    const port = this.config.port || arcamFmjDefaultPort;
    const timeoutMs = this.config.requestTimeoutMs || defaultTimeoutMs;

    return new Promise<TParsedPacket>((resolve, reject) => {
      let buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
      let settled = false;
      const socket = plugins.net.createConnection({ host, port });
      const timeout = setTimeout(() => finish(new Error(`Arcam FMJ TCP exchange timed out after ${timeoutMs}ms.`)), timeoutMs);

      const finish = (errorArg?: Error, packetArg?: TParsedPacket) => {
        if (settled) {
          return;
        }
        settled = true;
        clearTimeout(timeout);
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(packetArg as TParsedPacket);
      };

      socket.on('connect', () => {
        socket.write(requestArg, (errorArg) => {
          if (errorArg) {
            finish(errorArg);
            return;
          }
          if (!expectResponseArg) {
            finish(undefined, { type: 'sent' });
          }
        });
      });
      socket.on('data', (chunkArg) => {
        const chunk = typeof chunkArg === 'string' ? Buffer.from(chunkArg) : chunkArg;
        buffer = Buffer.concat([buffer, chunk]);
        while (true) {
          const parsed = parsePacket(buffer);
          if (!parsed) {
            return;
          }
          buffer = parsed.remaining;
          if (matchesArg(parsed.packet)) {
            finish(undefined, parsed.packet);
            return;
          }
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => finish(new Error('Arcam FMJ TCP connection closed before exchange completed.')));
    });
  }

  private commandPacketBytes(commandArg: IArcamFmjModeledCommand): Buffer {
    return Buffer.from([protocolStart, commandArg.zone, commandArg.commandCode, commandArg.data.length, ...commandArg.data, protocolEnd]);
  }

  private modeled(
    requestArg: IArcamFmjCommandRequest,
    zoneArg: number,
    apiModelArg: TArcamFmjApiModel,
    commandCodeArg: number,
    dataArg: number[],
    sendOnlyArg: boolean,
    usesRc5Arg: boolean
  ): IArcamFmjModeledCommand {
    const data = dataArg.map((valueArg) => clamp(Math.round(valueArg), 0, 255));
    return {
      command: requestArg.command,
      zone: zoneArg,
      apiModel: apiModelArg,
      commandCode: commandCodeArg,
      commandCodeName: commandNames[commandCodeArg] || `CODE_${commandCodeArg}`,
      data,
      dataHex: hex(data),
      sendOnly: sendOnlyArg,
      responseExpected: !sendOnlyArg,
      usesRc5: usesRc5Arg,
      source: requestArg.source,
      volumeLevel: requestArg.volumeLevel,
      muted: requestArg.muted,
    };
  }

  private lookupRc5(
    mapsArg: Record<TArcamFmjApiModel, Partial<Record<number, Record<'true' | 'false', number[]>>>>,
    apiModelArg: TArcamFmjApiModel,
    zoneArg: number,
    valueArg: boolean
  ): number[] {
    const command = mapsArg[apiModelArg]?.[zoneArg]?.[valueArg ? 'true' : 'false'];
    if (!command) {
      throw new Error(`Arcam FMJ ${apiModelArg} zone ${zoneArg} does not support this RC5 command.`);
    }
    return command;
  }

  private lookupRc5Source(apiModelArg: TArcamFmjApiModel, zoneArg: number, sourceArg: string): number[] {
    const command = rc5SourceMaps[apiModelArg]?.[zoneArg]?.[sourceArg];
    if (!command) {
      throw new Error(`Arcam FMJ ${apiModelArg} zone ${zoneArg} does not support source ${sourceArg}.`);
    }
    return command;
  }

  private lookupStatusSource(apiModelArg: TArcamFmjApiModel, zoneArg: number, sourceArg: string): number {
    const value = statusSourceMaps[apiModelArg]?.[zoneArg]?.[sourceArg];
    if (typeof value !== 'number') {
      throw new Error(`Arcam FMJ ${apiModelArg} zone ${zoneArg} does not support source ${sourceArg}.`);
    }
    return value;
  }

  private sourceFromStatusByte(apiModelArg: TArcamFmjApiModel, zoneArg: number, valueArg: number): TArcamFmjSource | undefined {
    const map = statusSourceMaps[apiModelArg]?.[zoneArg];
    if (!map) {
      return undefined;
    }
    return Object.entries(map).find((entryArg) => entryArg[1] === valueArg)?.[0] as TArcamFmjSource | undefined;
  }

  private sourceList(apiModelArg: TArcamFmjApiModel, zoneArg: number): TArcamFmjSource[] | undefined {
    const sourceMap = rc5SourceMaps[apiModelArg]?.[zoneArg] || statusSourceMaps[apiModelArg]?.[zoneArg];
    return sourceMap ? Object.keys(sourceMap) as TArcamFmjSource[] : undefined;
  }

  private mediaInfo(zoneArg: IArcamFmjZoneState) {
    if (zoneArg.source === 'DAB') {
      return {
        title: zoneArg.dabStation ? `DAB - ${zoneArg.dabStation}` : 'DAB',
        artist: zoneArg.dlsPdt,
        channel: zoneArg.dabStation,
        contentType: 'music',
        contentId: zoneArg.tunerPreset ? `preset:${zoneArg.tunerPreset}` : undefined,
      };
    }
    if (zoneArg.source === 'FM') {
      return {
        title: zoneArg.rdsInformation ? `FM - ${zoneArg.rdsInformation}` : 'FM',
        channel: zoneArg.rdsInformation,
        contentType: 'music',
        contentId: zoneArg.tunerPreset ? `preset:${zoneArg.tunerPreset}` : undefined,
      };
    }
    return zoneArg.source ? { title: zoneArg.source } : undefined;
  }

  private normalizeSnapshot(snapshotArg: IArcamFmjSnapshot, sourceArg: TArcamFmjSnapshotSource): IArcamFmjSnapshot {
    const deviceInfo = {
      ...this.deviceInfoFromConfig(),
      ...snapshotArg.deviceInfo,
    };
    const apiModel = deviceInfo.apiModel || this.apiModel(deviceInfo.model);
    deviceInfo.apiModel = apiModel;
    const online = snapshotArg.online ?? snapshotArg.zones.some((zoneArg) => zoneArg.available !== false);
    const zones = snapshotArg.zones.map((zoneArg) => {
      const zone: IArcamFmjZoneState = {
        ...zoneArg,
        name: zoneArg.name || this.zoneName(zoneArg.zone),
        available: zoneArg.available ?? online,
      };
      zone.sourceList = zone.sourceList || this.sourceList(apiModel, zone.zone);
      zone.volumeLevel = typeof zone.volumeLevel === 'number'
        ? clamp(zone.volumeLevel, 0, 1)
        : typeof zone.volume === 'number'
          ? clamp(zone.volume / 99, 0, 1)
          : undefined;
      zone.state = zone.state || (zone.power === false ? 'off' : zone.power === true ? 'on' : 'unknown');
      zone.media = zone.media || this.mediaInfo(zone);
      return zone;
    });
    return {
      ...snapshotArg,
      deviceInfo,
      zones,
      online,
      source: snapshotArg.source || sourceArg,
      lastUpdated: snapshotArg.lastUpdated || new Date().toISOString(),
    };
  }

  private deviceInfoFromConfig(amxArg?: Record<string, string>): IArcamFmjDeviceInfo {
    const model = amxArg?.['Device-Model'] || this.config.model;
    return {
      host: this.config.host,
      port: this.config.port || arcamFmjDefaultPort,
      name: this.config.name || (this.config.host ? `Arcam FMJ (${this.config.host})` : 'Arcam FMJ'),
      manufacturer: amxArg?.['Device-Make'] || this.config.manufacturer || 'Arcam',
      model: model || 'Arcam FMJ AVR',
      revision: amxArg?.['Device-Revision'] || this.config.revision,
      serialNumber: this.config.serialNumber,
      uniqueId: this.config.uniqueId || this.config.serialNumber || this.config.host,
      apiModel: this.apiModel(model),
      amx: amxArg,
    };
  }

  private manualZone(zoneArg: number, availableArg: boolean): IArcamFmjZoneState {
    const configured = this.config.zones?.find((itemArg) => itemArg.zone === zoneArg);
    return {
      zone: zoneArg,
      name: this.zoneName(zoneArg),
      power: false,
      state: 'off',
      available: availableArg,
      sourceList: this.sourceList(this.apiModel(), zoneArg),
      ...configured,
    };
  }

  private apiModel(modelArg = this.config.model): TArcamFmjApiModel {
    if (this.config.apiModel) {
      return this.config.apiModel;
    }
    const model = modelArg?.toUpperCase();
    const match = model ? modelSeries.find((seriesArg) => seriesArg.models.includes(model)) : undefined;
    return match?.apiModel || 'API450_SERIES';
  }

  private normalizeZone(zoneArg: number | undefined): number {
    return zoneArg && Number.isFinite(zoneArg) && zoneArg > 0 ? Math.round(zoneArg) : 1;
  }

  private zoneName(zoneArg: number): string {
    const configured = this.config.zones?.find((itemArg) => itemArg.zone === zoneArg)?.name;
    if (configured) {
      return configured;
    }
    return zoneArg === 1 ? 'Main Zone' : `Zone ${zoneArg}`;
  }

  private cloneValue<TValue>(valueArg: TValue): TValue {
    return valueArg === undefined ? valueArg : JSON.parse(JSON.stringify(valueArg)) as TValue;
  }
}

type TParsedPacket =
  | { type: 'response'; packet: IArcamFmjResponsePacket }
  | { type: 'amx'; values: Record<string, string> }
  | { type: 'sent' };

const parsePacket = (bufferArg: Buffer): { packet: TParsedPacket; remaining: Buffer } | undefined => {
  let buffer = bufferArg;
  while (buffer[0] === 0x00) {
    buffer = buffer.subarray(1);
  }
  if (!buffer.length) {
    return undefined;
  }

  if (buffer[0] === protocolStart) {
    if (buffer.length < 6) {
      return undefined;
    }
    const dataLength = buffer[4];
    const packetLength = 6 + dataLength;
    if (buffer.length < packetLength) {
      return undefined;
    }
    if (buffer[packetLength - 1] !== protocolEnd) {
      return { packet: { type: 'sent' }, remaining: buffer.subarray(1) };
    }
    const data = [...buffer.subarray(5, 5 + dataLength)];
    return {
      packet: {
        type: 'response',
        packet: {
          zone: buffer[1],
          commandCode: buffer[2],
          commandCodeName: commandNames[buffer[2]] || `CODE_${buffer[2]}`,
          answerCode: buffer[3],
          data,
          dataHex: hex(data),
        },
      },
      remaining: buffer.subarray(packetLength),
    };
  }

  if (buffer[0] === 0x01) {
    if (buffer.length < 5) {
      return undefined;
    }
    if (buffer.subarray(1, 5).toString('ascii') !== '^AMX') {
      return { packet: { type: 'sent' }, remaining: buffer.subarray(1) };
    }
    const end = buffer.indexOf(protocolEnd, 5);
    if (end < 0) {
      return undefined;
    }
    const amx = Buffer.concat([Buffer.from('AMX', 'ascii'), buffer.subarray(5, end + 1)]);
    return { packet: { type: 'amx', values: parseAmxValues(amx) }, remaining: buffer.subarray(end + 1) };
  }

  if (buffer.subarray(0, 3).toString('ascii') === 'AMX') {
    const end = buffer.indexOf(protocolEnd, 3);
    if (end < 0) {
      return undefined;
    }
    return { packet: { type: 'amx', values: parseAmxValues(buffer.subarray(0, end + 1)) }, remaining: buffer.subarray(end + 1) };
  }

  return { packet: { type: 'sent' }, remaining: buffer.subarray(1) };
};

const parseAmxValues = (bufferArg: Buffer): Record<string, string> => {
  const text = bufferArg.toString('ascii');
  if (!text.startsWith('AMXB')) {
    return {};
  }
  const values: Record<string, string> = {};
  for (const match of text.slice(4).matchAll(/<(.+?)=(.+?)>/g)) {
    values[match[1]] = match[2];
  }
  return values;
};

const parseIncomingVideo = (dataArg: number[]): IArcamFmjIncomingVideoParameters | undefined => {
  if (dataArg.length < 7) {
    return undefined;
  }
  return {
    horizontalResolution: dataArg[0] * 256 + dataArg[1],
    verticalResolution: dataArg[2] * 256 + dataArg[3],
    refreshRate: dataArg[4],
    interlaced: dataArg[5] === 0x01,
    aspectRatio: videoAspectRatios[dataArg[6]] || `CODE_${dataArg[6]}`,
    colorspace: dataArg.length >= 8 ? videoColorspaces[dataArg[7]] || `CODE_${dataArg[7]}` : undefined,
  };
};

const parseIncomingAudio = (formatArg: number[] | undefined, sampleRateArg: number[] | undefined): IArcamFmjIncomingAudioInfo | undefined => {
  if (!formatArg?.length && !sampleRateArg?.length) {
    return undefined;
  }
  return {
    format: formatArg?.length ? incomingAudioFormats[formatArg[0]] || `CODE_${formatArg[0]}` : undefined,
    config: formatArg && formatArg.length > 1 ? incomingAudioConfigs[formatArg[1]] || `CODE_${formatArg[1]}` : undefined,
    sampleRate: sampleRateArg?.length ? incomingAudioSampleRates[sampleRateArg[0]] || 0 : undefined,
  };
};

const normalizeSource = (sourceArg: string): string => {
  const source = sourceArg.trim().toUpperCase().replace(/[\s/-]+/g, '_').replace(/^ARC_EARC$/, 'ARC_ERC');
  const aliases: Record<string, string> = {
    ARC: 'ARC_ERC',
    EARC: 'ARC_ERC',
    ARC_ERC: 'ARC_ERC',
    BLUETOOTH: 'BT',
    NETUSB: 'NET_USB',
    NET_USB: 'NET_USB',
  };
  return aliases[source] || source;
};

const textValue = (dataArg: number[] | undefined): string | undefined => {
  if (!dataArg?.length) {
    return undefined;
  }
  return Buffer.from(dataArg).toString('utf8').trim() || undefined;
};

const clamp = (valueArg: number, minArg: number, maxArg: number): number => Math.max(minArg, Math.min(maxArg, valueArg));

const hex = (dataArg: number[]): string => dataArg.map((valueArg) => valueArg.toString(16).padStart(2, '0')).join('');
