import { dsmrObisReferenceByCode, dsmrProtocol, dsmrSensorDescriptions, dsmrVersions } from './dsmr.constants.js';
import type {
  IDsmrConfig,
  IDsmrConnectionInfo,
  IDsmrMbusDeviceSnapshot,
  IDsmrMeterInfo,
  IDsmrSensorDescription,
  IDsmrSensorState,
  IDsmrSnapshot,
  IDsmrStatusSnapshot,
  IDsmrStatusValueObject,
  IDsmrTelegramObject,
  IDsmrTelegramObjectGroup,
  TDsmrConnectionType,
  TDsmrProtocol,
  TDsmrSensorValue,
  TDsmrSnapshotSource,
  TDsmrVersion,
} from './dsmr.types.js';
import { dsmrDefaultDsmrVersion, dsmrDefaultNetworkPort, dsmrDefaultPrecision, dsmrDomain } from './dsmr.types.js';

type TDsmrConnectionConfig = Partial<Omit<IDsmrConfig, 'protocol' | 'dsmrVersion' | 'connectionType' | 'port'>> & {
  id?: string;
  name?: string;
  connectionType?: TDsmrConnectionType;
  type?: string;
  host?: string;
  port?: number | string;
  serialPort?: string;
  device?: string;
  path?: string;
  protocol?: string;
  dsmrVersion?: string;
  dsmr_version?: string;
  serialId?: string | null;
  serialIdGas?: string | null;
  manufacturer?: string;
  model?: string;
};

export interface IDsmrParserOptions {
  config?: IDsmrConfig;
  source?: TDsmrSnapshotSource;
  connected?: boolean;
  updatedAt?: string;
  dsmrVersion?: string;
  serialId?: string | null;
  serialIdGas?: string | null;
}

export class DsmrTelegramParser {
  public static parseTelegram(telegramArg: string, optionsArg: IDsmrParserOptions = {}): IDsmrSnapshot {
    const updatedAt = optionsArg.updatedAt || new Date().toISOString();
    const lines = telegramArg.split(/\r?\n/).map((lineArg) => lineArg.trim()).filter(Boolean);
    const header = lines.find((lineArg) => lineArg.startsWith('/'));
    const checksumLine = lines.find((lineArg) => lineArg.startsWith('!'));
    const checksum = checksumLine?.slice(1).trim() || undefined;
    const objects = lines.map((lineArg) => this.parseLine(lineArg)).filter((objectArg): objectArg is IDsmrTelegramObject => Boolean(objectArg));
    const dsmrVersion = this.dsmrVersion(optionsArg.config, optionsArg.dsmrVersion, objects);
    const meter = this.meterInfo(optionsArg.config, dsmrVersion, objects, optionsArg.serialId, optionsArg.serialIdGas);
    const sensors = this.sensorsFromObjects(objects, meter, dsmrVersion, updatedAt);
    const mbusDevices = this.mbusDevicesFromObjects(objects, sensors);

    return {
      meter,
      sensors,
      mbusDevices,
      telegram: {
        header,
        checksum,
        raw: telegramArg,
        objects,
      },
      connected: optionsArg.connected ?? objects.length > 0,
      source: optionsArg.source || 'telegram',
      updatedAt,
      raw: {
        objectCount: objects.length,
      },
    };
  }

  public static parseLine(lineArg: string): IDsmrTelegramObject | undefined {
    const line = lineArg.trim();
    const start = line.indexOf('(');
    if (start <= 0 || line.startsWith('/') || line.startsWith('!')) {
      return undefined;
    }
    const obisCode = line.slice(0, start).trim();
    if (!/^\d-\d:/.test(obisCode)) {
      return undefined;
    }
    const groups = [...line.matchAll(/\(([^()]*)\)/g)].map((matchArg) => this.parseGroup(matchArg[1]));
    if (!groups.length) {
      return undefined;
    }
    const valueGroup = this.valueGroup(groups);
    return {
      obisCode,
      obisReference: this.obisReference(obisCode),
      channel: this.channelForCode(obisCode),
      raw: line,
      groups,
      value: valueGroup?.value,
      unit: this.normalizeUnit(valueGroup?.unit),
    };
  }

  public static snapshotFromStatus(statusArg: IDsmrStatusSnapshot, configArg: IDsmrConfig = {}): IDsmrSnapshot {
    const updatedAt = statusArg.updatedAt || new Date().toISOString();
    const connection = this.connectionInfo({ ...configArg, ...statusArg.meter });
    const dsmrVersion = String(statusArg.meter?.dsmrVersion || connection.dsmrVersion || dsmrDefaultDsmrVersion);
    const meter: IDsmrMeterInfo = {
      ...connection,
      ...statusArg.meter,
      id: String(statusArg.meter?.id || connection.id),
      name: String(statusArg.meter?.name || connection.name),
      connectionType: this.connectionType(statusArg.meter || configArg),
      protocol: this.protocol(statusArg.meter?.protocol || configArg.protocol),
      dsmrVersion,
      serialId: statusArg.meter?.serialId ?? configArg.serialId ?? configArg.serial_id ?? null,
      serialIdGas: statusArg.meter?.serialIdGas ?? configArg.serialIdGas ?? configArg.serial_id_gas ?? null,
    };
    const configuredSensors = Array.isArray(statusArg.sensors) ? statusArg.sensors.map((sensorArg) => this.normalizeSensor(sensorArg, dsmrVersion, updatedAt)) : [];
    const sensors = configuredSensors.length ? configuredSensors : dsmrSensorDescriptions.flatMap((descriptionArg) => {
      if (!this.supportedDescription(descriptionArg, dsmrVersion)) {
        return [];
      }
      const statusValue = this.statusValue(statusArg, descriptionArg);
      if (!statusValue.exists) {
        return [];
      }
      const value = this.sensorValue(statusValue.value, descriptionArg, dsmrVersion);
      return [{
        key: descriptionArg.key,
        name: descriptionArg.name,
        obisReference: descriptionArg.obisReference,
        obisCode: descriptionArg.obisCodes?.[0],
        deviceType: descriptionArg.deviceType,
        value,
        rawValue: statusValue.rawValue ?? (statusValue.value === undefined || statusValue.value === null ? undefined : String(statusValue.value)),
        unit: this.normalizeUnit(statusValue.unit || descriptionArg.defaultUnit),
        deviceClass: descriptionArg.deviceClass,
        stateClass: descriptionArg.stateClass,
        entityCategory: descriptionArg.entityCategory,
        enabledByDefault: descriptionArg.enabledByDefault ?? true,
        available: statusValue.available ?? true,
        serialId: descriptionArg.deviceType === 'gas' ? meter.serialIdGas : meter.serialId,
        updatedAt: statusValue.updatedAt || updatedAt,
        attributes: statusValue.attributes,
      } satisfies IDsmrSensorState];
    });

    return {
      meter,
      sensors,
      connected: statusArg.connected ?? configArg.connected ?? true,
      source: 'status',
      updatedAt,
      raw: statusArg.raw || { status: true },
    };
  }

  public static emptySnapshot(configArg: IDsmrConfig = {}, connectedArg = configArg.connected ?? false): IDsmrSnapshot {
    const connection = this.connectionInfo(configArg);
    return {
      meter: {
        ...connection,
        serialId: configArg.serialId ?? configArg.serial_id ?? null,
        serialIdGas: configArg.serialIdGas ?? configArg.serial_id_gas ?? null,
      },
      sensors: [],
      connected: connectedArg,
      source: 'manual',
      updatedAt: new Date().toISOString(),
      raw: { configuredOnly: true },
    };
  }

  public static normalizeSnapshot(snapshotArg: IDsmrSnapshot, configArg: IDsmrConfig = {}): IDsmrSnapshot {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const dsmrVersion = String(snapshotArg.meter?.dsmrVersion || configArg.dsmrVersion || configArg.dsmr_version || dsmrDefaultDsmrVersion);
    const connection = this.connectionInfo({ ...configArg, ...snapshotArg.meter, dsmrVersion });
    const meter: IDsmrMeterInfo = {
      ...connection,
      ...snapshotArg.meter,
      dsmrVersion,
      protocol: this.protocol(snapshotArg.meter?.protocol || configArg.protocol),
      serialId: snapshotArg.meter?.serialId ?? configArg.serialId ?? configArg.serial_id ?? null,
      serialIdGas: snapshotArg.meter?.serialIdGas ?? configArg.serialIdGas ?? configArg.serial_id_gas ?? null,
    };
    return {
      ...snapshotArg,
      meter,
      sensors: snapshotArg.sensors.map((sensorArg) => this.normalizeSensor(sensorArg, dsmrVersion, updatedAt)),
      connected: snapshotArg.connected,
      updatedAt,
    };
  }

  public static connectionInfo(configArg: TDsmrConnectionConfig = {}): IDsmrConnectionInfo {
    const connectionType = this.connectionType(configArg);
    const serialPort = this.serialPort(configArg);
    const host = typeof configArg.host === 'string' && configArg.host.trim() ? configArg.host.trim() : undefined;
    const port = connectionType === 'network' ? this.networkPort(configArg.port) : serialPort;
    const id = this.slug(String(configArg.id || (host ? `${host}_${port || dsmrDefaultNetworkPort}` : serialPort || 'dsmr_meter')));
    return {
      id,
      name: typeof configArg.name === 'string' && configArg.name.trim() ? configArg.name.trim() : 'DSMR Smart Meter',
      connectionType,
      host: connectionType === 'network' ? host : undefined,
      port,
      serialPort: connectionType === 'serial' ? serialPort : undefined,
      protocol: this.protocol(configArg.protocol),
      dsmrVersion: this.dsmrVersion(configArg as IDsmrConfig),
    };
  }

  public static translateTariff(valueArg: string | number | boolean | null | undefined, dsmrVersionArg: string): string | null {
    let value = String(valueArg ?? '').trim();
    if (/^\d$/.test(value)) {
      value = `000${value}`;
    }
    if (dsmrVersionArg === '5B' || dsmrVersionArg === '5EONHU') {
      if (value === '0001') {
        value = '0002';
      } else if (value === '0002') {
        value = '0001';
      }
    }
    if (value === '0002') {
      return 'normal';
    }
    if (value === '0001') {
      return 'low';
    }
    return null;
  }

  private static sensorsFromObjects(objectsArg: IDsmrTelegramObject[], meterArg: IDsmrMeterInfo, dsmrVersionArg: string, updatedAtArg: string): IDsmrSensorState[] {
    const mbusDeviceTypes = new Map<number, number>();
    for (const object of objectsArg) {
      if (object.obisReference === 'MBUS_DEVICE_TYPE' && object.channel !== undefined) {
        const deviceType = this.numberValue(object.value);
        if (deviceType !== undefined) {
          mbusDeviceTypes.set(object.channel, deviceType);
        }
      }
    }
    return dsmrSensorDescriptions.flatMap((descriptionArg) => {
      if (!this.supportedDescription(descriptionArg, dsmrVersionArg)) {
        return [];
      }
      const object = this.objectForDescription(objectsArg, descriptionArg);
      if (!object) {
        return [];
      }
      if (descriptionArg.deviceType === 'gas' && object.channel !== undefined) {
        const mbusDeviceType = mbusDeviceTypes.get(object.channel);
        if (mbusDeviceType !== undefined && mbusDeviceType !== 3) {
          return [];
        }
      }
      return [{
        key: descriptionArg.key,
        name: descriptionArg.name,
        obisReference: descriptionArg.obisReference,
        obisCode: object.obisCode,
        deviceType: descriptionArg.deviceType,
        value: this.sensorValue(object.value, descriptionArg, dsmrVersionArg),
        rawValue: object.value,
        unit: this.normalizeUnit(object.unit || descriptionArg.defaultUnit),
        deviceClass: descriptionArg.deviceClass,
        stateClass: descriptionArg.stateClass,
        entityCategory: descriptionArg.entityCategory,
        enabledByDefault: descriptionArg.enabledByDefault ?? true,
        available: true,
        channel: object.channel,
        serialId: descriptionArg.deviceType === 'gas' ? meterArg.serialIdGas : meterArg.serialId,
        updatedAt: updatedAtArg,
      } satisfies IDsmrSensorState];
    });
  }

  private static mbusDevicesFromObjects(objectsArg: IDsmrTelegramObject[], sensorsArg: IDsmrSensorState[]): IDsmrMbusDeviceSnapshot[] {
    const devices = new Map<number, IDsmrMbusDeviceSnapshot>();
    for (const object of objectsArg) {
      if (object.channel === undefined) {
        continue;
      }
      const device = devices.get(object.channel) || { channel: object.channel };
      if (object.obisReference === 'MBUS_DEVICE_TYPE') {
        device.deviceType = this.numberValue(object.value);
      }
      if (object.obisReference === 'MBUS_EQUIPMENT_IDENTIFIER' || object.obisReference === 'EQUIPMENT_IDENTIFIER_GAS') {
        device.serialId = object.value;
      }
      devices.set(object.channel, device);
    }
    for (const sensor of sensorsArg) {
      if (sensor.channel === undefined || sensor.deviceType !== 'gas') {
        continue;
      }
      const device = devices.get(sensor.channel) || { channel: sensor.channel };
      device.reading = sensor;
      devices.set(sensor.channel, device);
    }
    return [...devices.values()];
  }

  private static meterInfo(configArg: IDsmrConfig | undefined, dsmrVersionArg: string, objectsArg: IDsmrTelegramObject[], serialIdArg?: string | null, serialIdGasArg?: string | null): IDsmrMeterInfo {
    const connection = this.connectionInfo({ ...configArg, dsmrVersion: dsmrVersionArg });
    const serialId = serialIdArg ?? configArg?.serialId ?? configArg?.serial_id ?? this.objectByReference(objectsArg, 'EQUIPMENT_IDENTIFIER')?.value ?? this.objectByReference(objectsArg, 'BELGIUM_EQUIPMENT_IDENTIFIER')?.value ?? null;
    const gasObject = this.objectByReference(objectsArg, 'EQUIPMENT_IDENTIFIER_GAS') || this.objectByReference(objectsArg, 'MBUS_EQUIPMENT_IDENTIFIER');
    const serialIdGas = serialIdGasArg ?? configArg?.serialIdGas ?? configArg?.serial_id_gas ?? gasObject?.value ?? null;
    return {
      ...connection,
      dsmrVersion: dsmrVersionArg,
      serialId,
      serialIdGas,
      manufacturer: 'DSMR',
      model: `DSMR ${dsmrVersionArg}`,
    };
  }

  private static objectForDescription(objectsArg: IDsmrTelegramObject[], descriptionArg: IDsmrSensorDescription): IDsmrTelegramObject | undefined {
    return objectsArg.find((objectArg) => {
      if (descriptionArg.obisCodes?.includes(objectArg.obisCode)) {
        return true;
      }
      if (descriptionArg.obisCodePatterns?.some((patternArg) => patternArg.test(objectArg.obisCode))) {
        return true;
      }
      return objectArg.obisReference === descriptionArg.obisReference;
    });
  }

  private static supportedDescription(descriptionArg: IDsmrSensorDescription, dsmrVersionArg: string): boolean {
    return !descriptionArg.dsmrVersions || descriptionArg.dsmrVersions.includes(dsmrVersionArg);
  }

  private static sensorValue(valueArg: unknown, descriptionArg: IDsmrSensorDescription, dsmrVersionArg: string): TDsmrSensorValue {
    if (descriptionArg.translateTariff) {
      return this.translateTariff(valueArg as string | number | boolean | null | undefined, dsmrVersionArg);
    }
    if (valueArg === undefined || valueArg === null) {
      return null;
    }
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    const numericValue = this.numberValue(valueArg);
    if (numericValue === undefined) {
      return String(valueArg);
    }
    if (descriptionArg.stateClass === 'total_increasing' && !numericValue) {
      return null;
    }
    return Number(numericValue.toFixed(dsmrDefaultPrecision));
  }

  private static normalizeSensor(sensorArg: IDsmrSensorState, dsmrVersionArg: string, updatedAtArg: string): IDsmrSensorState {
    const description = dsmrSensorDescriptions.find((descriptionArg) => descriptionArg.key === sensorArg.key || descriptionArg.obisReference === sensorArg.obisReference);
    const value = description ? this.sensorValue(sensorArg.value, description, dsmrVersionArg) : sensorArg.value;
    return {
      ...sensorArg,
      name: sensorArg.name || description?.name || sensorArg.key,
      obisReference: sensorArg.obisReference || description?.obisReference || sensorArg.key,
      deviceType: sensorArg.deviceType || description?.deviceType || 'electricity',
      value,
      unit: this.normalizeUnit(sensorArg.unit || description?.defaultUnit),
      deviceClass: sensorArg.deviceClass || description?.deviceClass,
      stateClass: sensorArg.stateClass || description?.stateClass,
      entityCategory: sensorArg.entityCategory || description?.entityCategory,
      enabledByDefault: sensorArg.enabledByDefault ?? description?.enabledByDefault ?? true,
      available: sensorArg.available,
      updatedAt: sensorArg.updatedAt || updatedAtArg,
    };
  }

  private static statusValue(statusArg: IDsmrStatusSnapshot, descriptionArg: IDsmrSensorDescription): { exists: boolean; value?: TDsmrSensorValue; unit?: string; rawValue?: string; available?: boolean; updatedAt?: string; attributes?: Record<string, unknown> } {
    const directValue = statusArg[descriptionArg.key] ?? statusArg[descriptionArg.obisReference];
    const values = statusArg.values || {};
    const value = values[descriptionArg.key] ?? values[descriptionArg.obisReference] ?? directValue;
    if (value === undefined) {
      return { exists: false };
    }
    if (this.isStatusValueObject(value)) {
      return {
        exists: true,
        value: value.value,
        unit: value.unit,
        rawValue: value.rawValue,
        available: value.available,
        updatedAt: value.updatedAt,
        attributes: value.attributes,
      };
    }
    return { exists: true, value: value as TDsmrSensorValue };
  }

  private static isStatusValueObject(valueArg: unknown): valueArg is IDsmrStatusValueObject {
    return Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) && ('value' in valueArg || 'unit' in valueArg || 'rawValue' in valueArg || 'available' in valueArg));
  }

  private static parseGroup(groupArg: string): IDsmrTelegramObjectGroup {
    const starIndex = groupArg.indexOf('*');
    if (starIndex >= 0) {
      return {
        raw: groupArg,
        value: groupArg.slice(0, starIndex).trim(),
        unit: groupArg.slice(starIndex + 1).trim() || undefined,
      };
    }
    return {
      raw: groupArg,
      value: groupArg.trim(),
    };
  }

  private static valueGroup(groupsArg: IDsmrTelegramObjectGroup[]): IDsmrTelegramObjectGroup | undefined {
    for (let index = groupsArg.length - 1; index >= 0; index--) {
      if (groupsArg[index].unit) {
        return groupsArg[index];
      }
    }
    return groupsArg[groupsArg.length - 1];
  }

  private static objectByReference(objectsArg: IDsmrTelegramObject[], referenceArg: string): IDsmrTelegramObject | undefined {
    return objectsArg.find((objectArg) => objectArg.obisReference === referenceArg);
  }

  private static obisReference(obisCodeArg: string): string | undefined {
    if (dsmrObisReferenceByCode[obisCodeArg]) {
      return dsmrObisReferenceByCode[obisCodeArg];
    }
    if (/^0-\d+:24\.1\.0$/.test(obisCodeArg)) {
      return 'MBUS_DEVICE_TYPE';
    }
    if (/^0-\d+:24\.2\.1$/.test(obisCodeArg)) {
      return 'HOURLY_GAS_METER_READING';
    }
    if (/^0-\d+:24\.3\.0$/.test(obisCodeArg)) {
      return 'GAS_METER_READING';
    }
    if (/^0-\d+:96\.1\.[01]$/.test(obisCodeArg)) {
      return this.channelForCode(obisCodeArg) ? 'MBUS_EQUIPMENT_IDENTIFIER' : 'EQUIPMENT_IDENTIFIER';
    }
    return undefined;
  }

  private static channelForCode(obisCodeArg: string): number | undefined {
    const match = obisCodeArg.match(/^0-(\d+):/);
    if (!match) {
      return undefined;
    }
    const channel = Number(match[1]);
    return channel > 0 ? channel : undefined;
  }

  private static dsmrVersion(configArg?: TDsmrConnectionConfig, overrideArg?: string, objectsArg: IDsmrTelegramObject[] = []): string {
    const configured = overrideArg || configArg?.dsmrVersion || configArg?.dsmr_version;
    if (typeof configured === 'string' && configured.trim()) {
      return configured.trim();
    }
    const versionObject = this.objectByReference(objectsArg, 'DSMR_VERSION');
    const rawVersion = versionObject?.value;
    if (rawVersion === '22') {
      return '2.2';
    }
    if (rawVersion === '40' || rawVersion === '42') {
      return '4';
    }
    if (rawVersion === '50') {
      return '5';
    }
    return dsmrDefaultDsmrVersion;
  }

  private static protocol(valueArg: unknown): TDsmrProtocol {
    return valueArg === 'rfxtrx_dsmr_protocol' ? 'rfxtrx_dsmr_protocol' : dsmrProtocol;
  }

  private static connectionType(configArg: TDsmrConnectionConfig): TDsmrConnectionType {
    const value = String(configArg.connectionType || configArg.type || '').toLowerCase();
    if (value === 'network') {
      return 'network';
    }
    if (value === 'serial') {
      return 'serial';
    }
    if (typeof configArg.host === 'string' && configArg.host.trim()) {
      return 'network';
    }
    return 'serial';
  }

  private static serialPort(configArg: TDsmrConnectionConfig): string | undefined {
    const value = configArg.serialPort || configArg.device || configArg.path || (typeof configArg.port === 'string' && !Number.isFinite(Number(configArg.port)) ? configArg.port : undefined);
    return typeof value === 'string' && value.trim() ? value.trim() : undefined;
  }

  private static networkPort(valueArg: unknown): number {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
      return Number(valueArg);
    }
    return dsmrDefaultNetworkPort;
  }

  private static 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;
  }

  private static normalizeUnit(unitArg: string | undefined): string | undefined {
    return unitArg?.trim() || undefined;
  }

  private static slug(valueArg: string): string {
    return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || dsmrDomain;
  }
}

export const isDsmrVersion = (valueArg: unknown): valueArg is TDsmrVersion => typeof valueArg === 'string' && (dsmrVersions as string[]).includes(valueArg);
