import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot } from '../../core/index.js';
import { KaleidescapeMapper } from './kaleidescape.mapper.js';
import type { IKaleidescapeConfig } from './kaleidescape.types.js';
import { kaleidescapeProfile } from './kaleidescape.types.js';

export class KaleidescapeClientError extends Error {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'KaleidescapeClientError';
  }
}

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

interface IKaleidescapeResponse {
  deviceId: string;
  seq: number;
  status: number;
  name: string;
  fields: string[];
}

interface IKaleidescapeState {
  system: {
    serialNumber?: string;
    cpdid?: string;
    ipAddress?: string;
    type?: string;
    protocol?: number;
    kosVersion?: string;
    friendlyName?: string;
    movieZones?: number;
    musicZones?: number;
  };
  power: {
    state?: string;
    readiness?: string;
    zone?: string[];
  };
  osd: {
    uiScreen: string;
    uiPopup: string;
    uiDialog: string;
    uiScreensaver: string;
    highlighted?: string;
  };
  movie: {
    playStatus: string;
    playSpeed: number;
    titleLength: number;
    titleLocation: number;
    mediaType: string;
  };
  automation: {
    movieLocation: string;
    videoMode: string;
    videoColorEotf: string;
    videoColorSpace: string;
    videoColorDepth: string;
    videoColorSampling: string;
    screenMaskRatio: string;
    screenMaskTopTrimRel: number;
    screenMaskBottomTrimRel: number;
    screenMaskConservativeRatio: string;
    screenMaskTopMaskAbs: number;
    screenMaskBottomMaskAbs: number;
    cinemascapeMode: string;
    cinemascapeMask: number;
  };
}

const localCpdid = '01';

const powerStates: Record<number, string> = { 0: 'standby', 1: 'on' };
const zoneStates: Record<number, string> = { 0: 'disabled', 1: 'available' };
const readinessStates: Record<number, string> = { 0: 'ready', 1: 'becoming_ready', 2: 'idle' };
const playStatuses: Record<number, string> = { 0: 'none', 1: 'paused', 2: 'playing', 4: 'forward', 6: 'reverse' };
const movieLocations: Record<number, string> = { 0: 'none', 3: 'content', 4: 'intermission', 5: 'credits', 6: 'disc_menu' };
const mediaTypes: Record<number, string> = { 0: 'none', 1: 'dvd', 2: 'stream', 3: 'bluray' };
const uiScreens: Record<number, string> = { 0: 'unknown', 1: 'movie_list', 2: 'movie_collections', 3: 'movie_covers', 4: 'parental_control', 7: 'playing_movie', 8: 'system_status', 9: 'music_list', 10: 'music_covers', 11: 'music_collections', 12: 'music_now_playing', 14: 'vault_summary', 15: 'system_settings', 16: 'movie_store', 17: 'paired_unit_lobby' };
const uiPopups: Record<number, string> = { 0: 'none', 1: 'details', 2: 'movie_status', 3: 'movie_not_status' };
const uiDialogs: Record<number, string> = { 0: 'none', 1: 'menu', 2: 'passcode', 3: 'question', 4: 'information', 5: 'warning', 6: 'error', 7: 'preplay', 8: 'warranty', 9: 'keyboard', 10: 'ip_config' };
const uiSavers: Record<number, string> = { 0: 'inactive', 1: 'active' };
const videoModes: Record<number, string> = { 0: 'none', 1: '480i60_4:3', 2: '480i60_16:9', 3: '480p60_4:3', 4: '480p60_16:9', 5: '576i50_4:3', 6: '576i50_16:9', 7: '576p50_4:3', 8: '576p50_16:9', 9: '720p60_ntsc_hd', 10: '720p50_pal_hd', 11: '1080i60_16:9', 12: '1080i50_16:9', 13: '1080p60_16:9', 14: '1080p50_16:9', 17: '1080p24_16:9', 19: '480i60_64:27', 20: '576i50_64:27', 21: '1080i60_64:27', 22: '1080i50_64:27', 23: '1080p60_64:27', 24: '1080p50_64:27', 25: '1080p23976_64:27', 26: '1080p24_64:27', 27: '3840x2160p23976_16:9', 28: '3840x2160p23976_64:27', 29: '3840x2160p30_16:9', 30: '3840x2160p30_64:27', 31: '3840x2160p60_16:9', 32: '3840x2160p60_64:27', 33: '3840x2160p25_16:9', 34: '3840x2160p25_64:27', 35: '3840x2160p50_16:9', 36: '3840x2160p50_64:27', 37: '3840x2160p24_16:9', 38: '3840x2160p24_64:27' };
const videoEotfs: Record<number, string> = { 0: 'unknown', 1: 'sdr', 2: 'hdr', 3: 'smtpest2084' };
const videoSpaces: Record<number, string> = { 0: 'default', 1: 'rgb', 2: 'bt601', 3: 'bt709', 4: 'bt2020' };
const videoDepths: Record<number, string> = { 0: 'unknown', 24: '24bit', 30: '30bit', 36: '36bit' };
const videoSamplings: Record<number, string> = { 0: 'none', 1: 'rgb', 2: 'ycbcr422', 3: 'ycbcr444', 4: 'ycbcr420' };
const screenRatios: Record<number, string> = { 0: 'none', 1: '1.33', 2: '1.66', 3: '1.78', 4: '1.85', 5: '2.35' };
const cinemascapeModes: Record<number, string> = { 0: 'none', 1: 'anamorphic', 2: 'letterbox', 3: 'native' };

const remoteRequests: Record<string, string> = {
  select: 'SELECT',
  up: 'UP',
  down: 'DOWN',
  left: 'LEFT',
  right: 'RIGHT',
  cancel: 'CANCEL',
  replay: 'REPLAY',
  scan_forward: 'SCAN_FORWARD',
  scan_reverse: 'SCAN_REVERSE',
  go_movie_covers: 'GO_MOVIE_COVERS',
  menu_toggle: 'KALEIDESCAPE_MENU_TOGGLE',
};

const defaultState = (): IKaleidescapeState => ({
  system: {},
  power: {},
  osd: { uiScreen: 'unknown', uiPopup: 'none', uiDialog: 'none', uiScreensaver: 'inactive' },
  movie: { playStatus: 'none', playSpeed: 0, titleLength: 0, titleLocation: 0, mediaType: 'none' },
  automation: {
    movieLocation: 'none',
    videoMode: 'none',
    videoColorEotf: 'unknown',
    videoColorSpace: 'default',
    videoColorDepth: 'unknown',
    videoColorSampling: 'none',
    screenMaskRatio: 'none',
    screenMaskTopTrimRel: 0,
    screenMaskBottomTrimRel: 0,
    screenMaskConservativeRatio: '',
    screenMaskTopMaskAbs: 0,
    screenMaskBottomMaskAbs: 0,
    cinemascapeMode: 'none',
    cinemascapeMask: 0,
  },
});

export class KaleidescapeClient extends SimpleLocalClient<IKaleidescapeConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IKaleidescapeConfig) {
    super(kaleidescapeProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.liveSnapshot) {
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }
    if (this.configArg.client || this.configArg.snapshotProvider || (!forceRefreshArg && this.configArg.snapshot)) {
      return super.getSnapshot(forceRefreshArg);
    }
    if (this.endpoint() && this.configArg.transport !== 'snapshot') {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.liveSnapshot = KaleidescapeMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }
    return super.getSnapshot(forceRefreshArg);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.liveSnapshot = 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() || this.configArg.transport === 'snapshot') {
      return super.execute(requestArg);
    }
    if (this.configArg.useTls) {
      return { success: false, error: 'Kaleidescape TLS is not implemented: Home Assistant pykaleidescape uses plain local TCP on the System Control Protocol port.' };
    }

    const connection = new KaleidescapeTcpConnection(this.endpoint()!, this.timeoutMs());
    try {
      await connection.connect();
      const data = await this.executeNativeRequest(connection, requestArg);
      this.liveSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    } finally {
      connection.close();
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    if (this.configArg.useTls) {
      throw new KaleidescapeClientError('Kaleidescape TLS is not implemented: Home Assistant pykaleidescape uses plain local TCP on the System Control Protocol port.');
    }
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new KaleidescapeClientError('Kaleidescape TCP polling requires config.host or tcp:// config.url.');
    }

    const connection = new KaleidescapeTcpConnection(endpoint, this.timeoutMs());
    const state = defaultState();
    try {
      await connection.connect();
      applyDeviceInfo(state, await connection.send('GET_DEVICE_INFO'));
      applySystemVersion(state, await connection.send('GET_SYSTEM_VERSION'));
      applyDeviceTypeName(state, await connection.send('GET_DEVICE_TYPE_NAME'));
      applyNumZones(state, await connection.send('GET_NUM_ZONES'));
      applyDevicePowerState(state, await connection.send('GET_DEVICE_POWER_STATE'));
      applySystemReadinessState(state, await connection.send('GET_SYSTEM_READINESS_STATE'));
      if ((state.system.movieZones || 0) > 0) {
        await optional(() => connection.send('GET_FRIENDLY_NAME').then((responseArg) => applyFriendlyName(state, responseArg)));
      }
      if (state.power.state === 'on') {
        await optional(() => connection.send('GET_UI_STATE').then((responseArg) => applyUiState(state, responseArg)));
        await optional(() => connection.send('GET_HIGHLIGHTED_SELECTION').then((responseArg) => { state.osd.highlighted = responseArg.fields[0]; }));
        await optional(() => connection.send('GET_PLAY_STATUS').then((responseArg) => applyPlayStatus(state, responseArg)));
        await optional(() => connection.send('GET_MOVIE_LOCATION').then((responseArg) => { state.automation.movieLocation = mapped(movieLocations, responseArg.fields[0], 'movie location'); }));
        await optional(() => connection.send('GET_VIDEO_MODE').then((responseArg) => { state.automation.videoMode = mapped(videoModes, responseArg.fields[2], 'video mode'); }));
        await optional(() => connection.send('GET_VIDEO_COLOR').then((responseArg) => applyVideoColor(state, responseArg)));
        await optional(() => connection.send('GET_SCREEN_MASK').then((responseArg) => applyScreenMask(state, responseArg)));
        await optional(() => connection.send('GET_CINEMASCAPE_MODE').then((responseArg) => { state.automation.cinemascapeMode = mapped(cinemascapeModes, responseArg.fields[0], 'cinemascape mode'); }));
      }
    } finally {
      connection.close();
    }

    const uniqueBase = this.configArg.uniqueId || this.configArg.deviceId || state.system.serialNumber || `${endpoint.host}:${endpoint.port}`;
    const name = this.configArg.name || state.system.friendlyName || kaleidescapeProfile.defaultName || 'Kaleidescape';
    const entities = this.entitiesFromState(uniqueBase, name, state);
    const rawData = {
      device: {
        id: uniqueBase,
        name,
        manufacturer: 'Kaleidescape',
        model: state.system.type || kaleidescapeProfile.model,
        serialNumber: state.system.serialNumber,
        host: endpoint.host,
        port: endpoint.port,
        protocol: 'tcp' as const,
        configurationUrl: `http://${endpoint.host}`,
        attributes: {
          cpdid: state.system.cpdid,
          ipAddress: state.system.ipAddress,
          kosVersion: state.system.kosVersion,
          protocolVersion: state.system.protocol,
          movieZones: state.system.movieZones,
          musicZones: state.system.musicZones,
        },
      },
      ...state,
      entities,
      fetchedAt: new Date().toISOString(),
    };

    return {
      ...KaleidescapeMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'tcp' }),
      capabilities: {
        localRead: true,
        localControl: true,
        liveControlRequiresExecutor: false,
        protocol: 'Kaleidescape System Control Protocol',
      },
    };
  }

  private async executeNativeRequest(connectionArg: KaleidescapeTcpConnection, requestArg: IServiceCallRequest): Promise<unknown> {
    const requestName = requestNameForService(requestArg.service);
    if (requestName) {
      await connectionArg.send(requestName);
      return { request: requestName };
    }
    if (requestArg.service === 'press' || requestArg.service === 'send_command') {
      const commands = remoteCommandsFromRequest(requestArg);
      if (!commands.length) {
        throw new KaleidescapeClientError('Kaleidescape remote control requires data.command, data.commands, or data.button.');
      }
      for (const command of commands) {
        const remoteRequest = remoteRequests[command.replace(/-/g, '_').toLowerCase()];
        if (!remoteRequest) {
          throw new KaleidescapeClientError(`${command} is not a known Kaleidescape remote command.`);
        }
        await connectionArg.send(remoteRequest);
      }
      return { commands };
    }
    throw new KaleidescapeClientError(`Unsupported Kaleidescape service: ${requestArg.service}`);
  }

  private entitiesFromState(uniqueBaseArg: string, nameArg: string, stateArg: IKaleidescapeState): ISimpleLocalEntitySnapshot[] {
    const mediaState = stateArg.power.state === 'standby'
      ? 'off'
      : ['playing', 'forward', 'reverse'].includes(stateArg.movie.playStatus)
        ? 'playing'
        : stateArg.movie.playStatus === 'paused'
          ? 'paused'
          : 'idle';
    const base = SimpleLocalMapper.slug(uniqueBaseArg);
    return [
      {
        id: 'media_player',
        uniqueId: `${base}_media_player`,
        name: nameArg,
        platform: 'media_player',
        state: mediaState,
        available: true,
        writable: true,
        attributes: {
          power: stateArg.power.state,
          readiness: stateArg.power.readiness,
          play_status: stateArg.movie.playStatus,
          media_position: stateArg.movie.titleLocation,
          media_duration: stateArg.movie.titleLength,
        },
      },
      sensor(base, 'media_location', 'Media Location', stateArg.automation.movieLocation),
      sensor(base, 'play_status', 'Play Status', stateArg.movie.playStatus),
      sensor(base, 'play_speed', 'Play Speed', stateArg.movie.playSpeed),
      sensor(base, 'video_mode', 'Video Mode', stateArg.automation.videoMode),
      sensor(base, 'video_color_eotf', 'Video Color EOTF', stateArg.automation.videoColorEotf),
      sensor(base, 'video_color_space', 'Video Color Space', stateArg.automation.videoColorSpace),
      sensor(base, 'video_color_depth', 'Video Color Depth', stateArg.automation.videoColorDepth),
      sensor(base, 'video_color_sampling', 'Video Color Sampling', stateArg.automation.videoColorSampling),
      sensor(base, 'screen_mask_ratio', 'Screen Mask Ratio', stateArg.automation.screenMaskRatio),
      sensor(base, 'screen_mask_top_trim_rel', 'Screen Mask Top Trim Rel', stateArg.automation.screenMaskTopTrimRel / 10, '%'),
      sensor(base, 'screen_mask_bottom_trim_rel', 'Screen Mask Bottom Trim Rel', stateArg.automation.screenMaskBottomTrimRel / 10, '%'),
      sensor(base, 'cinemascape_mode', 'Cinemascape Mode', stateArg.automation.cinemascapeMode),
    ];
  }

  private endpoint(): IKaleidescapeEndpoint | undefined {
    if (this.configArg.url) {
      try {
        const url = new URL(this.configArg.url);
        if (url.protocol !== 'tcp:') {
          return undefined;
        }
        return { host: url.hostname, port: url.port ? Number(url.port) : kaleidescapeProfile.defaultPort || 10000 };
      } catch {
        return undefined;
      }
    }
    if (!this.configArg.host) {
      return undefined;
    }
    return { host: this.configArg.host, port: this.configArg.port || kaleidescapeProfile.defaultPort || 10000 };
  }

  private timeoutMs(): number {
    return this.configArg.timeoutMs || 5000;
  }
}

class KaleidescapeTcpConnection {
  private socket?: plugins.net.Socket;
  private buffer = '';
  private seq = 0;

  constructor(private readonly endpointArg: IKaleidescapeEndpoint, private readonly timeoutMsArg: number) {}

  public async connect(): Promise<void> {
    this.socket = plugins.net.createConnection({ host: this.endpointArg.host, port: this.endpointArg.port });
    await new Promise<void>((resolve, reject) => {
      const timer = setTimeout(() => reject(new KaleidescapeClientError(`Kaleidescape TCP connect to ${this.endpointArg.host}:${this.endpointArg.port} timed out after ${this.timeoutMsArg}ms.`)), this.timeoutMsArg);
      const cleanup = () => {
        clearTimeout(timer);
        this.socket?.off('connect', onConnect);
        this.socket?.off('error', onError);
      };
      const onConnect = () => {
        cleanup();
        resolve();
      };
      const onError = (errorArg: Error) => {
        cleanup();
        reject(errorArg);
      };
      this.socket!.once('connect', onConnect);
      this.socket!.once('error', onError);
    });
  }

  public async send(nameArg: string, fieldsArg: string[] = []): Promise<IKaleidescapeResponse> {
    if (!this.socket) {
      throw new KaleidescapeClientError('Kaleidescape TCP socket is not connected.');
    }
    const seq = this.seq;
    this.seq = (this.seq + 1) % 10;
    const fields = fieldsArg.length ? `${fieldsArg.map(escapeField).join(':')}:` : '';
    const request = `${localCpdid}/${seq}/${nameArg}:${fields}\n`;
    await new Promise<void>((resolve, reject) => {
      this.socket!.write(request, 'latin1', (errorArg?: Error | null) => errorArg ? reject(errorArg) : resolve());
    });

    while (true) {
      const response = parseResponse(await this.readLine(`${nameArg} response`));
      if (response.seq !== seq || response.deviceId !== localCpdid) {
        continue;
      }
      if (response.status !== 0) {
        throw new KaleidescapeClientError(`Kaleidescape request ${nameArg} failed with status ${response.status}.`);
      }
      return response;
    }
  }

  public close(): void {
    this.socket?.removeAllListeners();
    this.socket?.destroy();
    this.socket = undefined;
  }

  private async readLine(labelArg: string): Promise<string> {
    if (!this.socket) {
      throw new KaleidescapeClientError('Kaleidescape TCP socket is not connected.');
    }
    return new Promise<string>((resolve, reject) => {
      const cleanup = () => {
        clearTimeout(timer);
        this.socket?.off('data', onData);
        this.socket?.off('error', onError);
        this.socket?.off('close', onClose);
      };
      const tryResolve = () => {
        const index = this.buffer.indexOf('\n');
        if (index < 0) {
          return;
        }
        const line = this.buffer.slice(0, index).trim();
        this.buffer = this.buffer.slice(index + 1);
        cleanup();
        resolve(line);
      };
      const onData = (chunkArg: Buffer) => {
        this.buffer += chunkArg.toString('latin1');
        tryResolve();
      };
      const onError = (errorArg: Error) => {
        cleanup();
        reject(errorArg);
      };
      const onClose = () => {
        cleanup();
        reject(new KaleidescapeClientError(`Kaleidescape TCP socket closed while waiting for ${labelArg}.`));
      };
      const timer = setTimeout(() => {
        cleanup();
        reject(new KaleidescapeClientError(`Kaleidescape ${labelArg} timed out after ${this.timeoutMsArg}ms.`));
      }, this.timeoutMsArg);

      this.socket!.on('data', onData);
      this.socket!.once('error', onError);
      this.socket!.once('close', onClose);
      tryResolve();
    });
  }
}

const requestNameForService = (serviceArg: string): string | undefined => ({
  turn_on: 'LEAVE_STANDBY',
  turn_off: 'ENTER_STANDBY',
  media_play: 'PLAY',
  play: 'PLAY',
  media_pause: 'PAUSE',
  pause: 'PAUSE',
  media_stop: 'STOP',
  stop: 'STOP',
  media_next_track: 'NEXT',
  next: 'NEXT',
  media_previous_track: 'PREVIOUS',
  previous: 'PREVIOUS',
})[serviceArg];

const remoteCommandsFromRequest = (requestArg: IServiceCallRequest): string[] => {
  const value = requestArg.data?.command ?? requestArg.data?.commands ?? requestArg.data?.button;
  if (Array.isArray(value)) {
    return value.map((itemArg) => String(itemArg));
  }
  return value === undefined ? [] : [String(value)];
};

const parseResponse = (lineArg: string): IKaleidescapeResponse => {
  const match = lineArg.match(/^([^/]+)\/([0-9!])\/(\d{3}):(.*)\/(\d+)$/);
  if (!match) {
    throw new KaleidescapeClientError(`Invalid Kaleidescape response: ${lineArg}`);
  }
  const rest = match[4] || '';
  if (!rest) {
    return { deviceId: match[1], seq: match[2] === '!' ? -1 : Number(match[2]), status: Number(match[3]), name: '', fields: [] };
  }
  const parts = splitFields(rest.endsWith(':') ? rest.slice(0, -1) : rest);
  const name = parts.shift() || '';
  return { deviceId: match[1], seq: match[2] === '!' ? -1 : Number(match[2]), status: Number(match[3]), name, fields: parts };
};

const splitFields = (valueArg: string): string[] => {
  const fields: string[] = [];
  let current = '';
  let escaping = false;
  for (const char of valueArg) {
    if (escaping) {
      current += char;
      escaping = false;
    } else if (char === '\\') {
      escaping = true;
    } else if (char === ':') {
      fields.push(current);
      current = '';
    } else {
      current += char;
    }
  }
  fields.push(current);
  return fields;
};

const escapeField = (valueArg: string): string => valueArg.replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/\//g, '\\/');

const applyDeviceInfo = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.system.serialNumber = serialNumber(responseArg.fields[1]);
  stateArg.system.cpdid = Number(responseArg.fields[2]) ? responseArg.fields[2] : '';
  stateArg.system.ipAddress = stripIp(responseArg.fields[3]);
};

const applySystemVersion = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.system.protocol = numberField(responseArg.fields[0]);
  stateArg.system.kosVersion = responseArg.fields[1];
};

const applyDeviceTypeName = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.system.type = responseArg.fields[0];
};

const applyNumZones = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.system.movieZones = numberField(responseArg.fields[0]);
  stateArg.system.musicZones = numberField(responseArg.fields[1]);
};

const applyDevicePowerState = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.power.state = mapped(powerStates, responseArg.fields[0], 'power state');
  stateArg.power.zone = responseArg.fields.slice(1).map((fieldArg) => mapped(zoneStates, fieldArg, 'zone state'));
};

const applySystemReadinessState = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.power.readiness = mapped(readinessStates, responseArg.fields[0], 'readiness state');
};

const applyFriendlyName = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.system.friendlyName = responseArg.fields[0];
};

const applyUiState = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.osd.uiScreen = mapped(uiScreens, responseArg.fields[0], 'ui screen');
  stateArg.osd.uiPopup = mapped(uiPopups, responseArg.fields[1], 'ui popup');
  stateArg.osd.uiDialog = mapped(uiDialogs, responseArg.fields[2], 'ui dialog');
  stateArg.osd.uiScreensaver = mapped(uiSavers, responseArg.fields[3], 'ui screensaver');
};

const applyPlayStatus = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.movie.playStatus = mapped(playStatuses, responseArg.fields[0], 'play status');
  stateArg.movie.playSpeed = numberField(responseArg.fields[1]);
  stateArg.movie.titleLength = numberField(responseArg.fields[3]);
  stateArg.movie.titleLocation = numberField(responseArg.fields[4]);
};

const applyVideoColor = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.automation.videoColorEotf = mapped(videoEotfs, responseArg.fields[0], 'video color eotf');
  stateArg.automation.videoColorSpace = mapped(videoSpaces, responseArg.fields[1], 'video color space');
  stateArg.automation.videoColorDepth = mapped(videoDepths, responseArg.fields[2], 'video color depth');
  stateArg.automation.videoColorSampling = mapped(videoSamplings, responseArg.fields[3], 'video color sampling');
};

const applyScreenMask = (stateArg: IKaleidescapeState, responseArg: IKaleidescapeResponse): void => {
  stateArg.automation.screenMaskRatio = mapped(screenRatios, responseArg.fields[0], 'screen mask ratio');
  stateArg.automation.screenMaskTopTrimRel = numberField(responseArg.fields[1]);
  stateArg.automation.screenMaskBottomTrimRel = numberField(responseArg.fields[2]);
  stateArg.automation.screenMaskConservativeRatio = mapped(screenRatios, responseArg.fields[3], 'screen mask conservative ratio');
  stateArg.automation.screenMaskTopMaskAbs = numberField(responseArg.fields[4]);
  stateArg.automation.screenMaskBottomMaskAbs = numberField(responseArg.fields[5]);
};

const mapped = (mapArg: Record<number, string>, valueArg: string | undefined, labelArg: string): string => {
  const value = numberField(valueArg);
  if (mapArg[value] === undefined) {
    throw new KaleidescapeClientError(`Kaleidescape returned unknown ${labelArg} value '${valueArg}'.`);
  }
  return mapArg[value];
};

const sensor = (baseArg: string, idArg: string, nameArg: string, stateArg: unknown, unitArg?: string): ISimpleLocalEntitySnapshot => ({
  id: idArg,
  uniqueId: `${baseArg}_${idArg}`,
  name: nameArg,
  platform: 'sensor',
  state: stateArg,
  available: true,
  attributes: unitArg ? { unit: unitArg } : undefined,
});

const serialNumber = (valueArg: string | undefined): string | undefined => valueArg ? valueArg.slice(-12).padStart(12, '0') : undefined;
const stripIp = (valueArg: string | undefined): string | undefined => valueArg?.replace(/\b0+(\d)/g, '$1');
const numberField = (valueArg: string | undefined): number => Number.parseInt(valueArg || '0', 10) || 0;
const optional = async (callbackArg: () => Promise<unknown>): Promise<void> => { try { await callbackArg(); } catch {} };
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
