import { DaikinMapper } from './daikin.mapper.js';
import type {
  IDaikinCommandRequest,
  IDaikinConfig,
  IDaikinControlSettings,
  IDaikinRawData,
  IDaikinRefreshResult,
  IDaikinSnapshot,
  IDaikinValueMap,
  TDaikinApiVariant,
} from './daikin.types.js';
import { daikinDefaultHttpPort, daikinDefaultHttpsPort, daikinDefaultTimeoutMs, daikinSkyFiPort } from './daikin.types.js';

export class DaikinApiError extends Error {}
export class DaikinApiConnectionError extends DaikinApiError {}
export class DaikinApiAuthorizationError extends DaikinApiError {}
export class DaikinUnsupportedCommandError extends DaikinApiError {}

const legacyResources = [
  'common/basic_info',
  'aircon/get_sensor_info',
  'aircon/get_model_info',
  'aircon/get_control_info',
  'aircon/get_target',
  'common/get_holiday',
  'common/get_notify',
  'aircon/get_day_power_ex',
  'aircon/get_week_power',
  'aircon/get_year_power',
];

const airbaseResources = [
  'skyfi/common/basic_info',
  'skyfi/aircon/get_control_info',
  'skyfi/aircon/get_model_info',
  'skyfi/aircon/get_sensor_info',
  'skyfi/aircon/get_zone_setting',
];

const resourceKeyMap: Record<string, keyof IDaikinRawData> = {
  'common/basic_info': 'basicInfo',
  'skyfi/common/basic_info': 'basicInfo',
  'aircon/get_control_info': 'controlInfo',
  'skyfi/aircon/get_control_info': 'controlInfo',
  'aircon/get_sensor_info': 'sensorInfo',
  'skyfi/aircon/get_sensor_info': 'sensorInfo',
  'aircon/get_model_info': 'modelInfo',
  'skyfi/aircon/get_model_info': 'modelInfo',
  'common/get_holiday': 'holiday',
  'common/get_notify': 'notify',
  'aircon/get_day_power_ex': 'dayPowerEx',
  'aircon/get_week_power': 'weekPower',
  'aircon/get_year_power': 'yearPower',
  'skyfi/aircon/get_zone_setting': 'zoneSetting',
};

const brp084ModeMap: Record<string, string> = {
  '0300': 'auto',
  '0200': 'cool',
  '0100': 'heat',
  '0000': 'fan',
  '0500': 'dry',
};

const brp084FanModeMap: Record<string, string> = {
  '0A00': 'auto',
  '0B00': 'quiet',
  '0300': '1',
  '0400': '2',
  '0500': '3',
  '0600': '4',
  '0700': '5',
};

const brp084Paths = {
  power: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_A002', 'p_01'],
  mode: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_01'],
  indoorTemp: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_A00B', 'p_01'],
  indoorHumidity: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_A00B', 'p_02'],
  outdoorTemp: ['/dsiot/edge/adr_0200.dgc_status', 'dgc_status', 'e_1003', 'e_A00D', 'p_01'],
  mac: ['/dsiot/edge.adp_i', 'adp_i', 'mac'],
  coolTemp: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_02'],
  heatTemp: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_03'],
  autoTemp: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_1D'],
  coolFan: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_09'],
  heatFan: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_0A'],
  autoFan: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_26'],
  fanFan: ['/dsiot/edge/adr_0100.dgc_status', 'dgc_status', 'e_1002', 'e_3001', 'p_28'],
  todayRuntime: ['/dsiot/edge/adr_0100.i_power.week_power', 'week_power', 'today_runtime'],
  weeklyData: ['/dsiot/edge/adr_0100.i_power.week_power', 'week_power', 'datas'],
};

export class DaikinClient {
  private currentSnapshot?: IDaikinSnapshot;

  constructor(private readonly config: IDaikinConfig) {}

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

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = DaikinMapper.toSnapshot({ config: this.config, source: 'snapshot', online: this.config.snapshot.online });
      return this.cloneSnapshot(this.currentSnapshot);
    }

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

    if (!forceRefreshArg && this.hasManualData()) {
      this.currentSnapshot = DaikinMapper.toSnapshot({ config: this.config, online: this.config.online ?? true, source: 'manual' });
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.host || this.config.url) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('No Daikin local HTTP endpoint, client, snapshot, or manual data is configured.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IDaikinRefreshResult> {
    try {
      this.currentSnapshot = undefined;
      const snapshot = await this.getSnapshot(Boolean(this.config.host || this.config.url || this.config.client));
      const success = snapshot.online && !snapshot.error;
      return { success, snapshot, error: success ? undefined : snapshot.error, data: { source: snapshot.source } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: this.cloneSnapshot(snapshot), error };
    }
  }

  public async fetchSnapshot(): Promise<IDaikinSnapshot> {
    const variant = this.config.apiVariant || 'auto';
    if (variant === 'skyfi' || this.config.password) {
      return this.snapshotFromRawData(await this.fetchSkyFiRawData(), 'http');
    }
    if (variant === 'airbase') {
      return this.snapshotFromRawData(await this.fetchLegacyRawData('airbase'), 'http');
    }
    if (variant === 'brp084') {
      return this.snapshotFromRawData(await this.fetchBrp084RawData(), 'http');
    }
    if (variant === 'brp072c' || this.config.apiKey) {
      return this.snapshotFromRawData(await this.fetchLegacyRawData('brp072c'), 'http');
    }

    const legacy = await this.fetchLegacyRawData('brp069');
    const legacyValues = DaikinMapper.valuesFromRawData(legacy);
    if (legacyValues.mode || legacyValues.pow || legacyValues.htemp || legacyValues.mac) {
      return this.snapshotFromRawData(legacy, 'http');
    }

    return this.snapshotFromRawData(await this.fetchLegacyRawData('airbase'), 'http');
  }

  public async execute(commandArg: IDaikinCommandRequest): Promise<unknown> {
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }
    if (this.config.client?.execute) {
      return this.config.client.execute(commandArg);
    }
    if (commandArg.action === 'refresh') {
      return (await this.refresh()).snapshot;
    }
    if (!this.config.host && !this.config.url) {
      throw new DaikinApiConnectionError('Daikin commands require config.host/config.url, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }
    if (commandArg.path && commandArg.method) {
      return this.requestKeyValue(commandArg.path, commandArg.query, this.requestVariant(commandArg.protocol), true);
    }

    switch (commandArg.action) {
      case 'set_control':
        return this.setControl(commandArg.settings || {}, this.requestVariant(commandArg.protocol));
      case 'set_holiday':
        return this.setHoliday(Boolean(commandArg.enabled), this.requestVariant(commandArg.protocol));
      case 'set_advanced_mode':
        return this.setAdvancedMode(commandArg.mode || 'powerful', Boolean(commandArg.enabled), this.requestVariant(commandArg.protocol));
      case 'set_streamer':
        return this.setStreamer(Boolean(commandArg.enabled), this.requestVariant(commandArg.protocol));
      case 'set_zone':
        return this.setZone(commandArg.zoneId, commandArg.zoneKey || 'zone_onoff', commandArg.zoneValue ?? (commandArg.enabled ? '1' : '0'), this.requestVariant(commandArg.protocol));
      default:
        throw new DaikinUnsupportedCommandError(`Unsupported Daikin command: ${commandArg.action}`);
    }
  }

  public async destroy(): Promise<void> {
    await this.config.client?.destroy?.();
  }

  private async snapshotFromClient(): Promise<IDaikinSnapshot> {
    const client = this.config.client;
    if (!client) {
      throw new DaikinApiConnectionError('No Daikin client configured.');
    }
    if (client.getSnapshot) {
      const result = await client.getSnapshot();
      if (this.isSnapshot(result)) {
        return DaikinMapper.toSnapshot({ config: { ...this.config, snapshot: result }, source: 'client', online: result.online });
      }
      return DaikinMapper.toSnapshot({ config: this.config, rawData: result, online: true, source: 'client' });
    }
    if (client.getRawData) {
      return DaikinMapper.toSnapshot({ config: this.config, rawData: await client.getRawData(), online: true, source: 'client' });
    }
    throw new DaikinApiConnectionError('Daikin client must expose getSnapshot() or getRawData().');
  }

  private snapshotFromRawData(rawDataArg: Partial<IDaikinRawData>, sourceArg: 'http' | 'manual' | 'client'): IDaikinSnapshot {
    return DaikinMapper.toSnapshot({ config: this.config, rawData: rawDataArg, online: true, source: sourceArg });
  }

  private async fetchLegacyRawData(variantArg: TDaikinApiVariant): Promise<Partial<IDaikinRawData>> {
    const resources = variantArg === 'airbase' ? airbaseResources : legacyResources;
    if (variantArg === 'brp072c' && this.config.apiKey) {
      await this.requestKeyValue('common/register_terminal', { key: this.config.apiKey }, variantArg, true);
    }

    const rawData: Partial<IDaikinRawData> = { variant: variantArg, resources: {}, fetchedAt: new Date().toISOString() };
    for (const resource of resources) {
      const data = await this.requestKeyValue(resource, undefined, variantArg, true).catch((errorArg) => {
        if (resource === resources[0]) {
          throw errorArg;
        }
        return {};
      });
      rawData.resources![resource] = data;
      const key = resourceKeyMap[resource];
      if (key) {
        (rawData as Record<string, unknown>)[key] = data;
      }
    }
    return rawData;
  }

  private async fetchSkyFiRawData(): Promise<Partial<IDaikinRawData>> {
    const ac = await this.requestSkyFi('ac.cgi');
    const zones = await this.requestSkyFi('zones.cgi').catch(() => ({}));
    return {
      variant: 'skyfi',
      skyfiAc: ac,
      skyfiZones: zones,
      values: { ...ac, ...zones },
      fetchedAt: new Date().toISOString(),
    };
  }

  private async fetchBrp084RawData(): Promise<Partial<IDaikinRawData>> {
    const payload = {
      requests: [
        { op: 2, to: '/dsiot/edge/adr_0100.dgc_status?filter=pv,pt,md' },
        { op: 2, to: '/dsiot/edge/adr_0200.dgc_status?filter=pv,pt,md' },
        { op: 2, to: '/dsiot/edge/adr_0100.i_power.week_power?filter=pv,pt,md' },
        { op: 2, to: '/dsiot/edge.adp_i' },
      ],
    };
    const response = await this.requestJson<Record<string, unknown>>('/dsiot/multireq', payload, 'brp084');
    const modeValue = this.findBrp084Value(response, brp084Paths.mode);
    const mode = brp084ModeMap[modeValue] || modeValue;
    const isOff = this.findBrp084Value(response, brp084Paths.power) === '00';
    const tempPath = mode === 'cool' ? brp084Paths.coolTemp : mode === 'heat' ? brp084Paths.heatTemp : brp084Paths.autoTemp;
    const fanPath = mode === 'cool' ? brp084Paths.coolFan : mode === 'heat' ? brp084Paths.heatFan : mode === 'fan' ? brp084Paths.fanFan : brp084Paths.autoFan;
    const values: IDaikinValueMap = {
      mac: this.findBrp084Value(response, brp084Paths.mac),
      pow: isOff ? '0' : '1',
      mode: isOff ? 'off' : mode,
      otemp: this.hexTemp(this.findBrp084Value(response, brp084Paths.outdoorTemp)),
      htemp: this.hexTemp(this.findBrp084Value(response, brp084Paths.indoorTemp), 1),
      hhum: this.hexInt(this.findBrp084Value(response, brp084Paths.indoorHumidity)),
      stemp: this.hexTemp(this.findBrp084Value(response, tempPath)),
      f_rate: brp084FanModeMap[this.findBrp084Value(response, fanPath)] || 'auto',
      datas: this.brp084WeeklyData(response),
    };
    return { variant: 'brp084', brp084: response, values, fetchedAt: new Date().toISOString() };
  }

  private async setControl(settingsArg: Partial<IDaikinControlSettings>, variantArg: TDaikinApiVariant): Promise<unknown> {
    if (variantArg === 'skyfi') {
      return this.setSkyFiControl(settingsArg);
    }
    if (variantArg === 'brp084') {
      throw new DaikinUnsupportedCommandError('Daikin BRP084 dsiot control writes are not implemented by this native port. Provide commandExecutor for BRP084 controls.');
    }
    return this.setLegacyControl(settingsArg, variantArg === 'airbase' ? 'airbase' : 'brp069');
  }

  private async setLegacyControl(settingsArg: Partial<IDaikinControlSettings>, variantArg: 'brp069' | 'airbase'): Promise<unknown> {
    const prefix = variantArg === 'airbase' ? 'skyfi/' : '';
    const current = await this.requestKeyValue(`${prefix}aircon/get_control_info`, undefined, variantArg, true);
    const values: IDaikinValueMap = { ...current };
    this.applyControlSettings(values, settingsArg, variantArg);

    if (settingsArg.mode === 'off') {
      values.pow = '0';
      values.mode = current.mode || values.mode;
    } else if ('mode' in settingsArg || Object.keys(settingsArg).length === 0) {
      values.pow = '1';
    }

    this.fillModeDefaults(values, current);
    const query = variantArg === 'airbase'
      ? {
          f_airside: values.f_airside || 0,
          f_auto: values.f_auto || '0',
          f_dir: values.f_dir || '0',
          f_rate: (values.f_rate || 'A')[0],
          lpw: '',
          mode: values.mode || current.mode || '0',
          pow: values.pow || current.pow || '1',
          shum: values.shum || current.shum || '--',
          stemp: values.stemp || current.stemp || '--',
        }
      : this.cleanQuery({
          mode: values.mode || current.mode || '0',
          pow: values.pow || current.pow || '1',
          shum: values.shum || current.shum || '--',
          stemp: values.stemp || current.stemp || '--',
          f_rate: values.f_rate !== undefined || current.f_rate !== undefined ? values.f_rate || current.f_rate : undefined,
          f_dir: values.f_dir !== undefined || current.f_dir !== undefined ? values.f_dir || current.f_dir : undefined,
          f_dir_ud: current.f_dir_ud !== undefined ? this.swingAxis(values.f_dir, 'ud') : undefined,
          f_dir_lr: current.f_dir_lr !== undefined ? this.swingAxis(values.f_dir, 'lr') : undefined,
        });
    return this.requestKeyValue(`${prefix}aircon/set_control_info`, query, variantArg, true);
  }

  private async setSkyFiControl(settingsArg: Partial<IDaikinControlSettings>): Promise<unknown> {
    const current = await this.requestSkyFi('ac.cgi');
    const values: IDaikinValueMap = { ...current };
    this.applyControlSettings(values, settingsArg, 'skyfi');
    if (settingsArg.mode === 'off') {
      return this.requestSkyFi('set.cgi', { p: '0' });
    }
    if ('mode' in settingsArg || Object.keys(settingsArg).length === 0) {
      values.opmode = '1';
      values.pow = '1';
    }
    return this.requestSkyFi('set.cgi', {
      p: values.opmode || values.pow || '1',
      t: values.settemp || values.stemp || current.settemp || current.stemp,
      f: values.fanspeed || values.f_rate || current.fanspeed || current.f_rate,
      m: values.acmode || values.mode || current.acmode || current.mode,
    });
  }

  private async setHoliday(enabledArg: boolean, variantArg: TDaikinApiVariant): Promise<unknown> {
    if (variantArg === 'skyfi' || variantArg === 'airbase' || variantArg === 'brp084') {
      throw new DaikinUnsupportedCommandError(`Daikin ${variantArg} holiday mode is not represented by a supported local request in this port.`);
    }
    return this.requestKeyValue('common/set_holiday', { en_hol: enabledArg ? '1' : '0' }, variantArg, true);
  }

  private async setAdvancedMode(modeArg: string, enabledArg: boolean, variantArg: TDaikinApiVariant): Promise<unknown> {
    if (variantArg === 'skyfi' || variantArg === 'brp084') {
      throw new DaikinUnsupportedCommandError(`Daikin ${variantArg} advanced modes are not represented by a supported local request in this port.`);
    }
    const mode = DaikinMapper.humanToDaikin('spmode_kind', modeArg, variantArg) || modeArg;
    return this.requestKeyValue(`${variantArg === 'airbase' ? 'skyfi/' : ''}aircon/set_special_mode`, { spmode_kind: mode, set_spmode: enabledArg ? '1' : '0' }, variantArg, true);
  }

  private async setStreamer(enabledArg: boolean, variantArg: TDaikinApiVariant): Promise<unknown> {
    if (variantArg === 'skyfi' || variantArg === 'brp084') {
      throw new DaikinUnsupportedCommandError(`Daikin ${variantArg} streamer mode is not represented by a supported local request in this port.`);
    }
    return this.requestKeyValue(`${variantArg === 'airbase' ? 'skyfi/' : ''}aircon/set_special_mode`, { en_streamer: enabledArg ? '1' : '0' }, variantArg, true);
  }

  private async setZone(zoneIdArg: number | undefined, keyArg: string, valueArg: string, variantArg: TDaikinApiVariant): Promise<unknown> {
    if (zoneIdArg === undefined || !Number.isInteger(zoneIdArg) || zoneIdArg < 0) {
      throw new DaikinUnsupportedCommandError('Daikin zone commands require a non-negative zoneId.');
    }
    if (variantArg === 'skyfi') {
      if (keyArg !== 'zone_onoff') {
        throw new DaikinUnsupportedCommandError('Daikin SkyFi only supports zone_onoff through setzone.cgi.');
      }
      return this.requestSkyFi('setzone.cgi', { z: zoneIdArg + 1, s: valueArg });
    }
    if (variantArg !== 'airbase') {
      throw new DaikinUnsupportedCommandError(`Daikin ${variantArg} zone commands are not represented by a supported local request in this port.`);
    }

    const current = await this.requestKeyValue('skyfi/aircon/get_zone_setting', undefined, variantArg, true);
    if (!current[keyArg]) {
      throw new DaikinUnsupportedCommandError(`Daikin AirBase zone setting ${keyArg} is not available on this device.`);
    }
    const group = this.listValue(current[keyArg]);
    if (zoneIdArg >= group.length) {
      throw new DaikinUnsupportedCommandError(`Daikin AirBase zone ${zoneIdArg} is outside the available range 0-${Math.max(0, group.length - 1)}.`);
    }
    group[zoneIdArg] = valueArg;
    const values = { ...current, [keyArg]: encodeURIComponent(group.join(';')).toLowerCase() };
    const query = [`zone_name=${values.zone_name || ''}`, `zone_onoff=${values.zone_onoff || ''}`];
    if (values.lztemp_c !== undefined && values.lztemp_h !== undefined) {
      query.push(`lztemp_c=${values.lztemp_c}`, `lztemp_h=${values.lztemp_h}`);
    }
    return this.requestKeyValue(`skyfi/aircon/set_zone_setting?${query.join('&')}`, undefined, variantArg, true);
  }

  private applyControlSettings(valuesArg: IDaikinValueMap, settingsArg: Partial<IDaikinControlSettings>, variantArg: TDaikinApiVariant): void {
    for (const [key, value] of Object.entries(settingsArg)) {
      if (value === undefined) {
        continue;
      }
      if (key === 'stemp') {
        valuesArg.stemp = DaikinMapper.formatTargetTemperature(Number(value));
        valuesArg.settemp = valuesArg.stemp;
      } else if (key === 'shum') {
        valuesArg.shum = String(value);
      } else if (key === 'mode') {
        const daikinMode = DaikinMapper.humanToDaikin('mode', String(value), variantArg);
        valuesArg.mode = daikinMode;
        valuesArg.acmode = daikinMode;
      } else if (key === 'f_rate') {
        const fanRate = DaikinMapper.humanToDaikin('f_rate', String(value), variantArg);
        valuesArg.f_rate = fanRate;
        valuesArg.fanspeed = fanRate;
      } else if (key === 'f_dir') {
        valuesArg.f_dir = DaikinMapper.humanToDaikin('f_dir', String(value), variantArg);
      } else {
        valuesArg[key] = String(value);
      }
    }
  }

  private fillModeDefaults(valuesArg: IDaikinValueMap, currentArg: IDaikinValueMap): void {
    const mode = valuesArg.mode || currentArg.mode;
    if (!mode) {
      return;
    }
    const defaults: Record<string, string> = { stemp: `dt${mode}`, shum: `dh${mode}`, f_rate: `dfr${mode}` };
    for (const [target, source] of Object.entries(defaults)) {
      if (valuesArg[target] === undefined && currentArg[source] !== undefined) {
        valuesArg[target] = currentArg[source];
      }
    }
  }

  private async requestSkyFi(pathArg: string, queryArg: Record<string, string | number | boolean | undefined> = {}): Promise<IDaikinValueMap> {
    const query = { pass: this.config.password || '', ...queryArg };
    const response = await this.fetchText(pathArg, query, 'skyfi', true);
    return DaikinMapper.parseSkyFiResponse(response);
  }

  private async requestKeyValue(pathArg: string, queryArg: Record<string, string | number | boolean | undefined> | undefined, variantArg: TDaikinApiVariant, allowEmptyArg = false): Promise<IDaikinValueMap> {
    if (this.config.client?.getResource && !this.config.host && !this.config.url) {
      return this.config.client.getResource(pathArg, queryArg);
    }
    const text = await this.fetchText(pathArg, queryArg, variantArg, allowEmptyArg);
    if (!text.trim()) {
      return {};
    }
    try {
      return DaikinMapper.parseKeyValueResponse(text);
    } catch (errorArg) {
      if (allowEmptyArg) {
        return {};
      }
      throw errorArg;
    }
  }

  private async requestJson<TValue>(pathArg: string, bodyArg: unknown, variantArg: TDaikinApiVariant): Promise<TValue> {
    const url = this.url(pathArg, undefined, variantArg);
    let response: Response;
    try {
      response = await globalThis.fetch(url, {
        method: 'POST',
        headers: this.headers(variantArg, { 'content-type': 'application/json', accept: 'application/json' }),
        body: JSON.stringify(bodyArg),
        signal: AbortSignal.timeout(this.config.timeoutMs || daikinDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new DaikinApiConnectionError(`Connection to ${url} failed: ${this.errorMessage(errorArg)}`);
    }
    await this.assertOk(response, url, false);
    return await response.json() as TValue;
  }

  private async fetchText(pathArg: string, queryArg: Record<string, string | number | boolean | undefined> | undefined, variantArg: TDaikinApiVariant, allowEmptyArg: boolean): Promise<string> {
    const url = this.url(pathArg, queryArg, variantArg);
    let response: Response;
    try {
      response = await globalThis.fetch(url, {
        method: 'GET',
        headers: this.headers(variantArg),
        signal: AbortSignal.timeout(this.config.timeoutMs || daikinDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new DaikinApiConnectionError(`Connection to ${url} failed: ${this.errorMessage(errorArg)}`);
    }
    if (response.status === 404 && allowEmptyArg) {
      await response.arrayBuffer().catch(() => undefined);
      return '';
    }
    await this.assertOk(response, url, allowEmptyArg);
    return await response.text();
  }

  private async assertOk(responseArg: Response, urlArg: string, allowEmptyArg: boolean): Promise<void> {
    if (responseArg.status === 403 || responseArg.status === 401) {
      throw new DaikinApiAuthorizationError('Daikin authentication failed. Check API key, UUID, or SkyFi password.');
    }
    if (!responseArg.ok) {
      const text = await responseArg.text().catch(() => '');
      if (allowEmptyArg && responseArg.status === 404) {
        return;
      }
      throw new DaikinApiConnectionError(`Daikin endpoint ${urlArg} failed with HTTP ${responseArg.status}${text ? `: ${text}` : ''}`);
    }
  }

  private url(pathArg: string, queryArg: Record<string, string | number | boolean | undefined> | undefined, variantArg: TDaikinApiVariant): string {
    if (/^https?:\/\//i.test(pathArg)) {
      return pathArg;
    }
    const base = this.baseUrl(variantArg);
    const url = new URL(pathArg.startsWith('/') ? pathArg : `/${pathArg}`, base.endsWith('/') ? base : `${base}/`);
    for (const [key, value] of Object.entries(queryArg || {})) {
      if (value !== undefined) {
        url.searchParams.set(key, String(value));
      }
    }
    return url.toString();
  }

  private baseUrl(variantArg: TDaikinApiVariant): string {
    const configured = this.config.url || this.config.host || 'localhost';
    if (/^https?:\/\//i.test(configured)) {
      const url = new URL(configured);
      return `${url.protocol}//${url.host}`;
    }
    const ssl = this.config.ssl ?? this.config.tls ?? variantArg === 'brp072c';
    const protocol = ssl ? 'https' : 'http';
    const defaultPort = variantArg === 'skyfi' ? daikinSkyFiPort : ssl ? daikinDefaultHttpsPort : daikinDefaultHttpPort;
    const port = this.config.port && this.config.port !== defaultPort ? `:${this.config.port}` : '';
    return `${protocol}://${this.hostForUrl(configured)}${port}`;
  }

  private headers(variantArg: TDaikinApiVariant, extraArg: Record<string, string> = {}): Record<string, string> {
    const headers = { ...extraArg };
    if (variantArg === 'brp072c' && this.config.uuid) {
      headers['x-daikin-uuid'] = this.config.uuid.replace(/-/g, '');
    }
    return headers;
  }

  private requestVariant(valueArg: IDaikinCommandRequest['protocol']): TDaikinApiVariant {
    if (valueArg && valueArg !== 'legacy' && valueArg !== 'auto') {
      return valueArg;
    }
    if (this.config.apiVariant && this.config.apiVariant !== 'auto') {
      return this.config.apiVariant;
    }
    if (this.config.password) {
      return 'skyfi';
    }
    if (this.config.apiKey) {
      return 'brp072c';
    }
    return 'brp069';
  }

  private findBrp084Value(dataArg: Record<string, unknown>, pathArg: string[]): string {
    const responses = Array.isArray(dataArg.responses) ? dataArg.responses as Array<Record<string, unknown>> : [];
    const from = pathArg[0];
    let nodes = responses.filter((responseArg) => responseArg.fr === from || responseArg.to === from).map((responseArg) => responseArg.pc as Record<string, unknown>).filter(Boolean);
    for (const key of pathArg.slice(1)) {
      let found: Record<string, unknown> | undefined;
      for (const node of nodes) {
        if (node.pn === key) {
          found = node;
          break;
        }
      }
      if (!found) {
        throw new DaikinApiConnectionError(`Daikin BRP084 response missing key ${key}.`);
      }
      if (pathArg[pathArg.length - 1] === key) {
        return String(found.pv ?? '');
      }
      nodes = Array.isArray(found.pch) ? found.pch as Array<Record<string, unknown>> : [];
    }
    return '';
  }

  private brp084WeeklyData(dataArg: Record<string, unknown>): string | undefined {
    try {
      const value = this.findBrp084Value(dataArg, brp084Paths.weeklyData);
      return Array.isArray(value) ? value.join('/') : String(value);
    } catch {
      return undefined;
    }
  }

  private hexTemp(valueArg: string, divisorArg = 2): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const parsed = Number.parseInt(valueArg.slice(0, 2), 16);
    return Number.isFinite(parsed) ? String(parsed / divisorArg) : undefined;
  }

  private hexInt(valueArg: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const parsed = Number.parseInt(valueArg, 16);
    return Number.isFinite(parsed) ? String(parsed) : undefined;
  }

  private swingAxis(valueArg: string | undefined, axisArg: 'ud' | 'lr'): string {
    const value = valueArg || '0';
    if (axisArg === 'ud') {
      return value === '1' || value === '3' ? 'S' : '0';
    }
    return value === '2' || value === '3' ? 'S' : '0';
  }

  private listValue(valueArg?: string): string[] {
    if (!valueArg) {
      return [];
    }
    let decoded = valueArg;
    try {
      decoded = decodeURIComponent(valueArg.replace(/\+/g, '%20'));
    } catch {
      decoded = valueArg;
    }
    return decoded.includes(';') ? decoded.split(';') : decoded.split('');
  }

  private cleanQuery(queryArg: Record<string, string | number | boolean | undefined>): Record<string, string | number | boolean | undefined> {
    const cleaned: Record<string, string | number | boolean | undefined> = {};
    for (const [key, value] of Object.entries(queryArg)) {
      if (value !== undefined) {
        cleaned[key] = value;
      }
    }
    return cleaned;
  }

  private offlineSnapshot(errorArg: string): IDaikinSnapshot {
    return DaikinMapper.toSnapshot({ config: this.config, online: false, source: 'runtime', error: errorArg });
  }

  private hasManualData(): boolean {
    return Boolean(this.config.snapshot || this.config.rawData || this.config.values);
  }

  private isSnapshot(valueArg: unknown): valueArg is IDaikinSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'climate' in valueArg);
  }

  private hostForUrl(hostArg: string): string {
    return hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
  }

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

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