import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { INoboHubApiData, INoboHubConfig } from './nobo_hub.types.js';
import { noboHubDefaultPort, noboHubProfile } from './nobo_hub.types.js';

const overrideModeNames: Record<string, string> = {
  '0': 'normal',
  '1': 'comfort',
  '2': 'eco',
  '3': 'away',
};

export class NoboHubMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<INoboHubConfig>, 'profile'>): ISimpleLocalSnapshot {
    const rawData = optionsArg.rawData ?? optionsArg.config.rawData;
    const apiData = this.apiData(rawData);
    if (apiData) {
      return this.snapshotFromApiData(optionsArg.config, apiData, optionsArg.online ?? true, optionsArg.source || 'tcp', optionsArg.error);
    }
    return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: noboHubProfile });
  }

  public static toSnapshotFromRaw(configArg: INoboHubConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
    return this.toSnapshot({ config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
  }

  public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
    return SimpleLocalMapper.toDevices(noboHubProfile, snapshotArg);
  }

  public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
    return SimpleLocalMapper.toEntities(noboHubProfile, snapshotArg);
  }

  public static slug(valueArg: unknown): string {
    return SimpleLocalMapper.slug(valueArg);
  }

  private static snapshotFromApiData(configArg: INoboHubConfig, rawDataArg: INoboHubApiData, onlineArg: boolean, sourceArg: ISimpleLocalSnapshot['source'], errorArg?: string): ISimpleLocalSnapshot {
    const hubInfo = this.record(rawDataArg.hubInfo);
    const zones = this.recordMap(rawDataArg.zones);
    const components = this.recordMap(rawDataArg.components);
    const weekProfiles = this.recordMap(rawDataArg.weekProfiles);
    const overrides = this.recordMap(rawDataArg.overrides);
    const temperatures = this.record(rawDataArg.temperatures);
    const serial = this.stringValue(hubInfo.serial) || configArg.serial || configArg.uniqueId || configArg.deviceId;
    const host = this.endpointHost(configArg);
    const port = this.endpointPort(configArg);
    const entities: ISimpleLocalEntitySnapshot[] = [];
    const globalOverride = Object.values(overrides).find((overrideArg) => this.stringValue(overrideArg.target_type) === '0' && this.stringValue(overrideArg.mode) !== '0');

    entities.push({
      id: 'global_override',
      name: 'Global Override',
      platform: 'select',
      state: overrideModeNames[this.stringValue(globalOverride?.mode) || '0'] || 'normal',
      available: onlineArg,
      writable: true,
      attributes: { options: ['normal', 'comfort', 'eco', 'away'] },
    });

    for (const zone of Object.values(zones)) {
      const zoneId = this.stringValue(zone.zone_id);
      if (!zoneId) {
        continue;
      }
      const zoneName = this.stringValue(zone.name) || `Zone ${zoneId}`;
      const zoneOverride = Object.values(overrides).find((overrideArg) => this.stringValue(overrideArg.target_type) === '1' && this.stringValue(overrideArg.target_id) === zoneId && this.stringValue(overrideArg.mode) !== '0');
      const override = zoneOverride || globalOverride;
      const preset = overrideModeNames[this.stringValue(override?.mode) || '0'] || 'normal';
      const temperature = this.zoneTemperature(zoneId, components, temperatures);
      const profileId = this.stringValue(zone.week_profile_id);
      const profile = profileId ? weekProfiles[profileId] : undefined;
      entities.push({
        id: `zone_${zoneId}_climate`,
        name: zoneName,
        platform: 'climate',
        state: preset === 'normal' ? 'auto' : 'heat',
        available: onlineArg,
        writable: true,
        attributes: this.cleanAttributes({
          zoneId,
          preset,
          currentTemperature: temperature,
          comfortTemperature: this.numberValue(zone.temp_comfort_c),
          ecoTemperature: this.numberValue(zone.temp_eco_c),
          weekProfileId: profileId,
          weekProfileName: this.stringValue(profile?.name),
          overrideAllowed: this.stringValue(zone.override_allowed) === '1',
        }),
      });
      entities.push({
        id: `zone_${zoneId}_profile`,
        name: `${zoneName} Week Profile`,
        platform: 'select',
        state: this.stringValue(profile?.name) || profileId || 'unknown',
        available: onlineArg,
        writable: true,
        attributes: this.cleanAttributes({
          zoneId,
          weekProfileId: profileId,
          options: Object.values(weekProfiles).map((profileArg) => this.stringValue(profileArg.name) || this.stringValue(profileArg.week_profile_id)).filter(Boolean),
        }),
      });
      if (temperature !== undefined) {
        entities.push({
          id: `zone_${zoneId}_temperature`,
          name: `${zoneName} Temperature`,
          platform: 'sensor',
          state: temperature,
          unit: '°C',
          available: onlineArg,
          writable: false,
          attributes: { deviceClass: 'temperature', stateClass: 'measurement', zoneId },
        });
      }
    }

    for (const component of Object.values(components)) {
      const serialNumber = this.stringValue(component.serial);
      const temperature = serialNumber ? this.temperatureValue(temperatures[serialNumber]) : undefined;
      if (!serialNumber || temperature === undefined) {
        continue;
      }
      entities.push({
        id: `component_${serialNumber}_temperature`,
        name: `${this.stringValue(component.name) || serialNumber} Temperature`,
        platform: 'sensor',
        state: temperature,
        unit: '°C',
        available: onlineArg,
        writable: false,
        attributes: { deviceClass: 'temperature', stateClass: 'measurement', componentSerial: serialNumber, zoneId: this.stringValue(component.zone_id) },
      });
    }

    return {
      device: {
        id: configArg.uniqueId || serial || (host ? `${host}:${port || noboHubDefaultPort}` : undefined) || noboHubProfile.domain,
        name: configArg.name || this.stringValue(hubInfo.name) || noboHubProfile.defaultName || noboHubProfile.displayName,
        manufacturer: 'Glen Dimplex Nordic AS',
        model: 'Nobø Ecohub',
        serialNumber: serial,
        host,
        port,
        protocol: 'tcp',
        configurationUrl: host ? `tcp://${host}:${port || noboHubDefaultPort}` : undefined,
        attributes: this.cleanAttributes({
          softwareVersion: this.stringValue(hubInfo.software_version),
          hardwareVersion: this.stringValue(hubInfo.hardware_version),
          productionDate: this.stringValue(hubInfo.production_date),
          zoneCount: Object.keys(zones).length,
          componentCount: Object.keys(components).length,
        }),
      },
      entities: entities.length ? entities : [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: onlineArg, available: true }],
      online: onlineArg,
      updatedAt: this.stringValue(rawDataArg.fetchedAt) || new Date().toISOString(),
      source: sourceArg,
      rawData: rawDataArg,
      error: errorArg,
      capabilities: {
        localRead: true,
        localControl: Boolean(configArg.host && configArg.serial),
        liveControlRequiresExecutor: false,
      },
    };
  }

  private static apiData(valueArg: unknown): INoboHubApiData | undefined {
    const value = this.record(valueArg);
    return value && ('hubInfo' in value || 'zones' in value || 'components' in value) ? value as INoboHubApiData : undefined;
  }

  private static zoneTemperature(zoneIdArg: string, componentsArg: Record<string, Record<string, unknown>>, temperaturesArg: Record<string, unknown>): number | undefined {
    for (const component of Object.values(componentsArg)) {
      const componentZoneId = this.stringValue(component.zone_id);
      const tempSensorZoneId = this.stringValue(component.tempsensor_for_zone_id);
      if (componentZoneId !== zoneIdArg && tempSensorZoneId !== zoneIdArg) {
        continue;
      }
      const serial = this.stringValue(component.serial);
      const temperature = serial ? this.temperatureValue(temperaturesArg[serial]) : undefined;
      if (temperature !== undefined) {
        return temperature;
      }
    }
    return undefined;
  }

  private static temperatureValue(valueArg: unknown): number | undefined {
    const value = this.numberValue(valueArg);
    return value === undefined ? undefined : value / 100;
  }

  private static endpointHost(configArg: INoboHubConfig): string | undefined {
    if (configArg.host) {
      return this.urlHost(configArg.host) || configArg.host;
    }
    if (configArg.url) {
      return this.urlHost(configArg.url);
    }
    return undefined;
  }

  private static endpointPort(configArg: INoboHubConfig): number | undefined {
    if (configArg.port) {
      return configArg.port;
    }
    const source = configArg.url || configArg.host;
    if (!source) {
      return undefined;
    }
    try {
      const url = new URL(source.includes('://') ? source : `tcp://${source}`);
      return url.port ? Number(url.port) : noboHubDefaultPort;
    } catch {
      return noboHubDefaultPort;
    }
  }

  private static urlHost(valueArg: string): string | undefined {
    try {
      return new URL(valueArg.includes('://') ? valueArg : `tcp://${valueArg}`).hostname;
    } catch {
      return undefined;
    }
  }

  private static record(valueArg: unknown): Record<string, unknown> {
    return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : {};
  }

  private static recordMap(valueArg: unknown): Record<string, Record<string, unknown>> {
    const map = this.record(valueArg);
    return Object.fromEntries(Object.entries(map).filter((entryArg): entryArg is [string, Record<string, unknown>] => Boolean(entryArg[1] && typeof entryArg[1] === 'object' && !Array.isArray(entryArg[1]))));
  }

  private static stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg ? valueArg.replace(/\u00a0/g, ' ') : typeof valueArg === 'number' && Number.isFinite(valueArg) ? String(valueArg) : undefined;
  }

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

  private static cleanAttributes(attributesArg?: Record<string, unknown>): Record<string, unknown> | undefined {
    const entries = Object.entries(attributesArg || {}).filter(([, value]) => value !== undefined && value !== null && value !== '');
    return entries.length ? Object.fromEntries(entries) : undefined;
  }
}
