import type {
  IAirgradientCommand,
  IAirgradientCommandResult,
  IAirgradientConfig,
  IAirgradientDevice,
  IAirgradientDeviceConfig,
  IAirgradientDeviceInfo,
  IAirgradientEvent,
  IAirgradientMeasures,
  IAirgradientSnapshot,
  TAirgradientConfigField,
  TAirgradientConfigurationControl,
  TAirgradientLedBarMode,
  TAirgradientPmStandard,
  TAirgradientProtocol,
  TAirgradientTemperatureUnit,
} from './airgradient.types.js';
import { airgradientDefaultPort } from './airgradient.types.js';

const airgradientManufacturer = 'AirGradient';
const defaultTimeoutMs = 10000;

type TAirgradientEventHandler = (eventArg: IAirgradientEvent) => void;

export class AirgradientApiError extends Error {
  constructor(messageArg: string, public readonly status?: number) {
    super(messageArg);
    this.name = 'AirgradientApiError';
  }
}

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

export class AirgradientClient {
  private currentSnapshot?: IAirgradientSnapshot;
  private readonly events: IAirgradientEvent[] = [];
  private readonly eventHandlers = new Set<TAirgradientEventHandler>();

  constructor(private readonly config: IAirgradientConfig) {}

  public async getSnapshot(): Promise<IAirgradientSnapshot> {
    if (this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
      return this.currentSnapshot;
    }

    if (this.config.host) {
      try {
        this.currentSnapshot = this.normalizeSnapshot(await this.fetchSnapshot());
        return this.currentSnapshot;
      } catch (errorArg) {
        this.emit({ type: 'error', data: { message: this.errorMessage(errorArg) }, timestamp: Date.now() });
        this.currentSnapshot = this.normalizeSnapshot(this.snapshotFromConfig(false));
        return this.currentSnapshot;
      }
    }

    this.currentSnapshot = this.normalizeSnapshot(this.snapshotFromConfig(this.config.connected === true));
    return this.currentSnapshot;
  }

  public onEvent(handlerArg: TAirgradientEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async sendCommand(commandArg: IAirgradientCommand): Promise<IAirgradientCommandResult> {
    this.emit({ type: 'command_mapped', command: commandArg, timestamp: Date.now(), deviceId: this.commandDeviceId(commandArg), entityId: this.commandEntityId(commandArg) });

    try {
      const result = await this.executeCommand(commandArg);
      this.emit({
        type: result.success ? 'command_executed' : 'command_failed',
        command: commandArg,
        data: result,
        timestamp: Date.now(),
        deviceId: this.commandDeviceId(commandArg),
        entityId: this.commandEntityId(commandArg),
      });
      return result;
    } catch (errorArg) {
      const result = { success: false, error: this.errorMessage(errorArg), data: { command: commandArg } };
      this.emit({ type: 'command_failed', command: commandArg, data: result, timestamp: Date.now(), deviceId: this.commandDeviceId(commandArg), entityId: this.commandEntityId(commandArg) });
      return result;
    }
  }

  public async refresh(): Promise<IAirgradientSnapshot> {
    this.currentSnapshot = undefined;
    const snapshot = await this.getSnapshot();
    this.emit({ type: 'snapshot_refreshed', data: snapshot, timestamp: Date.now() });
    return snapshot;
  }

  public async destroy(): Promise<void> {
    this.eventHandlers.clear();
  }

  private async executeCommand(commandArg: IAirgradientCommand): Promise<IAirgradientCommandResult> {
    if (this.config.commandExecutor) {
      return this.commandResult(await this.config.commandExecutor(commandArg), commandArg);
    }

    if (commandArg.type === 'refresh') {
      return { success: true, data: await this.refresh() };
    }

    if (!this.config.host) {
      return {
        success: false,
        error: 'AirGradient live local configuration requires config.host, or provide commandExecutor for snapshot/manual configs.',
        data: { command: commandArg },
      };
    }

    await this.putConfig(commandArg.payload);
    this.patchConfig(commandArg.field, commandArg.value);
    return { success: true, data: { field: commandArg.field, value: commandArg.value } };
  }

  private async fetchSnapshot(): Promise<IAirgradientSnapshot> {
    const measures = await this.getCurrentMeasures();
    const deviceConfig = await this.getDeviceConfig();
    const latestFirmwareVersion = this.config.checkFirmwareUpdate && measures.serialNumber
      ? await this.getLatestFirmwareVersion(measures.serialNumber).catch(() => undefined)
      : this.config.latestFirmwareVersion;

    const device: IAirgradientDevice = {
      id: this.config.uniqueId || measures.serialNumber || this.config.host,
      host: this.config.host,
      port: this.config.port || airgradientDefaultPort,
      protocol: this.protocol(),
      name: this.config.name || this.config.deviceInfo?.name || this.deviceName(measures, this.config.deviceInfo),
      manufacturer: airgradientManufacturer,
      model: this.config.model || measures.model,
      modelName: modelName(measures.model) || this.config.deviceInfo?.modelName,
      serialNumber: measures.serialNumber || this.config.serialNumber,
      firmwareVersion: measures.firmwareVersion || this.config.firmwareVersion,
      latestFirmwareVersion,
      online: true,
      measures,
      config: deviceConfig,
      metadata: {
        ...this.config.deviceInfo,
        source: 'local-http',
      },
    };
    return {
      connected: true,
      updatedAt: new Date().toISOString(),
      devices: [device, ...this.normalizeConfiguredDevices(true)],
      events: [...(this.config.events || []), ...this.events],
    };
  }

  private snapshotFromConfig(connectedArg: boolean): IAirgradientSnapshot {
    const devices = this.normalizeConfiguredDevices(connectedArg);
    if (!devices.length && (this.config.host || this.config.measures || this.config.deviceConfig || this.config.deviceInfo || this.config.serialNumber || this.config.model || this.config.name)) {
      devices.push({
        id: this.config.uniqueId || this.config.serialNumber || this.config.host,
        host: this.config.host,
        port: this.config.port || airgradientDefaultPort,
        protocol: this.protocol(),
        name: this.config.name || this.config.deviceInfo?.name,
        manufacturer: airgradientManufacturer,
        model: this.config.model || this.config.deviceInfo?.model || this.config.measures?.model,
        modelName: this.config.deviceInfo?.modelName,
        serialNumber: this.config.serialNumber || this.config.deviceInfo?.serialNumber || this.config.measures?.serialNumber,
        firmwareVersion: this.config.firmwareVersion || this.config.deviceInfo?.firmwareVersion || this.config.measures?.firmwareVersion,
        latestFirmwareVersion: this.config.latestFirmwareVersion || this.config.deviceInfo?.latestFirmwareVersion,
        online: connectedArg,
        measures: this.config.measures,
        config: this.config.deviceConfig,
        metadata: this.config.deviceInfo,
      });
    }
    return {
      connected: connectedArg,
      updatedAt: new Date().toISOString(),
      devices,
      events: [...(this.config.events || []), ...this.events],
    };
  }

  private normalizeConfiguredDevices(connectedArg: boolean): IAirgradientDevice[] {
    const devices: IAirgradientDevice[] = [];
    for (const device of this.config.devices || []) {
      devices.push({ online: connectedArg, manufacturer: airgradientManufacturer, ...device });
    }
    for (const entry of this.config.manualEntries || []) {
      if (entry.snapshot) {
        devices.push(...this.normalizeSnapshot(entry.snapshot).devices);
      } else {
        devices.push({
          id: entry.id || entry.serialNumber || entry.host,
          host: entry.host,
          port: entry.port || airgradientDefaultPort,
          protocol: entry.protocol || this.protocol(),
          name: entry.name,
          manufacturer: entry.manufacturer || airgradientManufacturer,
          model: entry.model || entry.measures?.model,
          serialNumber: entry.serialNumber || entry.measures?.serialNumber,
          firmwareVersion: entry.firmwareVersion || entry.measures?.firmwareVersion,
          latestFirmwareVersion: entry.latestFirmwareVersion,
          online: this.booleanValue(entry.metadata?.connected) ?? connectedArg,
          measures: entry.measures,
          config: entry.deviceConfig,
          metadata: entry.metadata,
        });
      }
    }
    return devices;
  }

  private normalizeSnapshot(snapshotArg: IAirgradientSnapshot): IAirgradientSnapshot {
    const devices = snapshotArg.devices.map((deviceArg) => this.normalizeDevice(deviceArg, snapshotArg.connected));
    return {
      ...snapshotArg,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
      connected: Boolean(snapshotArg.connected),
      devices,
      events: snapshotArg.events || [],
    };
  }

  private normalizeDevice(deviceArg: IAirgradientDevice, connectedArg: boolean): IAirgradientDevice {
    const measures = deviceArg.measures ? normalizeMeasures(deviceArg.measures) : undefined;
    const deviceConfig = deviceArg.config ? normalizeDeviceConfig(deviceArg.config) : undefined;
    const serialNumber = deviceArg.serialNumber || measures?.serialNumber;
    const model = deviceArg.model || measures?.model;
    const firmwareVersion = deviceArg.firmwareVersion || measures?.firmwareVersion;
    return {
      ...deviceArg,
      id: deviceArg.id || serialNumber || deviceArg.host || deviceArg.name,
      host: deviceArg.host || this.config.host,
      port: deviceArg.port || this.config.port || airgradientDefaultPort,
      protocol: deviceArg.protocol || this.protocol(),
      name: deviceArg.name || this.deviceName(measures, deviceArg),
      manufacturer: deviceArg.manufacturer || airgradientManufacturer,
      model,
      modelName: deviceArg.modelName || modelName(model),
      serialNumber,
      firmwareVersion,
      latestFirmwareVersion: deviceArg.latestFirmwareVersion || this.config.latestFirmwareVersion,
      online: deviceArg.online ?? connectedArg,
      measures,
      config: deviceConfig,
    };
  }

  private async getCurrentMeasures(): Promise<IAirgradientMeasures> {
    const raw = await this.getJson<Record<string, unknown>>('measures/current');
    return parseMeasures(raw);
  }

  private async getDeviceConfig(): Promise<IAirgradientDeviceConfig> {
    const raw = await this.getJson<Record<string, unknown>>('config');
    return parseDeviceConfig(raw);
  }

  private async putConfig(payloadArg: Partial<Record<TAirgradientConfigField, unknown>>): Promise<void> {
    await this.requestJson('config', { method: 'PUT', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(payloadArg) });
  }

  private async getLatestFirmwareVersion(serialNumberArg: string): Promise<string> {
    const url = `http://hw.airgradient.com/sensors/airgradient:${encodeURIComponent(serialNumberArg)}/generic/os/firmware`;
    const response = await this.fetchWithTimeout(url, { method: 'GET', headers: { accept: 'application/json' } });
    const raw = await this.jsonResponse<Record<string, unknown>>(response, url);
    const targetVersion = stringValue(raw.targetVersion);
    if (!targetVersion) {
      throw new AirgradientParseError('AirGradient firmware version response is missing targetVersion.');
    }
    return targetVersion;
  }

  private async getJson<TValue>(pathArg: string): Promise<TValue> {
    return this.requestJson<TValue>(pathArg, { method: 'GET', headers: { accept: 'application/json' } });
  }

  private async requestJson<TValue = unknown>(pathArg: string, initArg: RequestInit): Promise<TValue> {
    const url = this.url(pathArg);
    const response = await this.fetchWithTimeout(url, initArg);
    return this.jsonResponse<TValue>(response, url);
  }

  private async jsonResponse<TValue>(responseArg: Response, urlArg: string): Promise<TValue> {
    const text = await responseArg.text();
    if (responseArg.status !== 200) {
      throw new AirgradientApiError(`AirGradient request ${urlArg} failed with HTTP ${responseArg.status}: ${text}`, responseArg.status);
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new AirgradientParseError(`AirGradient response from ${urlArg} is not valid JSON: ${this.errorMessage(errorArg)}`);
    }
  }

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = globalThis.setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } catch (errorArg) {
      throw new AirgradientApiError(`Error occurred while communicating with AirGradient: ${this.errorMessage(errorArg)}`);
    } finally {
      globalThis.clearTimeout(timeout);
    }
  }

  private patchConfig(fieldArg: TAirgradientConfigField, valueArg: unknown): void {
    const deviceConfig = this.currentSnapshot?.devices[0]?.config;
    if (!deviceConfig) {
      return;
    }
    if (fieldArg === 'abcDays') {
      deviceConfig.co2AutomaticBaselineCalibrationDays = typeof valueArg === 'number' ? valueArg : deviceConfig.co2AutomaticBaselineCalibrationDays;
    } else if (fieldArg === 'pmStandard') {
      deviceConfig.pmStandard = pmStandardValue(valueArg) || deviceConfig.pmStandard;
    } else if (fieldArg === 'temperatureUnit') {
      deviceConfig.temperatureUnit = temperatureUnitValue(valueArg) || deviceConfig.temperatureUnit;
    } else if (fieldArg === 'configurationControl') {
      deviceConfig.configurationControl = configurationControlValue(valueArg) || deviceConfig.configurationControl;
    } else if (fieldArg === 'ledBarMode') {
      deviceConfig.ledBarMode = ledBarModeValue(valueArg) || deviceConfig.ledBarMode;
    } else if (fieldArg === 'displayBrightness') {
      deviceConfig.displayBrightness = typeof valueArg === 'number' ? valueArg : deviceConfig.displayBrightness;
    } else if (fieldArg === 'ledBarBrightness') {
      deviceConfig.ledBarBrightness = typeof valueArg === 'number' ? valueArg : deviceConfig.ledBarBrightness;
    } else if (fieldArg === 'postDataToAirGradient') {
      deviceConfig.postDataToAirGradient = typeof valueArg === 'boolean' ? valueArg : deviceConfig.postDataToAirGradient;
    } else if (fieldArg === 'noxLearningOffset') {
      deviceConfig.noxLearningOffset = typeof valueArg === 'number' ? valueArg : deviceConfig.noxLearningOffset;
    } else if (fieldArg === 'tvocLearningOffset') {
      deviceConfig.tvocLearningOffset = typeof valueArg === 'number' ? valueArg : deviceConfig.tvocLearningOffset;
    }
  }

  private commandResult(resultArg: unknown, commandArg: IAirgradientCommand): IAirgradientCommandResult {
    if (this.isCommandResult(resultArg)) {
      return resultArg;
    }
    return { success: true, data: resultArg ?? { command: commandArg } };
  }

  private isCommandResult(valueArg: unknown): valueArg is IAirgradientCommandResult {
    return typeof valueArg === 'object' && valueArg !== null && 'success' in valueArg;
  }

  private commandDeviceId(commandArg: IAirgradientCommand): string | undefined {
    return commandArg.type === 'set_config' ? commandArg.deviceId : undefined;
  }

  private commandEntityId(commandArg: IAirgradientCommand): string | undefined {
    return commandArg.type === 'set_config' ? commandArg.entityId : undefined;
  }

  private emit(eventArg: IAirgradientEvent): void {
    this.events.push(eventArg);
    for (const handler of this.eventHandlers) {
      handler(eventArg);
    }
  }

  private url(pathArg: string): string {
    const normalizedPath = pathArg.replace(/^\/+/, '');
    const port = this.config.port && this.config.port !== airgradientDefaultPort ? `:${this.config.port}` : '';
    return `${this.protocol()}://${this.hostWithoutScheme()}${port}/${normalizedPath}`;
  }

  private protocol(): TAirgradientProtocol {
    return this.config.protocol || 'http';
  }

  private hostWithoutScheme(): string {
    const value = (this.config.host || '').trim().replace(/\/$/, '');
    try {
      return new URL(value).host;
    } catch {
      return value.replace(/^https?:\/\//i, '');
    }
  }

  private deviceName(measuresArg?: IAirgradientMeasures, infoArg?: IAirgradientDeviceInfo): string {
    const model = modelName(measuresArg?.model || infoArg?.model) || measuresArg?.model || infoArg?.model || 'Monitor';
    return this.config.name || infoArg?.name || `${airgradientManufacturer} ${model}`;
  }

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

  private booleanValue(valueArg: unknown): boolean | undefined {
    return typeof valueArg === 'boolean' ? valueArg : undefined;
  }

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

const parseMeasures = (rawArg: Record<string, unknown>): IAirgradientMeasures => {
  const signalStrength = requiredNumber(rawArg, 'wifi');
  const serialNumber = requiredString(rawArg, 'serialno');
  const bootTime = requiredNumber(rawArg, 'bootCount');
  const firmwareVersion = requiredString(rawArg, 'firmware');
  const model = requiredString(rawArg, 'model');
  return normalizeMeasures({
    signalStrength,
    serialNumber,
    bootTime,
    firmwareVersion,
    model,
    rco2: nullableNumber(rawArg.rco2),
    pm01: nullableNumber(rawArg.pm01),
    pm02: nullableNumber(rawArg.pm02Compensated) ?? nullableNumber(rawArg.pm02),
    rawPm02: nullableNumber(rawArg.pm02),
    compensatedPm02: nullableNumber(rawArg.pm02Compensated),
    pm10: nullableNumber(rawArg.pm10),
    totalVolatileOrganicComponentIndex: nullableNumber(rawArg.tvocIndex),
    rawTotalVolatileOrganicComponent: nullableNumber(rawArg.tvocRaw),
    pm003Count: nullableNumber(rawArg.pm003Count),
    nitrogenIndex: nullableNumber(rawArg.noxIndex),
    rawNitrogen: nullableNumber(rawArg.noxRaw),
    ambientTemperature: nullableNumber(rawArg.atmpCompensated) ?? nullableNumber(rawArg.atmp),
    rawAmbientTemperature: nullableNumber(rawArg.atmp),
    compensatedAmbientTemperature: nullableNumber(rawArg.atmpCompensated),
    relativeHumidity: nullableNumber(rawArg.rhumCompensated) ?? nullableNumber(rawArg.rhum),
    rawRelativeHumidity: nullableNumber(rawArg.rhum),
    compensatedRelativeHumidity: nullableNumber(rawArg.rhumCompensated),
    raw: rawArg,
  });
};

const parseDeviceConfig = (rawArg: Record<string, unknown>): IAirgradientDeviceConfig => {
  const config = normalizeDeviceConfig({
    country: requiredString(rawArg, 'country'),
    pmStandard: pmStandardValue(requiredString(rawArg, 'pmStandard')),
    ledBarMode: ledBarModeValue(requiredString(rawArg, 'ledBarMode')),
    co2AutomaticBaselineCalibrationDays: requiredNumber(rawArg, 'abcDays'),
    temperatureUnit: temperatureUnitValue(requiredString(rawArg, 'temperatureUnit')),
    configurationControl: configurationControlValue(requiredString(rawArg, 'configurationControl')),
    postDataToAirGradient: requiredBoolean(rawArg, 'postDataToAirGradient'),
    ledBarBrightness: requiredNumber(rawArg, 'ledBarBrightness'),
    displayBrightness: requiredNumber(rawArg, 'displayBrightness'),
    noxLearningOffset: requiredNumber(rawArg, 'noxLearningOffset'),
    tvocLearningOffset: requiredNumber(rawArg, 'tvocLearningOffset'),
    raw: rawArg,
  });
  return config;
};

export const normalizeMeasures = (measuresArg: IAirgradientMeasures): IAirgradientMeasures => {
  const raw = measuresArg as Record<string, unknown>;
  const compensatedPm02 = numberAlias(raw, 'compensatedPm02', 'pm02Compensated');
  const compensatedAmbientTemperature = numberAlias(raw, 'compensatedAmbientTemperature', 'atmpCompensated');
  const compensatedRelativeHumidity = numberAlias(raw, 'compensatedRelativeHumidity', 'rhumCompensated');
  return {
    ...measuresArg,
    signalStrength: numberAlias(raw, 'signalStrength', 'wifi'),
    serialNumber: stringAlias(raw, 'serialNumber', 'serialno', 'serial_number'),
    bootTime: numberAlias(raw, 'bootTime', 'bootCount'),
    firmwareVersion: stringAlias(raw, 'firmwareVersion', 'firmware'),
    model: stringAlias(raw, 'model'),
    rco2: nullableNumberAlias(raw, 'rco2'),
    pm01: nullableNumberAlias(raw, 'pm01'),
    pm02: compensatedPm02 ?? nullableNumberAlias(raw, 'pm02'),
    rawPm02: nullableNumberAlias(raw, 'rawPm02', 'pm02'),
    compensatedPm02,
    pm10: nullableNumberAlias(raw, 'pm10'),
    totalVolatileOrganicComponentIndex: nullableNumberAlias(raw, 'totalVolatileOrganicComponentIndex', 'tvocIndex'),
    rawTotalVolatileOrganicComponent: nullableNumberAlias(raw, 'rawTotalVolatileOrganicComponent', 'tvocRaw'),
    pm003Count: nullableNumberAlias(raw, 'pm003Count'),
    nitrogenIndex: nullableNumberAlias(raw, 'nitrogenIndex', 'noxIndex'),
    rawNitrogen: nullableNumberAlias(raw, 'rawNitrogen', 'noxRaw'),
    ambientTemperature: compensatedAmbientTemperature ?? nullableNumberAlias(raw, 'ambientTemperature', 'atmp'),
    rawAmbientTemperature: nullableNumberAlias(raw, 'rawAmbientTemperature', 'atmp'),
    compensatedAmbientTemperature,
    relativeHumidity: compensatedRelativeHumidity ?? nullableNumberAlias(raw, 'relativeHumidity', 'rhum'),
    rawRelativeHumidity: nullableNumberAlias(raw, 'rawRelativeHumidity', 'rhum'),
    compensatedRelativeHumidity,
  };
};

export const normalizeDeviceConfig = (configArg: IAirgradientDeviceConfig): IAirgradientDeviceConfig => {
  const raw = configArg as Record<string, unknown>;
  return {
    ...configArg,
    country: stringAlias(raw, 'country'),
    pmStandard: pmStandardValue(valueAlias(raw, 'pmStandard')),
    ledBarMode: ledBarModeValue(valueAlias(raw, 'ledBarMode')),
    co2AutomaticBaselineCalibrationDays: numberAlias(raw, 'co2AutomaticBaselineCalibrationDays', 'abcDays'),
    temperatureUnit: temperatureUnitValue(valueAlias(raw, 'temperatureUnit')),
    configurationControl: configurationControlValue(valueAlias(raw, 'configurationControl')),
    postDataToAirGradient: booleanAlias(raw, 'postDataToAirGradient'),
    ledBarBrightness: numberAlias(raw, 'ledBarBrightness'),
    displayBrightness: numberAlias(raw, 'displayBrightness'),
    noxLearningOffset: numberAlias(raw, 'noxLearningOffset'),
    tvocLearningOffset: numberAlias(raw, 'tvocLearningOffset'),
  };
};

export const modelName = (modelArg?: string): string | undefined => {
  if (!modelArg) {
    return undefined;
  }
  if (modelArg.startsWith('I-9PSL')) {
    return 'AirGradient ONE';
  }
  if (modelArg.startsWith('O-1')) {
    return 'AirGradient Open Air';
  }
  if (modelArg.includes('DIY')) {
    return 'AirGradient DIY';
  }
  return undefined;
};

const requiredString = (recordArg: Record<string, unknown>, keyArg: string): string => {
  const value = stringValue(recordArg[keyArg]);
  if (!value) {
    throw new AirgradientParseError(`AirGradient response is missing required string field ${keyArg}.`);
  }
  return value;
};

const requiredNumber = (recordArg: Record<string, unknown>, keyArg: string): number => {
  const value = numberValue(recordArg[keyArg]);
  if (value === undefined) {
    throw new AirgradientParseError(`AirGradient response is missing required number field ${keyArg}.`);
  }
  return value;
};

const requiredBoolean = (recordArg: Record<string, unknown>, keyArg: string): boolean => {
  const value = recordArg[keyArg];
  if (typeof value !== 'boolean') {
    throw new AirgradientParseError(`AirGradient response is missing required boolean field ${keyArg}.`);
  }
  return value;
};

const valueAlias = (recordArg: Record<string, unknown>, ...keysArg: string[]): unknown => {
  for (const key of keysArg) {
    if (recordArg[key] !== undefined) {
      return recordArg[key];
    }
  }
  return undefined;
};

const stringAlias = (recordArg: Record<string, unknown>, ...keysArg: string[]): string | undefined => stringValue(valueAlias(recordArg, ...keysArg));
const numberAlias = (recordArg: Record<string, unknown>, ...keysArg: string[]): number | undefined => numberValue(valueAlias(recordArg, ...keysArg));
const booleanAlias = (recordArg: Record<string, unknown>, ...keysArg: string[]): boolean | undefined => {
  const value = valueAlias(recordArg, ...keysArg);
  return typeof value === 'boolean' ? value : undefined;
};
const nullableNumberAlias = (recordArg: Record<string, unknown>, ...keysArg: string[]): number | null | undefined => nullableNumber(valueAlias(recordArg, ...keysArg));

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

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

const nullableNumber = (valueArg: unknown): number | null | undefined => valueArg === null ? null : numberValue(valueArg);

const pmStandardValue = (valueArg: unknown): TAirgradientPmStandard | undefined => valueArg === 'ugm3' || valueArg === 'us-aqi' ? valueArg : undefined;
const temperatureUnitValue = (valueArg: unknown): TAirgradientTemperatureUnit | undefined => valueArg === 'c' || valueArg === 'f' ? valueArg : undefined;
const configurationControlValue = (valueArg: unknown): TAirgradientConfigurationControl | undefined => valueArg === 'cloud' || valueArg === 'local' || valueArg === 'both' ? valueArg : undefined;
const ledBarModeValue = (valueArg: unknown): TAirgradientLedBarMode | undefined => valueArg === 'off' || valueArg === 'co2' || valueArg === 'pm' ? valueArg : undefined;
