import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  IMikrotikActionDescriptor,
  IMikrotikClientDevice,
  IMikrotikCommand,
  IMikrotikConfig,
  IMikrotikEvent,
  IMikrotikInterfaceStats,
  IMikrotikManualEntry,
  IMikrotikResourceInfo,
  IMikrotikRouterInfo,
  IMikrotikSensorMap,
  IMikrotikSnapshot,
  IMikrotikTrafficStats,
  TMikrotikAction,
  TMikrotikApiPath,
  TMikrotikClientAction,
  TMikrotikInterfaceAction,
  TMikrotikProtocol,
  TMikrotikRouterAction,
} from './mikrotik.types.js';
import { mikrotikDefaultApiPort, mikrotikDomain, mikrotikManufacturer } from './mikrotik.types.js';

type TMikrotikSensorDescriptor = {
  key: keyof IMikrotikSensorMap & string;
  name: string;
  unit?: string;
  deviceClass?: string;
  stateClass?: string;
  entityCategory?: string;
  transform?: (valueArg: unknown) => unknown;
};

const routerSensorDescriptors: TMikrotikSensorDescriptor[] = [
  { key: 'connected_clients', name: 'Clients Connected', unit: 'clients', stateClass: 'measurement' },
  { key: 'cpu_load', name: 'CPU Load', unit: '%', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'cpu_count', name: 'CPU Count', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'cpu_frequency', name: 'CPU Frequency', unit: 'MHz', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'memory_free', name: 'Memory Free', unit: 'MB', deviceClass: 'data_size', stateClass: 'measurement', entityCategory: 'diagnostic', transform: (valueArg) => MikrotikMapper.bytesToMegabytes(MikrotikMapper.numberValue(valueArg)) },
  { key: 'memory_total', name: 'Memory Total', unit: 'MB', deviceClass: 'data_size', stateClass: 'measurement', entityCategory: 'diagnostic', transform: (valueArg) => MikrotikMapper.bytesToMegabytes(MikrotikMapper.numberValue(valueArg)) },
  { key: 'memory_used', name: 'Memory Used', unit: 'MB', deviceClass: 'data_size', stateClass: 'measurement', entityCategory: 'diagnostic', transform: (valueArg) => MikrotikMapper.bytesToMegabytes(MikrotikMapper.numberValue(valueArg)) },
  { key: 'memory_usage_percent', name: 'Memory Usage', unit: '%', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'hdd_free', name: 'HDD Free', unit: 'MB', deviceClass: 'data_size', stateClass: 'measurement', entityCategory: 'diagnostic', transform: (valueArg) => MikrotikMapper.bytesToMegabytes(MikrotikMapper.numberValue(valueArg)) },
  { key: 'hdd_total', name: 'HDD Total', unit: 'MB', deviceClass: 'data_size', stateClass: 'measurement', entityCategory: 'diagnostic', transform: (valueArg) => MikrotikMapper.bytesToMegabytes(MikrotikMapper.numberValue(valueArg)) },
  { key: 'bad_blocks', name: 'Bad Blocks', unit: '%', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'uptime', name: 'Uptime', unit: 's', deviceClass: 'duration', stateClass: 'total', entityCategory: 'diagnostic', transform: (valueArg) => MikrotikMapper.durationSeconds(valueArg) ?? valueArg },
  { key: 'routeros_version', name: 'RouterOS Version', entityCategory: 'diagnostic' },
  { key: 'firmware', name: 'Firmware', entityCategory: 'diagnostic' },
  { key: 'rx_bytes', name: 'Download', unit: 'GB', deviceClass: 'data_size', stateClass: 'total_increasing', transform: (valueArg) => MikrotikMapper.bytesToGigabytes(MikrotikMapper.numberValue(valueArg)) },
  { key: 'tx_bytes', name: 'Upload', unit: 'GB', deviceClass: 'data_size', stateClass: 'total_increasing', transform: (valueArg) => MikrotikMapper.bytesToGigabytes(MikrotikMapper.numberValue(valueArg)) },
  { key: 'rx_rate', name: 'Download Speed', unit: 'Mbit/s', deviceClass: 'data_rate', stateClass: 'measurement' },
  { key: 'tx_rate', name: 'Upload Speed', unit: 'Mbit/s', deviceClass: 'data_rate', stateClass: 'measurement' },
  { key: 'temperature', name: 'Temperature', unit: 'C', deviceClass: 'temperature', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'voltage', name: 'Voltage', unit: 'V', deviceClass: 'voltage', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'current', name: 'Current', unit: 'A', deviceClass: 'current', stateClass: 'measurement', entityCategory: 'diagnostic' },
  { key: 'fan_speed', name: 'Fan Speed', unit: 'rpm', stateClass: 'measurement', entityCategory: 'diagnostic' },
];

export class MikrotikMapper {
  public static toSnapshot(configArg: IMikrotikConfig, connectedArg?: boolean, eventsArg: IMikrotikEvent[] = []): IMikrotikSnapshot {
    const source = configArg.snapshot;
    const manualSnapshots = (configArg.manualEntries || [])
      .map((entryArg) => entryArg.snapshot)
      .filter((snapshotArg): snapshotArg is IMikrotikSnapshot => Boolean(snapshotArg));
    const manualData = this.mergeManualEntries(configArg.manualEntries || []);
    const resources = this.resourceInfo([
      source?.resources,
      ...manualSnapshots.map((snapshotArg) => snapshotArg.resources),
      configArg.resources,
      manualData.resources,
    ]);
    const router = this.routerInfo(configArg, source, manualSnapshots, manualData.router, resources);
    const interfaces = this.uniqueInterfaces([
      ...(source?.interfaces || []),
      ...manualSnapshots.flatMap((snapshotArg) => snapshotArg.interfaces || []),
      ...(configArg.interfaces || []),
      ...manualData.interfaces,
    ]);
    const devices = this.uniqueClients([
      ...(source?.devices || []),
      ...manualSnapshots.flatMap((snapshotArg) => snapshotArg.devices || []),
      ...(configArg.devices || []),
      ...(configArg.clients || []),
      ...manualData.devices,
    ]);
    const traffic = this.trafficInfo([
      source?.traffic,
      ...manualSnapshots.map((snapshotArg) => snapshotArg.traffic),
      configArg.traffic,
      manualData.traffic,
    ], interfaces);
    const sensors = this.sensorMap([
      source?.sensors,
      ...manualSnapshots.map((snapshotArg) => snapshotArg.sensors),
      configArg.sensors,
      manualData.sensors,
    ], resources, devices, interfaces, traffic, router);
    const actions = this.uniqueActions([
      ...(source?.actions || []),
      ...manualSnapshots.flatMap((snapshotArg) => snapshotArg.actions || []),
      ...(configArg.actions || []),
      ...manualData.actions,
      ...this.actionsFromRouter(router),
      ...this.actionsFromClients(devices),
      ...this.actionsFromInterfaces(interfaces),
    ]);
    const hasManualData = Boolean(
      source
      || manualSnapshots.length
      || configArg.router
      || configArg.resources
      || configArg.traffic
      || configArg.devices?.length
      || configArg.clients?.length
      || configArg.interfaces?.length
      || configArg.sensors
      || configArg.actions?.length
      || manualData.hasData
    );

    return {
      connected: connectedArg ?? configArg.connected ?? source?.connected ?? hasManualData,
      source: source?.source || (hasManualData ? 'manual' : 'runtime'),
      updatedAt: source?.updatedAt || new Date().toISOString(),
      router,
      resources,
      devices,
      interfaces,
      sensors,
      traffic,
      actions,
      events: [...(source?.events || []), ...(configArg.events || []), ...eventsArg],
      error: source?.error,
      metadata: {
        ...source?.metadata,
        ...configArg.metadata,
        upstreamPlatforms: ['device_tracker'],
        liveRouterOsApiImplemented: false,
        commandExecutorConfigured: Boolean(configArg.commandExecutor || configArg.nativeClient?.executeCommand),
      },
    };
  }

  public static toDevices(snapshotArg: IMikrotikSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [this.routerDevice(snapshotArg, updatedAt)];
    for (const client of snapshotArg.devices) {
      devices.push(this.clientDevice(client, snapshotArg, updatedAt));
    }
    return devices;
  }

  public static toEntities(snapshotArg: IMikrotikSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const usedIds = new Map<string, number>();
    const routerDeviceId = this.routerDeviceId(snapshotArg);
    const routerName = this.routerName(snapshotArg);
    const uniqueBase = this.uniqueBase(snapshotArg);

    entities.push(this.entity('binary_sensor', `${routerName} Connected`, routerDeviceId, `${uniqueBase}_connected`, snapshotArg.connected ? 'on' : 'off', usedIds, {
      deviceClass: 'connectivity',
      host: snapshotArg.router.host,
      port: snapshotArg.router.port,
      protocol: snapshotArg.router.protocol,
      verifySsl: snapshotArg.router.verifySsl,
    }, true));

    for (const descriptor of routerSensorDescriptors) {
      const rawValue = snapshotArg.sensors[descriptor.key];
      if (rawValue === undefined) {
        continue;
      }
      const value = this.sensorValue(rawValue, descriptor);
      if (value === undefined) {
        continue;
      }
      entities.push(this.entity('sensor', `${routerName} ${descriptor.name}`, routerDeviceId, `${uniqueBase}_${this.slug(descriptor.key)}`, value, usedIds, {
        nativeKey: descriptor.key,
        unit: descriptor.unit,
        deviceClass: descriptor.deviceClass,
        stateClass: descriptor.stateClass,
        entityCategory: descriptor.entityCategory,
      }, snapshotArg.connected));
    }

    for (const iface of snapshotArg.interfaces) {
      this.pushInterfaceEntities(entities, snapshotArg, iface, usedIds);
    }

    for (const client of snapshotArg.devices) {
      this.pushClientEntities(entities, snapshotArg, client, usedIds);
    }

    for (const action of this.snapshotActions(snapshotArg)) {
      const button = this.actionButton(snapshotArg, action, usedIds);
      if (button) {
        entities.push(button);
      }
    }

    return entities;
  }

  public static commandForService(snapshotArg: IMikrotikSnapshot, requestArg: IServiceCallRequest): IMikrotikCommand | undefined {
    const actions = this.snapshotActions(snapshotArg);
    const targetEntity = this.findTargetEntity(snapshotArg, requestArg);

    if (requestArg.domain === mikrotikDomain && requestArg.service === 'reboot') {
      const action = actions.find((actionArg) => actionArg.target === 'router' && actionArg.action === 'reboot');
      return action ? this.command(snapshotArg, requestArg, action) : undefined;
    }

    if (requestArg.domain === mikrotikDomain && (requestArg.service === 'arp_ping' || requestArg.service === 'disconnect_client')) {
      const serviceAction = requestArg.service === 'arp_ping' ? 'arp_ping' : 'disconnect';
      const mac = this.normalizeMac(this.stringValue(requestArg.data?.mac) || this.stringValue(requestArg.data?.macAddress) || this.stringValue(targetEntity?.attributes?.mac));
      const action = actions.find((actionArg) => actionArg.target === 'client' && actionArg.action === serviceAction && (!actionArg.mac || this.normalizeMac(actionArg.mac) === mac));
      return action && mac ? this.command(snapshotArg, requestArg, { ...action, mac }, targetEntity) : undefined;
    }

    if (requestArg.domain === mikrotikDomain && (requestArg.service === 'enable_interface' || requestArg.service === 'disable_interface')) {
      const enabled = requestArg.service === 'enable_interface';
      const interfaceName = this.stringValue(requestArg.data?.interface) || this.stringValue(requestArg.data?.name) || this.stringValue(targetEntity?.attributes?.interfaceName);
      const interfaceId = this.stringValue(requestArg.data?.id) || this.stringValue(targetEntity?.attributes?.interfaceId);
      const action = actions.find((actionArg) => actionArg.target === 'interface' && actionArg.action === 'set_enabled' && this.interfaceActionMatches(actionArg, interfaceId, interfaceName));
      return action ? this.command(snapshotArg, requestArg, action, targetEntity, { enabled }) : undefined;
    }

    if (requestArg.domain === 'button' && requestArg.service === 'press' && targetEntity?.attributes?.nativeAction) {
      const nativeAction = this.stringValue(targetEntity.attributes.nativeAction) as TMikrotikAction | undefined;
      const action = actions.find((actionArg) => nativeAction && actionArg.action === nativeAction && this.actionMatchesEntity(actionArg, targetEntity));
      return action ? this.command(snapshotArg, requestArg, action, targetEntity) : undefined;
    }

    if ((requestArg.domain === 'switch' && (requestArg.service === 'turn_on' || requestArg.service === 'turn_off')) || requestArg.service === 'set_state') {
      if (!targetEntity) {
        return undefined;
      }
      const enabled = requestArg.service === 'turn_on' || requestArg.data?.enabled === true || requestArg.data?.state === true;
      const action = this.switchActionForEntity(snapshotArg, targetEntity);
      return action ? this.command(snapshotArg, requestArg, action, targetEntity, { enabled }) : undefined;
    }

    return undefined;
  }

  public static toIntegrationEvent(eventArg: IMikrotikEvent): IIntegrationEvent {
    return {
      type: eventArg.type === 'command_failed' || eventArg.type === 'error' ? 'error' : 'state_changed',
      integrationDomain: mikrotikDomain,
      deviceId: eventArg.deviceId,
      entityId: eventArg.entityId,
      data: eventArg,
      timestamp: eventArg.timestamp || Date.now(),
    };
  }

  public static normalizeMac(valueArg?: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const compact = valueArg.replace(/[^0-9a-f]/gi, '').toLowerCase();
    return compact.length === 12 ? compact.match(/.{1,2}/g)?.join(':') : valueArg.toLowerCase();
  }

  public static defaultPort(_verifySslArg = false): number {
    return mikrotikDefaultApiPort;
  }

  public static protocol(verifySslArg = false): TMikrotikProtocol {
    return verifySslArg ? 'routeros-api-ssl' : 'routeros-api';
  }

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

  public static numberValue(valueArg: unknown): number | undefined {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (typeof valueArg === 'string' && valueArg.trim()) {
      const parsed = Number(valueArg.replace(/[^0-9.-]/g, ''));
      return Number.isFinite(parsed) ? parsed : undefined;
    }
    return undefined;
  }

  public static bytesToMegabytes(valueArg: number | undefined): number | undefined {
    return valueArg === undefined ? undefined : valueArg / 1048576;
  }

  public static bytesToGigabytes(valueArg: number | undefined): number | undefined {
    return valueArg === undefined ? undefined : valueArg / 1000000000;
  }

  public static durationSeconds(valueArg: unknown): number | undefined {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (valueArg instanceof Date) {
      return undefined;
    }
    if (typeof valueArg !== 'string' || !valueArg.trim()) {
      return undefined;
    }
    const value = valueArg.trim();
    if (/^\d+$/.test(value)) {
      return Number(value);
    }
    const timeMatch = value.match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i);
    if (timeMatch && timeMatch.slice(1).some(Boolean)) {
      const [, weeks, days, hours, minutes, seconds] = timeMatch;
      return (Number(weeks || 0) * 604800) + (Number(days || 0) * 86400) + (Number(hours || 0) * 3600) + (Number(minutes || 0) * 60) + Number(seconds || 0);
    }
    const colonMatch = value.match(/^(?:(\d+)d)?(?:(\d+):)?(\d{1,2}):(\d{2})(?::(\d{2}))?$/i);
    if (colonMatch) {
      const [, days, hours, minutes, secondsOrUndefined, trailingSeconds] = colonMatch;
      const hasTrailingSeconds = trailingSeconds !== undefined;
      return (Number(days || 0) * 86400)
        + (Number(hours || 0) * 3600)
        + (Number(minutes || 0) * (hasTrailingSeconds ? 60 : 1))
        + Number(hasTrailingSeconds ? trailingSeconds : secondsOrUndefined || 0);
    }
    return undefined;
  }

  private static routerInfo(configArg: IMikrotikConfig, sourceArg: IMikrotikSnapshot | undefined, manualSnapshotsArg: IMikrotikSnapshot[], manualRouterArg: IMikrotikRouterInfo | undefined, resourcesArg: IMikrotikResourceInfo): IMikrotikRouterInfo {
    const manualRouter = manualRouterArg || manualSnapshotsArg.find((snapshotArg) => snapshotArg.router)?.router;
    const router = {
      ...sourceArg?.router,
      ...manualRouter,
      ...configArg.router,
    };
    const verifySsl = configArg.verifySsl ?? router.verifySsl ?? sourceArg?.router.verifySsl ?? false;
    const protocol = configArg.protocol || router.protocol || this.protocol(verifySsl);
    const host = configArg.host || router.host || sourceArg?.router.host;
    const port = configArg.port || router.port || (host ? this.defaultPort(verifySsl) : undefined);
    const mac = this.normalizeMac(configArg.uniqueId || router.macAddress || sourceArg?.router.macAddress);
    const serialNumber = router.serialNumber || configArg.uniqueId || sourceArg?.router.serialNumber;
    const name = configArg.name || router.name || router.identity || host || 'Mikrotik';
    return {
      ...router,
      id: router.id || serialNumber || mac || (host ? `${host}:${port || this.defaultPort(verifySsl)}` : undefined) || name,
      host,
      port,
      name,
      identity: router.identity || name,
      model: this.stringValue(configArg.model) || router.model || router.boardName || this.stringValue(resourcesArg.boardName) || this.stringValue(resourcesArg['board-name']) || 'RouterOS device',
      boardName: router.boardName || this.stringValue(resourcesArg.boardName) || this.stringValue(resourcesArg['board-name']),
      routerOsVersion: router.routerOsVersion || this.stringValue(resourcesArg.routerOsVersion) || this.stringValue(resourcesArg.version),
      architectureName: router.architectureName || this.stringValue(resourcesArg.architectureName) || this.stringValue(resourcesArg['architecture-name']),
      macAddress: mac || router.macAddress,
      serialNumber,
      firmware: router.firmware || router.currentFirmware || this.stringValue(resourcesArg.version),
      manufacturer: router.manufacturer || mikrotikManufacturer,
      verifySsl,
      protocol,
      configurationUrl: router.configurationUrl || (host ? `http://${host}` : undefined),
    };
  }

  private static mergeManualEntries(entriesArg: IMikrotikManualEntry[]): {
    router?: IMikrotikRouterInfo;
    resources?: IMikrotikResourceInfo;
    traffic?: IMikrotikTrafficStats;
    devices: IMikrotikClientDevice[];
    interfaces: IMikrotikInterfaceStats[];
    sensors?: IMikrotikSensorMap;
    actions: IMikrotikActionDescriptor[];
    hasData: boolean;
  } {
    const devices: IMikrotikClientDevice[] = [];
    const interfaces: IMikrotikInterfaceStats[] = [];
    const actions: IMikrotikActionDescriptor[] = [];
    const sensors: IMikrotikSensorMap = {};
    let router: IMikrotikRouterInfo | undefined;
    let resources: IMikrotikResourceInfo | undefined;
    let traffic: IMikrotikTrafficStats | undefined;
    let hasData = false;
    for (const entry of entriesArg) {
      if (entry.router) {
        router = { ...router, ...entry.router };
        hasData = true;
      } else if (!router && (entry.host || entry.name || entry.model || entry.macAddress || entry.serialNumber)) {
        router = {
          id: entry.id || entry.serialNumber || entry.macAddress || entry.host,
          host: entry.host,
          port: entry.port,
          name: entry.name,
          model: entry.model,
          serialNumber: entry.serialNumber,
          macAddress: entry.macAddress,
          manufacturer: entry.manufacturer,
          verifySsl: entry.verifySsl,
          protocol: this.protocol(entry.verifySsl || false),
        };
        hasData = true;
      }
      if (entry.resources) {
        resources = { ...resources, ...entry.resources };
        hasData = true;
      }
      if (entry.traffic) {
        traffic = { ...traffic, ...entry.traffic };
        hasData = true;
      }
      devices.push(...(entry.devices || []), ...(entry.clients || []));
      interfaces.push(...(entry.interfaces || []));
      Object.assign(sensors, entry.sensors || {});
      actions.push(...(entry.actions || []));
      hasData = hasData || Boolean(entry.devices?.length || entry.clients?.length || entry.interfaces?.length || entry.sensors || entry.actions?.length);
    }
    return { router, resources, traffic, devices, interfaces, sensors: Object.keys(sensors).length ? sensors : undefined, actions, hasData };
  }

  private static resourceInfo(sourcesArg: Array<IMikrotikResourceInfo | undefined>): IMikrotikResourceInfo {
    const resources: IMikrotikResourceInfo = {};
    for (const source of sourcesArg) {
      Object.assign(resources, source || {});
    }
    resources.freeMemory ??= this.numberValue(resources['free-memory']);
    resources.totalMemory ??= this.numberValue(resources['total-memory']);
    resources.cpuCount ??= this.numberValue(resources['cpu-count']);
    resources.cpuFrequency ??= this.numberValue(resources['cpu-frequency']);
    resources.cpuLoad ??= this.numberValue(resources['cpu-load']);
    resources.freeHddSpace ??= this.numberValue(resources['free-hdd-space']);
    resources.totalHddSpace ??= this.numberValue(resources['total-hdd-space']);
    resources.badBlocks ??= this.numberValue(resources['bad-blocks']);
    resources.architectureName ||= this.stringValue(resources['architecture-name']);
    resources.boardName ||= this.stringValue(resources['board-name']);
    resources.routerOsVersion ||= this.stringValue(resources.version);
    if (resources.usedMemory === undefined && resources.totalMemory !== undefined && resources.freeMemory !== undefined) {
      resources.usedMemory = resources.totalMemory - resources.freeMemory;
    }
    if (resources.memoryUsagePercent === undefined && resources.totalMemory && resources.usedMemory !== undefined) {
      resources.memoryUsagePercent = resources.usedMemory / resources.totalMemory * 100;
    }
    return this.cleanAttributes(resources as Record<string, unknown>) as IMikrotikResourceInfo;
  }

  private static trafficInfo(sourcesArg: Array<IMikrotikTrafficStats | undefined>, interfacesArg: IMikrotikInterfaceStats[]): IMikrotikTrafficStats | undefined {
    const traffic: IMikrotikTrafficStats = {};
    for (const source of sourcesArg) {
      Object.assign(traffic, source || {});
    }
    traffic.rxBytes ??= this.numberValue(traffic.downloadBytes) ?? this.sumInterfaces(interfacesArg, 'rxBytes', 'downloadBytes', 'rx-byte');
    traffic.txBytes ??= this.numberValue(traffic.uploadBytes) ?? this.sumInterfaces(interfacesArg, 'txBytes', 'uploadBytes', 'tx-byte');
    traffic.rxRateMbps ??= this.numberValue(traffic.downloadRateMbps) ?? this.rateMbpsFromTraffic(traffic, 'rx') ?? this.sumInterfaceRatesMbps(interfacesArg, 'rx');
    traffic.txRateMbps ??= this.numberValue(traffic.uploadRateMbps) ?? this.rateMbpsFromTraffic(traffic, 'tx') ?? this.sumInterfaceRatesMbps(interfacesArg, 'tx');
    const cleaned = this.cleanAttributes(traffic as Record<string, unknown>) as IMikrotikTrafficStats;
    return Object.keys(cleaned).length ? cleaned : undefined;
  }

  private static sensorMap(sourcesArg: Array<IMikrotikSensorMap | undefined>, resourcesArg: IMikrotikResourceInfo, devicesArg: IMikrotikClientDevice[], interfacesArg: IMikrotikInterfaceStats[], trafficArg: IMikrotikTrafficStats | undefined, routerArg: IMikrotikRouterInfo): IMikrotikSensorMap {
    const sensors: IMikrotikSensorMap = {};
    for (const source of sourcesArg) {
      Object.assign(sensors, source || {});
    }
    sensors.connected_clients ??= devicesArg.filter((deviceArg) => this.clientConnected(deviceArg)).length;
    sensors.cpu_load ??= this.numberValue(resourcesArg.cpuLoad);
    sensors.cpu_count ??= this.numberValue(resourcesArg.cpuCount);
    sensors.cpu_frequency ??= this.numberValue(resourcesArg.cpuFrequency);
    sensors.memory_free ??= this.numberValue(resourcesArg.freeMemory);
    sensors.memory_total ??= this.numberValue(resourcesArg.totalMemory);
    sensors.memory_used ??= this.numberValue(resourcesArg.usedMemory);
    sensors.memory_usage_percent ??= this.numberValue(resourcesArg.memoryUsagePercent);
    sensors.hdd_free ??= this.numberValue(resourcesArg.freeHddSpace);
    sensors.hdd_total ??= this.numberValue(resourcesArg.totalHddSpace);
    sensors.bad_blocks ??= this.numberValue(resourcesArg.badBlocks);
    sensors.uptime ??= resourcesArg.uptime;
    sensors.routeros_version ??= this.stringValue(resourcesArg.routerOsVersion) || this.stringValue(resourcesArg.version) || routerArg.routerOsVersion;
    sensors.firmware ??= routerArg.currentFirmware || routerArg.firmware;
    sensors.temperature ??= this.numberValue(resourcesArg.temperature);
    sensors.voltage ??= this.numberValue(resourcesArg.voltage);
    sensors.current ??= this.numberValue(resourcesArg.current);
    sensors.fan_speed ??= this.numberValue(resourcesArg.fanSpeed ?? resourcesArg['fan-speed']);
    sensors.rx_bytes ??= this.numberValue(trafficArg?.rxBytes) ?? this.sumInterfaces(interfacesArg, 'rxBytes', 'downloadBytes', 'rx-byte');
    sensors.tx_bytes ??= this.numberValue(trafficArg?.txBytes) ?? this.sumInterfaces(interfacesArg, 'txBytes', 'uploadBytes', 'tx-byte');
    sensors.rx_rate ??= this.numberValue(trafficArg?.rxRateMbps) ?? this.sumInterfaceRatesMbps(interfacesArg, 'rx');
    sensors.tx_rate ??= this.numberValue(trafficArg?.txRateMbps) ?? this.sumInterfaceRatesMbps(interfacesArg, 'tx');
    return this.cleanAttributes(sensors) as IMikrotikSensorMap;
  }

  private static routerDevice(snapshotArg: IMikrotikSnapshot, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
    const sensors = snapshotArg.sensors;
    const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
      { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
      { id: 'connected_clients', capability: 'sensor', name: 'Connected Clients', readable: true, writable: false },
    ];
    const state: plugins.shxInterfaces.data.IDeviceState[] = [
      { featureId: 'connectivity', value: snapshotArg.connected ? 'online' : 'offline', updatedAt: updatedAtArg },
      { featureId: 'connected_clients', value: sensors.connected_clients ?? snapshotArg.devices.filter((deviceArg) => this.clientConnected(deviceArg)).length, updatedAt: updatedAtArg },
    ];
    this.addFeatureState(features, state, 'cpu_load', 'CPU Load', sensors.cpu_load, updatedAtArg, '%');
    this.addFeatureState(features, state, 'memory_usage', 'Memory Usage', sensors.memory_usage_percent, updatedAtArg, '%');
    this.addFeatureState(features, state, 'download_speed', 'Download Speed', sensors.rx_rate, updatedAtArg, 'Mbit/s');
    this.addFeatureState(features, state, 'upload_speed', 'Upload Speed', sensors.tx_rate, updatedAtArg, 'Mbit/s');

    return {
      id: this.routerDeviceId(snapshotArg),
      integrationDomain: mikrotikDomain,
      name: this.routerName(snapshotArg),
      protocol: 'unknown',
      manufacturer: snapshotArg.router.manufacturer || mikrotikManufacturer,
      model: snapshotArg.router.model || snapshotArg.resources.boardName || 'RouterOS device',
      online: snapshotArg.connected,
      features,
      state,
      metadata: this.cleanAttributes({
        host: snapshotArg.router.host,
        port: snapshotArg.router.port,
        protocol: snapshotArg.router.protocol,
        verifySsl: snapshotArg.router.verifySsl,
        macAddress: snapshotArg.router.macAddress,
        serialNumber: snapshotArg.router.serialNumber,
        firmware: snapshotArg.router.firmware || snapshotArg.router.currentFirmware,
        routerOsVersion: snapshotArg.router.routerOsVersion || snapshotArg.resources.version,
        architectureName: snapshotArg.router.architectureName || snapshotArg.resources.architectureName,
        configurationUrl: snapshotArg.router.configurationUrl,
        supportsCapsman: snapshotArg.router.supportsCapsman,
        supportsWireless: snapshotArg.router.supportsWireless,
        supportsWifiwave2: snapshotArg.router.supportsWifiwave2,
        supportsWifi: snapshotArg.router.supportsWifi,
        source: snapshotArg.source,
        liveRouterOsApiImplemented: false,
      }),
    };
  }

  private static clientDevice(clientArg: IMikrotikClientDevice, snapshotArg: IMikrotikSnapshot, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
    return {
      id: this.clientDeviceId(clientArg),
      integrationDomain: mikrotikDomain,
      name: this.clientName(clientArg),
      protocol: 'unknown',
      manufacturer: clientArg.manufacturer || 'Unknown',
      model: clientArg.model || 'Network client',
      online: this.clientConnected(clientArg) && snapshotArg.connected,
      features: [
        { id: 'presence', capability: 'sensor', name: 'Presence', readable: true, writable: false },
        { id: 'ip_address', capability: 'sensor', name: 'IP Address', readable: true, writable: false },
      ],
      state: [
        { featureId: 'presence', value: this.clientConnected(clientArg), updatedAt: updatedAtArg },
        { featureId: 'ip_address', value: this.clientIp(clientArg) || null, updatedAt: updatedAtArg },
      ],
      metadata: this.cleanAttributes({
        mac: this.clientMac(clientArg),
        ipAddress: this.clientIp(clientArg),
        hostname: this.clientName(clientArg),
        connectedTo: clientArg.connectedTo || clientArg.interface,
        ssid: clientArg.ssid,
        source: clientArg.source,
        comment: clientArg.comment,
        lastSeen: this.dateString(clientArg.lastSeen || clientArg.lastActivity || clientArg['last-seen']),
      }),
    };
  }

  private static pushInterfaceEntities(entitiesArg: IIntegrationEntity[], snapshotArg: IMikrotikSnapshot, ifaceArg: IMikrotikInterfaceStats, usedIdsArg: Map<string, number>): void {
    const deviceId = this.routerDeviceId(snapshotArg);
    const ifaceId = this.interfaceId(ifaceArg);
    const ifaceKey = this.slug(String(ifaceId));
    const ifaceName = this.interfaceName(ifaceArg);
    const values: Array<[string, string, unknown, string | undefined, Record<string, unknown>]> = [
      ['rx_bytes', 'Download', this.bytesToGigabytes(this.interfaceBytes(ifaceArg, 'rx')), 'GB', { deviceClass: 'data_size', stateClass: 'total_increasing' }],
      ['tx_bytes', 'Upload', this.bytesToGigabytes(this.interfaceBytes(ifaceArg, 'tx')), 'GB', { deviceClass: 'data_size', stateClass: 'total_increasing' }],
      ['rx_rate', 'Download Speed', this.interfaceRateMbps(ifaceArg, 'rx'), 'Mbit/s', { deviceClass: 'data_rate', stateClass: 'measurement' }],
      ['tx_rate', 'Upload Speed', this.interfaceRateMbps(ifaceArg, 'tx'), 'Mbit/s', { deviceClass: 'data_rate', stateClass: 'measurement' }],
      ['rx_packets', 'RX Packets', this.numberValue(ifaceArg.rxPackets ?? ifaceArg['rx-packet']), 'packets', { stateClass: 'total_increasing', entityCategory: 'diagnostic' }],
      ['tx_packets', 'TX Packets', this.numberValue(ifaceArg.txPackets ?? ifaceArg['tx-packet']), 'packets', { stateClass: 'total_increasing', entityCategory: 'diagnostic' }],
      ['rx_drops', 'RX Drops', this.numberValue(ifaceArg.rxDrops ?? ifaceArg['rx-drop']), 'packets', { stateClass: 'total_increasing', entityCategory: 'diagnostic' }],
      ['tx_drops', 'TX Drops', this.numberValue(ifaceArg.txDrops ?? ifaceArg['tx-drop']), 'packets', { stateClass: 'total_increasing', entityCategory: 'diagnostic' }],
      ['rx_errors', 'RX Errors', this.numberValue(ifaceArg.rxErrors ?? ifaceArg['rx-error']), 'errors', { stateClass: 'total_increasing', entityCategory: 'diagnostic' }],
      ['tx_errors', 'TX Errors', this.numberValue(ifaceArg.txErrors ?? ifaceArg['tx-error']), 'errors', { stateClass: 'total_increasing', entityCategory: 'diagnostic' }],
    ];
    for (const [key, name, value, unit, attrs] of values) {
      if (value === undefined) {
        continue;
      }
      entitiesArg.push(this.entity('sensor', `${this.routerName(snapshotArg)} ${ifaceName} ${name}`, deviceId, `${this.uniqueBase(snapshotArg)}_interface_${ifaceKey}_${key}`, value, usedIdsArg, {
        ...attrs,
        unit,
        interfaceId: ifaceId,
        interfaceName: ifaceArg.name,
        interfaceType: ifaceArg.type,
        ssid: ifaceArg.ssid,
      }, snapshotArg.connected && this.interfaceEnabled(ifaceArg) !== false));
    }
    if (this.interfaceConnected(ifaceArg) !== undefined) {
      entitiesArg.push(this.entity('binary_sensor', `${this.routerName(snapshotArg)} ${ifaceName} Link`, deviceId, `${this.uniqueBase(snapshotArg)}_interface_${ifaceKey}_link`, this.interfaceConnected(ifaceArg) ? 'on' : 'off', usedIdsArg, {
        deviceClass: 'connectivity',
        interfaceId: ifaceId,
        interfaceName: ifaceArg.name,
        interfaceType: ifaceArg.type,
      }, snapshotArg.connected));
    }
    if (this.interfaceHasSetEnabled(ifaceArg)) {
      entitiesArg.push(this.entity('switch', `${this.routerName(snapshotArg)} ${ifaceName}`, deviceId, `${this.uniqueBase(snapshotArg)}_interface_${ifaceKey}_enabled`, this.interfaceEnabled(ifaceArg) === false ? 'off' : 'on', usedIdsArg, {
        nativeType: 'interface',
        nativeAction: 'set_enabled',
        interfaceId: ifaceId,
        interfaceName: ifaceArg.name,
        interfaceType: ifaceArg.type,
        entityCategory: 'config',
        writable: true,
      }, snapshotArg.connected));
    }
  }

  private static pushClientEntities(entitiesArg: IIntegrationEntity[], snapshotArg: IMikrotikSnapshot, clientArg: IMikrotikClientDevice, usedIdsArg: Map<string, number>): void {
    const mac = this.clientMac(clientArg);
    const deviceId = this.clientDeviceId(clientArg);
    const clientName = this.clientName(clientArg);
    entitiesArg.push(this.entity('binary_sensor', `${clientName} Connected`, deviceId, `${this.slug(mac || clientName)}_connected`, this.clientConnected(clientArg) ? 'on' : 'off', usedIdsArg, {
      deviceClass: 'connectivity',
      nativePlatform: 'device_tracker',
      mac,
      ipAddress: this.clientIp(clientArg),
      hostname: clientName,
      connectedTo: clientArg.connectedTo || clientArg.interface,
      interface: clientArg.interface,
      ssid: clientArg.ssid,
      comment: clientArg.comment,
      signalStrength: this.numberValue(clientArg.signalStrength ?? clientArg['signal-strength']),
      signalToNoise: this.numberValue(clientArg.signalToNoise ?? clientArg['signal-to-noise']),
      rxRate: clientArg.rxRate ?? clientArg['rx-rate'],
      txRate: clientArg.txRate ?? clientArg['tx-rate'],
      uptime: clientArg.uptime,
      lastSeen: this.dateString(clientArg.lastSeen || clientArg.lastActivity || clientArg['last-seen']),
    }, snapshotArg.connected));

    const clientSensors: Array<[string, string, unknown, string | undefined, Record<string, unknown>]> = [
      ['signal_strength', 'Signal Strength', this.numberValue(clientArg.signalStrength ?? clientArg['signal-strength']), 'dBm', { deviceClass: 'signal_strength', stateClass: 'measurement', entityCategory: 'diagnostic' }],
      ['signal_to_noise', 'Signal To Noise', this.numberValue(clientArg.signalToNoise ?? clientArg['signal-to-noise']), 'dB', { stateClass: 'measurement', entityCategory: 'diagnostic' }],
      ['rx_rate', 'RX Rate', this.numberValue(clientArg.rxRate ?? clientArg['rx-rate']), undefined, { entityCategory: 'diagnostic' }],
      ['tx_rate', 'TX Rate', this.numberValue(clientArg.txRate ?? clientArg['tx-rate']), undefined, { entityCategory: 'diagnostic' }],
      ['uptime', 'Uptime', this.durationSeconds(clientArg.uptime), 's', { deviceClass: 'duration', stateClass: 'total', entityCategory: 'diagnostic' }],
    ];
    for (const [key, name, value, unit, attrs] of clientSensors) {
      if (value === undefined) {
        continue;
      }
      entitiesArg.push(this.entity('sensor', `${clientName} ${name}`, deviceId, `${this.slug(mac || clientName)}_${key}`, value, usedIdsArg, {
        ...attrs,
        unit,
        mac,
        interface: clientArg.interface,
      }, snapshotArg.connected && this.clientConnected(clientArg)));
    }
  }

  private static actionButton(snapshotArg: IMikrotikSnapshot, actionArg: IMikrotikActionDescriptor, usedIdsArg: Map<string, number>): IIntegrationEntity | undefined {
    if (actionArg.target === 'router' && actionArg.action === 'reboot') {
      return this.entity('button', `${this.routerName(snapshotArg)} Reboot`, this.routerDeviceId(snapshotArg), `${this.uniqueBase(snapshotArg)}_reboot`, 'available', usedIdsArg, {
        nativeType: 'router_action',
        nativeAction: 'reboot',
        actionTarget: 'router',
        entityCategory: 'config',
        writable: true,
      }, snapshotArg.connected, actionArg.entityId);
    }
    if (actionArg.target !== 'client' || !actionArg.mac) {
      return undefined;
    }
    const client = snapshotArg.devices.find((clientArg) => this.clientMac(clientArg) === this.normalizeMac(actionArg.mac));
    if (!client) {
      return undefined;
    }
    return this.entity('button', `${this.clientName(client)} ${this.title(actionArg.action)}`, this.clientDeviceId(client), `${this.slug(actionArg.mac)}_${this.slug(actionArg.action)}`, 'available', usedIdsArg, {
      nativeType: 'client_action',
      nativeAction: actionArg.action,
      actionTarget: 'client',
      mac: this.clientMac(client),
      ipAddress: this.clientIp(client),
      interface: client.interface,
      entityCategory: 'diagnostic',
      writable: true,
    }, snapshotArg.connected && this.clientConnected(client), actionArg.entityId);
  }

  private static command(snapshotArg: IMikrotikSnapshot, requestArg: IServiceCallRequest, actionArg: IMikrotikActionDescriptor, entityArg?: IIntegrationEntity, payloadArg: Record<string, unknown> = {}): IMikrotikCommand | undefined {
    const path = this.commandPath(actionArg);
    if (!path) {
      return undefined;
    }
    const payload = this.cleanAttributes({ ...(requestArg.data || {}), ...payloadArg, actionMetadata: actionArg.metadata });
    const params = this.commandParams(snapshotArg, actionArg, entityArg, payload);
    if (!params) {
      return undefined;
    }
    return {
      type: actionArg.target === 'router' ? 'router.action' : actionArg.target === 'client' ? 'client.action' : 'interface.set',
      service: requestArg.service,
      action: actionArg.action,
      path,
      params,
      target: requestArg.target,
      routerId: this.routerDeviceId(snapshotArg),
      mac: actionArg.mac ? this.normalizeMac(actionArg.mac) : this.normalizeMac(this.stringValue(entityArg?.attributes?.mac)),
      interfaceId: actionArg.id || this.stringValue(entityArg?.attributes?.interfaceId),
      interfaceName: actionArg.interfaceName || this.stringValue(entityArg?.attributes?.interfaceName),
      entityId: entityArg?.id || actionArg.entityId || requestArg.target.entityId,
      deviceId: entityArg?.deviceId || actionArg.deviceId || requestArg.target.deviceId,
      payload,
    };
  }

  private static commandPath(actionArg: IMikrotikActionDescriptor): TMikrotikApiPath | undefined {
    if (actionArg.command) {
      return actionArg.command;
    }
    if (actionArg.target === 'router' && actionArg.action === 'reboot') {
      return '/system/reboot';
    }
    if (actionArg.target === 'interface' && actionArg.action === 'set_enabled') {
      return '/interface/set';
    }
    if (actionArg.target === 'client' && actionArg.action === 'arp_ping') {
      return '/ping';
    }
    return undefined;
  }

  private static commandParams(snapshotArg: IMikrotikSnapshot, actionArg: IMikrotikActionDescriptor, entityArg: IIntegrationEntity | undefined, payloadArg: Record<string, unknown>): Record<string, unknown> | undefined {
    const base = { ...(actionArg.params || {}) };
    if (actionArg.target === 'router' && actionArg.action === 'reboot') {
      return base;
    }
    if (actionArg.target === 'interface' && actionArg.action === 'set_enabled') {
      const id = actionArg.id || this.stringValue(entityArg?.attributes?.interfaceId) || this.stringValue(payloadArg.id);
      const interfaceName = actionArg.interfaceName || this.stringValue(entityArg?.attributes?.interfaceName) || this.stringValue(payloadArg.interface) || this.stringValue(payloadArg.name);
      const enabled = payloadArg.enabled === true || payloadArg.state === true;
      const numbers = id || interfaceName;
      if (!numbers) {
        return undefined;
      }
      return this.cleanAttributes({ ...base, numbers, disabled: enabled ? 'no' : 'yes' });
    }
    if (actionArg.target === 'client' && actionArg.action === 'arp_ping') {
      const client = this.findClient(snapshotArg, actionArg.mac || this.stringValue(entityArg?.attributes?.mac));
      const address = this.stringValue(payloadArg.address) || this.stringValue(payloadArg.ipAddress) || (client ? this.clientIp(client) : undefined);
      const iface = this.stringValue(payloadArg.interface) || this.stringValue(entityArg?.attributes?.interface) || client?.interface;
      if (!address || !iface) {
        return undefined;
      }
      return this.cleanAttributes({ ...base, address, interface: iface, 'arp-ping': 'yes', interval: payloadArg.interval || '100ms', count: payloadArg.count || 3 });
    }
    if (actionArg.target === 'client' && actionArg.action === 'disconnect' && actionArg.command) {
      return this.cleanAttributes({ ...base, mac: actionArg.mac || this.stringValue(entityArg?.attributes?.mac) });
    }
    return undefined;
  }

  private static findTargetEntity(snapshotArg: IMikrotikSnapshot, requestArg: IServiceCallRequest): IIntegrationEntity | undefined {
    const targetEntityId = requestArg.target.entityId;
    if (!targetEntityId) {
      return undefined;
    }
    return this.toEntities(snapshotArg).find((entityArg) => entityArg.id === targetEntityId || entityArg.uniqueId === targetEntityId);
  }

  private static switchActionForEntity(snapshotArg: IMikrotikSnapshot, entityArg: IIntegrationEntity): IMikrotikActionDescriptor | undefined {
    if (entityArg.attributes?.nativeType !== 'interface') {
      return undefined;
    }
    const interfaceId = this.stringValue(entityArg.attributes.interfaceId);
    const interfaceName = this.stringValue(entityArg.attributes.interfaceName);
    return this.snapshotActions(snapshotArg).find((actionArg) => actionArg.target === 'interface' && actionArg.action === 'set_enabled' && this.interfaceActionMatches(actionArg, interfaceId, interfaceName));
  }

  private static actionMatchesEntity(actionArg: IMikrotikActionDescriptor, entityArg: IIntegrationEntity): boolean {
    if (actionArg.target === 'router') {
      return entityArg.attributes?.actionTarget === 'router';
    }
    if (actionArg.target === 'client') {
      return this.normalizeMac(actionArg.mac) === this.normalizeMac(this.stringValue(entityArg.attributes?.mac));
    }
    if (actionArg.target === 'interface') {
      return this.interfaceActionMatches(actionArg, this.stringValue(entityArg.attributes?.interfaceId), this.stringValue(entityArg.attributes?.interfaceName));
    }
    return false;
  }

  private static interfaceActionMatches(actionArg: IMikrotikActionDescriptor, interfaceIdArg?: string, interfaceNameArg?: string): boolean {
    return String(actionArg.id || '') === String(interfaceIdArg || '')
      || Boolean(actionArg.interfaceName && interfaceNameArg && actionArg.interfaceName === interfaceNameArg)
      || Boolean(actionArg.id && interfaceNameArg && String(actionArg.id) === interfaceNameArg);
  }

  private static actionsFromRouter(routerArg: IMikrotikRouterInfo): IMikrotikActionDescriptor[] {
    return (routerArg.actions || []).map((actionArg) => ({ target: 'router', action: actionArg }));
  }

  private static actionsFromClients(devicesArg: IMikrotikClientDevice[]): IMikrotikActionDescriptor[] {
    const actions: IMikrotikActionDescriptor[] = [];
    for (const device of devicesArg) {
      const mac = this.clientMac(device);
      if (!mac) {
        continue;
      }
      for (const action of device.actions || []) {
        actions.push({ target: 'client', action, mac });
      }
    }
    return actions;
  }

  private static actionsFromInterfaces(interfacesArg: IMikrotikInterfaceStats[]): IMikrotikActionDescriptor[] {
    const actions: IMikrotikActionDescriptor[] = [];
    for (const iface of interfacesArg) {
      if (!this.interfaceHasSetEnabled(iface)) {
        continue;
      }
      actions.push({ target: 'interface', action: 'set_enabled', id: this.interfaceId(iface), interfaceName: iface.name });
    }
    return actions;
  }

  private static snapshotActions(snapshotArg: IMikrotikSnapshot): IMikrotikActionDescriptor[] {
    return this.uniqueActions([
      ...(snapshotArg.actions || []),
      ...this.actionsFromRouter(snapshotArg.router),
      ...this.actionsFromClients(snapshotArg.devices),
      ...this.actionsFromInterfaces(snapshotArg.interfaces),
    ]);
  }

  private static uniqueClients(devicesArg: IMikrotikClientDevice[]): IMikrotikClientDevice[] {
    const seen = new Map<string, IMikrotikClientDevice>();
    for (const device of devicesArg) {
      const key = this.clientMac(device) || device.id || this.clientIp(device) || this.clientName(device);
      if (!key) {
        continue;
      }
      seen.set(key, {
        ...seen.get(key),
        ...device,
        mac: this.clientMac(device) || device.mac,
        ipAddress: this.clientIp(device) || device.ipAddress,
        name: this.clientName(device),
      });
    }
    return [...seen.values()];
  }

  private static uniqueInterfaces(interfacesArg: IMikrotikInterfaceStats[]): IMikrotikInterfaceStats[] {
    const seen = new Map<string, IMikrotikInterfaceStats>();
    for (const iface of interfacesArg) {
      if (!iface.name) {
        continue;
      }
      const key = String(this.interfaceId(iface));
      const normalized = this.normalizeInterface(iface);
      seen.set(key, { ...seen.get(key), ...normalized });
    }
    return [...seen.values()];
  }

  private static uniqueActions(actionsArg: IMikrotikActionDescriptor[]): IMikrotikActionDescriptor[] {
    const seen = new Map<string, IMikrotikActionDescriptor>();
    for (const action of actionsArg) {
      const mac = this.normalizeMac(action.mac);
      const key = [action.target, action.action, mac || action.id || action.interfaceName || action.entityId || action.deviceId || 'router'].join(':');
      seen.set(key, { ...action, mac });
    }
    return [...seen.values()];
  }

  private static normalizeInterface(ifaceArg: IMikrotikInterfaceStats): IMikrotikInterfaceStats {
    const disabled = this.booleanValue(ifaceArg.disabled ?? ifaceArg['disabled']);
    const running = this.booleanValue(ifaceArg.running ?? ifaceArg['running']);
    return {
      ...ifaceArg,
      id: ifaceArg.id || ifaceArg['.id'],
      label: ifaceArg.label || ifaceArg.comment || ifaceArg.name,
      macAddress: this.normalizeMac(ifaceArg.macAddress || this.stringValue(ifaceArg['mac-address'])),
      actualMtu: this.numberValue(ifaceArg.actualMtu ?? ifaceArg['actual-mtu']),
      rxBytes: this.numberValue(ifaceArg.rxBytes ?? ifaceArg['rx-byte'] ?? ifaceArg.downloadBytes),
      txBytes: this.numberValue(ifaceArg.txBytes ?? ifaceArg['tx-byte'] ?? ifaceArg.uploadBytes),
      rxPackets: this.numberValue(ifaceArg.rxPackets ?? ifaceArg['rx-packet']),
      txPackets: this.numberValue(ifaceArg.txPackets ?? ifaceArg['tx-packet']),
      rxDrops: this.numberValue(ifaceArg.rxDrops ?? ifaceArg['rx-drop']),
      txDrops: this.numberValue(ifaceArg.txDrops ?? ifaceArg['tx-drop']),
      rxErrors: this.numberValue(ifaceArg.rxErrors ?? ifaceArg['rx-error']),
      txErrors: this.numberValue(ifaceArg.txErrors ?? ifaceArg['tx-error']),
      rxBitsPerSecond: this.numberValue(ifaceArg.rxBitsPerSecond ?? ifaceArg['rx-bits-per-second']),
      txBitsPerSecond: this.numberValue(ifaceArg.txBitsPerSecond ?? ifaceArg['tx-bits-per-second']),
      disabled,
      enabled: ifaceArg.enabled ?? (disabled !== undefined ? !disabled : undefined),
      running,
      connected: ifaceArg.connected ?? running,
      lastLinkUpTime: ifaceArg.lastLinkUpTime || ifaceArg['last-link-up-time'],
    };
  }

  private static routerDeviceId(snapshotArg: IMikrotikSnapshot): string {
    return `${mikrotikDomain}.router.${this.uniqueBase(snapshotArg)}`;
  }

  private static clientDeviceId(clientArg: IMikrotikClientDevice): string {
    return `${mikrotikDomain}.client.${this.slug(this.clientMac(clientArg) || clientArg.id || this.clientIp(clientArg) || this.clientName(clientArg))}`;
  }

  private static routerName(snapshotArg: IMikrotikSnapshot): string {
    return snapshotArg.router.name || snapshotArg.router.identity || snapshotArg.router.host || 'Mikrotik';
  }

  private static uniqueBase(snapshotArg: IMikrotikSnapshot): string {
    return this.slug(snapshotArg.router.serialNumber || snapshotArg.router.macAddress || snapshotArg.router.id || snapshotArg.router.host || this.routerName(snapshotArg));
  }

  private static clientName(clientArg: IMikrotikClientDevice): string {
    return clientArg.name || clientArg.hostname || clientArg.hostName || this.stringValue(clientArg['host-name']) || clientArg.comment || clientArg.macAddress || clientArg.mac || this.stringValue(clientArg['mac-address']) || this.clientIp(clientArg) || 'Unknown device';
  }

  private static clientMac(clientArg: IMikrotikClientDevice): string | undefined {
    return this.normalizeMac(clientArg.macAddress || clientArg.mac || this.stringValue(clientArg['mac-address']));
  }

  private static clientIp(clientArg: IMikrotikClientDevice): string | undefined {
    return this.stringValue(clientArg.ipAddress || clientArg.ip || clientArg.activeAddress || clientArg.address || clientArg['active-address']);
  }

  private static clientConnected(clientArg: IMikrotikClientDevice): boolean {
    if (clientArg.connected !== undefined) {
      return clientArg.connected;
    }
    return Boolean(clientArg.lastSeen || clientArg.lastActivity || clientArg['last-seen'] || clientArg.activeAddress || clientArg['active-address'] || clientArg.interface);
  }

  private static findClient(snapshotArg: IMikrotikSnapshot, macArg?: string): IMikrotikClientDevice | undefined {
    const mac = this.normalizeMac(macArg);
    if (!mac) {
      return undefined;
    }
    return snapshotArg.devices.find((clientArg) => this.clientMac(clientArg) === mac);
  }

  private static interfaceId(ifaceArg: IMikrotikInterfaceStats): string | number {
    return ifaceArg.id || ifaceArg['.id'] || ifaceArg.name;
  }

  private static interfaceName(ifaceArg: IMikrotikInterfaceStats): string {
    return ifaceArg.label || ifaceArg.comment || ifaceArg.name;
  }

  private static interfaceEnabled(ifaceArg: IMikrotikInterfaceStats): boolean | undefined {
    if (ifaceArg.enabled !== undefined) {
      return ifaceArg.enabled;
    }
    const disabled = this.booleanValue(ifaceArg.disabled ?? ifaceArg['disabled']);
    return disabled === undefined ? undefined : !disabled;
  }

  private static interfaceConnected(ifaceArg: IMikrotikInterfaceStats): boolean | undefined {
    return ifaceArg.connected ?? this.booleanValue(ifaceArg.running ?? ifaceArg['running']);
  }

  private static interfaceHasSetEnabled(ifaceArg: IMikrotikInterfaceStats): boolean {
    return ifaceArg.actions?.includes('set_enabled') || this.interfaceEnabled(ifaceArg) !== undefined;
  }

  private static interfaceBytes(ifaceArg: IMikrotikInterfaceStats, directionArg: 'rx' | 'tx'): number | undefined {
    return directionArg === 'rx'
      ? this.numberValue(ifaceArg.rxBytes ?? ifaceArg.downloadBytes ?? ifaceArg['rx-byte'])
      : this.numberValue(ifaceArg.txBytes ?? ifaceArg.uploadBytes ?? ifaceArg['tx-byte']);
  }

  private static interfaceRateMbps(ifaceArg: IMikrotikInterfaceStats, directionArg: 'rx' | 'tx'): number | undefined {
    const mbps = directionArg === 'rx'
      ? this.numberValue(ifaceArg.rxBitsPerSecond ?? ifaceArg['rx-bits-per-second'])
      : this.numberValue(ifaceArg.txBitsPerSecond ?? ifaceArg['tx-bits-per-second']);
    if (mbps !== undefined) {
      return mbps / 1000000;
    }
    const bytesPerSecond = directionArg === 'rx' ? this.numberValue(ifaceArg.rxRate) : this.numberValue(ifaceArg.txRate);
    return bytesPerSecond === undefined ? undefined : bytesPerSecond / 125000;
  }

  private static sumInterfaces(interfacesArg: IMikrotikInterfaceStats[], primaryKeyArg: keyof IMikrotikInterfaceStats, fallbackKeyArg: keyof IMikrotikInterfaceStats, rawKeyArg: string): number | undefined {
    let total = 0;
    let found = false;
    for (const iface of interfacesArg) {
      const value = this.numberValue(iface[primaryKeyArg]) ?? this.numberValue(iface[fallbackKeyArg]) ?? this.numberValue(iface[rawKeyArg]);
      if (value !== undefined) {
        total += value;
        found = true;
      }
    }
    return found ? total : undefined;
  }

  private static sumInterfaceRatesMbps(interfacesArg: IMikrotikInterfaceStats[], directionArg: 'rx' | 'tx'): number | undefined {
    let total = 0;
    let found = false;
    for (const iface of interfacesArg) {
      const value = this.interfaceRateMbps(iface, directionArg);
      if (value !== undefined) {
        total += value;
        found = true;
      }
    }
    return found ? total : undefined;
  }

  private static rateMbpsFromTraffic(trafficArg: IMikrotikTrafficStats, directionArg: 'rx' | 'tx'): number | undefined {
    const explicit = directionArg === 'rx' ? this.numberValue(trafficArg.rxRateMbps) : this.numberValue(trafficArg.txRateMbps);
    if (explicit !== undefined) {
      return explicit;
    }
    const bits = directionArg === 'rx' ? this.numberValue(trafficArg.rxBitsPerSecond) : this.numberValue(trafficArg.txBitsPerSecond);
    if (bits !== undefined) {
      return bits / 1000000;
    }
    const bytes = directionArg === 'rx' ? this.numberValue(trafficArg.rxRate) : this.numberValue(trafficArg.txRate);
    return bytes === undefined ? undefined : bytes / 125000;
  }

  private static entity(platformArg: TEntityPlatform, nameArg: string, deviceIdArg: string, uniqueIdArg: string, stateArg: unknown, usedIdsArg: Map<string, number>, attributesArg: Record<string, unknown> = {}, availableArg = true, explicitIdArg?: string): IIntegrationEntity {
    const baseId = explicitIdArg || `${platformArg}.${this.slug(nameArg)}`;
    const used = usedIdsArg.get(baseId) || 0;
    usedIdsArg.set(baseId, used + 1);
    return {
      id: used ? `${baseId}_${used + 1}` : baseId,
      uniqueId: `${mikrotikDomain}_${this.slug(uniqueIdArg)}`,
      integrationDomain: mikrotikDomain,
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes(attributesArg),
      available: availableArg,
    };
  }

  private static sensorValue(valueArg: unknown, descriptorArg: TMikrotikSensorDescriptor): unknown {
    if (valueArg === undefined || valueArg === null) {
      return valueArg;
    }
    return descriptorArg.transform ? descriptorArg.transform(valueArg) : valueArg;
  }

  private static addFeatureState(featuresArg: plugins.shxInterfaces.data.IDeviceFeature[], stateArg: plugins.shxInterfaces.data.IDeviceState[], idArg: string, nameArg: string, valueArg: unknown, updatedAtArg: string, unitArg?: string): void {
    if (valueArg === undefined) {
      return;
    }
    featuresArg.push({ id: idArg, capability: 'sensor', name: nameArg, readable: true, writable: false, unit: unitArg });
    stateArg.push({ featureId: idArg, value: this.deviceStateValue(valueArg), updatedAt: updatedAtArg });
  }

  private static deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
    if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean' || valueArg === null) {
      return valueArg;
    }
    if (valueArg && typeof valueArg === 'object') {
      return valueArg as Record<string, unknown>;
    }
    return null;
  }

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

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

  private static booleanValue(valueArg: unknown): boolean | undefined {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'string') {
      if (['true', '1', 'yes', 'on'].includes(valueArg.toLowerCase())) {
        return true;
      }
      if (['false', '0', 'no', 'off'].includes(valueArg.toLowerCase())) {
        return false;
      }
    }
    return undefined;
  }

  private static dateString(valueArg: IMikrotikClientDevice['lastSeen']): string | undefined {
    if (valueArg instanceof Date) {
      return valueArg.toISOString();
    }
    if (typeof valueArg === 'number') {
      return new Date(valueArg < 100000000000 ? valueArg * 1000 : valueArg).toISOString();
    }
    return typeof valueArg === 'string' ? valueArg : undefined;
  }

  private static title(valueArg: string): string {
    return valueArg.replace(/_/g, ' ').replace(/\b\w/g, (matchArg) => matchArg.toUpperCase());
  }
}
