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 { ILandisgyrHeatMeterConfig, ILandisgyrHeatMeterSnapshot } from './landisgyr_heat_meter.types.js';
import { landisgyrHeatMeterDefaultName, landisgyrHeatMeterDomain, landisgyrHeatMeterProfile } from './landisgyr_heat_meter.types.js';

interface ILandisgyrParsedRawData extends Record<string, unknown> {
  model?: string;
  raw_response?: string;
  rawResponse?: string;
}

interface ILandisgyrSensorDefinition {
  key: string;
  name: string;
  unit?: string;
  deviceClass?: string;
  stateClass?: string;
  icon?: string;
  entityCategory?: string;
  value(rawDataArg: ILandisgyrParsedRawData): unknown;
}

const sensorDefinitions: ILandisgyrSensorDefinition[] = [
  sensor('heat_usage_mwh', 'Heat usage MWh', (rawDataArg) => numberValue(rawDataArg.heat_usage_mwh) ?? kwhToMwh(numberValue(rawDataArg.heat_usage_kwh)), { unit: 'MWh', deviceClass: 'energy', stateClass: 'total', icon: 'mdi:fire' }),
  sensor('volume_usage_m3', 'Volume usage', (rawDataArg) => numberValue(rawDataArg.volume_usage_m3), { unit: 'm3', deviceClass: 'volume', stateClass: 'total', icon: 'mdi:fire' }),
  sensor('heat_usage_gj', 'Heat usage GJ', (rawDataArg) => numberValue(rawDataArg.heat_usage_gj), { unit: 'GJ', deviceClass: 'energy', stateClass: 'total', icon: 'mdi:fire' }),
  sensor('heat_previous_year_mwh', 'Heat previous year MWh', (rawDataArg) => numberValue(rawDataArg.heat_previous_year_mwh) ?? kwhToMwh(numberValue(rawDataArg.heat_previous_year_kwh)), { unit: 'MWh', deviceClass: 'energy', entityCategory: 'diagnostic', icon: 'mdi:fire' }),
  sensor('heat_previous_year_gj', 'Heat previous year GJ', (rawDataArg) => numberValue(rawDataArg.heat_previous_year_gj), { unit: 'GJ', deviceClass: 'energy', entityCategory: 'diagnostic', icon: 'mdi:fire' }),
  sensor('volume_previous_year_m3', 'Volume usage previous year', (rawDataArg) => numberValue(rawDataArg.volume_previous_year_m3), { unit: 'm3', deviceClass: 'volume', entityCategory: 'diagnostic', icon: 'mdi:fire' }),
  sensor('ownership_number', 'Ownership number', (rawDataArg) => stringValue(rawDataArg.ownership_number), { icon: 'mdi:identifier', entityCategory: 'diagnostic' }),
  sensor('error_number', 'Error number', (rawDataArg) => stringValue(rawDataArg.error_number), { icon: 'mdi:home-alert', entityCategory: 'diagnostic' }),
  sensor('device_number', 'Device number', (rawDataArg) => stringValue(rawDataArg.device_number), { icon: 'mdi:identifier', entityCategory: 'diagnostic' }),
  sensor('measurement_period_minutes', 'Measurement period minutes', (rawDataArg) => numberValue(rawDataArg.measurement_period_minutes), { unit: 'min', deviceClass: 'duration', entityCategory: 'diagnostic' }),
  sensor('power_max_kw', 'Power max', (rawDataArg) => numberValue(rawDataArg.power_max_kw), { unit: 'kW', deviceClass: 'power', entityCategory: 'diagnostic' }),
  sensor('power_max_previous_year_kw', 'Power max previous year', (rawDataArg) => numberValue(rawDataArg.power_max_previous_year_kw), { unit: 'kW', deviceClass: 'power', entityCategory: 'diagnostic' }),
  sensor('flowrate_max_m3ph', 'Flowrate max', (rawDataArg) => numberValue(rawDataArg.flowrate_max_m3ph), { unit: 'm3/h', icon: 'mdi:water-outline', entityCategory: 'diagnostic' }),
  sensor('flowrate_max_previous_year_m3ph', 'Flowrate max previous year', (rawDataArg) => numberValue(rawDataArg.flowrate_max_previous_year_m3ph), { unit: 'm3/h', icon: 'mdi:water-outline', entityCategory: 'diagnostic' }),
  sensor('return_temperature_max_c', 'Return temperature max', (rawDataArg) => numberValue(rawDataArg.return_temperature_max_c), { unit: '°C', deviceClass: 'temperature', entityCategory: 'diagnostic' }),
  sensor('return_temperature_max_previous_year_c', 'Return temperature max previous year', (rawDataArg) => numberValue(rawDataArg.return_temperature_max_previous_year_c), { unit: '°C', deviceClass: 'temperature', entityCategory: 'diagnostic' }),
  sensor('flow_temperature_max_c', 'Flow temperature max', (rawDataArg) => numberValue(rawDataArg.flow_temperature_max_c), { unit: '°C', deviceClass: 'temperature', entityCategory: 'diagnostic' }),
  sensor('flow_temperature_max_previous_year_c', 'Flow temperature max previous year', (rawDataArg) => numberValue(rawDataArg.flow_temperature_max_previous_year_c), { unit: '°C', deviceClass: 'temperature', entityCategory: 'diagnostic' }),
  sensor('operating_hours', 'Operating hours', (rawDataArg) => numberValue(rawDataArg.operating_hours), { unit: 'h', deviceClass: 'duration', entityCategory: 'diagnostic' }),
  sensor('flow_hours', 'Flow hours', (rawDataArg) => numberValue(rawDataArg.flow_hours), { unit: 'h', deviceClass: 'duration', entityCategory: 'diagnostic' }),
  sensor('fault_hours', 'Fault hours', (rawDataArg) => numberValue(rawDataArg.fault_hours), { unit: 'h', deviceClass: 'duration', entityCategory: 'diagnostic' }),
  sensor('fault_hours_previous_year', 'Fault hours previous year', (rawDataArg) => numberValue(rawDataArg.fault_hours_previous_year), { unit: 'h', deviceClass: 'duration', entityCategory: 'diagnostic' }),
  sensor('yearly_set_day', 'Yearly set day', (rawDataArg) => stringValue(rawDataArg.yearly_set_day), { icon: 'mdi:clock-outline', entityCategory: 'diagnostic' }),
  sensor('monthly_set_day', 'Monthly set day', (rawDataArg) => stringValue(rawDataArg.monthly_set_day), { icon: 'mdi:clock-outline', entityCategory: 'diagnostic' }),
  sensor('meter_date_time', 'Meter date time', (rawDataArg) => timestampValue(rawDataArg.meter_date_time), { deviceClass: 'timestamp', icon: 'mdi:clock-outline', entityCategory: 'diagnostic' }),
  sensor('measuring_range_m3ph', 'Measuring range', (rawDataArg) => numberValue(rawDataArg.measuring_range_m3ph), { unit: 'm3/h', icon: 'mdi:water-outline', entityCategory: 'diagnostic' }),
  sensor('settings_and_firmware', 'Settings and firmware', (rawDataArg) => stringValue(rawDataArg.settings_and_firmware), { entityCategory: 'diagnostic' }),
];

export class LandisgyrHeatMeterMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<ILandisgyrHeatMeterConfig>, 'profile'>): ILandisgyrHeatMeterSnapshot {
    const rawSnapshot = this.snapshotValue(optionsArg.rawData) || (optionsArg.rawData === undefined ? optionsArg.config.snapshot : undefined);
    if (rawSnapshot) {
      return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: landisgyrHeatMeterProfile }) as ILandisgyrHeatMeterSnapshot;
    }

    const rawData = this.rawData(optionsArg.config, optionsArg.rawData);
    const deviceNumber = stringValue(rawData.device_number) || optionsArg.config.deviceNumber || optionsArg.config.uniqueId;
    const model = stringValue(rawData.model) || optionsArg.config.model || 'Landis+Gyr Ultraheat';
    const entities = this.entitiesFromRaw(rawData, optionsArg.online ?? optionsArg.config.online ?? Boolean(rawData.raw_response || rawData.rawResponse));
    const online = optionsArg.online ?? optionsArg.config.online ?? entities.length > 0;

    return {
      device: {
        id: optionsArg.config.uniqueId || deviceNumber || optionsArg.config.filePath || landisgyrHeatMeterDomain,
        name: optionsArg.config.name || landisgyrHeatMeterDefaultName,
        manufacturer: 'Landis & Gyr',
        model,
        serialNumber: deviceNumber,
        protocol: optionsArg.config.filePath ? 'local' : 'unknown',
        attributes: cleanAttributes({ filePath: optionsArg.config.filePath, sourceModel: rawData.model }),
      },
      entities,
      online,
      updatedAt: new Date().toISOString(),
      source: optionsArg.source || (optionsArg.config.filePath ? 'file' : rawData.raw_response || rawData.rawResponse ? 'manual' : 'runtime'),
      rawData: Object.keys(rawData).length ? rawData : undefined,
      error: optionsArg.error,
      capabilities: {
        localRead: Boolean(optionsArg.config.filePath),
        localControl: false,
        liveControlRequiresExecutor: false,
      },
    };
  }

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

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

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

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

  public static clone<TValue>(valueArg: TValue): TValue {
    return SimpleLocalMapper.clone(valueArg);
  }

  public static parseRawTelegram(modelArg: string | undefined, rawResponseArg: string): ILandisgyrParsedRawData {
    const rawData: ILandisgyrParsedRawData = { model: modelArg, raw_response: rawResponseArg };
    assign(rawData, 'heat_usage_gj', matchNumber(rawResponseArg, /6\.8\((.*?)\*GJ\)/i));
    assign(rawData, 'heat_usage_mwh', matchNumber(rawResponseArg, /6\.8\((.*?)\*MWh\)/i));
    assign(rawData, 'heat_usage_kwh', matchNumber(rawResponseArg, /6\.8\((.*?)\*kWh\)/i));
    assign(rawData, 'volume_usage_m3', matchNumber(rawResponseArg, /6\.26\((.*?)\*m3\)/i));
    assign(rawData, 'ownership_number', matchString(rawResponseArg, /9\.21\((.*?)\)/i));
    assign(rawData, 'volume_previous_year_m3', matchNumber(rawResponseArg, /6\.26\*01\((.*?)\*m3\)/i));
    assign(rawData, 'heat_previous_year_gj', matchNumber(rawResponseArg, /6\.8\*01\((.*?)\*GJ\)/i));
    assign(rawData, 'heat_previous_year_mwh', matchNumber(rawResponseArg, /6\.8\*01\((.*?)\*MWh\)/i));
    assign(rawData, 'heat_previous_year_kwh', matchNumber(rawResponseArg, /6\.8\*01\((.*?)\*kWh\)/i));
    assign(rawData, 'error_number', matchString(rawResponseArg, /F\((.*?)\)/i));
    assign(rawData, 'device_number', matchString(rawResponseArg, /9\.20\((.*?)\)/i));
    assign(rawData, 'measurement_period_minutes', matchInteger(rawResponseArg, /6\.35\((.*?)\*m\)/i));
    assign(rawData, 'power_max_kw', matchNumber(rawResponseArg, /6\.6\((.*?)\*kW\)/i));
    assign(rawData, 'power_max_previous_year_kw', matchNumber(rawResponseArg, /6\.6\*01\((.*?)\*kW\)/i));
    assign(rawData, 'flowrate_max_m3ph', matchNumber(rawResponseArg, /6\.33\((.*?)\*m3ph\)/i));
    assign(rawData, 'flowrate_max_previous_year_m3ph', matchNumber(rawResponseArg, /6\.33\*01\((.*?)\*m3ph\)/i));
    assign(rawData, 'flow_temperature_max_c', matchNumber(rawResponseArg, /9\.4\((.*?)\*C/i));
    assign(rawData, 'return_temperature_max_c', matchNumber(rawResponseArg, /9\.4\(.*?\*C&(.*?)\*C/i));
    assign(rawData, 'flow_temperature_max_previous_year_c', matchNumber(rawResponseArg, /9\.4\*01\((.*?)\*C/i));
    assign(rawData, 'return_temperature_max_previous_year_c', matchNumber(rawResponseArg, /9\.4\*01\(.*?\*C&(.*?)\*C/i));
    assign(rawData, 'operating_hours', matchInteger(rawResponseArg, /6\.31\((.*?)\*h\)/i));
    assign(rawData, 'fault_hours', matchInteger(rawResponseArg, /6\.32\((.*?)\*h\)/i));
    assign(rawData, 'fault_hours_previous_year', matchInteger(rawResponseArg, /6\.32\*01\((.*?)\*h\)/i));
    assign(rawData, 'yearly_set_day', matchString(rawResponseArg, /6\.36\((.*?)\)/i)?.replace(/&/g, ' '));
    assign(rawData, 'monthly_set_day', matchString(rawResponseArg, /6\.36\*02\((.*?)\)/i)?.replace(/&/g, ' '));
    assign(rawData, 'meter_date_time', matchString(rawResponseArg, /9\.36\((.*?)\)/i));
    assign(rawData, 'measuring_range_m3ph', matchNumber(rawResponseArg, /9\.24\((.*?)\*m3ph\)/i));
    assign(rawData, 'settings_and_firmware', matchString(rawResponseArg, /9\.1\((.*?)\)/i)?.replace(/&/g, ' '));
    assign(rawData, 'flow_hours', matchInteger(rawResponseArg, /9\.31\((.*?)\*h\)/i));
    return rawData;
  }

  private static rawData(configArg: ILandisgyrHeatMeterConfig, rawDataArg?: unknown): ILandisgyrParsedRawData {
    const rawValue = rawDataArg ?? configArg.rawData ?? configArg.rawText;
    if (typeof rawValue === 'string') {
      const split = splitFileContent(rawValue, configArg.model);
      return this.parseRawTelegram(split.model, split.rawResponse);
    }
    const rawObject = objectValue(rawValue) as ILandisgyrParsedRawData | undefined;
    if (rawObject) {
      if (typeof rawObject.raw_response === 'string' || typeof rawObject.rawResponse === 'string') {
        return { ...rawObject, ...this.parseRawTelegram(stringValue(rawObject.model) || configArg.model, stringValue(rawObject.raw_response ?? rawObject.rawResponse) || '') };
      }
      return this.clone(rawObject);
    }
    return cleanAttributes({ model: configArg.model, device_number: configArg.deviceNumber }) as ILandisgyrParsedRawData;
  }

  private static entitiesFromRaw(rawDataArg: ILandisgyrParsedRawData, onlineArg: boolean): ISimpleLocalEntitySnapshot[] {
    const entities: ISimpleLocalEntitySnapshot[] = [];
    for (const definitionArg of sensorDefinitions) {
      const state = definitionArg.value(rawDataArg);
      if (state === undefined || state === null || state === '') {
        continue;
      }
      entities.push({
          id: definitionArg.key,
          name: `Heat Meter ${definitionArg.name}`,
          platform: 'sensor' as const,
          state,
          unit: definitionArg.unit,
          deviceClass: definitionArg.deviceClass,
          stateClass: definitionArg.stateClass,
          available: onlineArg,
          attributes: cleanAttributes({ key: definitionArg.key, icon: definitionArg.icon, entityCategory: definitionArg.entityCategory }),
      });
    }
    return entities;
  }

  private static snapshotValue(valueArg: unknown): ISimpleLocalSnapshot | undefined {
    return valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg ? valueArg as ISimpleLocalSnapshot : undefined;
  }
}

function sensor(keyArg: string, nameArg: string, valueArg: (rawDataArg: ILandisgyrParsedRawData) => unknown, optionsArg: Omit<ILandisgyrSensorDefinition, 'key' | 'name' | 'value'> = {}): ILandisgyrSensorDefinition {
  return { key: keyArg, name: nameArg, value: valueArg, ...optionsArg };
}

function splitFileContent(valueArg: string, modelArg?: string): { model?: string; rawResponse: string } {
  const firstLineEnd = valueArg.indexOf('\n');
  if (firstLineEnd === -1) {
    return { model: modelArg, rawResponse: valueArg };
  }
  const firstLine = valueArg.slice(0, firstLineEnd).trim();
  const model = modelArg || firstLine.slice(1, 9) || firstLine;
  return { model, rawResponse: valueArg.slice(firstLineEnd + 1) };
}

function assign(targetArg: Record<string, unknown>, keyArg: string, valueArg: unknown): void {
  if (valueArg !== undefined && valueArg !== null && valueArg !== '') {
    targetArg[keyArg] = valueArg;
  }
}

function matchString(valueArg: string, regexArg: RegExp): string | undefined {
  const value = regexArg.exec(valueArg)?.[1];
  return value === undefined ? undefined : value.trim();
}

function matchNumber(valueArg: string, regexArg: RegExp): number | undefined {
  return numberValue(matchString(valueArg, regexArg));
}

function matchInteger(valueArg: string, regexArg: RegExp): number | undefined {
  const value = matchNumber(valueArg, regexArg);
  return value === undefined ? undefined : Math.round(value);
}

function kwhToMwh(valueArg: number | undefined): number | undefined {
  return valueArg === undefined ? undefined : valueArg / 1000;
}

function timestampValue(valueArg: unknown): string | undefined {
  const value = stringValue(valueArg);
  if (!value) {
    return undefined;
  }
  const normalized = value.includes('&') ? value.replace('&', 'T') : value;
  const date = new Date(`${normalized}${/[zZ]|[+-]\d\d:?\d\d$/.test(normalized) ? '' : 'Z'}`);
  return Number.isNaN(date.valueOf()) ? value : date.toISOString();
}

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

function stringValue(valueArg: unknown): string | undefined {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : typeof valueArg === 'number' && Number.isFinite(valueArg) ? String(valueArg) : undefined;
}

function 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;
}

function cleanAttributes<TValue extends Record<string, unknown>>(attributesArg: TValue): TValue {
  return Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined)) as TValue;
}
