import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult, type TEntityPlatform } from '../../core/index.js';
import { LookinMapper } from './lookin.mapper.js';
import type { ILookinConfig, ILookinSnapshot } from './lookin.types.js';
import { lookinProfile } from './lookin.types.js';

export class LookinClient extends SimpleLocalClient<ILookinConfig> {
  private nativeSnapshot?: ILookinSnapshot;

  constructor(private readonly configArg: ILookinConfig) {
    super(lookinProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ILookinSnapshot> {
    if (!forceRefreshArg && this.nativeSnapshot) {
      return clone(this.nativeSnapshot);
    }

    if (this.shouldUseNativeHttp(forceRefreshArg)) {
      try {
        this.nativeSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.nativeSnapshot = LookinMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
      return clone(this.nativeSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.nativeSnapshot = snapshot.rawData === undefined && snapshot.entities.length
      ? LookinMapper.toSnapshot({ config: { ...this.configArg, snapshot }, online: snapshot.online, source: snapshot.source, error: snapshot.error })
      : LookinMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return clone(this.nativeSnapshot);
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.endpoint()) {
      return super.execute(requestArg);
    }

    try {
      const data = await this.executeNativeHttp(requestArg);
      this.nativeSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private shouldUseNativeHttp(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    return true;
  }

  private async fetchSnapshot(): Promise<ILookinSnapshot> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('LOOKin local HTTP snapshots require config.host or config.url.');
    }

    const device = normalizeDeviceInfo(await this.requestJson<Record<string, unknown>>('/device'));
    const devices = await this.requestJson<Array<Record<string, unknown>>>('/data');
    if (!Array.isArray(devices)) {
      throw new Error('LOOKin /data response must be a device array.');
    }

    const remotes: Record<string, unknown>[] = [];
    for (const deviceSummary of devices) {
      const uuid = stringValue(deviceSummary.UUID);
      if (!uuid || !platformForType(stringValue(deviceSummary.Type))) {
        continue;
      }
      remotes.push({ ...deviceSummary, ...(await this.requestJson<Record<string, unknown>>(`/data/${encodeURIComponent(uuid)}`)), UUID: uuid });
    }

    const errors: Record<string, string> = {};
    let meteo: Record<string, unknown> | undefined;
    if (device.model >= 2) {
      try {
        meteo = await this.requestJson<Record<string, unknown>>('/sensors/meteo');
      } catch (errorArg) {
        errors.meteo = errorMessage(errorArg);
      }
    }

    const rawData = { device: device.raw, devices, remotes, meteo, errors: Object.keys(errors).length ? errors : undefined };
    const entities = entitiesFromLookinData(remotes, meteo, device.raw);
    const snapshot: ILookinSnapshot = {
      device: {
        id: this.configArg.uniqueId || device.id,
        name: this.configArg.name || device.name || lookinProfile.defaultName,
        manufacturer: 'LOOKin',
        model: device.modelName,
        serialNumber: device.id,
        host: endpoint.host,
        port: endpoint.port,
        protocol: endpoint.protocol,
        configurationUrl: `${endpoint.baseUrl}/device`,
        attributes: {
          firmware: stringValue(device.raw.Firmware),
          status: stringValue(device.raw.Status),
          modelIndex: device.model,
          mrdc: stringValue(device.raw.MRDC),
        },
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData,
      capabilities: {
        localRead: true,
        localControl: true,
        udpPush: false,
      },
    };

    return LookinMapper.toSnapshot({ config: this.configArg, rawData: snapshot, online: true, source: 'http' });
  }

  private async executeNativeHttp(requestArg: IServiceCallRequest): Promise<Record<string, unknown>> {
    const snapshot = await this.getSnapshot();
    const remote = selectRemote(snapshot, requestArg);
    const uuid = stringValue(remote.UUID);
    if (!uuid) {
      throw new Error('LOOKin command target does not include a UUID.');
    }

    if (requestArg.service === 'set_hvac_mode' || requestArg.service === 'set_temperature' || requestArg.service === 'set_fan_mode' || requestArg.service === 'set_swing_mode' || (requestArg.service === 'turn_on' && platformForType(stringValue(remote.Type)) === 'climate') || (requestArg.service === 'turn_off' && platformForType(stringValue(remote.Type)) === 'climate')) {
      const status = climateStatusForService(remote, requestArg);
      await this.requestOk(`/commands/ir/localremote/${uuid}${status}`);
      return { uuid, status };
    }

    if (requestArg.service === 'select_source') {
      const signal = mediaSourceSignal(requestArg.data);
      await this.sendRemoteCommand(uuid, 'mode', signal);
      return { uuid, command: 'mode', signal };
    }

    const command = remoteCommandForService(requestArg.service, remote);
    if (!command) {
      throw new Error(`Unsupported LOOKin service: ${requestArg.domain}.${requestArg.service}`);
    }
    await this.sendRemoteCommand(uuid, command, 'FF');
    return { uuid, command, signal: 'FF' };
  }

  private async sendRemoteCommand(uuidArg: string, commandArg: string, signalArg: string): Promise<void> {
    const code = commandToCode[commandArg];
    if (!code) {
      throw new Error(`${commandArg} is not a documented LOOKin IR command.`);
    }
    await this.requestOk(`/commands/ir/localremote/${uuidArg}${code}${signalArg}`);
  }

  private async requestJson<TValue>(pathArg: string): Promise<TValue> {
    const response = await this.request(pathArg);
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`LOOKin HTTP ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new Error(`LOOKin HTTP ${pathArg} returned invalid JSON: ${errorMessage(errorArg)}`);
    }
  }

  private async requestOk(pathArg: string): Promise<void> {
    const response = await this.request(pathArg);
    const text = await response.text();
    if (![200, 201, 204].includes(response.status)) {
      throw new Error(`LOOKin HTTP ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
    }
  }

  private async request(pathArg: string): Promise<Response> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('LOOKin local HTTP requests require config.host or config.url.');
    }
    const timeoutMs = this.configArg.timeoutMs || 9000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      return await globalThis.fetch(`${endpoint.baseUrl}${pathArg}`, {
        method: 'GET',
        headers: { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' },
        signal: controller.signal,
      });
    } finally {
      clearTimeout(timer);
    }
  }

  private endpoint(): ILookinEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    if (rawUrl) {
      const parsed = parseEndpoint(rawUrl, this.configArg.useTls);
      if (parsed) {
        return parsed;
      }
    }

    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const parsedHost = parseEndpoint(host, this.configArg.useTls);
    if (parsedHost) {
      return this.configArg.port ? { ...parsedHost, port: this.configArg.port, baseUrl: `${parsedHost.protocol}://${parsedHost.host}${portPart(parsedHost.protocol, this.configArg.port)}` } : parsedHost;
    }

    const protocol = this.configArg.useTls ? 'https' : 'http';
    const port = this.configArg.port;
    return { protocol, host, port, baseUrl: `${protocol}://${host}${portPart(protocol, port)}` };
  }
}

interface ILookinEndpoint {
  protocol: 'http' | 'https';
  host: string;
  port?: number;
  baseUrl: string;
}

interface INormalizedLookinDevice {
  id: string;
  name?: string;
  model: number;
  modelName: string;
  raw: Record<string, unknown>;
}

const lookinModelNames = ['LOOKin Remote', 'LOOKin Remote', 'LOOKin Remote2'];
const typeToPlatform: Record<string, TEntityPlatform> = {
  '01': 'media_player',
  '02': 'media_player',
  '03': 'light',
  EF: 'climate',
};
const codeToName: Record<string, string> = {
  '01': 'Tv',
  '02': 'Media',
  '03': 'Light',
  EF: 'Climate Control',
};
const commandToCode: Record<string, string> = {
  power: '01',
  poweron: '02',
  poweroff: '03',
  mode: '04',
  mute: '05',
  volup: '06',
  voldown: '07',
  chup: '08',
  chdown: '09',
  swing: '0A',
  speed: '0B',
  cursor: '0C',
  menu: '0D',
};
const hvacModes = ['off', 'auto', 'cool', 'heat', 'dry', 'fan_only'];
const fanModes = ['auto', 'low', 'medium', 'high'];
const swingModes = ['off', 'both'];

const normalizeDeviceInfo = (rawArg: Record<string, unknown>): INormalizedLookinDevice => {
  const id = stringValue(rawArg.ID)?.toUpperCase();
  if (!id) {
    throw new Error('LOOKin /device response does not include ID.');
  }
  const model = Number.parseInt((stringValue(rawArg.MRDC) || '00').slice(0, 2), 16);
  return {
    id,
    name: stringValue(rawArg.Name),
    model,
    modelName: lookinModelNames[model] || 'LOOKin Remote',
    raw: rawArg,
  };
};

const entitiesFromLookinData = (remotesArg: Record<string, unknown>[], meteoArg: Record<string, unknown> | undefined, deviceArg: Record<string, unknown>): ISimpleLocalEntitySnapshot[] => {
  const entities: ISimpleLocalEntitySnapshot[] = [{
    id: 'hub_status',
    name: 'Hub Status',
    platform: 'sensor',
    state: stringValue(deviceArg.Status) || 'online',
    available: true,
    attributes: { device: deviceArg },
  }];

  if (meteoArg) {
    entities.push({ id: 'temperature', name: 'Temperature', platform: 'sensor', state: numberValue(meteoArg.Temperature), unit: 'C', deviceClass: 'temperature', stateClass: 'measurement', available: true });
    entities.push({ id: 'humidity', name: 'Humidity', platform: 'sensor', state: numberValue(meteoArg.Humidity), unit: '%', deviceClass: 'humidity', stateClass: 'measurement', available: true });
  }

  for (const remote of remotesArg) {
    const uuid = stringValue(remote.UUID);
    const platform = platformForType(stringValue(remote.Type));
    if (!uuid || !platform) {
      continue;
    }
    entities.push(entityFromRemote(remote, uuid, platform));
  }

  return entities;
};

const entityFromRemote = (remoteArg: Record<string, unknown>, uuidArg: string, platformArg: TEntityPlatform): ISimpleLocalEntitySnapshot => {
  const status = remoteStatus(remoteArg);
  const baseAttributes = {
    uuid: uuidArg,
    type: stringValue(remoteArg.Type),
    deviceType: codeToName[stringValue(remoteArg.Type) || ''] || 'Unknown',
    status,
    lastStatus: stringValue(remoteArg.LastStatus),
    functions: functionNames(remoteArg),
  };

  if (platformArg === 'light') {
    return { id: uuidArg, uniqueId: `lookin_${uuidArg}`, name: stringValue(remoteArg.Name) || uuidArg, platform: 'light', state: status.charAt(0) === '1', writable: true, available: true, attributes: baseAttributes };
  }
  if (platformArg === 'media_player') {
    return { id: uuidArg, uniqueId: `lookin_${uuidArg}`, name: stringValue(remoteArg.Name) || uuidArg, platform: 'media_player', state: status.charAt(0) === '1' ? 'on' : 'off', writable: true, available: true, attributes: { ...baseAttributes, muted: status.charAt(2) === '0' } };
  }
  if (platformArg === 'climate') {
    const climate = climateFromStatus(status);
    return { id: uuidArg, uniqueId: `lookin_${uuidArg}`, name: stringValue(remoteArg.Name) || uuidArg, platform: 'climate', state: climate.hvacMode, writable: true, available: true, attributes: { ...baseAttributes, targetTemperature: climate.temperature, fanMode: climate.fanMode, swingMode: climate.swingMode } };
  }
  return { id: uuidArg, uniqueId: `lookin_${uuidArg}`, name: stringValue(remoteArg.Name) || uuidArg, platform: 'sensor', state: status, available: true, attributes: baseAttributes };
};

const selectRemote = (snapshotArg: ILookinSnapshot, requestArg: IServiceCallRequest): Record<string, unknown> => {
  const rawData = snapshotArg.rawData && typeof snapshotArg.rawData === 'object' ? snapshotArg.rawData as { remotes?: Record<string, unknown>[] } : {};
  const remotes = rawData.remotes || [];
  const data = requestArg.data || {};
  const requestedUuid = stringValue(data.uuid) || stringValue(data.UUID) || uuidFromEntityTarget(snapshotArg, requestArg);
  if (requestedUuid) {
    const remote = remotes.find((remoteArg) => stringValue(remoteArg.UUID) === requestedUuid);
    if (remote) {
      return remote;
    }
    throw new Error(`LOOKin command target UUID not found: ${requestedUuid}`);
  }

  const desiredPlatform = platformForService(requestArg);
  const candidates = desiredPlatform ? remotes.filter((remoteArg) => platformForType(stringValue(remoteArg.Type)) === desiredPlatform) : remotes;
  if (candidates.length === 1) {
    return candidates[0];
  }
  throw new Error(`LOOKin command ${requestArg.service} requires data.uuid or a single ${desiredPlatform || 'remote'} target.`);
};

const uuidFromEntityTarget = (snapshotArg: ILookinSnapshot, requestArg: IServiceCallRequest): string | undefined => {
  const target = requestArg.target.entityId || requestArg.target.deviceId;
  if (!target) {
    return undefined;
  }
  const entity = snapshotArg.entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || target.endsWith(entityArg.id || ''));
  return stringValue(entity?.attributes?.uuid);
};

const platformForService = (requestArg: IServiceCallRequest): TEntityPlatform | undefined => {
  if (requestArg.domain === 'light' || requestArg.domain === 'media_player' || requestArg.domain === 'climate') {
    return requestArg.domain;
  }
  if (requestArg.service.startsWith('set_')) {
    return 'climate';
  }
  if (requestArg.service.startsWith('volume_') || requestArg.service.startsWith('media_') || requestArg.service === 'select_source') {
    return 'media_player';
  }
  return undefined;
};

const remoteCommandForService = (serviceArg: string, remoteArg: Record<string, unknown>): string | undefined => {
  if (serviceArg === 'turn_on') {
    return functionNames(remoteArg).includes('poweron') ? 'poweron' : 'power';
  }
  if (serviceArg === 'turn_off') {
    return functionNames(remoteArg).includes('poweroff') ? 'poweroff' : 'power';
  }
  const serviceToCommand: Record<string, string> = {
    toggle: 'power',
    volume_up: 'volup',
    volume_down: 'voldown',
    volume_mute: 'mute',
    media_next_track: 'chup',
    media_previous_track: 'chdown',
  };
  return serviceToCommand[serviceArg];
};

const climateStatusForService = (remoteArg: Record<string, unknown>, requestArg: IServiceCallRequest): string => {
  const status = remoteStatus(remoteArg);
  const current = climateFromStatus(status);
  let hvacModeIndex = current.hvacModeIndex;
  let temperature = current.temperature;
  let fanModeIndex = current.fanModeIndex;
  let swingModeIndex = current.swingModeIndex;
  const data = requestArg.data || {};

  if (requestArg.service === 'turn_off') {
    hvacModeIndex = 0;
  } else if (requestArg.service === 'turn_on') {
    hvacModeIndex = hvacModeIndex === 0 ? 1 : hvacModeIndex;
  } else if (requestArg.service === 'set_hvac_mode') {
    hvacModeIndex = modeIndex(hvacModes, stringValue(data.hvac_mode) || stringValue(data.hvacMode), 'HVAC mode');
  } else if (requestArg.service === 'set_temperature') {
    const requestedTemperature = numberValue(data.temperature);
    if (requestedTemperature === undefined) {
      throw new Error('LOOKin set_temperature requires data.temperature.');
    }
    temperature = requestedTemperature;
    if (stringValue(data.hvac_mode) || stringValue(data.hvacMode)) {
      hvacModeIndex = modeIndex(hvacModes, stringValue(data.hvac_mode) || stringValue(data.hvacMode), 'HVAC mode');
    } else if (hvacModeIndex === 0) {
      hvacModeIndex = 1;
    }
  } else if (requestArg.service === 'set_fan_mode') {
    fanModeIndex = modeIndex(fanModes, stringValue(data.fan_mode) || stringValue(data.fanMode), 'fan mode');
  } else if (requestArg.service === 'set_swing_mode') {
    swingModeIndex = modeIndex(swingModes, stringValue(data.swing_mode) || stringValue(data.swingMode), 'swing mode');
  }

  if (temperature < 16 || temperature > 30) {
    throw new Error('LOOKin climate temperature must be between 16 and 30 C.');
  }
  return `${hvacModeIndex}${temperature.toString(16).toUpperCase().slice(-1)}${fanModeIndex}${swingModeIndex}`;
};

const climateFromStatus = (statusArg: string): { hvacModeIndex: number; hvacMode: string; temperature: number; fanModeIndex: number; fanMode: string; swingModeIndex: number; swingMode: string } => {
  const status = statusArg.length === 4 ? statusArg : '0000';
  const hvacModeIndex = numberFromChar(status.charAt(0), 0);
  const fanModeIndex = numberFromChar(status.charAt(2), 0);
  const swingModeIndex = numberFromChar(status.charAt(3), 0);
  return {
    hvacModeIndex,
    hvacMode: hvacModes[hvacModeIndex] || 'off',
    temperature: Number.parseInt(status.charAt(1) || '0', 16) + 16,
    fanModeIndex,
    fanMode: fanModes[fanModeIndex] || 'auto',
    swingModeIndex,
    swingMode: swingModes[swingModeIndex] || 'off',
  };
};

const mediaSourceSignal = (dataArg: Record<string, unknown> | undefined): string => {
  const signal = stringValue(dataArg?.signal);
  if (signal) {
    return signal.toUpperCase();
  }
  const source = stringValue(dataArg?.source) || stringValue(dataArg?.source_id) || stringValue(dataArg?.sourceId);
  const inputMatch = /^INPUT_(\d+)$/i.exec(source || '');
  if (inputMatch) {
    return Number(inputMatch[1]).toString(16).padStart(2, '0').toUpperCase();
  }
  if (source && /^[0-9a-f]{2}$/i.test(source)) {
    return source.toUpperCase();
  }
  throw new Error('LOOKin select_source requires data.signal, data.source as INPUT_<n>, or a two-digit hex source.');
};

const functionNames = (remoteArg: Record<string, unknown>): string[] => Array.isArray(remoteArg.Functions)
  ? remoteArg.Functions.map((functionArg) => functionArg && typeof functionArg === 'object' ? stringValue((functionArg as Record<string, unknown>).Name) : undefined).filter((valueArg): valueArg is string => Boolean(valueArg))
  : [];

const remoteStatus = (remoteArg: Record<string, unknown>): string => stringValue(remoteArg.Status) || (stringValue(remoteArg.LastStatus) ? `0${stringValue(remoteArg.LastStatus)!.slice(1)}` : '0000');

const platformForType = (typeArg: string | undefined): TEntityPlatform | undefined => typeArg ? typeToPlatform[typeArg.toUpperCase()] : undefined;

const modeIndex = (modesArg: string[], valueArg: string | undefined, labelArg: string): number => {
  const index = valueArg ? modesArg.indexOf(valueArg) : -1;
  if (index < 0) {
    throw new Error(`Unsupported LOOKin ${labelArg}: ${valueArg || '(missing)'}`);
  }
  return index;
};

const parseEndpoint = (valueArg: string, useTlsArg?: boolean): ILookinEndpoint | undefined => {
  try {
    const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg) ? valueArg : `${useTlsArg ? 'https' : 'http'}://${valueArg}`);
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = url.port ? Number(url.port) : undefined;
    return { protocol, host: url.hostname, port, baseUrl: `${protocol}://${url.hostname}${portPart(protocol, port)}` };
  } catch {
    return undefined;
  }
};

const portPart = (protocolArg: 'http' | 'https', portArg: number | undefined): string => portArg && !((protocolArg === 'https' && portArg === 443) || (protocolArg === 'http' && portArg === 80)) ? `:${portArg}` : '';

const numberFromChar = (valueArg: string, fallbackArg: number): number => /^\d$/.test(valueArg) ? Number(valueArg) : fallbackArg;

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

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

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;
