import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, IServiceCallRequest } from '../../core/types.js';
import type {
  IModbusCoilState,
  IModbusCommand,
  IModbusEntitySnapshot,
  IModbusEvent,
  IModbusHubSnapshot,
  IModbusRegisterState,
  IModbusSnapshot,
  TModbusCoilType,
  TModbusDataType,
  TModbusEntityPlatform,
  TModbusRegisterType,
} from './modbus.types.js';

const modbusDomain = 'modbus';

export class ModbusMapper {
  public static toDevices(snapshotArg: IModbusSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [];

    for (const hub of snapshotArg.hubs) {
      devices.push({
        id: this.hubDeviceId(hub),
        integrationDomain: modbusDomain,
        name: hub.name,
        protocol: 'unknown',
        manufacturer: hub.manufacturer || 'Modbus',
        model: hub.model || `${hub.type.toUpperCase()} hub`,
        online: hub.online && !hub.unsupportedReason,
        features: [
          { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
          { id: 'configured_entities', capability: 'sensor', name: 'Configured entities', readable: true, writable: false },
        ],
        state: [
          { featureId: 'connectivity', value: hub.online && !hub.unsupportedReason ? 'online' : 'offline', updatedAt },
          { featureId: 'configured_entities', value: hub.entities.length, updatedAt },
        ],
        metadata: this.cleanAttributes({
          type: hub.type,
          host: hub.host,
          port: hub.port,
          serialNumber: hub.serialNumber,
          unsupportedReason: hub.unsupportedReason,
          ...hub.metadata,
        }),
      });

      for (const slave of hub.slaves) {
        devices.push({
          id: this.slaveDeviceId(hub, slave.unitId),
          integrationDomain: modbusDomain,
          name: slave.name,
          protocol: 'unknown',
          manufacturer: slave.manufacturer || hub.manufacturer || 'Modbus',
          model: slave.model || 'Modbus slave',
          online: hub.online && slave.online && !hub.unsupportedReason,
          features: [
            { id: 'registers', capability: 'sensor', name: 'Configured registers', readable: true, writable: false },
            { id: 'coils', capability: 'sensor', name: 'Configured coils', readable: true, writable: false },
          ],
          state: [
            { featureId: 'registers', value: slave.registers.length, updatedAt },
            { featureId: 'coils', value: slave.coils.length, updatedAt },
          ],
          metadata: this.cleanAttributes({
            hubId: hub.id,
            hubName: hub.name,
            unitId: slave.unitId,
            viaDevice: this.hubDeviceId(hub),
            serialNumber: slave.serialNumber,
            ...slave.metadata,
          }),
        });
      }
    }

    return devices;
  }

  public static toEntities(snapshotArg: IModbusSnapshot): IIntegrationEntity[] {
    const usedIds = new Map<string, number>();
    return snapshotArg.hubs.flatMap((hubArg) => hubArg.entities.map((entityArg) => this.toEntity(hubArg, entityArg, usedIds)));
  }

  public static commandForService(snapshotArg: IModbusSnapshot, requestArg: IServiceCallRequest): IModbusCommand | undefined {
    const targetEntity = this.findEntity(snapshotArg, requestArg);
    if (requestArg.domain === modbusDomain && ['refresh', 'reload'].includes(requestArg.service)) {
      return { type: 'refresh', ...this.commandTarget(snapshotArg, requestArg, targetEntity) };
    }
    if (requestArg.domain === modbusDomain && requestArg.service === 'stop') {
      return { type: 'stop', ...this.commandTarget(snapshotArg, requestArg, targetEntity) };
    }
    if (requestArg.domain === modbusDomain && requestArg.service === 'restart') {
      return { type: 'restart', ...this.commandTarget(snapshotArg, requestArg, targetEntity) };
    }

    if (requestArg.domain === modbusDomain && ['read_register', 'read_holding_register', 'read_input_register'].includes(requestArg.service)) {
      const address = this.addressValue(requestArg, targetEntity);
      if (address === undefined) {
        return undefined;
      }
      return {
        type: 'read_register',
        ...this.commandTarget(snapshotArg, requestArg, targetEntity),
        address,
        count: this.numberData(requestArg, 'count') || targetEntity?.register?.count || 1,
        inputType: this.registerType(requestArg, targetEntity, requestArg.service === 'read_input_register' ? 'input' : undefined),
        dataType: this.dataType(requestArg, targetEntity),
      };
    }

    if (requestArg.domain === modbusDomain && ['read_coil', 'read_discrete_input'].includes(requestArg.service)) {
      const address = this.addressValue(requestArg, targetEntity);
      if (address === undefined) {
        return undefined;
      }
      return {
        type: 'read_coil',
        ...this.commandTarget(snapshotArg, requestArg, targetEntity),
        address,
        count: this.numberData(requestArg, 'count') || targetEntity?.coil?.count || 1,
        inputType: this.coilType(requestArg, targetEntity, requestArg.service === 'read_discrete_input' ? 'discrete_input' : undefined),
      };
    }

    if (requestArg.domain === modbusDomain && requestArg.service === 'write_register') {
      const address = this.addressValue(requestArg, targetEntity);
      const value = this.numberOrArrayData(requestArg, 'value');
      return address !== undefined && value !== undefined ? {
        type: 'write_register',
        ...this.commandTarget(snapshotArg, requestArg, targetEntity),
        address,
        value,
      } : undefined;
    }

    if (requestArg.domain === modbusDomain && requestArg.service === 'write_coil') {
      const address = this.addressValue(requestArg, targetEntity);
      const value = this.boolOrArrayData(requestArg, 'state') ?? this.boolOrArrayData(requestArg, 'value');
      return address !== undefined && value !== undefined ? {
        type: 'write_coil',
        ...this.commandTarget(snapshotArg, requestArg, targetEntity),
        address,
        value,
      } : undefined;
    }

    if (requestArg.domain === 'switch' && ['turn_on', 'turn_off'].includes(requestArg.service) && targetEntity?.platform === 'switch') {
      const commandValue = requestArg.service === 'turn_on' ? this.attributeNumber(targetEntity, 'commandOn') ?? 1 : this.attributeNumber(targetEntity, 'commandOff') ?? 0;
      const writeType = String(targetEntity.attributes?.writeType || 'coil');
      if (writeType === 'holding' || writeType === 'holdings' || targetEntity.register) {
        return {
          type: 'write_register',
          ...this.commandTarget(snapshotArg, requestArg, targetEntity),
          address: targetEntity.address,
          value: commandValue,
        };
      }
      return {
        type: 'write_coil',
        ...this.commandTarget(snapshotArg, requestArg, targetEntity),
        address: targetEntity.address,
        value: commandValue,
      };
    }

    if (requestArg.domain === 'number' && requestArg.service === 'set_value' && targetEntity?.platform === 'number') {
      const value = this.numberData(requestArg, 'value');
      return value !== undefined ? {
        type: 'write_register',
        ...this.commandTarget(snapshotArg, requestArg, targetEntity),
        address: targetEntity.address,
        value,
      } : undefined;
    }

    return undefined;
  }

  public static toIntegrationEvent(eventArg: IModbusEvent): IIntegrationEvent {
    return {
      type: eventArg.type === 'command_failed' ? 'error' : 'state_changed',
      integrationDomain: modbusDomain,
      deviceId: eventArg.deviceId,
      entityId: eventArg.entityId,
      data: eventArg,
      timestamp: eventArg.timestamp,
    };
  }

  public static hubDeviceId(hubArg: Pick<IModbusHubSnapshot, 'id'>): string {
    return `modbus.hub.${this.slug(hubArg.id)}`;
  }

  public static slaveDeviceId(hubArg: Pick<IModbusHubSnapshot, 'id'>, unitIdArg: number): string {
    return `modbus.slave.${this.slug(hubArg.id)}.${unitIdArg}`;
  }

  public static decodeRegisterValue(registerArg: IModbusRegisterState): number | string | number[] | null {
    if (registerArg.value !== undefined) {
      return registerArg.value;
    }
    const registers = registerArg.registers || [];
    if (!registers.length) {
      return null;
    }
    const dataType = this.normalizedDataType(registerArg.dataType);
    if (dataType === 'custom') {
      return registers;
    }

    const bytes = Buffer.alloc(registers.length * 2);
    registers.forEach((registerValueArg, indexArg) => bytes.writeUInt16BE(registerValueArg & 0xffff, indexArg * 2));
    let value: number | string;
    if (dataType === 'string') {
      value = bytes.toString('utf8').replace(/\0+$/g, '');
    } else if (dataType === 'int16') {
      value = bytes.readInt16BE(0);
    } else if (dataType === 'int32') {
      value = bytes.readInt32BE(0);
    } else if (dataType === 'uint32') {
      value = bytes.readUInt32BE(0);
    } else if (dataType === 'int64') {
      value = Number(bytes.readBigInt64BE(0));
    } else if (dataType === 'uint64') {
      value = Number(bytes.readBigUInt64BE(0));
    } else if (dataType === 'float16') {
      value = this.decodeFloat16(bytes.readUInt16BE(0));
    } else if (dataType === 'float32') {
      value = bytes.readFloatBE(0);
    } else if (dataType === 'float64') {
      value = bytes.readDoubleBE(0);
    } else {
      value = bytes.readUInt16BE(0);
    }
    return typeof value === 'number' ? this.processNumericValue(value, registerArg) : value;
  }

  public static registerCountForDataType(dataTypeArg: TModbusDataType | undefined): number {
    const dataType = this.normalizedDataType(dataTypeArg || 'int16');
    if (dataType === 'int32' || dataType === 'uint32' || dataType === 'float32') {
      return 2;
    }
    if (dataType === 'int64' || dataType === 'uint64' || dataType === 'float64') {
      return 4;
    }
    return 1;
  }

  private static toEntity(hubArg: IModbusHubSnapshot, entityArg: IModbusEntitySnapshot, usedIdsArg: Map<string, number>): IIntegrationEntity {
    const platform = entityArg.platform;
    const id = entityArg.id.includes('.') ? entityArg.id : this.uniqueEntityId(platform, entityArg.name, usedIdsArg);
    const state = this.entityState(entityArg);
    return {
      id,
      uniqueId: entityArg.uniqueId,
      integrationDomain: modbusDomain,
      deviceId: this.slaveDeviceId(hubArg, entityArg.unitId),
      platform,
      name: entityArg.name,
      state,
      attributes: this.cleanAttributes({
        hubId: hubArg.id,
        hubName: hubArg.name,
        unitId: entityArg.unitId,
        address: entityArg.address,
        writable: entityArg.writable,
        registerType: entityArg.register?.inputType,
        coilType: entityArg.coil?.inputType,
        dataType: entityArg.register?.dataType,
        count: entityArg.register?.count || entityArg.coil?.count,
        ...entityArg.attributes,
      }),
      available: entityArg.available && hubArg.online && !hubArg.unsupportedReason,
    };
  }

  private static entityState(entityArg: IModbusEntitySnapshot): unknown {
    if (entityArg.state !== undefined) {
      return entityArg.state;
    }
    if (entityArg.platform === 'sensor') {
      return entityArg.register ? this.decodeRegisterValue(entityArg.register) : null;
    }
    if (entityArg.platform === 'number') {
      const value = entityArg.register ? this.decodeRegisterValue(entityArg.register) : null;
      return typeof value === 'number' && Number.isFinite(value) ? value : null;
    }
    const boolValue = this.coilBoolean(entityArg.coil);
    return boolValue === undefined ? 'unknown' : boolValue ? 'on' : 'off';
  }

  private static coilBoolean(coilArg: IModbusCoilState | undefined): boolean | undefined {
    if (!coilArg) {
      return undefined;
    }
    if (typeof coilArg.value === 'boolean') {
      return coilArg.value;
    }
    if (typeof coilArg.value === 'number') {
      return Boolean(coilArg.value & 1);
    }
    if (coilArg.bits?.length) {
      return Boolean(coilArg.bits[0]);
    }
    return undefined;
  }

  private static uniqueEntityId(platformArg: TModbusEntityPlatform, nameArg: string, usedIdsArg: Map<string, number>): string {
    const baseId = `${platformArg}.${this.slug(nameArg)}`;
    const seen = usedIdsArg.get(baseId) || 0;
    usedIdsArg.set(baseId, seen + 1);
    return seen ? `${baseId}_${seen + 1}` : baseId;
  }

  private static findEntity(snapshotArg: IModbusSnapshot, requestArg: IServiceCallRequest): IModbusEntitySnapshot | undefined {
    const target = requestArg.target.entityId || requestArg.target.deviceId;
    if (!target) {
      return undefined;
    }
    for (const hub of snapshotArg.hubs) {
      const entity = hub.entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || this.slaveDeviceId(hub, entityArg.unitId) === target);
      if (entity) {
        return entity;
      }
    }
    return undefined;
  }

  private static commandTarget(snapshotArg: IModbusSnapshot, requestArg: IServiceCallRequest, entityArg?: IModbusEntitySnapshot): Pick<IModbusCommand, 'hub' | 'hubId' | 'unitId' | 'entityId' | 'deviceId' | 'uniqueId'> {
    const firstHub = snapshotArg.hubs[0];
    return {
      hub: this.stringData(requestArg, 'hub') || entityArg?.hubName || firstHub?.name,
      hubId: this.stringData(requestArg, 'hubId') || entityArg?.hubId || firstHub?.id,
      unitId: this.numberData(requestArg, 'unitId') ?? this.numberData(requestArg, 'unit') ?? this.numberData(requestArg, 'slave') ?? entityArg?.unitId ?? 1,
      entityId: requestArg.target.entityId,
      deviceId: requestArg.target.deviceId,
      uniqueId: entityArg?.uniqueId,
    };
  }

  private static addressValue(requestArg: IServiceCallRequest, entityArg?: IModbusEntitySnapshot): number | undefined {
    return this.numberData(requestArg, 'address') ?? entityArg?.address;
  }

  private static registerType(requestArg: IServiceCallRequest, entityArg?: IModbusEntitySnapshot, fallbackArg?: TModbusRegisterType): TModbusRegisterType {
    const value = this.stringData(requestArg, 'inputType') || this.stringData(requestArg, 'input_type') || this.stringData(requestArg, 'registerType') || this.stringData(requestArg, 'register_type') || entityArg?.register?.inputType || fallbackArg || 'holding';
    return value === 'input' ? 'input' : 'holding';
  }

  private static coilType(requestArg: IServiceCallRequest, entityArg?: IModbusEntitySnapshot, fallbackArg?: TModbusCoilType): TModbusCoilType {
    const value = this.stringData(requestArg, 'inputType') || this.stringData(requestArg, 'input_type') || this.stringData(requestArg, 'coilType') || this.stringData(requestArg, 'coil_type') || entityArg?.coil?.inputType || fallbackArg || 'coil';
    return value === 'discrete_input' ? 'discrete_input' : 'coil';
  }

  private static dataType(requestArg: IServiceCallRequest, entityArg?: IModbusEntitySnapshot): TModbusDataType | undefined {
    const value = this.stringData(requestArg, 'dataType') || this.stringData(requestArg, 'data_type') || entityArg?.register?.dataType;
    return value as TModbusDataType | undefined;
  }

  private static numberData(requestArg: IServiceCallRequest, keyArg: string): number | undefined {
    const value = requestArg.data?.[keyArg];
    return this.toNumber(value);
  }

  private static attributeNumber(entityArg: IModbusEntitySnapshot, keyArg: string): number | undefined {
    return this.toNumber(entityArg.attributes?.[keyArg]);
  }

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

  private static stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
    const value = requestArg.data?.[keyArg];
    return typeof value === 'string' && value.trim() ? value.trim() : undefined;
  }

  private static numberOrArrayData(requestArg: IServiceCallRequest, keyArg: string): number | number[] | undefined {
    const value = requestArg.data?.[keyArg];
    if (Array.isArray(value)) {
      const values = value.map((entryArg) => this.toNumber(entryArg)).filter((entryArg): entryArg is number => entryArg !== undefined);
      return values.length === value.length ? values : undefined;
    }
    return this.toNumber(value);
  }

  private static boolOrArrayData(requestArg: IServiceCallRequest, keyArg: string): boolean | boolean[] | number | number[] | undefined {
    const value = requestArg.data?.[keyArg];
    if (Array.isArray(value)) {
      const values = value.map((entryArg) => this.toBooleanOrNumber(entryArg)).filter((entryArg): entryArg is boolean | number => entryArg !== undefined);
      return values.length === value.length ? values.map((entryArg) => typeof entryArg === 'boolean' ? entryArg : Boolean(entryArg)) : undefined;
    }
    return this.toBooleanOrNumber(value);
  }

  private static toBooleanOrNumber(valueArg: unknown): boolean | number | undefined {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (typeof valueArg === 'string') {
      const value = valueArg.trim().toLowerCase();
      if (value === 'true' || value === 'on') {
        return true;
      }
      if (value === 'false' || value === 'off') {
        return false;
      }
      if (value && Number.isFinite(Number(value))) {
        return Number(value);
      }
    }
    return undefined;
  }

  private static normalizedDataType(dataTypeArg: TModbusDataType): Exclude<TModbusDataType, 'int' | 'integer' | 'uint' | 'float'> {
    if (dataTypeArg === 'int' || dataTypeArg === 'integer') {
      return 'int16';
    }
    if (dataTypeArg === 'uint') {
      return 'uint16';
    }
    if (dataTypeArg === 'float') {
      return 'float32';
    }
    return dataTypeArg;
  }

  private static decodeFloat16(valueArg: number): number {
    const sign = (valueArg & 0x8000) ? -1 : 1;
    const exponent = (valueArg >> 10) & 0x1f;
    const fraction = valueArg & 0x03ff;
    if (exponent === 0) {
      return sign * Math.pow(2, -14) * (fraction / 1024);
    }
    if (exponent === 31) {
      return fraction ? Number.NaN : sign * Number.POSITIVE_INFINITY;
    }
    return sign * Math.pow(2, exponent - 15) * (1 + fraction / 1024);
  }

  private static processNumericValue(valueArg: number, registerArg: IModbusRegisterState): number | null {
    const metadata = registerArg.metadata || {};
    const nanValue = this.toNumberValue(metadata.nanValue) ?? this.toNumberValue(metadata.nan_value);
    if (nanValue !== undefined && (valueArg === nanValue || valueArg === -nanValue)) {
      return null;
    }
    if (Number.isNaN(valueArg)) {
      return null;
    }
    const scale = this.toNumberValue(metadata.scale) ?? 1;
    const offset = this.toNumberValue(metadata.offset) ?? 0;
    let value = valueArg * scale + offset;
    const min = this.toNumberValue(metadata.minValue) ?? this.toNumberValue(metadata.min_value);
    const max = this.toNumberValue(metadata.maxValue) ?? this.toNumberValue(metadata.max_value);
    const zeroSuppress = this.toNumberValue(metadata.zeroSuppress) ?? this.toNumberValue(metadata.zero_suppress);
    const precision = this.toNumberValue(metadata.precision);
    if (min !== undefined && value < min) {
      value = min;
    }
    if (max !== undefined && value > max) {
      value = max;
    }
    if (zeroSuppress !== undefined && Math.abs(value) <= zeroSuppress) {
      value = 0;
    }
    return precision !== undefined ? Number(value.toFixed(precision)) : value;
  }

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

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

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