import * as plugins from '../../plugins.js';
import { ModbusMapper } from './modbus.mapper.js';
import type {
  IModbusCoilConfig,
  IModbusCoilState,
  IModbusCommand,
  IModbusCommandResult,
  IModbusConfig,
  IModbusEntityConfigBase,
  IModbusEntitySnapshot,
  IModbusEvent,
  IModbusHubConfig,
  IModbusHubSnapshot,
  IModbusManualTcpEntry,
  IModbusRegisterConfig,
  IModbusRegisterState,
  IModbusSlaveConfig,
  IModbusSlaveSnapshot,
  IModbusSnapshot,
  IModbusSwitchConfig,
  IModbusTcpCommandShape,
  IModbusTcpResponse,
  TModbusCoilType,
  TModbusDataType,
  TModbusRegisterType,
  TModbusTransport,
  TModbusWriteType,
} from './modbus.types.js';

const defaultHubName = 'modbus_hub';
const defaultTimeoutMs = 5000;
const defaultTcpPort = 502;

type TModbusEventHandler = (eventArg: IModbusEvent) => void;

export class ModbusUnsupportedError extends Error {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'ModbusUnsupportedError';
  }
}

export class ModbusClient {
  private snapshot?: IModbusSnapshot;
  private transactionId = 1;
  private readonly eventHandlers = new Set<TModbusEventHandler>();

  constructor(private readonly config: IModbusConfig) {}

  public async getSnapshot(): Promise<IModbusSnapshot> {
    if (this.config.snapshot) {
      this.snapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
      return this.snapshot;
    }
    if (!this.snapshot) {
      this.snapshot = this.snapshotFromConfig(this.config.connected ?? true);
    }
    return this.snapshot;
  }

  public async refresh(): Promise<IModbusSnapshot> {
    this.snapshot = undefined;
    const snapshot = await this.getSnapshot();
    this.emit({ type: 'snapshot_refreshed', data: snapshot, timestamp: Date.now() });
    return snapshot;
  }

  public onEvent(handlerArg: TModbusEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async sendCommand(commandArg: IModbusCommand): Promise<IModbusCommandResult> {
    this.emit({ type: 'command_mapped', command: commandArg, hubName: commandArg.hub, hubId: commandArg.hubId, unitId: this.commandUnitId(commandArg), entityId: commandArg.entityId, deviceId: commandArg.deviceId, uniqueId: commandArg.uniqueId, timestamp: Date.now() });
    try {
      const result = await this.executeCommand(commandArg);
      this.emit({
        type: result.success ? 'command_executed' : 'command_failed',
        command: commandArg,
        hubName: commandArg.hub,
        hubId: commandArg.hubId,
        unitId: this.commandUnitId(commandArg),
        entityId: commandArg.entityId,
        deviceId: commandArg.deviceId,
        uniqueId: commandArg.uniqueId,
        data: result,
        timestamp: Date.now(),
      });
      return result;
    } catch (errorArg) {
      const result: IModbusCommandResult = { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg), data: { command: commandArg } };
      this.emit({ type: 'command_failed', command: commandArg, data: result, timestamp: Date.now() });
      return result;
    }
  }

  public tcpCommandShape(commandArg: IModbusCommand, hubArg?: IModbusHubSnapshot): IModbusTcpCommandShape {
    const unitId = this.commandUnitId(commandArg);
    const transactionId = this.nextTransactionId();
    const commandPdu = this.pduForCommand(commandArg);
    const frame = this.tcpFrame(transactionId, unitId, commandPdu.pdu);
    return {
      transport: 'tcp',
      host: hubArg?.host,
      port: this.numericPort(hubArg?.port),
      unitId,
      transactionId,
      functionCode: commandPdu.functionCode,
      functionName: commandPdu.functionName,
      address: commandPdu.address,
      quantity: commandPdu.quantity,
      values: commandPdu.values,
      mbap: {
        transactionId,
        protocolId: 0,
        length: commandPdu.pdu.length + 1,
        unitId,
      },
      pduHex: commandPdu.pdu.toString('hex'),
      requestHex: frame.toString('hex'),
    };
  }

  public async destroy(): Promise<void> {
    this.eventHandlers.clear();
  }

  private async executeCommand(commandArg: IModbusCommand): Promise<IModbusCommandResult> {
    if (commandArg.type === 'refresh') {
      return { success: true, data: await this.refresh() };
    }

    const snapshot = await this.getSnapshot();
    if (commandArg.type === 'stop' || commandArg.type === 'restart') {
      this.patchHubOnline(snapshot, commandArg, commandArg.type === 'restart');
      return { success: true, data: { mode: 'snapshot', command: commandArg } };
    }

    if (this.config.commandExecutor) {
      return this.commandResult(await this.config.commandExecutor(commandArg));
    }

    const hub = this.resolveHub(snapshot, commandArg);
    if (!hub) {
      return { success: false, error: `Modbus hub not found: ${commandArg.hub || commandArg.hubId || defaultHubName}` };
    }

    const unsupportedReason = hub.unsupportedReason || this.unsupportedReason(hub.type);
    if (unsupportedReason) {
      return { success: false, error: unsupportedReason };
    }

    const shape = this.tcpCommandShape(commandArg, hub);
    if (!hub.host || !this.numericPort(hub.port)) {
      const snapshotResult = this.snapshotCommand(commandArg, snapshot, shape);
      return snapshotResult || { success: false, error: 'No live Modbus TCP host is configured for this hub.', data: { shape } };
    }

    const response = await this.executeTcpCommand(hub, shape);
    this.patchSnapshotFromResponse(snapshot, commandArg, response);
    return { success: true, data: { response, shape } };
  }

  private async executeTcpCommand(hubArg: IModbusHubSnapshot, shapeArg: IModbusTcpCommandShape): Promise<IModbusTcpResponse> {
    if (hubArg.type !== 'tcp') {
      throw new ModbusUnsupportedError(this.unsupportedReason(hubArg.type) || `Unsupported Modbus transport: ${hubArg.type}`);
    }
    const host = hubArg.host;
    const port = this.numericPort(hubArg.port);
    if (!host || !port) {
      throw new Error('Modbus TCP command requires host and numeric port.');
    }
    const request = Buffer.from(shapeArg.requestHex, 'hex');
    const response = await this.requestTcp(host, port, request, this.timeoutMs(hubArg));
    return this.parseTcpResponse(response, shapeArg);
  }

  private async requestTcp(hostArg: string, portArg: number, frameArg: Buffer, timeoutMsArg: number): Promise<Buffer> {
    return await new Promise<Buffer>((resolve, reject) => {
      const socket = new plugins.net.Socket();
      const chunks: Buffer[] = [];
      let settled = false;
      const finish = (errorArg?: Error, dataArg?: Buffer) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
        } else {
          resolve(dataArg || Buffer.alloc(0));
        }
      };
      socket.setTimeout(timeoutMsArg, () => finish(new Error(`Modbus TCP request timed out after ${timeoutMsArg}ms.`)));
      socket.once('error', (errorArg) => finish(errorArg));
      socket.once('close', () => {
        if (!settled) {
          finish(new Error('Modbus TCP connection closed before a full response was received.'));
        }
      });
      socket.on('data', (dataArg) => {
        chunks.push(Buffer.isBuffer(dataArg) ? dataArg : Buffer.from(dataArg));
        const buffer = Buffer.concat(chunks);
        if (buffer.length < 7) {
          return;
        }
        const responseLength = buffer.readUInt16BE(4);
        const totalLength = 6 + responseLength;
        if (buffer.length >= totalLength) {
          finish(undefined, buffer.subarray(0, totalLength));
        }
      });
      socket.connect(portArg, hostArg, () => socket.write(frameArg));
    });
  }

  private parseTcpResponse(responseArg: Buffer, shapeArg: IModbusTcpCommandShape): IModbusTcpResponse {
    if (responseArg.length < 8) {
      throw new Error('Modbus TCP response is shorter than the MBAP header.');
    }
    const transactionId = responseArg.readUInt16BE(0);
    const protocolId = responseArg.readUInt16BE(2);
    const unitId = responseArg.readUInt8(6);
    const pdu = responseArg.subarray(7);
    const functionCode = pdu.readUInt8(0);
    if (transactionId !== shapeArg.transactionId) {
      throw new Error(`Modbus TCP transaction mismatch: expected ${shapeArg.transactionId}, got ${transactionId}.`);
    }
    if (protocolId !== 0) {
      throw new Error(`Unsupported Modbus TCP protocol id: ${protocolId}.`);
    }
    if (functionCode & 0x80) {
      const exceptionCode = pdu.readUInt8(1);
      throw new Error(`Modbus exception response ${exceptionCode} for function ${functionCode & 0x7f}.`);
    }
    if (functionCode !== shapeArg.functionCode) {
      throw new Error(`Modbus function mismatch: expected ${shapeArg.functionCode}, got ${functionCode}.`);
    }

    const response: IModbusTcpResponse = {
      transactionId,
      unitId,
      functionCode,
      rawHex: responseArg.toString('hex'),
    };
    if (functionCode === 1 || functionCode === 2) {
      const byteCount = pdu.readUInt8(1);
      const bits: boolean[] = [];
      const quantity = shapeArg.quantity || byteCount * 8;
      for (let index = 0; index < quantity; index++) {
        bits.push(Boolean(pdu[2 + Math.floor(index / 8)] & (1 << (index % 8))));
      }
      response.bits = bits;
      response.quantity = quantity;
    } else if (functionCode === 3 || functionCode === 4) {
      const byteCount = pdu.readUInt8(1);
      const registers: number[] = [];
      for (let index = 0; index < byteCount / 2; index++) {
        registers.push(pdu.readUInt16BE(2 + index * 2));
      }
      response.registers = registers;
      response.quantity = registers.length;
    } else if (functionCode === 5 || functionCode === 6) {
      response.address = pdu.readUInt16BE(1);
      response.quantity = 1;
    } else if (functionCode === 15 || functionCode === 16) {
      response.address = pdu.readUInt16BE(1);
      response.quantity = pdu.readUInt16BE(3);
    }
    return response;
  }

  private snapshotCommand(commandArg: IModbusCommand, snapshotArg: IModbusSnapshot, shapeArg: IModbusTcpCommandShape): IModbusCommandResult | undefined {
    if (commandArg.type === 'read_register') {
      const register = this.findRegister(snapshotArg, commandArg.hub || commandArg.hubId, this.commandUnitId(commandArg), commandArg.address, commandArg.inputType || 'holding');
      return register ? { success: true, data: { source: 'snapshot', shape: shapeArg, registers: register.registers, value: ModbusMapper.decodeRegisterValue(register) } } : undefined;
    }
    if (commandArg.type === 'read_coil') {
      const coil = this.findCoil(snapshotArg, commandArg.hub || commandArg.hubId, this.commandUnitId(commandArg), commandArg.address, commandArg.inputType || 'coil');
      return coil ? { success: true, data: { source: 'snapshot', shape: shapeArg, bits: coil.bits, value: coil.value } } : undefined;
    }
    if (commandArg.type === 'write_register' || commandArg.type === 'write_coil') {
      this.patchSnapshotFromCommand(snapshotArg, commandArg);
      return { success: true, data: { source: 'snapshot', shape: shapeArg } };
    }
    return undefined;
  }

  private pduForCommand(commandArg: IModbusCommand): { pdu: Buffer; functionCode: number; functionName: string; address?: number; quantity?: number; values?: number[] | boolean[] } {
    if (commandArg.type === 'read_register') {
      const functionCode = commandArg.inputType === 'input' ? 4 : 3;
      const quantity = commandArg.count || ModbusMapper.registerCountForDataType(commandArg.dataType || 'int16');
      const pdu = Buffer.alloc(5);
      pdu.writeUInt8(functionCode, 0);
      pdu.writeUInt16BE(commandArg.address, 1);
      pdu.writeUInt16BE(quantity, 3);
      return { pdu, functionCode, functionName: functionCode === 4 ? 'read_input_registers' : 'read_holding_registers', address: commandArg.address, quantity };
    }
    if (commandArg.type === 'read_coil') {
      const functionCode = commandArg.inputType === 'discrete_input' ? 2 : 1;
      const quantity = commandArg.count || 1;
      const pdu = Buffer.alloc(5);
      pdu.writeUInt8(functionCode, 0);
      pdu.writeUInt16BE(commandArg.address, 1);
      pdu.writeUInt16BE(quantity, 3);
      return { pdu, functionCode, functionName: functionCode === 2 ? 'read_discrete_inputs' : 'read_coils', address: commandArg.address, quantity };
    }
    if (commandArg.type === 'write_register') {
      const values = Array.isArray(commandArg.value) ? commandArg.value : [commandArg.value];
      if (values.length === 1) {
        const pdu = Buffer.alloc(5);
        pdu.writeUInt8(6, 0);
        pdu.writeUInt16BE(commandArg.address, 1);
        pdu.writeUInt16BE(values[0] & 0xffff, 3);
        return { pdu, functionCode: 6, functionName: 'write_register', address: commandArg.address, quantity: 1, values };
      }
      const pdu = Buffer.alloc(6 + values.length * 2);
      pdu.writeUInt8(16, 0);
      pdu.writeUInt16BE(commandArg.address, 1);
      pdu.writeUInt16BE(values.length, 3);
      pdu.writeUInt8(values.length * 2, 5);
      values.forEach((valueArg, indexArg) => pdu.writeUInt16BE(valueArg & 0xffff, 6 + indexArg * 2));
      return { pdu, functionCode: 16, functionName: 'write_registers', address: commandArg.address, quantity: values.length, values };
    }
    if (commandArg.type === 'write_coil') {
      const values = this.coilValues(commandArg.value);
      if (values.length === 1) {
        const pdu = Buffer.alloc(5);
        pdu.writeUInt8(5, 0);
        pdu.writeUInt16BE(commandArg.address, 1);
        pdu.writeUInt16BE(values[0] ? 0xff00 : 0x0000, 3);
        return { pdu, functionCode: 5, functionName: 'write_coil', address: commandArg.address, quantity: 1, values };
      }
      const byteCount = Math.ceil(values.length / 8);
      const pdu = Buffer.alloc(6 + byteCount);
      pdu.writeUInt8(15, 0);
      pdu.writeUInt16BE(commandArg.address, 1);
      pdu.writeUInt16BE(values.length, 3);
      pdu.writeUInt8(byteCount, 5);
      values.forEach((valueArg, indexArg) => {
        if (valueArg) {
          pdu[6 + Math.floor(indexArg / 8)] |= 1 << (indexArg % 8);
        }
      });
      return { pdu, functionCode: 15, functionName: 'write_coils', address: commandArg.address, quantity: values.length, values };
    }
    throw new Error(`Modbus command has no TCP PDU shape: ${commandArg.type}`);
  }

  private tcpFrame(transactionIdArg: number, unitIdArg: number, pduArg: Buffer): Buffer {
    const frame = Buffer.alloc(7 + pduArg.length);
    frame.writeUInt16BE(transactionIdArg, 0);
    frame.writeUInt16BE(0, 2);
    frame.writeUInt16BE(pduArg.length + 1, 4);
    frame.writeUInt8(unitIdArg, 6);
    pduArg.copy(frame, 7);
    return frame;
  }

  private snapshotFromConfig(connectedArg: boolean): IModbusSnapshot {
    const updatedAt = new Date().toISOString();
    const hubs = this.hubsFromConfig().map((hubArg, indexArg) => this.hubSnapshotFromConfig(hubArg, indexArg, connectedArg, updatedAt));
    return {
      hubs,
      events: [],
      connected: connectedArg && hubs.some((hubArg) => hubArg.online),
      updatedAt,
    };
  }

  private hubSnapshotFromConfig(hubArg: IModbusHubConfig, indexArg: number, connectedArg: boolean, updatedAtArg: string): IModbusHubSnapshot {
    const type = this.transportValue(hubArg.type) || 'tcp';
    const name = hubArg.name || (indexArg ? `${defaultHubName}_${indexArg}` : defaultHubName);
    const id = slug(hubArg.id || name || `${hubArg.host || 'modbus'}_${hubArg.port || defaultTcpPort}`);
    const unsupportedReason = this.unsupportedReason(type);
    const slaveMap = new Map<number, IModbusSlaveSnapshot>();
    const registers: IModbusRegisterState[] = [];
    const coils: IModbusCoilState[] = [];
    const entities: IModbusEntitySnapshot[] = [];
    const defaultUnitId = this.unitIdFromConfig(hubArg, hubArg);

    for (const slave of hubArg.slaves || []) {
      this.ensureSlave(slaveMap, id, this.unitIdFromConfig(slave, hubArg), slave, connectedArg && !unsupportedReason);
    }

    const addRegister = (registerArg: IModbusRegisterConfig, platformArg: 'sensor' | 'number') => {
      const unitId = this.unitIdFromConfig(registerArg, hubArg);
      const register = this.registerStateFromConfig(registerArg, id, unitId, updatedAtArg);
      registers.push(register);
      this.ensureSlave(slaveMap, id, unitId, undefined, connectedArg && !unsupportedReason).registers.push(register);
      entities.push(this.registerEntityFromConfig(registerArg, platformArg, id, name, register, connectedArg && !unsupportedReason));
    };
    const addCoil = (coilArg: IModbusCoilConfig | IModbusSwitchConfig, platformArg: 'binary_sensor' | 'switch') => {
      const unitId = this.unitIdFromConfig(coilArg, hubArg);
      const writeType = this.writeTypeValue((coilArg as IModbusSwitchConfig).writeType || (coilArg as IModbusSwitchConfig).write_type);
      const isRegisterBackedSwitch = platformArg === 'switch' && (writeType === 'holding' || writeType === 'holdings');
      if (isRegisterBackedSwitch) {
        const register = this.registerStateFromSwitchConfig(coilArg as IModbusSwitchConfig, id, unitId, updatedAtArg);
        registers.push(register);
        this.ensureSlave(slaveMap, id, unitId, undefined, connectedArg && !unsupportedReason).registers.push(register);
        entities.push(this.switchRegisterEntityFromConfig(coilArg as IModbusSwitchConfig, id, name, register, connectedArg && !unsupportedReason));
        return;
      }
      const coil = this.coilStateFromConfig(coilArg, id, unitId, updatedAtArg);
      coils.push(coil);
      this.ensureSlave(slaveMap, id, unitId, undefined, connectedArg && !unsupportedReason).coils.push(coil);
      entities.push(this.coilEntityFromConfig(coilArg, platformArg, id, name, coil, connectedArg && !unsupportedReason));
    };

    for (const register of hubArg.registers || []) {
      addRegister(register, register.platform === 'number' || register.writable ? 'number' : 'sensor');
    }
    for (const sensor of hubArg.sensors || []) {
      addRegister(sensor, 'sensor');
    }
    for (const number of hubArg.numbers || []) {
      addRegister(number, 'number');
    }
    for (const coil of hubArg.coils || []) {
      addCoil(coil, coil.platform === 'switch' || coil.writable ? 'switch' : 'binary_sensor');
    }
    for (const binarySensor of [...(hubArg.binarySensors || []), ...(hubArg.binary_sensors || [])]) {
      addCoil(binarySensor, 'binary_sensor');
    }
    for (const switchArg of hubArg.switches || []) {
      addCoil(switchArg, 'switch');
    }

    if (!slaveMap.size && defaultUnitId) {
      this.ensureSlave(slaveMap, id, defaultUnitId, undefined, connectedArg && !unsupportedReason);
    }

    return {
      id,
      name,
      type,
      host: hubArg.host,
      port: hubArg.port || (type === 'tcp' ? defaultTcpPort : undefined),
      online: connectedArg && hubArg.connected !== false && !unsupportedReason,
      manufacturer: hubArg.manufacturer || 'Modbus',
      model: hubArg.model || `${type.toUpperCase()} hub`,
      serialNumber: hubArg.serialNumber || hubArg.serial_number,
      unsupportedReason,
      slaves: [...slaveMap.values()],
      registers,
      coils,
      entities,
      metadata: this.cleanAttributes({
        timeoutMs: this.timeoutMs(hubArg),
        messageWaitMilliseconds: hubArg.messageWaitMilliseconds ?? hubArg.message_wait_milliseconds,
        delay: hubArg.delay,
        ...hubArg.metadata,
      }),
    };
  }

  private registerStateFromConfig(configArg: IModbusRegisterConfig, hubIdArg: string, unitIdArg: number, updatedAtArg: string): IModbusRegisterState {
    const dataType = this.dataTypeValue(configArg.dataType || configArg.data_type);
    return {
      hubId: hubIdArg,
      unitId: unitIdArg,
      address: configArg.address,
      count: configArg.count || ModbusMapper.registerCountForDataType(dataType),
      inputType: this.registerTypeValue(configArg.inputType || configArg.input_type || configArg.registerType || configArg.register_type),
      dataType,
      registers: configArg.registers,
      value: configArg.value,
      updatedAt: updatedAtArg,
      metadata: this.registerMetadata(configArg),
    };
  }

  private registerStateFromSwitchConfig(configArg: IModbusSwitchConfig, hubIdArg: string, unitIdArg: number, updatedAtArg: string): IModbusRegisterState {
    return {
      hubId: hubIdArg,
      unitId: unitIdArg,
      address: configArg.address,
      count: 1,
      inputType: 'holding',
      dataType: 'uint16',
      value: typeof configArg.value === 'number' ? configArg.value : configArg.value ? 1 : 0,
      updatedAt: updatedAtArg,
    };
  }

  private coilStateFromConfig(configArg: IModbusCoilConfig | IModbusSwitchConfig, hubIdArg: string, unitIdArg: number, updatedAtArg: string): IModbusCoilState {
    const slaveCount = configArg.slaveCount || configArg.slave_count || configArg.virtualCount || configArg.virtual_count || 0;
    return {
      hubId: hubIdArg,
      unitId: unitIdArg,
      address: configArg.address,
      count: configArg.count || slaveCount + 1,
      inputType: this.coilTypeValue(configArg.inputType || configArg.input_type || configArg.coilType || configArg.coil_type),
      bits: configArg.bits,
      value: configArg.value,
      updatedAt: updatedAtArg,
    };
  }

  private registerEntityFromConfig(configArg: IModbusRegisterConfig, platformArg: 'sensor' | 'number', hubIdArg: string, hubNameArg: string, registerArg: IModbusRegisterState, availableArg: boolean): IModbusEntitySnapshot {
    const uniqueId = configArg.uniqueId || configArg.unique_id || `modbus_${hubIdArg}_slave_${registerArg.unitId}_${platformArg}_${registerArg.address}`;
    return {
      id: `${platformArg}.${slug(configArg.name)}`,
      name: configArg.name,
      uniqueId,
      platform: platformArg,
      hubId: hubIdArg,
      hubName: hubNameArg,
      unitId: registerArg.unitId,
      address: registerArg.address,
      available: availableArg && configArg.available !== false,
      writable: platformArg === 'number' || Boolean(configArg.writable),
      register: registerArg,
      attributes: this.cleanAttributes({
        unit: configArg.unitOfMeasurement || configArg.unit_of_measurement,
        deviceClass: configArg.deviceClass || configArg.device_class,
        stateClass: configArg.stateClass || configArg.state_class,
        min: configArg.min ?? configArg.minValue ?? configArg.min_value,
        max: configArg.max ?? configArg.maxValue ?? configArg.max_value,
        step: configArg.step,
        scanInterval: configArg.scanInterval ?? configArg.scan_interval,
        scale: configArg.scale,
        offset: configArg.offset,
        precision: configArg.precision,
        swap: configArg.swap,
        ...configArg.metadata,
      }),
    };
  }

  private coilEntityFromConfig(configArg: IModbusCoilConfig | IModbusSwitchConfig, platformArg: 'binary_sensor' | 'switch', hubIdArg: string, hubNameArg: string, coilArg: IModbusCoilState, availableArg: boolean): IModbusEntitySnapshot {
    const switchConfig = configArg as IModbusSwitchConfig;
    const uniqueId = configArg.uniqueId || configArg.unique_id || `modbus_${hubIdArg}_slave_${coilArg.unitId}_${platformArg}_${coilArg.address}`;
    return {
      id: `${platformArg}.${slug(configArg.name)}`,
      name: configArg.name,
      uniqueId,
      platform: platformArg,
      hubId: hubIdArg,
      hubName: hubNameArg,
      unitId: coilArg.unitId,
      address: coilArg.address,
      available: availableArg && configArg.available !== false,
      writable: platformArg === 'switch' || Boolean(configArg.writable),
      coil: coilArg,
      attributes: this.cleanAttributes({
        deviceClass: configArg.deviceClass || configArg.device_class,
        scanInterval: configArg.scanInterval ?? configArg.scan_interval,
        writeType: this.writeTypeValue(switchConfig.writeType || switchConfig.write_type) || 'coil',
        commandOn: switchConfig.commandOn ?? switchConfig.command_on ?? 1,
        commandOff: switchConfig.commandOff ?? switchConfig.command_off ?? 0,
        verify: switchConfig.verify,
        ...configArg.metadata,
      }),
    };
  }

  private switchRegisterEntityFromConfig(configArg: IModbusSwitchConfig, hubIdArg: string, hubNameArg: string, registerArg: IModbusRegisterState, availableArg: boolean): IModbusEntitySnapshot {
    const commandOn = configArg.commandOn ?? configArg.command_on ?? 1;
    const commandOff = configArg.commandOff ?? configArg.command_off ?? 0;
    const value = ModbusMapper.decodeRegisterValue(registerArg);
    return {
      id: `switch.${slug(configArg.name)}`,
      name: configArg.name,
      uniqueId: configArg.uniqueId || configArg.unique_id || `modbus_${hubIdArg}_slave_${registerArg.unitId}_switch_${registerArg.address}`,
      platform: 'switch',
      hubId: hubIdArg,
      hubName: hubNameArg,
      unitId: registerArg.unitId,
      address: registerArg.address,
      available: availableArg && configArg.available !== false,
      writable: true,
      register: registerArg,
      state: value === commandOn ? 'on' : value === commandOff ? 'off' : 'unknown',
      attributes: this.cleanAttributes({
        deviceClass: configArg.deviceClass || configArg.device_class,
        scanInterval: configArg.scanInterval ?? configArg.scan_interval,
        writeType: this.writeTypeValue(configArg.writeType || configArg.write_type) || 'holding',
        commandOn,
        commandOff,
        verify: configArg.verify,
        ...configArg.metadata,
      }),
    };
  }

  private hubsFromConfig(): IModbusHubConfig[] {
    if (this.config.hubs?.length) {
      return this.config.hubs;
    }
    if (this.config.modbus?.length) {
      return this.config.modbus;
    }
    if (this.config.manualEntries?.length) {
      return this.config.manualEntries.map((entryArg) => this.hubFromManualEntry(entryArg));
    }
    return [];
  }

  private hubFromManualEntry(entryArg: IModbusManualTcpEntry): IModbusHubConfig {
    return {
      id: entryArg.id,
      name: entryArg.name || defaultHubName,
      type: 'tcp',
      host: entryArg.host,
      port: entryArg.port || defaultTcpPort,
      slave: entryArg.unitId || entryArg.slave || 1,
      manufacturer: entryArg.manufacturer || 'Modbus',
      model: entryArg.model || 'TCP hub',
      serialNumber: entryArg.serialNumber,
      metadata: entryArg.metadata,
    };
  }

  private ensureSlave(slaveMapArg: Map<number, IModbusSlaveSnapshot>, hubIdArg: string, unitIdArg: number, slaveArg: IModbusSlaveConfig | undefined, onlineArg: boolean): IModbusSlaveSnapshot {
    const existing = slaveMapArg.get(unitIdArg);
    if (existing) {
      return existing;
    }
    const slave: IModbusSlaveSnapshot = {
      id: slug(slaveArg?.id || `${hubIdArg}_${unitIdArg}`),
      hubId: hubIdArg,
      unitId: unitIdArg,
      name: slaveArg?.name || `Modbus unit ${unitIdArg}`,
      online: onlineArg && slaveArg?.connected !== false,
      manufacturer: slaveArg?.manufacturer,
      model: slaveArg?.model,
      serialNumber: slaveArg?.serialNumber || slaveArg?.serial_number,
      registers: [],
      coils: [],
      metadata: slaveArg?.metadata,
    };
    slaveMapArg.set(unitIdArg, slave);
    return slave;
  }

  private normalizeSnapshot(snapshotArg: IModbusSnapshot): IModbusSnapshot {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const hubs = snapshotArg.hubs.map((hubArg) => ({
      ...hubArg,
      id: slug(hubArg.id || hubArg.name),
      name: hubArg.name || defaultHubName,
      type: this.transportValue(hubArg.type) || 'tcp',
      online: hubArg.online && !this.unsupportedReason(hubArg.type),
      unsupportedReason: hubArg.unsupportedReason || this.unsupportedReason(hubArg.type),
      slaves: hubArg.slaves || [],
      registers: hubArg.registers || [],
      coils: hubArg.coils || [],
      entities: hubArg.entities || [],
    }));
    return {
      ...snapshotArg,
      hubs,
      events: snapshotArg.events || [],
      connected: snapshotArg.connected && hubs.some((hubArg) => hubArg.online),
      updatedAt,
    };
  }

  private resolveHub(snapshotArg: IModbusSnapshot, commandArg: IModbusCommand): IModbusHubSnapshot | undefined {
    if (commandArg.hubId) {
      return snapshotArg.hubs.find((hubArg) => hubArg.id === commandArg.hubId || hubArg.name === commandArg.hubId);
    }
    if (commandArg.hub) {
      return snapshotArg.hubs.find((hubArg) => hubArg.name === commandArg.hub || hubArg.id === commandArg.hub);
    }
    return snapshotArg.hubs[0];
  }

  private findRegister(snapshotArg: IModbusSnapshot, hubArg: string | undefined, unitIdArg: number, addressArg: number, inputTypeArg: TModbusRegisterType): IModbusRegisterState | undefined {
    const hubs = hubArg ? snapshotArg.hubs.filter((entryArg) => entryArg.name === hubArg || entryArg.id === hubArg) : snapshotArg.hubs;
    return hubs.flatMap((entryArg) => entryArg.registers).find((registerArg) => registerArg.unitId === unitIdArg && registerArg.address === addressArg && registerArg.inputType === inputTypeArg);
  }

  private findCoil(snapshotArg: IModbusSnapshot, hubArg: string | undefined, unitIdArg: number, addressArg: number, inputTypeArg: TModbusCoilType): IModbusCoilState | undefined {
    const hubs = hubArg ? snapshotArg.hubs.filter((entryArg) => entryArg.name === hubArg || entryArg.id === hubArg) : snapshotArg.hubs;
    return hubs.flatMap((entryArg) => entryArg.coils).find((coilArg) => coilArg.unitId === unitIdArg && coilArg.address === addressArg && coilArg.inputType === inputTypeArg);
  }

  private patchSnapshotFromResponse(snapshotArg: IModbusSnapshot, commandArg: IModbusCommand, responseArg: IModbusTcpResponse): void {
    if (commandArg.type === 'read_register' && responseArg.registers) {
      this.patchRegister(snapshotArg, commandArg, responseArg.registers);
      this.emit({ type: 'register_read', command: commandArg, data: responseArg, timestamp: Date.now() });
    } else if (commandArg.type === 'write_register') {
      this.patchRegister(snapshotArg, commandArg, Array.isArray(commandArg.value) ? commandArg.value : [commandArg.value]);
      this.emit({ type: 'register_written', command: commandArg, data: responseArg, timestamp: Date.now() });
    } else if (commandArg.type === 'read_coil' && responseArg.bits) {
      this.patchCoil(snapshotArg, commandArg, responseArg.bits);
      this.emit({ type: 'coil_read', command: commandArg, data: responseArg, timestamp: Date.now() });
    } else if (commandArg.type === 'write_coil') {
      this.patchCoil(snapshotArg, commandArg, this.coilValues(commandArg.value));
      this.emit({ type: 'coil_written', command: commandArg, data: responseArg, timestamp: Date.now() });
    }
  }

  private patchSnapshotFromCommand(snapshotArg: IModbusSnapshot, commandArg: IModbusCommand): void {
    if (commandArg.type === 'write_register') {
      this.patchRegister(snapshotArg, commandArg, Array.isArray(commandArg.value) ? commandArg.value : [commandArg.value]);
    } else if (commandArg.type === 'write_coil') {
      this.patchCoil(snapshotArg, commandArg, this.coilValues(commandArg.value));
    }
  }

  private patchRegister(snapshotArg: IModbusSnapshot, commandArg: IModbusCommand, registersArg: number[]): void {
    if (commandArg.type !== 'read_register' && commandArg.type !== 'write_register') {
      return;
    }
    const hub = this.resolveHub(snapshotArg, commandArg);
    if (!hub) {
      return;
    }
    const inputType = commandArg.type === 'read_register' ? commandArg.inputType || 'holding' : 'holding';
    const register = hub.registers.find((registerArg) => registerArg.unitId === this.commandUnitId(commandArg) && registerArg.address === commandArg.address && registerArg.inputType === inputType);
    const updatedAt = new Date().toISOString();
    if (register) {
      register.registers = registersArg;
      register.value = undefined;
      register.updatedAt = updatedAt;
    }
  }

  private patchCoil(snapshotArg: IModbusSnapshot, commandArg: IModbusCommand, bitsArg: boolean[]): void {
    if (commandArg.type !== 'read_coil' && commandArg.type !== 'write_coil') {
      return;
    }
    const hub = this.resolveHub(snapshotArg, commandArg);
    if (!hub) {
      return;
    }
    const inputType = commandArg.type === 'read_coil' ? commandArg.inputType || 'coil' : 'coil';
    const coil = hub.coils.find((coilArg) => coilArg.unitId === this.commandUnitId(commandArg) && coilArg.address === commandArg.address && coilArg.inputType === inputType);
    const updatedAt = new Date().toISOString();
    if (coil) {
      coil.bits = bitsArg;
      coil.value = bitsArg[0] ?? false;
      coil.updatedAt = updatedAt;
    }
  }

  private patchHubOnline(snapshotArg: IModbusSnapshot, commandArg: IModbusCommand, onlineArg: boolean): void {
    const hub = this.resolveHub(snapshotArg, commandArg);
    if (!hub) {
      return;
    }
    hub.online = onlineArg && !hub.unsupportedReason;
    for (const slave of hub.slaves) {
      slave.online = hub.online;
    }
    snapshotArg.connected = snapshotArg.hubs.some((hubArg) => hubArg.online);
    snapshotArg.updatedAt = new Date().toISOString();
  }

  private commandResult(resultArg: IModbusCommandResult | unknown): IModbusCommandResult {
    if (resultArg && typeof resultArg === 'object' && 'success' in resultArg) {
      return resultArg as IModbusCommandResult;
    }
    return { success: true, data: resultArg };
  }

  private emit(eventArg: IModbusEvent): void {
    for (const handler of this.eventHandlers) {
      handler(eventArg);
    }
  }

  private commandUnitId(commandArg: IModbusCommand): number {
    return commandArg.unitId || commandArg.slave || 1;
  }

  private nextTransactionId(): number {
    const current = this.transactionId;
    this.transactionId = this.transactionId >= 0xffff ? 1 : this.transactionId + 1;
    return current;
  }

  private coilValues(valueArg: boolean | boolean[] | number | number[]): boolean[] {
    const values = Array.isArray(valueArg) ? valueArg : [valueArg];
    return values.map((value) => typeof value === 'boolean' ? value : Boolean(value));
  }

  private registerTypeValue(valueArg: unknown): TModbusRegisterType {
    return valueArg === 'input' ? 'input' : 'holding';
  }

  private coilTypeValue(valueArg: unknown): TModbusCoilType | TModbusRegisterType {
    if (valueArg === 'discrete_input') {
      return 'discrete_input';
    }
    if (valueArg === 'holding' || valueArg === 'input') {
      return valueArg;
    }
    return 'coil';
  }

  private dataTypeValue(valueArg: unknown): TModbusDataType {
    return typeof valueArg === 'string' && valueArg ? valueArg as TModbusDataType : 'int16';
  }

  private writeTypeValue(valueArg: unknown): TModbusWriteType | undefined {
    return valueArg === 'holding' || valueArg === 'holdings' || valueArg === 'coil' || valueArg === 'coils' ? valueArg : undefined;
  }

  private transportValue(valueArg: unknown): TModbusTransport | undefined {
    return valueArg === 'tcp' || valueArg === 'udp' || valueArg === 'rtuovertcp' || valueArg === 'serial' ? valueArg : undefined;
  }

  private unsupportedReason(typeArg: TModbusTransport): string | undefined {
    if (typeArg === 'serial') {
      return 'Modbus RTU serial is explicitly unsupported in this TypeScript runtime; configure TCP or provide a snapshot/commandExecutor.';
    }
    if (typeArg === 'udp') {
      return 'Modbus UDP transport is not implemented in this TypeScript runtime.';
    }
    if (typeArg === 'rtuovertcp') {
      return 'Modbus RTU-over-TCP framer is not implemented in this TypeScript runtime.';
    }
    return undefined;
  }

  private unitIdFromConfig(configArg: IModbusEntityConfigBase | IModbusSlaveConfig | IModbusHubConfig, hubArg: IModbusHubConfig): number {
    return configArg.unitId || configArg.unit || configArg.slave || configArg.deviceAddress || configArg.device_address || hubArg.slave || hubArg.deviceAddress || hubArg.device_address || 1;
  }

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

  private timeoutMs(hubArg: Pick<IModbusHubConfig, 'timeout' | 'timeoutMs'> | Pick<IModbusHubSnapshot, 'metadata'>): number {
    const metadataTimeout = 'metadata' in hubArg ? this.numberValue(hubArg.metadata?.timeoutMs) : undefined;
    if ('timeoutMs' in hubArg && hubArg.timeoutMs) {
      return hubArg.timeoutMs;
    }
    if ('timeout' in hubArg && hubArg.timeout) {
      return hubArg.timeout * 1000;
    }
    return metadataTimeout || this.config.timeoutMs || defaultTimeoutMs;
  }

  private numberValue(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 registerMetadata(configArg: IModbusRegisterConfig): Record<string, unknown> {
    return this.cleanAttributes({
      scale: configArg.scale,
      offset: configArg.offset,
      precision: configArg.precision,
      swap: configArg.swap,
      minValue: configArg.minValue ?? configArg.min_value,
      maxValue: configArg.maxValue ?? configArg.max_value,
      nanValue: configArg.nanValue ?? configArg.nan_value,
      zeroSuppress: configArg.zeroSuppress ?? configArg.zero_suppress,
      structure: configArg.structure,
    });
  }

  private cloneSnapshot(snapshotArg: IModbusSnapshot): IModbusSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IModbusSnapshot;
  }

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

const slug = (valueArg: string): string => valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'modbus';
