import type {
  IBleboxConfig,
  IBleboxCoverCommandRequest,
  IBleboxDeviceInfo,
  IBleboxLightStatePatch,
  IBleboxProductProfile,
  IBleboxSnapshot,
  TBleboxJsonValue,
} from './blebox.types.js';

interface IBleboxProfileTemplate {
  minApiLevel: number;
  apiPath: string;
  extendedStatePath?: string;
  supports: IBleboxProductProfile['supports'];
}

const defaultPort = 80;
const defaultSetupTimeoutMs = 10000;
const defaultApiLevel = 20151206;

const profileTemplates: Record<string, IBleboxProfileTemplate[]> = {
  airSensor: [
    { minApiLevel: 20180403, apiPath: '/api/air/state', supports: { sensor: true } },
  ],
  dimmerBox: [
    { minApiLevel: 20151206, apiPath: '/api/dimmer/state', supports: { light: true } },
    { minApiLevel: 20170829, apiPath: '/api/dimmer/state', supports: { light: true } },
  ],
  gateBox: [
    { minApiLevel: 20151206, apiPath: '/api/gate/state', supports: { cover: true } },
    { minApiLevel: 20200831, apiPath: '/state/extended', extendedStatePath: '/state/extended', supports: { cover: true } },
  ],
  gateController: [
    { minApiLevel: 20180604, apiPath: '/api/gatecontroller/state', extendedStatePath: '/api/gatecontroller/extended/state', supports: { cover: true } },
  ],
  multiSensor: [
    { minApiLevel: 20200831, apiPath: '/state', extendedStatePath: '/state/extended', supports: { sensor: true, binary_sensor: true } },
    { minApiLevel: 20210413, apiPath: '/state', extendedStatePath: '/state/extended', supports: { sensor: true, binary_sensor: true } },
    { minApiLevel: 20220114, apiPath: '/state', extendedStatePath: '/state/extended', supports: { sensor: true, binary_sensor: true } },
    { minApiLevel: 20230606, apiPath: '/state', extendedStatePath: '/state/extended', supports: { sensor: true, binary_sensor: true } },
  ],
  shutterBox: [
    { minApiLevel: 20180604, apiPath: '/api/shutter/state', extendedStatePath: '/api/shutter/extended/state', supports: { cover: true } },
  ],
  switchBox: [
    { minApiLevel: 20180604, apiPath: '/api/relay/state', supports: { switch: true } },
    { minApiLevel: 20190808, apiPath: '/api/relay/extended/state', extendedStatePath: '/api/relay/extended/state', supports: { switch: true, sensor: true } },
    { minApiLevel: 20200229, apiPath: '/state/extended', extendedStatePath: '/state/extended', supports: { switch: true, sensor: true } },
    { minApiLevel: 20200831, apiPath: '/state/extended', extendedStatePath: '/state/extended', supports: { switch: true, sensor: true } },
    { minApiLevel: 20220114, apiPath: '/state/extended', extendedStatePath: '/state/extended', supports: { switch: true, sensor: true } },
  ],
  switchBoxD: [
    { minApiLevel: 20190808, apiPath: '/api/relay/extended/state', extendedStatePath: '/api/relay/extended/state', supports: { switch: true, sensor: true } },
    { minApiLevel: 20200229, apiPath: '/state/extended', extendedStatePath: '/state/extended', supports: { switch: true, sensor: true } },
    { minApiLevel: 20200831, apiPath: '/state/extended', extendedStatePath: '/state/extended', supports: { switch: true, sensor: true } },
  ],
  tempSensor: [
    { minApiLevel: 20180604, apiPath: '/api/tempsensor/state', supports: { sensor: true } },
  ],
  wLightBox: [
    { minApiLevel: 20151206, apiPath: '/api/device/state', supports: { light: true } },
    { minApiLevel: 20190808, apiPath: '/api/rgbw/state', extendedStatePath: '/api/rgbw/extended/state', supports: { light: true } },
    { minApiLevel: 20200229, apiPath: '/api/rgbw/state', extendedStatePath: '/api/rgbw/extended/state', supports: { light: true } },
  ],
  wLightBoxS: [
    { minApiLevel: 20151206, apiPath: '/api/device/state', supports: { light: true } },
    { minApiLevel: 20180718, apiPath: '/api/light/state', supports: { light: true } },
    { minApiLevel: 20200229, apiPath: '/api/rgbw/state', extendedStatePath: '/api/rgbw/extended/state', supports: { light: true } },
  ],
};

export class BleboxClient {
  constructor(private readonly config: IBleboxConfig) {}

  public async getSnapshot(): Promise<IBleboxSnapshot> {
    if (this.config.snapshot) {
      return this.config.snapshot;
    }
    return this.identifyAndReadState();
  }

  public async identifyAndReadState(): Promise<IBleboxSnapshot> {
    const rawInfo = await this.fetchInitialInfo();
    const device = this.extractDeviceInfo(rawInfo);
    const profile = resolveBleboxProductProfile(device);

    let state = rawInfo;
    if (profile.apiPath !== '/api/device/state') {
      state = await this.requestJson(profile.apiPath);
    }

    let extendedState: TBleboxJsonValue | undefined;
    if (profile.extendedStatePath) {
      try {
        extendedState = await this.requestJson(profile.extendedStatePath);
      } catch {
        extendedState = undefined;
      }
    }

    return {
      device,
      state,
      extendedState,
      rawInfo,
      host: this.config.host,
      port: this.config.port || defaultPort,
      profile,
    };
  }

  public async setSwitchState(unitIdArg: number, onArg: boolean, snapshotArg?: IBleboxSnapshot): Promise<void> {
    const snapshot = snapshotArg ?? await this.getSnapshot();
    const productType = normalizedProductType(snapshot.device);
    const apiLevel = numericApiLevel(snapshot.device);
    let path: string;

    if (productType === 'switchBoxD' || productType === 'switchBox' && apiLevel >= 20220114) {
      path = `/s/${unitIdArg}/${onArg ? 1 : 0}`;
    } else {
      path = `/s/${onArg ? 1 : 0}`;
    }

    await this.commandGet(path);
  }

  public async setLightState(patchArg: IBleboxLightStatePatch, snapshotArg?: IBleboxSnapshot): Promise<void> {
    const snapshot = snapshotArg ?? await this.getSnapshot();
    const productType = normalizedProductType(snapshot.device);

    if (productType === 'dimmerBox') {
      const brightness = patchArg.on ? patchArg.brightness ?? 255 : 0;
      this.assertByte(brightness, 'brightness');
      await this.requestJson('/api/dimmer/set', {
        method: 'POST',
        body: { dimmer: { desiredBrightness: brightness } },
      });
      return;
    }

    const desiredColor = patchArg.on ? this.desiredLightColor(snapshot, patchArg) : this.offLightColor(snapshot);
    const usesLegacyLightPath = productType === 'wLightBoxS' && numericApiLevel(snapshot.device) < 20200229;
    await this.requestJson(usesLegacyLightPath ? '/api/light/set' : '/api/rgbw/set', {
      method: 'POST',
      body: usesLegacyLightPath ? { light: { desiredColor } } : { rgbw: { desiredColor } },
    });

    if (patchArg.on && patchArg.effect !== undefined) {
      const effectIndex = this.effectIndex(snapshot, patchArg.effect);
      await this.commandGet(`/s/x/${effectIndex}`);
    }
  }

  public async runCoverCommand(commandArg: IBleboxCoverCommandRequest, snapshotArg?: IBleboxSnapshot): Promise<void> {
    const snapshot = snapshotArg ?? await this.getSnapshot();
    const productType = normalizedProductType(snapshot.device);
    const path = this.coverCommandPath(productType, commandArg);
    await this.commandGet(path);
  }

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

  private async fetchInitialInfo(): Promise<TBleboxJsonValue> {
    try {
      return await this.requestJson('/api/device/state');
    } catch {
      return this.requestJson('/info');
    }
  }

  private async commandGet(pathArg: string): Promise<void> {
    await this.requestJson(pathArg);
  }

  private async requestJson(pathArg: string, optionsArg: { method?: 'GET' | 'POST'; body?: unknown } = {}): Promise<TBleboxJsonValue> {
    if (!this.config.host) {
      throw new Error('BleBox host is required when fixture snapshot data is not provided.');
    }

    const method = optionsArg.method || 'GET';
    const headers: Record<string, string> = {};
    let body: string | undefined;
    if (optionsArg.body !== undefined) {
      headers['content-type'] = 'application/json';
      body = typeof optionsArg.body === 'string' ? optionsArg.body : JSON.stringify(optionsArg.body);
    }
    if (this.config.username && this.config.password) {
      headers.authorization = `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`;
    }

    const response = await globalThis.fetch(`${this.baseUrl()}${pathArg}`, {
      method,
      headers,
      body,
      signal: AbortSignal.timeout(this.config.timeoutMs || defaultSetupTimeoutMs),
    });
    if (!response.ok) {
      const text = await response.text();
      throw new Error(`BleBox request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    return response.json() as Promise<TBleboxJsonValue>;
  }

  private extractDeviceInfo(valueArg: TBleboxJsonValue): IBleboxDeviceInfo {
    const root = asRecord(valueArg);
    if (!root) {
      throw new Error('BleBox device response is not a JSON object.');
    }
    const device = asRecord(root.device) || root;
    return device as unknown as IBleboxDeviceInfo;
  }

  private desiredLightColor(snapshotArg: IBleboxSnapshot, patchArg: IBleboxLightStatePatch): string {
    if (patchArg.rgbwwColor) {
      patchArg.rgbwwColor.forEach((valueArg, indexArg) => this.assertByte(valueArg, `rgbwwColor[${indexArg}]`));
      return patchArg.rgbwwColor.map((valueArg) => valueArg.toString(16).padStart(2, '0')).join('');
    }
    if (patchArg.rgbwColor) {
      patchArg.rgbwColor.forEach((valueArg, indexArg) => this.assertByte(valueArg, `rgbwColor[${indexArg}]`));
      return patchArg.rgbwColor.map((valueArg) => valueArg.toString(16).padStart(2, '0')).join('');
    }
    if (patchArg.rgbColor) {
      patchArg.rgbColor.forEach((valueArg, indexArg) => this.assertByte(valueArg, `rgbColor[${indexArg}]`));
      const current = this.currentDesiredColor(snapshotArg);
      const white = current.length >= 8 ? current.slice(6, 8) : '';
      return `${patchArg.rgbColor.map((valueArg) => valueArg.toString(16).padStart(2, '0')).join('')}${white}`;
    }
    if (patchArg.colorTempKelvin !== undefined) {
      const brightness = patchArg.brightness ?? this.lightBrightness(snapshotArg) ?? 255;
      this.assertByte(brightness, 'brightness');
      const mired = Math.round(1000000 / patchArg.colorTempKelvin);
      const normalized = Math.max(0, Math.min(255, Math.round((mired - 154) * 255 / (370 - 154))));
      const cold = normalized < 128 ? 255 : Math.max(0, Math.min(255, (255 - normalized) * 2));
      const warm = normalized < 128 ? Math.min(255, normalized * 2) : 255;
      return [warm, cold].map((valueArg) => Math.round(valueArg * brightness / 255).toString(16).padStart(2, '0')).join('');
    }
    if (patchArg.brightness !== undefined) {
      this.assertByte(patchArg.brightness, 'brightness');
      const current = this.currentDesiredColor(snapshotArg);
      if (current.length <= 2) {
        return patchArg.brightness.toString(16).padStart(2, '0');
      }
      const channels = current.match(/../g)?.map((valueArg) => Number.parseInt(valueArg, 16)) || [255, 255, 255, 255];
      const max = Math.max(...channels, 1);
      return channels.map((valueArg) => Math.round(valueArg * patchArg.brightness! / max).toString(16).padStart(2, '0')).join('');
    }

    const lastOn = this.currentLastOnColor(snapshotArg);
    if (lastOn && Number.parseInt(lastOn, 16) > 0) {
      return lastOn;
    }
    const current = this.currentDesiredColor(snapshotArg);
    return 'f'.repeat(Math.max(current.length, normalizedProductType(snapshotArg.device) === 'wLightBoxS' ? 2 : 8));
  }

  private offLightColor(snapshotArg: IBleboxSnapshot): string {
    return '0'.repeat(Math.max(this.currentDesiredColor(snapshotArg).length, normalizedProductType(snapshotArg.device) === 'wLightBoxS' ? 2 : 8));
  }

  private currentDesiredColor(snapshotArg: IBleboxSnapshot): string {
    const colorState = this.lightStateRecord(snapshotArg);
    const desired = colorState?.desiredColor;
    return typeof desired === 'string' && desired.length > 0 ? desired.replace(/-/g, '0') : normalizedProductType(snapshotArg.device) === 'wLightBoxS' ? 'ff' : 'ffffffff';
  }

  private currentLastOnColor(snapshotArg: IBleboxSnapshot): string | undefined {
    const lastOn = this.lightStateRecord(snapshotArg)?.lastOnColor;
    return typeof lastOn === 'string' ? lastOn.replace(/-/g, '0') : undefined;
  }

  private lightBrightness(snapshotArg: IBleboxSnapshot): number | undefined {
    const desired = this.currentDesiredColor(snapshotArg);
    const channels = desired.match(/../g)?.map((valueArg) => Number.parseInt(valueArg, 16)) || [];
    return channels.length ? Math.max(...channels) : undefined;
  }

  private lightStateRecord(snapshotArg: IBleboxSnapshot): Record<string, TBleboxJsonValue | undefined> | undefined {
    const extended = asRecord(snapshotArg.extendedState);
    const state = asRecord(snapshotArg.state);
    return asRecord(extended?.rgbw) || asRecord(state?.rgbw) || asRecord(state?.light) || asRecord(extended?.light);
  }

  private effectIndex(snapshotArg: IBleboxSnapshot, effectArg: string | number): number {
    if (typeof effectArg === 'number') {
      if (!Number.isInteger(effectArg) || effectArg < 0) {
        throw new Error('BleBox light effect must be a non-negative integer.');
      }
      return effectArg;
    }
    const effectNames = asRecord(this.lightStateRecord(snapshotArg)?.effectsNames);
    if (!effectNames) {
      throw new Error('BleBox light effects are not available for this snapshot.');
    }
    for (const [key, value] of Object.entries(effectNames)) {
      if (value === effectArg) {
        return Number(key);
      }
    }
    throw new Error(`BleBox light effect is not supported: ${effectArg}`);
  }

  private coverCommandPath(productTypeArg: string, commandArg: IBleboxCoverCommandRequest): string {
    if (commandArg.command === 'set_cover_position') {
      this.assertPercent(commandArg.position, 'position');
    }
    if (commandArg.command === 'set_cover_tilt_position') {
      this.assertPercent(commandArg.tiltPosition, 'tiltPosition');
    }

    if (productTypeArg === 'shutterBox') {
      if (commandArg.command === 'open_cover') return '/s/u';
      if (commandArg.command === 'close_cover') return '/s/d';
      if (commandArg.command === 'stop_cover') return '/s/s';
      if (commandArg.command === 'set_cover_position') return `/s/p/${100 - commandArg.position!}`;
      if (commandArg.command === 'open_cover_tilt') return '/s/t/0';
      if (commandArg.command === 'close_cover_tilt') return '/s/t/100';
      if (commandArg.command === 'set_cover_tilt_position') return `/s/t/${100 - commandArg.tiltPosition!}`;
    }

    if (productTypeArg === 'gateController') {
      if (commandArg.command === 'open_cover') return '/s/o';
      if (commandArg.command === 'close_cover') return '/s/c';
      if (commandArg.command === 'stop_cover') return '/s/s';
      if (commandArg.command === 'set_cover_position') return `/s/p/${100 - commandArg.position!}`;
    }

    if (productTypeArg === 'gateBox') {
      if (commandArg.command === 'open_cover' || commandArg.command === 'close_cover') return '/s/p';
      if (commandArg.command === 'stop_cover') return '/s/s';
    }

    throw new Error(`Unsupported BleBox cover command for ${productTypeArg}: ${commandArg.command}`);
  }

  private assertByte(valueArg: number, nameArg: string): void {
    if (!Number.isInteger(valueArg) || valueArg < 0 || valueArg > 255) {
      throw new Error(`BleBox ${nameArg} must be an integer between 0 and 255.`);
    }
  }

  private assertPercent(valueArg: number | undefined, nameArg: string): void {
    if (!Number.isInteger(valueArg) || valueArg! < 0 || valueArg! > 100) {
      throw new Error(`BleBox ${nameArg} must be an integer between 0 and 100.`);
    }
  }

  private baseUrl(): string {
    const port = this.config.port && this.config.port !== defaultPort ? `:${this.config.port}` : '';
    return `${this.config.protocol || 'http'}://${this.config.host}${port}`;
  }
}

export const resolveBleboxProductProfile = (deviceArg: IBleboxDeviceInfo): IBleboxProductProfile => {
  const productType = normalizedProductType(deviceArg);
  const apiLevel = numericApiLevel(deviceArg);
  const templates = profileTemplates[productType] || [];
  const template = [...templates].sort((leftArg, rightArg) => rightArg.minApiLevel - leftArg.minApiLevel)
    .find((entryArg) => apiLevel >= entryArg.minApiLevel);
  return {
    productType,
    model: productType,
    apiLevel,
    apiPath: template?.apiPath || '/api/device/state',
    extendedStatePath: template?.extendedStatePath,
    supports: template?.supports || {},
  };
};

export const normalizedProductType = (deviceArg: IBleboxDeviceInfo): string => {
  const type = String(deviceArg.type || deviceArg.product || 'blebox');
  const product = String(deviceArg.product || type);
  return type === 'wLightBox' && product === 'wLightBoxS' ? 'wLightBoxS' : type;
};

export const numericApiLevel = (deviceArg: IBleboxDeviceInfo): number => {
  const value = Number(deviceArg.apiLevel ?? defaultApiLevel);
  return Number.isFinite(value) ? value : defaultApiLevel;
};

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