import * as plugins from '../../plugins.js';
import type {
  IHeosCommandRequest,
  IHeosConfig,
  IHeosGroup,
  IHeosHost,
  IHeosMediaItem,
  IHeosMessage,
  IHeosMusicSource,
  IHeosNowPlayingMedia,
  IHeosPlayer,
  IHeosSnapshot,
  THeosPlayState,
  THeosRepeatType,
} from './heos.types.js';
import { heosDefaultPort } from './heos.types.js';

const defaultTimeoutMs = 15000;
const heosLineSeparator = '\r\n';
const musicSourceAuxInput = 1027;
const musicSourceFavorites = 1028;

const commandNames = {
  checkAccount: 'system/check_account',
  signIn: 'system/sign_in',
  getPlayers: 'player/get_players',
  getPlayState: 'player/get_play_state',
  setPlayState: 'player/set_play_state',
  getNowPlaying: 'player/get_now_playing_media',
  getVolume: 'player/get_volume',
  setVolume: 'player/set_volume',
  volumeUp: 'player/volume_up',
  volumeDown: 'player/volume_down',
  getMute: 'player/get_mute',
  setMute: 'player/set_mute',
  getPlayMode: 'player/get_play_mode',
  playNext: 'player/play_next',
  playPrevious: 'player/play_previous',
  getGroups: 'group/get_groups',
  getGroupVolume: 'group/get_volume',
  setGroupVolume: 'group/set_volume',
  groupVolumeUp: 'group/volume_up',
  groupVolumeDown: 'group/volume_down',
  getGroupMute: 'group/get_mute',
  setGroup: 'group/set_group',
  getMusicSources: 'browse/get_music_sources',
  browse: 'browse/browse',
  playPreset: 'browse/play_preset',
  playInput: 'browse/play_input',
  playStream: 'browse/play_stream',
} as const;

export class HeosCommandError extends Error {
  constructor(public readonly command: string, messageArg: string, public readonly response?: IHeosMessage) {
    super(`HEOS command ${command} failed: ${messageArg}`);
    this.name = 'HeosCommandError';
  }
}

export class HeosClient {
  private currentSnapshot?: IHeosSnapshot;

  constructor(private readonly config: IHeosConfig) {
    this.currentSnapshot = config.snapshot ? this.normalizeSnapshot(this.cloneValue(config.snapshot)) : undefined;
  }

  public async getSnapshot(): Promise<IHeosSnapshot> {
    if (this.currentSnapshot) {
      return this.normalizeSnapshot(this.cloneValue(this.currentSnapshot));
    }
    if (!this.config.host) {
      return this.normalizeSnapshot({
        system: { host: undefined, currentHost: undefined, isSignedIn: false },
        players: [],
        groups: [],
        musicSources: [],
        favorites: {},
        inputSources: [],
        lastUpdated: new Date().toISOString(),
      });
    }

    const signedInUsername = await this.signInOrCheckAccount().catch(() => undefined);
    const players = await this.getPlayers();
    const [groups, musicSources, inputSources, favorites] = await Promise.all([
      this.getGroups().catch(() => []),
      this.getMusicSources().catch(() => []),
      this.getInputSources().catch(() => []),
      signedInUsername ? this.getFavorites().catch(() => ({})) : Promise.resolve({}),
    ]);

    return this.normalizeSnapshot({
      system: {
        host: this.config.host,
        currentHost: this.config.host,
        signedInUsername,
        isSignedIn: Boolean(signedInUsername),
        hosts: players.map((playerArg) => this.playerToHost(playerArg)),
        preferredHosts: players.map((playerArg) => this.playerToHost(playerArg)).filter((hostArg) => hostArg.preferredHost),
      },
      players,
      groups,
      musicSources,
      inputSources,
      favorites,
      lastUpdated: new Date().toISOString(),
    });
  }

  public async validateConnection(): Promise<IHeosSnapshot> {
    const players = await this.getPlayers();
    return this.normalizeSnapshot({
      system: {
        host: this.config.host,
        currentHost: this.config.host,
        hosts: players.map((playerArg) => this.playerToHost(playerArg)),
      },
      players,
      lastUpdated: new Date().toISOString(),
    });
  }

  public async execute(requestArg: IHeosCommandRequest): Promise<unknown> {
    if (requestArg.command === 'play') {
      return this.setPlayState(this.requiredPlayerId(requestArg), 'play');
    }
    if (requestArg.command === 'pause') {
      return this.setPlayState(this.requiredPlayerId(requestArg), 'pause');
    }
    if (requestArg.command === 'stop') {
      return this.setPlayState(this.requiredPlayerId(requestArg), 'stop');
    }
    if (requestArg.command === 'previous_track') {
      return this.command(commandNames.playPrevious, { pid: this.requiredPlayerId(requestArg) });
    }
    if (requestArg.command === 'next_track') {
      return this.command(commandNames.playNext, { pid: this.requiredPlayerId(requestArg) });
    }
    if (requestArg.command === 'volume_up') {
      return this.command(commandNames.volumeUp, { pid: this.requiredPlayerId(requestArg), step: this.step(requestArg.step) });
    }
    if (requestArg.command === 'volume_down') {
      return this.command(commandNames.volumeDown, { pid: this.requiredPlayerId(requestArg), step: this.step(requestArg.step) });
    }
    if (requestArg.command === 'set_volume') {
      return this.command(commandNames.setVolume, { pid: this.requiredPlayerId(requestArg), level: this.volumePercent(requestArg) });
    }
    if (requestArg.command === 'mute') {
      if (typeof requestArg.muted !== 'boolean') {
        throw new Error('HEOS mute command requires muted.');
      }
      return this.command(commandNames.setMute, { pid: this.requiredPlayerId(requestArg), state: requestArg.muted ? 'on' : 'off' });
    }
    if (requestArg.command === 'select_source') {
      return this.selectSource(requestArg);
    }
    if (requestArg.command === 'play_preset') {
      return this.command(commandNames.playPreset, { pid: this.requiredPlayerId(requestArg), preset: this.requiredNumber(requestArg.preset, 'HEOS play_preset requires preset.') });
    }
    if (requestArg.command === 'play_input') {
      if (!requestArg.source) {
        throw new Error('HEOS play_input requires source.');
      }
      return this.command(commandNames.playInput, { pid: this.requiredPlayerId(requestArg), input: requestArg.source });
    }
    if (requestArg.command === 'play_media') {
      if (!requestArg.url && !requestArg.mediaId) {
        throw new Error('HEOS play_media requires url or mediaId.');
      }
      return this.command(commandNames.playStream, { pid: this.requiredPlayerId(requestArg), url: requestArg.url || requestArg.mediaId || '' });
    }
    if (requestArg.command === 'set_group' || requestArg.command === 'join') {
      if (!requestArg.playerIds?.length) {
        throw new Error('HEOS set_group requires playerIds.');
      }
      return this.setGroup(requestArg.playerIds);
    }
    if (requestArg.command === 'unjoin') {
      return this.unjoinPlayer(this.requiredPlayerId(requestArg));
    }
    if (requestArg.command === 'group_volume_set') {
      return this.command(commandNames.setGroupVolume, { gid: this.requiredGroupId(requestArg), level: this.volumePercent(requestArg) });
    }
    if (requestArg.command === 'group_volume_up') {
      return this.command(commandNames.groupVolumeUp, { gid: this.requiredGroupId(requestArg), step: this.step(requestArg.step) });
    }
    if (requestArg.command === 'group_volume_down') {
      return this.command(commandNames.groupVolumeDown, { gid: this.requiredGroupId(requestArg), step: this.step(requestArg.step) });
    }
    throw new Error(`Unsupported HEOS command: ${requestArg.command}`);
  }

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

  private async getPlayers(): Promise<IHeosPlayer[]> {
    const response = await this.command(commandNames.getPlayers);
    const players = this.payloadArray(response).map((itemArg) => this.playerFromData(itemArg));
    return Promise.all(players.map((playerArg) => this.refreshPlayer(playerArg)));
  }

  private async refreshPlayer(playerArg: IHeosPlayer): Promise<IHeosPlayer> {
    const [state, nowPlayingMedia, volume, muted, playMode] = await Promise.all([
      this.getPlayState(playerArg.playerId).catch(() => undefined),
      this.getNowPlaying(playerArg.playerId).catch(() => undefined),
      this.getVolume(playerArg.playerId).catch(() => undefined),
      this.getMute(playerArg.playerId).catch(() => undefined),
      this.getPlayMode(playerArg.playerId).catch(() => undefined),
    ]);
    return {
      ...playerArg,
      state: state || playerArg.state,
      volume: volume ?? playerArg.volume,
      muted: muted ?? playerArg.muted,
      repeat: playMode?.repeat || playerArg.repeat,
      shuffle: playMode?.shuffle ?? playerArg.shuffle,
      nowPlayingMedia: nowPlayingMedia || playerArg.nowPlayingMedia,
      available: true,
    };
  }

  private async getGroups(): Promise<IHeosGroup[]> {
    const response = await this.command(commandNames.getGroups);
    const groups = this.payloadArray(response).map((itemArg) => this.groupFromData(itemArg));
    return Promise.all(groups.map(async (groupArg) => ({
      ...groupArg,
      volume: await this.getGroupVolume(groupArg.groupId).catch(() => groupArg.volume),
      muted: await this.getGroupMute(groupArg.groupId).catch(() => groupArg.muted),
    })));
  }

  private async getMusicSources(): Promise<IHeosMusicSource[]> {
    const response = await this.command(commandNames.getMusicSources);
    return this.payloadArray(response).map((itemArg) => this.musicSourceFromData(itemArg));
  }

  private async getInputSources(): Promise<IHeosMediaItem[]> {
    const root = await this.browse(musicSourceAuxInput);
    const inputs: IHeosMediaItem[] = [];
    for (const item of root) {
      if (!item.containerId && !item.browsable) {
        continue;
      }
      const children = await this.browse(item.sourceId, item.containerId).catch(() => []);
      inputs.push(...children);
    }
    return inputs;
  }

  private async getFavorites(): Promise<Record<number, IHeosMediaItem>> {
    const favorites = await this.browse(musicSourceFavorites);
    const result: Record<number, IHeosMediaItem> = {};
    favorites.forEach((favoriteArg, indexArg) => {
      result[indexArg + 1] = favoriteArg;
    });
    return result;
  }

  private async browse(sourceIdArg: number, containerIdArg?: string): Promise<IHeosMediaItem[]> {
    const response = await this.command(commandNames.browse, containerIdArg ? { sid: sourceIdArg, cid: containerIdArg } : { sid: sourceIdArg });
    return this.payloadArray(response).map((itemArg) => this.mediaItemFromData(itemArg, sourceIdArg, containerIdArg));
  }

  private async signInOrCheckAccount(): Promise<string | undefined> {
    if (this.config.username && this.config.password) {
      const response = await this.command(commandNames.signIn, { un: this.config.username, pw: this.config.password });
      return response.message.un;
    }
    const response = await this.command(commandNames.checkAccount);
    return response.message.un;
  }

  private async getPlayState(playerIdArg: number): Promise<THeosPlayState> {
    const response = await this.command(commandNames.getPlayState, { pid: playerIdArg });
    return response.message.state || 'unknown';
  }

  private async setPlayState(playerIdArg: number, stateArg: 'play' | 'pause' | 'stop'): Promise<IHeosMessage> {
    return this.command(commandNames.setPlayState, { pid: playerIdArg, state: stateArg });
  }

  private async getNowPlaying(playerIdArg: number): Promise<IHeosNowPlayingMedia> {
    const response = await this.command(commandNames.getNowPlaying, { pid: playerIdArg });
    const payload = this.payloadRecord(response);
    return this.nowPlayingFromData({ ...payload, ...response.message });
  }

  private async getVolume(playerIdArg: number): Promise<number | undefined> {
    const response = await this.command(commandNames.getVolume, { pid: playerIdArg });
    return this.numberValue(response.message.level);
  }

  private async getMute(playerIdArg: number): Promise<boolean | undefined> {
    const response = await this.command(commandNames.getMute, { pid: playerIdArg });
    return response.message.state === 'on';
  }

  private async getPlayMode(playerIdArg: number): Promise<{ repeat?: THeosRepeatType; shuffle?: boolean }> {
    const response = await this.command(commandNames.getPlayMode, { pid: playerIdArg });
    return { repeat: response.message.repeat, shuffle: response.message.shuffle === 'on' };
  }

  private async getGroupVolume(groupIdArg: number): Promise<number | undefined> {
    const response = await this.command(commandNames.getGroupVolume, { gid: groupIdArg });
    return this.numberValue(response.message.level);
  }

  private async getGroupMute(groupIdArg: number): Promise<boolean | undefined> {
    const response = await this.command(commandNames.getGroupMute, { gid: groupIdArg });
    return response.message.state === 'on';
  }

  private async selectSource(requestArg: IHeosCommandRequest): Promise<IHeosMessage> {
    if (!requestArg.source) {
      throw new Error('HEOS select_source requires source.');
    }
    const snapshot = await this.getSnapshot();
    const favoriteEntry = Object.entries(snapshot.favorites || {}).find(([, favoriteArg]) => favoriteArg.name === requestArg.source);
    if (favoriteEntry) {
      return this.command(commandNames.playPreset, { pid: this.requiredPlayerId(requestArg), preset: Number(favoriteEntry[0]) });
    }
    const inputSource = (snapshot.inputSources || []).find((sourceArg) => sourceArg.name === requestArg.source || sourceArg.mediaId === requestArg.source);
    if (inputSource) {
      const params: Record<string, string | number | boolean> = { pid: this.requiredPlayerId(requestArg), input: inputSource.mediaId || inputSource.name };
      if (inputSource.sourceId && inputSource.sourceId !== musicSourceAuxInput) {
        params.spid = inputSource.sourceId;
      }
      return this.command(commandNames.playInput, params);
    }
    throw new Error(`Unknown HEOS source: ${requestArg.source}`);
  }

  private async setGroup(playerIdsArg: number[]): Promise<IHeosMessage> {
    const uniquePlayerIds = [...new Set(playerIdsArg)];
    if (!uniquePlayerIds.length) {
      throw new Error('HEOS set_group requires at least one player id.');
    }
    return this.command(commandNames.setGroup, { pid: uniquePlayerIds.join(',') });
  }

  private async unjoinPlayer(playerIdArg: number): Promise<IHeosMessage | undefined> {
    const snapshot = await this.getSnapshot();
    const group = (snapshot.groups || []).find((groupArg) => groupArg.leadPlayerId === playerIdArg || groupArg.memberPlayerIds.includes(playerIdArg));
    if (!group) {
      return undefined;
    }
    if (group.leadPlayerId === playerIdArg) {
      return this.setGroup([playerIdArg]);
    }
    return this.setGroup([group.leadPlayerId, ...group.memberPlayerIds.filter((memberArg) => memberArg !== playerIdArg)]);
  }

  private async command(commandArg: string, parametersArg: Record<string, string | number | boolean> = {}): Promise<IHeosMessage> {
    const uri = this.commandUri(commandArg, parametersArg);
    if (this.config.commandExecutor) {
      const result = await this.config.commandExecutor.execute({
        command: commandArg,
        parameters: parametersArg,
        uri,
        host: this.config.host,
        port: this.config.port || heosDefaultPort,
      });
      return this.executorResultToMessage(commandArg, result);
    }
    return this.requestTcp(commandArg, parametersArg, uri);
  }

  private async requestTcp(commandArg: string, parametersArg: Record<string, string | number | boolean>, uriArg: string): Promise<IHeosMessage> {
    const host = this.config.host;
    if (!host) {
      throw new Error('HEOS command transport requires config.host or commandExecutor.');
    }
    const port = this.config.port || heosDefaultPort;
    const timeoutMs = this.config.timeoutMs || defaultTimeoutMs;

    return new Promise<IHeosMessage>((resolve, reject) => {
      let buffer = '';
      let settled = false;
      const socket = plugins.net.createConnection({ host, port });

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

      const handleLine = (lineArg: string) => {
        if (!lineArg.trim()) {
          return;
        }
        let message: IHeosMessage;
        try {
          message = this.parseRawMessage(lineArg);
        } catch (errorArg) {
          finish(errorArg instanceof Error ? errorArg : new Error(String(errorArg)));
          return;
        }
        if (this.isUnderProcess(message) || message.command.startsWith('event/')) {
          return;
        }
        if (message.command !== commandArg) {
          return;
        }
        if (!message.result) {
          finish(new HeosCommandError(commandArg, this.commandErrorText(message), message));
          return;
        }
        finish(undefined, message);
      };

      socket.setEncoding('utf8');
      socket.setTimeout(timeoutMs, () => finish(new Error(`HEOS TCP command ${commandArg} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => socket.write(`${uriArg}${heosLineSeparator}`));
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => finish(new Error(`HEOS TCP connection closed before ${commandArg} completed.`)));
      socket.on('data', (chunkArg) => {
        buffer += chunkArg;
        const lines = buffer.split(/\r?\n/);
        buffer = lines.pop() || '';
        for (const line of lines) {
          handleLine(line);
        }
      });

      void parametersArg;
    });
  }

  private playerFromData(dataArg: Record<string, unknown>): IHeosPlayer {
    const version = this.stringValue(dataArg.version);
    const playerId = this.requiredParsedNumber(dataArg.pid, 'HEOS player payload missing pid.');
    return {
      name: this.stringValue(dataArg.name) || `HEOS ${playerId}`,
      playerId,
      model: this.stringValue(dataArg.model) || 'HEOS Player',
      serial: this.stringValue(dataArg.serial),
      version,
      supportedVersion: this.isSupportedVersion(version),
      ipAddress: this.stringValue(dataArg.ip),
      network: this.stringValue(dataArg.network) || 'unknown',
      lineOut: this.stringValue(dataArg.lineout) || this.numberValue(dataArg.lineout),
      control: this.stringValue(dataArg.control) || this.numberValue(dataArg.control),
      groupId: this.numberValue(dataArg.gid),
      available: true,
    };
  }

  private groupFromData(dataArg: Record<string, unknown>): IHeosGroup {
    const players = Array.isArray(dataArg.players) ? dataArg.players.filter((itemArg): itemArg is Record<string, unknown> => this.isRecord(itemArg)) : [];
    const leader = players.find((playerArg) => this.stringValue(playerArg.role) === 'leader');
    const members = players.filter((playerArg) => this.stringValue(playerArg.role) !== 'leader').map((playerArg) => this.numberValue(playerArg.pid)).filter((valueArg): valueArg is number => typeof valueArg === 'number');
    const groupId = this.requiredParsedNumber(dataArg.gid, 'HEOS group payload missing gid.');
    return {
      name: this.stringValue(dataArg.name) || `HEOS Group ${groupId}`,
      groupId,
      leadPlayerId: this.numberValue(leader?.pid) || groupId,
      memberPlayerIds: members,
    };
  }

  private musicSourceFromData(dataArg: Record<string, unknown>): IHeosMusicSource {
    return {
      sourceId: this.requiredParsedNumber(dataArg.sid, 'HEOS music source payload missing sid.'),
      name: this.stringValue(dataArg.name) || 'HEOS Source',
      type: this.stringValue(dataArg.type) || 'music_service',
      imageUrl: this.stringValue(dataArg.image_url),
      available: this.stringValue(dataArg.available) === 'true',
      serviceUsername: this.stringValue(dataArg.service_username),
    };
  }

  private mediaItemFromData(dataArg: Record<string, unknown>, sourceIdArg?: number, containerIdArg?: string): IHeosMediaItem {
    const sourceId = this.numberValue(dataArg.sid) || sourceIdArg;
    if (!sourceId) {
      throw new Error('HEOS media item payload missing sid.');
    }
    return {
      sourceId,
      name: this.decodeName(this.stringValue(dataArg.name) || 'HEOS Media'),
      type: this.stringValue(dataArg.type) || 'container',
      imageUrl: this.stringValue(dataArg.image_url),
      playable: this.stringValue(dataArg.playable) === 'yes',
      browsable: dataArg.sid !== undefined || this.stringValue(dataArg.container) === 'yes',
      containerId: this.stringValue(dataArg.cid) || containerIdArg,
      mediaId: this.stringValue(dataArg.mid),
      artist: this.stringValue(dataArg.artist),
      album: this.stringValue(dataArg.album),
      albumId: this.stringValue(dataArg.album_id),
    };
  }

  private nowPlayingFromData(dataArg: Record<string, unknown>): IHeosNowPlayingMedia {
    const sourceId = this.numberValue(dataArg.sid);
    const type = this.stringValue(dataArg.type);
    return {
      type,
      song: this.stringValue(dataArg.song),
      station: this.stringValue(dataArg.station),
      album: this.stringValue(dataArg.album),
      artist: this.stringValue(dataArg.artist),
      imageUrl: this.stringValue(dataArg.image_url),
      albumId: this.stringValue(dataArg.album_id),
      mediaId: this.stringValue(dataArg.mid),
      queueId: this.numberValue(dataArg.qid),
      sourceId,
      currentPosition: this.numberValue(dataArg.cur_pos),
      duration: this.numberValue(dataArg.duration),
      supportedControls: this.supportedControls(sourceId, type),
    };
  }

  private playerToHost(playerArg: IHeosPlayer): IHeosHost {
    return {
      name: playerArg.name,
      model: playerArg.model,
      serial: playerArg.serial,
      version: playerArg.version,
      ipAddress: playerArg.ipAddress,
      network: playerArg.network,
      supportedVersion: playerArg.supportedVersion,
      preferredHost: playerArg.network === 'wired' && playerArg.supportedVersion !== false && Boolean(playerArg.ipAddress),
    };
  }

  private supportedControls(sourceIdArg: number | undefined, typeArg: string | undefined): string[] {
    if (sourceIdArg === undefined) {
      return [];
    }
    if (sourceIdArg === musicSourceAuxInput) {
      return ['play', 'stop'];
    }
    if (typeArg === 'station') {
      return ['play', 'pause', 'stop', 'play_next'];
    }
    return ['play', 'pause', 'stop', 'play_next', 'play_previous'];
  }

  private normalizeSnapshot(snapshotArg: IHeosSnapshot): IHeosSnapshot {
    const sourceList = snapshotArg.sourceList || [
      ...Object.values(snapshotArg.favorites || {}).map((favoriteArg) => favoriteArg.name),
      ...(snapshotArg.inputSources || []).map((sourceArg) => sourceArg.name),
    ].filter(Boolean);
    return {
      ...snapshotArg,
      system: {
        ...snapshotArg.system,
        isSignedIn: snapshotArg.system.isSignedIn ?? Boolean(snapshotArg.system.signedInUsername),
      },
      players: snapshotArg.players || [],
      groups: snapshotArg.groups || [],
      musicSources: snapshotArg.musicSources || [],
      favorites: snapshotArg.favorites || {},
      inputSources: snapshotArg.inputSources || [],
      sourceList: [...new Set(sourceList)],
      lastUpdated: snapshotArg.lastUpdated || new Date().toISOString(),
    };
  }

  private commandUri(commandArg: string, parametersArg: Record<string, string | number | boolean>): string {
    const params = this.encodeQuery(parametersArg);
    return params ? `heos://${commandArg}?${params}` : `heos://${commandArg}`;
  }

  private encodeQuery(parametersArg: Record<string, string | number | boolean>): string {
    const pairs: string[] = [];
    for (const key of Object.keys(parametersArg).sort()) {
      const value = parametersArg[key];
      if (key === 'url') {
        pairs.push(`${key}=${value}`);
      } else {
        pairs.unshift(`${key}=${this.quoteCliValue(value)}`);
      }
    }
    return pairs.join('&');
  }

  private quoteCliValue(valueArg: string | number | boolean): string {
    return String(valueArg).replace(/%/g, '%25').replace(/&/g, '%26').replace(/=/g, '%3D');
  }

  private parseRawMessage(rawArg: string): IHeosMessage {
    const container = JSON.parse(rawArg) as { heos?: { command?: string; result?: string; message?: string }; payload?: unknown; options?: unknown };
    if (!container.heos?.command) {
      throw new Error(`Invalid HEOS response: ${rawArg}`);
    }
    return {
      command: container.heos.command,
      result: (container.heos.result || 'success') === 'success',
      message: this.parseMessage(container.heos.message || ''),
      payload: container.payload,
      options: container.options,
      raw: rawArg,
    };
  }

  private parseMessage(valueArg: string): Record<string, string> {
    const result: Record<string, string> = {};
    const params = new URLSearchParams(valueArg);
    params.forEach((value, key) => {
      result[key] = value;
    });
    if (!Object.keys(result).length && valueArg) {
      result[valueArg] = '';
    }
    return result;
  }

  private executorResultToMessage(commandArg: string, resultArg: unknown): IHeosMessage {
    if (this.isHeosMessage(resultArg)) {
      if (!resultArg.result) {
        throw new HeosCommandError(commandArg, this.commandErrorText(resultArg), resultArg);
      }
      return resultArg;
    }
    return { command: commandArg, result: true, message: {}, payload: resultArg };
  }

  private payloadArray(responseArg: IHeosMessage): Record<string, unknown>[] {
    return Array.isArray(responseArg.payload) ? responseArg.payload.filter((itemArg): itemArg is Record<string, unknown> => this.isRecord(itemArg)) : [];
  }

  private payloadRecord(responseArg: IHeosMessage): Record<string, unknown> {
    return this.isRecord(responseArg.payload) ? responseArg.payload : {};
  }

  private requiredPlayerId(requestArg: IHeosCommandRequest): number {
    if (typeof requestArg.playerId === 'number') {
      return requestArg.playerId;
    }
    throw new Error('HEOS command requires playerId.');
  }

  private requiredGroupId(requestArg: IHeosCommandRequest): number {
    if (typeof requestArg.groupId === 'number') {
      return requestArg.groupId;
    }
    throw new Error('HEOS group command requires groupId.');
  }

  private volumePercent(requestArg: IHeosCommandRequest): number {
    const value = requestArg.volume ?? (typeof requestArg.volumeLevel === 'number' ? requestArg.volumeLevel * 100 : undefined);
    if (typeof value !== 'number' || !Number.isFinite(value)) {
      throw new Error('HEOS volume command requires volumeLevel or volume.');
    }
    return Math.max(0, Math.min(100, Math.round(value)));
  }

  private step(valueArg: number | undefined): number {
    const value = typeof valueArg === 'number' ? valueArg : 5;
    return Math.max(1, Math.min(10, Math.round(value)));
  }

  private requiredNumber(valueArg: unknown, errorArg: string): number {
    if (typeof valueArg !== 'number' || !Number.isFinite(valueArg)) {
      throw new Error(errorArg);
    }
    return valueArg;
  }

  private requiredParsedNumber(valueArg: unknown, errorArg: string): number {
    const value = this.numberValue(valueArg);
    if (typeof value !== 'number') {
      throw new Error(errorArg);
    }
    return value;
  }

  private numberValue(valueArg: unknown): number | undefined {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (typeof valueArg === 'string' && valueArg.trim()) {
      const parsed = Number(valueArg);
      return Number.isFinite(parsed) ? parsed : undefined;
    }
    return undefined;
  }

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

  private isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
    return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
  }

  private isHeosMessage(valueArg: unknown): valueArg is IHeosMessage {
    return this.isRecord(valueArg) && typeof valueArg.command === 'string' && typeof valueArg.result === 'boolean' && this.isRecord(valueArg.message);
  }

  private isUnderProcess(messageArg: IHeosMessage): boolean {
    return Object.keys(messageArg.message).some((keyArg) => keyArg.includes('command under process'));
  }

  private commandErrorText(messageArg: IHeosMessage): string {
    return messageArg.message.text || messageArg.message.error || messageArg.message.errno || 'Command failed';
  }

  private decodeName(valueArg: string): string {
    try {
      return decodeURIComponent(valueArg);
    } catch {
      return valueArg;
    }
  }

  private isSupportedVersion(versionArg: string | undefined): boolean | undefined {
    if (!versionArg) {
      return undefined;
    }
    const versionParts = versionArg.split('.').map((partArg) => Number(partArg));
    const targetParts = [3, 34, 0];
    for (let index = 0; index < targetParts.length; index++) {
      const versionPart = versionParts[index] || 0;
      if (versionPart > targetParts[index]) {
        return true;
      }
      if (versionPart < targetParts[index]) {
        return false;
      }
    }
    return true;
  }

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