import type {
  IFoscamCamera,
  IFoscamClientLike,
  IFoscamCommandRequest,
  IFoscamCommandResponse,
  IFoscamConfig,
  IFoscamDeviceInfo,
  IFoscamHttpCommand,
  IFoscamNumber,
  IFoscamRefreshResult,
  IFoscamSnapshot,
  IFoscamSnapshotImage,
  IFoscamSwitch,
  TFoscamEntityKey,
  TFoscamProtocol,
  TFoscamPtzMovement,
  TFoscamStream,
} from './foscam.types.js';
import {
  foscamDefaultPort,
  foscamDefaultRtspPort,
  foscamDefaultSnapshotTimeoutMs,
  foscamDefaultTimeoutMs,
  foscamNumberDescriptions,
  foscamSwitchDescriptions,
} from './foscam.types.js';

export const foscamSuccess = 0;
export const foscamAuthError = -2;
export const foscamCommandDeniedError = -3;
export const foscamUnavailableError = -8;

const ptzCommands: Record<TFoscamPtzMovement, string> = {
  up: 'ptzMoveUp',
  down: 'ptzMoveDown',
  left: 'ptzMoveLeft',
  right: 'ptzMoveRight',
  top_left: 'ptzMoveTopLeft',
  top_right: 'ptzMoveTopRight',
  bottom_left: 'ptzMoveBottomLeft',
  bottom_right: 'ptzMoveBottomRight',
};

export class FoscamApiError extends Error {}

export class FoscamApiConnectionError extends FoscamApiError {}

export class FoscamApiAuthorizationError extends FoscamApiError {}

export class FoscamApiCommandError extends FoscamApiError {
  constructor(public readonly result: number, messageArg: string) {
    super(messageArg);
    this.name = 'FoscamApiCommandError';
  }
}

export class FoscamClient {
  private currentSnapshot?: IFoscamSnapshot;

  constructor(private readonly config: IFoscamConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IFoscamSnapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot), 'snapshot');
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient(this.config.client);
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.hasLiveTarget()) {
      try {
        this.currentSnapshot = await this.fetchLiveSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.snapshotFromConfig(this.config.connected ?? false, undefined, this.hasManualData() ? 'manual' : 'offline');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IFoscamRefreshResult> {
    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient(this.config.client);
        return { success: this.currentSnapshot.connected && !this.currentSnapshot.error, snapshot: this.cloneSnapshot(this.currentSnapshot), data: { source: 'client' } };
      } catch (errorArg) {
        const error = this.errorMessage(errorArg);
        this.currentSnapshot = this.offlineSnapshot(error);
        return { success: false, snapshot: this.cloneSnapshot(this.currentSnapshot), error };
      }
    }

    if (!this.hasLiveTarget()) {
      const snapshot = await this.getSnapshot();
      return {
        success: false,
        snapshot,
        error: 'Foscam refresh requires a configured HTTP endpoint or injected client.',
      };
    }

    try {
      this.currentSnapshot = await this.fetchLiveSnapshot();
      return { success: true, snapshot: this.cloneSnapshot(this.currentSnapshot), data: { source: 'http' } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      this.currentSnapshot = this.offlineSnapshot(error);
      return { success: false, snapshot: this.cloneSnapshot(this.currentSnapshot), error };
    }
  }

  public async validateConnection(): Promise<IFoscamSnapshot> {
    const productInfo = await this.requireCommandData('getProductAllInfo');
    const devInfo = await this.optionalCommandData('getDevInfo');
    return this.normalizeSnapshot(this.snapshotFromLiveData({ productInfo, devInfo }), 'http');
  }

  public async getSnapshotImage(): Promise<IFoscamSnapshotImage> {
    const response = await this.sendCommand('snapPicture2', {}, true, this.config.snapshotTimeoutMs || foscamDefaultSnapshotTimeoutMs);
    if (!response.raw) {
      throw new FoscamApiConnectionError('Foscam snapPicture2 did not return image data.');
    }
    return {
      contentType: response.contentType || 'image/jpeg',
      data: response.raw,
    };
  }

  public async execute(commandArg: IFoscamCommandRequest): Promise<unknown> {
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }

    if (this.config.client && !this.hasLiveTarget()) {
      return this.executeWithClient(this.config.client, commandArg);
    }

    if (commandArg.type === 'refresh') {
      return this.refresh();
    }

    if (commandArg.type === 'stream_source') {
      const snapshot = await this.getSnapshot();
      return {
        cameraId: snapshot.camera.id,
        stream: snapshot.camera.stream,
        streamSourceUrl: snapshot.camera.streamSourceUrl,
        snapshotUrl: snapshot.camera.snapshotUrl,
        rtspUrl: snapshot.camera.rtspUrl,
        verified: false,
      };
    }

    if (!this.hasLiveTarget()) {
      throw new FoscamApiConnectionError('Foscam commands require config.host, config.url, config.client, or commandExecutor.');
    }

    if (commandArg.type === 'snapshot_image') {
      if (commandArg.filename) {
        throw new FoscamApiError('Foscam snapshot file writes are not implemented; request image data without data.filename.');
      }
      const image = await this.getSnapshotImage();
      return { contentType: image.contentType, dataBase64: Buffer.from(image.data).toString('base64') };
    }

    if (commandArg.type === 'ptz') {
      if (!commandArg.movement) {
        throw new FoscamApiError('Foscam PTZ command requires a movement value.');
      }
      const responses = await this.performPtz(commandArg.movement, commandArg.travelTime);
      return { ok: true, command: commandArg.type, responses };
    }

    if (commandArg.type === 'ptz_preset') {
      if (!commandArg.presetName) {
        throw new FoscamApiError('Foscam PTZ preset command requires presetName.');
      }
      const response = await this.requestOk(this.httpCommand('ptz_preset', 'ptzGotoPresetPoint', { name: commandArg.presetName }));
      return { ok: true, command: commandArg.type, responses: [response] };
    }

    if (commandArg.type === 'enable_motion_detection' || commandArg.type === 'disable_motion_detection') {
      const enabled = commandArg.type === 'enable_motion_detection';
      const response = await this.setMotionDetectionField('isEnable', enabled);
      this.patchCachedSwitch('motion_detection', enabled);
      this.patchCachedCamera({ motionDetectionEnabled: enabled });
      return { ok: true, command: commandArg.type, responses: [response] };
    }

    if (commandArg.type === 'set_switch') {
      if (!commandArg.key || typeof commandArg.enabled !== 'boolean') {
        throw new FoscamApiError('Foscam set_switch command requires key and enabled.');
      }
      const responses = await this.setSwitch(commandArg.key, commandArg.enabled);
      this.patchCachedSwitch(commandArg.key, commandArg.enabled);
      if (commandArg.key === 'motion_detection') {
        this.patchCachedCamera({ motionDetectionEnabled: commandArg.enabled });
      }
      return { ok: true, command: commandArg.type, responses };
    }

    if (commandArg.type === 'set_number') {
      if (!commandArg.key || typeof commandArg.value !== 'number') {
        throw new FoscamApiError('Foscam set_number command requires key and numeric value.');
      }
      const response = await this.setNumber(commandArg.key, commandArg.value);
      this.patchCachedNumber(commandArg.key, commandArg.value);
      return { ok: true, command: commandArg.type, responses: [response] };
    }

    throw new FoscamApiError(`Unsupported Foscam command: ${commandArg.type}`);
  }

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

  private async fetchLiveSnapshot(): Promise<IFoscamSnapshot> {
    const productInfo = await this.requireCommandData('getProductAllInfo');
    const [devInfo, portInfo, infraLedConfig, mirrorFlipSetting, sleepState, whiteLight, sirenConfig, audioVolume, speakVolume, voiceEnableState, ledEnableState, swCapabilities, motionConfig] = await Promise.all([
      this.optionalCommandData('getDevInfo'),
      this.optionalCommandData('getPortInfo'),
      this.optionalCommandData('getInfraLedConfig'),
      this.optionalCommandData('getMirrorAndFlipSetting'),
      this.optionalCommandData('getAlexaState'),
      this.optionalCommandData('getWhiteLightBrightness'),
      this.optionalCommandData('getSirenConfig'),
      this.optionalCommandData('getAudioVolume'),
      this.optionalCommandData('getSpeakVolume'),
      this.optionalCommandData('getVoiceEnableState'),
      this.optionalCommandData('getLedEnableState'),
      this.optionalCommandData('getSWCapabilities'),
      this.optionalCommandData('getMotionDetectConfig'),
    ]);

    const reserve4 = numberValue(productInfo.reserve4) ?? 0;
    const modelInt = numberValue(productInfo.model) ?? 7002;
    const supportsWdr = modelInt > 7001 && Boolean(reserve4 & 256);
    const supportsHdr = modelInt > 7001 && Boolean(reserve4 & 128);
    const [wdrMode, hdrMode] = await Promise.all([
      supportsWdr ? this.optionalCommandData('getWdrMode') : Promise.resolve({}),
      supportsHdr ? this.optionalCommandData('getHdrMode') : Promise.resolve({}),
    ]);

    return this.normalizeSnapshot(this.snapshotFromLiveData({
      productInfo,
      devInfo,
      portInfo,
      infraLedConfig,
      mirrorFlipSetting,
      sleepState,
      whiteLight,
      sirenConfig,
      audioVolume,
      speakVolume,
      voiceEnableState,
      ledEnableState,
      swCapabilities,
      motionConfig,
      wdrMode,
      hdrMode,
    }), 'http');
  }

  private snapshotFromLiveData(inputArg: {
    productInfo: Record<string, string | undefined>;
    devInfo?: Record<string, string | undefined>;
    portInfo?: Record<string, string | undefined>;
    infraLedConfig?: Record<string, string | undefined>;
    mirrorFlipSetting?: Record<string, string | undefined>;
    sleepState?: Record<string, string | undefined>;
    whiteLight?: Record<string, string | undefined>;
    sirenConfig?: Record<string, string | undefined>;
    audioVolume?: Record<string, string | undefined>;
    speakVolume?: Record<string, string | undefined>;
    voiceEnableState?: Record<string, string | undefined>;
    ledEnableState?: Record<string, string | undefined>;
    swCapabilities?: Record<string, string | undefined>;
    motionConfig?: Record<string, string | undefined>;
    wdrMode?: Record<string, string | undefined>;
    hdrMode?: Record<string, string | undefined>;
  }): IFoscamSnapshot {
    const productInfo = inputArg.productInfo || {};
    const devInfo = inputArg.devInfo || {};
    const motionConfig = inputArg.motionConfig || {};
    const reserve4 = numberValue(productInfo.reserve4) ?? 0;
    const modelInt = numberValue(productInfo.model) ?? 7002;
    const supportsWdr = modelInt > 7001 && Boolean(reserve4 & 256);
    const supportsHdr = modelInt > 7001 && Boolean(reserve4 & 128);
    const swCapabilities1 = numberValue(inputArg.swCapabilities?.swCapabilities1) ?? 0;
    const swCapabilities2 = numberValue(inputArg.swCapabilities?.swCapabilities2) ?? 0;
    const supportsSpeakVolume = Boolean(swCapabilities1 & 32);
    const supportsPetDetection = Boolean(swCapabilities2 & 512);
    const supportsCarDetection = Boolean(swCapabilities2 & 256);
    const supportsHumanDetection = Boolean(swCapabilities2 & 128);
    const currentSettings: Partial<Record<TFoscamEntityKey, unknown>> = {
      motion_detection: booleanValue(motionConfig.isEnable) ?? false,
      is_flip: inputArg.mirrorFlipSetting?.isFlip === '1',
      is_mirror: inputArg.mirrorFlipSetting?.isMirror === '1',
      is_open_ir: inputArg.infraLedConfig?.mode === '1',
      sleep_switch: inputArg.sleepState?.state === '1',
      is_open_white_light: inputArg.whiteLight?.enable === '1',
      is_siren_alarm: inputArg.sirenConfig?.sirenEnable === '1',
      is_turn_off_volume: !(inputArg.voiceEnableState?.isEnable === '1'),
      is_turn_off_light: !(inputArg.ledEnableState?.isEnable === '0'),
      is_open_wdr: supportsWdr ? booleanValue(inputArg.wdrMode?.mode) ?? false : undefined,
      is_open_hdr: supportsHdr ? booleanValue(inputArg.hdrMode?.mode) ?? false : undefined,
      pet_detection: supportsPetDetection ? motionConfig.petEnable === '1' : undefined,
      car_detection: supportsCarDetection ? motionConfig.carEnable === '1' : undefined,
      human_detection: supportsHumanDetection ? motionConfig.humanEnable === '1' : undefined,
      device_volume: numberValue(inputArg.audioVolume?.volume) ?? 0,
      speak_volume: numberValue(inputArg.speakVolume?.SpeakVolume) ?? 0,
    };
    const deviceInfo = this.deviceInfo(true, {
      name: devInfo.devName || this.config.name,
      manufacturer: 'Foscam',
      model: devInfo.productName || productInfo.productName || productInfo.model || this.config.model,
      productName: devInfo.productName || productInfo.productName,
      firmwareVersion: devInfo.firmwareVer,
      hardwareVersion: devInfo.hardwareVer,
      rtspPort: numberValue(inputArg.portInfo?.rtspPort) || numberValue(inputArg.portInfo?.mediaPort) || this.config.rtspPort || foscamDefaultRtspPort,
      productInfo,
      rawDevInfo: devInfo,
    });

    return {
      deviceInfo,
      camera: this.camera(deviceInfo, true, { motionDetectionEnabled: Boolean(currentSettings.motion_detection) }),
      switches: this.switchesFromSettings(currentSettings, true, {
        is_open_wdr: supportsWdr,
        is_open_hdr: supportsHdr,
        pet_detection: supportsPetDetection,
        car_detection: supportsCarDetection,
        human_detection: supportsHumanDetection,
      }),
      numbers: this.numbersFromSettings(currentSettings, true, { speak_volume: supportsSpeakVolume }),
      currentSettings,
      connected: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      metadata: {
        supportsSpeakVolume,
        supportsPetDetection,
        supportsCarDetection,
        supportsHumanDetection,
        supportsWdr,
        supportsHdr,
      },
    };
  }

  private snapshotFromConfig(connectedArg: boolean, errorArg?: string, sourceArg: IFoscamSnapshot['source'] = 'manual'): IFoscamSnapshot {
    const currentSettings = { ...this.config.snapshot?.currentSettings, ...this.config.currentSettings };
    const deviceInfo = this.deviceInfo(connectedArg);
    return this.normalizeSnapshot({
      deviceInfo,
      camera: this.camera(deviceInfo, connectedArg, this.config.camera),
      switches: this.config.switches || this.config.snapshot?.switches || this.switchesFromSettings(currentSettings, connectedArg),
      numbers: this.config.numbers || this.config.snapshot?.numbers || this.numbersFromSettings(currentSettings, connectedArg),
      currentSettings,
      connected: connectedArg,
      updatedAt: new Date().toISOString(),
      source: sourceArg,
      error: errorArg,
      metadata: this.config.snapshot?.metadata,
    }, sourceArg);
  }

  private normalizeSnapshot(snapshotArg: IFoscamSnapshot, sourceArg: IFoscamSnapshot['source'] = snapshotArg.source): IFoscamSnapshot {
    const connected = Boolean(snapshotArg.connected && snapshotArg.deviceInfo.online !== false);
    const deviceInfo = {
      ...this.deviceInfo(connected),
      ...snapshotArg.deviceInfo,
      online: connected,
    };
    const currentSettings = snapshotArg.currentSettings || {};
    return {
      ...snapshotArg,
      deviceInfo,
      camera: this.normalizeCamera(snapshotArg.camera, deviceInfo, connected),
      switches: snapshotArg.switches || this.switchesFromSettings(currentSettings, connected),
      numbers: snapshotArg.numbers || this.numbersFromSettings(currentSettings, connected),
      currentSettings,
      connected,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
      source: sourceArg,
    };
  }

  private normalizeCamera(cameraArg: IFoscamCamera | undefined, deviceInfoArg: IFoscamDeviceInfo, connectedArg: boolean): IFoscamCamera {
    const baseCamera = this.camera(deviceInfoArg, connectedArg, cameraArg);
    return {
      ...baseCamera,
      ...cameraArg,
      stream: cameraArg?.stream || baseCamera.stream,
      snapshotUrl: cameraArg?.snapshotUrl || baseCamera.snapshotUrl,
      rtspUrl: cameraArg?.rtspUrl || baseCamera.rtspUrl,
      streamSourceUrl: cameraArg?.streamSourceUrl || cameraArg?.rtspUrl || baseCamera.streamSourceUrl,
      available: connectedArg && cameraArg?.available !== false,
    };
  }

  private camera(deviceInfoArg: IFoscamDeviceInfo, connectedArg: boolean, overridesArg: Partial<IFoscamCamera> = {}): IFoscamCamera {
    const stream = overridesArg.stream || this.stream();
    const name = overridesArg.name || `${deviceInfoArg.name || 'Foscam'} Camera`;
    return {
      id: overridesArg.id || 'camera',
      name,
      stream,
      snapshotUrl: overridesArg.snapshotUrl || this.commandUrl('snapPicture2'),
      rtspUrl: overridesArg.rtspUrl || this.rtspUrl(stream),
      streamSourceUrl: overridesArg.streamSourceUrl || overridesArg.rtspUrl || this.rtspUrl(stream),
      available: connectedArg && overridesArg.available !== false,
      supportsPtz: overridesArg.supportsPtz ?? true,
      motionDetectionEnabled: overridesArg.motionDetectionEnabled,
      attributes: overridesArg.attributes,
    };
  }

  private deviceInfo(connectedArg: boolean, liveArg: Partial<IFoscamDeviceInfo> = {}): IFoscamDeviceInfo {
    const endpoint = this.endpoint();
    const serialNumber = liveArg.serialNumber || this.config.deviceInfo?.serialNumber || this.config.uniqueId;
    return {
      ...this.config.deviceInfo,
      ...liveArg,
      id: this.config.deviceInfo?.id || this.config.uniqueId || serialNumber || endpoint.host || 'manual-foscam',
      name: liveArg.name || this.config.deviceInfo?.name || this.config.name || endpoint.host || 'Foscam Camera',
      manufacturer: liveArg.manufacturer || this.config.deviceInfo?.manufacturer || this.config.manufacturer || 'Foscam',
      model: liveArg.model || this.config.deviceInfo?.model || this.config.model,
      serialNumber,
      host: this.config.deviceInfo?.host || endpoint.host,
      port: this.config.deviceInfo?.port || endpoint.port,
      protocol: this.config.deviceInfo?.protocol || endpoint.protocol,
      rtspPort: liveArg.rtspPort || this.config.deviceInfo?.rtspPort || this.config.rtspPort || foscamDefaultRtspPort,
      url: this.config.deviceInfo?.url || this.baseUrl(),
      online: connectedArg,
    };
  }

  private switchesFromSettings(settingsArg: Partial<Record<TFoscamEntityKey, unknown>>, connectedArg: boolean, existsArg: Partial<Record<TFoscamEntityKey, boolean>> = {}): IFoscamSwitch[] {
    return foscamSwitchDescriptions
      .filter((descriptionArg) => existsArg[descriptionArg.key] !== false)
      .map((descriptionArg) => ({
        key: descriptionArg.key,
        name: descriptionArg.name,
        isOn: booleanValue(settingsArg[descriptionArg.key]) ?? false,
        available: connectedArg,
        exists: true,
        entityCategory: descriptionArg.entityCategory,
      }));
  }

  private numbersFromSettings(settingsArg: Partial<Record<TFoscamEntityKey, unknown>>, connectedArg: boolean, existsArg: Partial<Record<TFoscamEntityKey, boolean>> = {}): IFoscamNumber[] {
    return foscamNumberDescriptions
      .filter((descriptionArg) => existsArg[descriptionArg.key] !== false)
      .map((descriptionArg) => ({
        key: descriptionArg.key,
        name: descriptionArg.name,
        value: numberValue(settingsArg[descriptionArg.key]) ?? 0,
        min: descriptionArg.min,
        max: descriptionArg.max,
        step: descriptionArg.step,
        available: connectedArg,
        exists: true,
        entityCategory: descriptionArg.entityCategory,
      }));
  }

  private async executeWithClient(clientArg: IFoscamClientLike, commandArg: IFoscamCommandRequest): Promise<unknown> {
    if (clientArg.execute) {
      return clientArg.execute(commandArg);
    }
    if (commandArg.type === 'refresh') {
      return this.refresh();
    }
    if (commandArg.type === 'snapshot_image' && clientArg.getSnapshotImage) {
      const image = await clientArg.getSnapshotImage();
      return { contentType: image.contentType, dataBase64: Buffer.from(image.data).toString('base64') };
    }
    throw new FoscamApiConnectionError('Foscam command is not available on the injected client and no HTTP endpoint or commandExecutor is configured.');
  }

  private async snapshotFromClient(clientArg: IFoscamClientLike): Promise<IFoscamSnapshot> {
    if (clientArg.getSnapshot) {
      const result = await clientArg.getSnapshot();
      if (isFoscamSnapshot(result)) {
        return this.normalizeSnapshot(result, 'client');
      }
      return this.snapshotFromConfig(true, undefined, 'client');
    }

    const [productInfo, devInfo] = await Promise.all([
      clientArg.getProductAllInfo ? clientArg.getProductAllInfo().catch(() => undefined) : undefined,
      clientArg.getDevInfo ? clientArg.getDevInfo().catch(() => undefined) : undefined,
    ]);
    if (!productInfo && !devInfo) {
      throw new FoscamApiConnectionError('Foscam client must expose getSnapshot() or raw product/device info getters.');
    }
    return this.normalizeSnapshot(this.snapshotFromLiveData({
      productInfo: recordStrings(productInfo),
      devInfo: recordStrings(devInfo),
    }), 'client');
  }

  private async setSwitch(keyArg: TFoscamEntityKey, enabledArg: boolean): Promise<IFoscamCommandResponse[]> {
    if (keyArg === 'motion_detection') {
      return [await this.setMotionDetectionField('isEnable', enabledArg)];
    }
    if (keyArg === 'is_flip') {
      return [await this.requestOk(this.httpCommand('flip', 'flipVideo', { isFlip: enabledArg ? 1 : 0 }))];
    }
    if (keyArg === 'is_mirror') {
      return [await this.requestOk(this.httpCommand('mirror', 'mirrorVideo', { isMirror: enabledArg ? 1 : 0 }))];
    }
    if (keyArg === 'is_open_ir') {
      const mode = await this.requestOk(this.httpCommand('infrared_mode', 'setInfraLedConfig', { mode: enabledArg ? 1 : 0 }));
      const led = await this.requestOk(this.httpCommand('infrared_led', enabledArg ? 'openInfraLed' : 'closeInfraLed'));
      return [mode, led];
    }
    if (keyArg === 'sleep_switch') {
      return [await this.requestOk(this.httpCommand('sleep', enabledArg ? 'alexaSleep' : 'alexaWakeUp'))];
    }
    if (keyArg === 'is_open_white_light') {
      return [await this.requestOk(this.httpCommand('white_light', enabledArg ? 'openWhiteLight' : 'closeWhiteLight'))];
    }
    if (keyArg === 'is_siren_alarm') {
      return [await this.requestOk(this.httpCommand('siren_alarm', 'setSirenConfig', { sirenEnable: enabledArg ? 1 : 0, sirenvolume: 100, reserved: 0 }))];
    }
    if (keyArg === 'is_turn_off_volume') {
      return [await this.requestOk(this.httpCommand('volume_muted', 'setVoiceEnableState', { isEnable: enabledArg ? 0 : 1 }))];
    }
    if (keyArg === 'is_turn_off_light') {
      return [await this.requestOk(this.httpCommand('light', 'setLedEnableState', { isEnable: enabledArg ? 1 : 0 }))];
    }
    if (keyArg === 'is_open_hdr') {
      return [await this.requestOk(this.httpCommand('hdr', 'setHdrMode', { mode: enabledArg ? 1 : 0 }))];
    }
    if (keyArg === 'is_open_wdr') {
      return [await this.requestOk(this.httpCommand('wdr', 'setWdrMode', { mode: enabledArg ? 1 : 0 }))];
    }
    if (keyArg === 'pet_detection') {
      return [await this.setMotionDetectionField('petEnable', enabledArg)];
    }
    if (keyArg === 'car_detection') {
      return [await this.setMotionDetectionField('carEnable', enabledArg)];
    }
    if (keyArg === 'human_detection') {
      return [await this.setMotionDetectionField('humanEnable', enabledArg)];
    }
    throw new FoscamApiError(`Unsupported Foscam switch key: ${keyArg}`);
  }

  private async setNumber(keyArg: TFoscamEntityKey, valueArg: number): Promise<IFoscamCommandResponse> {
    const value = Math.max(0, Math.min(100, Math.round(valueArg)));
    if (keyArg === 'device_volume') {
      return this.requestOk(this.httpCommand('device_volume', 'setAudioVolume', { volume: value }));
    }
    if (keyArg === 'speak_volume') {
      return this.requestOk(this.httpCommand('speak_volume', 'setSpeakVolume', { SpeakVolume: value }));
    }
    throw new FoscamApiError(`Unsupported Foscam number key: ${keyArg}`);
  }

  private async setMotionDetectionField(fieldArg: string, enabledArg: boolean): Promise<IFoscamCommandResponse> {
    const currentConfig = await this.requireCommandData('getMotionDetectConfig');
    return this.requestOk(this.httpCommand('motion_detection', 'setMotionDetectConfig', cleanParams({ ...currentConfig, [fieldArg]: enabledArg ? 1 : 0 })));
  }

  private async performPtz(movementArg: TFoscamPtzMovement, travelTimeArg = 0.125): Promise<IFoscamCommandResponse[]> {
    const command = ptzCommands[movementArg];
    const start = await this.requestOk(this.httpCommand('ptz_start', command));
    await sleep(Math.max(0, Math.min(1, travelTimeArg)) * 1000);
    const stop = await this.requestOk(this.httpCommand('ptz_stop', 'ptzStopRun'));
    return [start, stop];
  }

  private async requestOk(commandArg: IFoscamHttpCommand): Promise<IFoscamCommandResponse> {
    const response = await this.sendCommand(commandArg.command, commandArg.params);
    if (response.result !== foscamSuccess) {
      this.throwForResult(response.result, commandArg.command);
    }
    return {
      ok: true,
      label: commandArg.label,
      method: commandArg.method,
      command: commandArg.command,
      params: commandArg.params,
      result: response.result,
      data: response.data,
    };
  }

  private async requireCommandData(commandArg: string, paramsArg: Record<string, string | number | boolean> = {}): Promise<Record<string, string | undefined>> {
    const response = await this.sendCommand(commandArg, paramsArg);
    if (response.result !== foscamSuccess) {
      this.throwForResult(response.result, commandArg);
    }
    return response.data;
  }

  private async optionalCommandData(commandArg: string, paramsArg: Record<string, string | number | boolean> = {}): Promise<Record<string, string | undefined>> {
    try {
      const response = await this.sendCommand(commandArg, paramsArg);
      return response.result === foscamSuccess ? response.data : {};
    } catch {
      return {};
    }
  }

  private async sendCommand(commandArg: string, paramsArg: Record<string, string | number | boolean> = {}, rawArg = false, timeoutMsArg = this.config.timeoutMs || foscamDefaultTimeoutMs): Promise<{
    result: number;
    data: Record<string, string | undefined>;
    raw?: Uint8Array;
    contentType?: string;
  }> {
    const url = this.commandUrl(commandArg, paramsArg);
    if (!url) {
      throw new FoscamApiConnectionError('Foscam live HTTP client requires config.host or config.url.');
    }
    const response = await this.fetchWithTimeout(url, timeoutMsArg);
    if (!response.ok) {
      if (response.status === 401 || response.status === 403) {
        throw new FoscamApiAuthorizationError('Foscam authentication failed.');
      }
      const text = await response.text().catch(() => '');
      throw new FoscamApiConnectionError(`Foscam command ${commandArg} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }
    if (rawArg) {
      return {
        result: foscamSuccess,
        data: {},
        raw: new Uint8Array(await response.arrayBuffer()),
        contentType: response.headers.get('content-type') || undefined,
      };
    }
    return parseFoscamResponse(await response.text());
  }

  private async fetchWithTimeout(urlArg: string, timeoutMsArg: number): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), timeoutMsArg);
    try {
      return await globalThis.fetch(urlArg, { signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private throwForResult(resultArg: number, commandArg: string): never {
    if (resultArg === foscamAuthError) {
      throw new FoscamApiAuthorizationError(`Foscam command ${commandArg} failed authentication.`);
    }
    if (resultArg === foscamUnavailableError) {
      throw new FoscamApiConnectionError(`Foscam command ${commandArg} could not reach a camera.`);
    }
    if (resultArg === foscamCommandDeniedError) {
      throw new FoscamApiCommandError(resultArg, `Foscam command ${commandArg} is not supported or access was denied.`);
    }
    throw new FoscamApiCommandError(resultArg, `Foscam command ${commandArg} failed with result ${resultArg}.`);
  }

  private httpCommand(labelArg: string, commandArg: string, paramsArg: Record<string, string | number | boolean> = {}): IFoscamHttpCommand {
    return { label: labelArg, method: 'GET', command: commandArg, params: paramsArg, expect: 'ok' };
  }

  private commandUrl(commandArg: string, paramsArg: Record<string, string | number | boolean> = {}): string | undefined {
    const baseUrl = this.baseUrl();
    if (!baseUrl) {
      return undefined;
    }
    const url = new URL(`${baseUrl}/cgi-bin/CGIProxy.fcgi`);
    url.searchParams.set('usr', this.config.username || '');
    url.searchParams.set('pwd', this.config.password || '');
    url.searchParams.set('cmd', commandArg);
    for (const [key, value] of Object.entries(paramsArg)) {
      url.searchParams.set(key, String(value));
    }
    return url.toString();
  }

  private rtspUrl(streamArg: TFoscamStream): string | undefined {
    const endpoint = this.endpoint();
    if (!endpoint.host) {
      return undefined;
    }
    const credentials = this.rtspCredentials();
    return `rtsp://${credentials}${endpoint.host}:${this.config.rtspPort || foscamDefaultRtspPort}/video${streamArg}`;
  }

  private baseUrl(): string | undefined {
    const endpoint = this.endpoint();
    if (!endpoint.host) {
      return undefined;
    }
    return `${endpoint.protocol}://${endpoint.host}:${endpoint.port}`;
  }

  private endpoint(): { protocol: TFoscamProtocol; host?: string; port: number } {
    const url = safeUrl(this.config.url || this.config.host);
    if (url) {
      const protocol = url.protocol === 'https:' ? 'https' : 'http';
      return {
        protocol,
        host: url.hostname,
        port: url.port ? Number(url.port) : protocol === 'https' ? 443 : foscamDefaultPort,
      };
    }
    return {
      protocol: this.config.protocol || 'http',
      host: this.config.host,
      port: this.config.port || foscamDefaultPort,
    };
  }

  private rtspCredentials(): string {
    if (!this.config.username || this.config.password === undefined) {
      return '';
    }
    return `${encodeURIComponent(this.config.username)}:${encodeURIComponent(this.config.password)}@`;
  }

  private stream(): TFoscamStream {
    return this.config.stream || 'Main';
  }

  private hasLiveTarget(): boolean {
    return Boolean(this.baseUrl());
  }

  private hasManualData(): boolean {
    return Boolean(this.config.deviceInfo || this.config.camera || this.config.switches || this.config.numbers || this.config.currentSettings || this.config.snapshot);
  }

  private offlineSnapshot(errorArg?: string): IFoscamSnapshot {
    return this.snapshotFromConfig(false, errorArg, 'offline');
  }

  private patchCachedCamera(valuesArg: Partial<IFoscamCamera>): void {
    if (!this.currentSnapshot) {
      return;
    }
    Object.assign(this.currentSnapshot.camera, valuesArg);
  }

  private patchCachedSwitch(keyArg: TFoscamEntityKey, isOnArg: boolean): void {
    if (!this.currentSnapshot) {
      return;
    }
    this.currentSnapshot.currentSettings[keyArg] = isOnArg;
    for (const switchArg of this.currentSnapshot.switches) {
      if (switchArg.key === keyArg) {
        switchArg.isOn = isOnArg;
      }
    }
  }

  private patchCachedNumber(keyArg: TFoscamEntityKey, valueArg: number): void {
    if (!this.currentSnapshot) {
      return;
    }
    this.currentSnapshot.currentSettings[keyArg] = valueArg;
    for (const numberArg of this.currentSnapshot.numbers) {
      if (numberArg.key === keyArg) {
        numberArg.value = valueArg;
      }
    }
  }

  private cloneSnapshot(snapshotArg: IFoscamSnapshot): IFoscamSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IFoscamSnapshot;
  }

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

const sleep = (msArg: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, msArg));

const safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const parseFoscamResponse = (textArg: string): { result: number; data: Record<string, string | undefined> } => {
  const content = textArg.replace(/^[\s\S]*?<CGI_Result[^>]*>/i, '').replace(/<\/CGI_Result>[\s\S]*$/i, '');
  const data: Record<string, string | undefined> = {};
  for (const match of content.matchAll(/<([A-Za-z0-9_]+)>([\s\S]*?)<\/\1>/g)) {
    if (match[1] === 'result') {
      continue;
    }
    data[match[1]] = xmlDecode(match[2]);
  }
  const resultMatch = content.match(/<result>(-?\d+)<\/result>/);
  if (!resultMatch) {
    throw new FoscamApiConnectionError('Foscam response did not contain a CGI result code.');
  }
  return { result: Number(resultMatch[1]), data };
};

const xmlDecode = (valueArg: string): string => {
  return valueArg
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, '&');
};

const cleanParams = (paramsArg: Record<string, string | number | boolean | undefined>): Record<string, string | number | boolean> => {
  return Object.fromEntries(Object.entries(paramsArg).filter(([, valueArg]) => valueArg !== undefined)) as Record<string, string | number | boolean>;
};

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

const booleanValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'number') {
    return valueArg !== 0;
  }
  if (typeof valueArg === 'string') {
    if (['true', 'yes', 'on', '1'].includes(valueArg.toLowerCase())) {
      return true;
    }
    if (['false', 'no', 'off', '0'].includes(valueArg.toLowerCase())) {
      return false;
    }
  }
  return undefined;
};

const isFoscamSnapshot = (valueArg: unknown): valueArg is IFoscamSnapshot => {
  const value = recordValue(valueArg);
  return Boolean(value?.deviceInfo && value?.camera && Array.isArray(value?.switches) && Array.isArray(value?.numbers));
};

const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
};

const recordStrings = (valueArg: unknown): Record<string, string | undefined> => {
  const record = recordValue(valueArg);
  if (!record) {
    return {};
  }
  return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, value === undefined || value === null ? undefined : String(value)]));
};
