import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { MysensorsMapper } from './mysensors.mapper.js';
import type { IMysensorsConfig } from './mysensors.types.js';
import { mysensorsDefaultCollectMs, mysensorsDefaultTcpPort, mysensorsDefaultTimeoutMs, mysensorsProfile, type TMysensorsGatewayType } from './mysensors.types.js';

interface IMysensorsEndpoint {
  host: string;
  port: number;
}

interface IMysensorsMessage {
  nodeId: number;
  childId: number;
  command: number;
  commandName: string;
  ack: boolean;
  subType: number;
  subTypeName: string;
  payload: string;
  raw: string;
}

interface IMysensorsPresentation {
  type: number;
  typeName: string;
  description?: string;
}

const messageTypeNames: Record<number, string> = {
  0: 'presentation',
  1: 'set',
  2: 'req',
  3: 'internal',
  4: 'stream',
};

const presentationTypeNames: Record<number, string> = {
  0: 'S_DOOR',
  1: 'S_MOTION',
  2: 'S_SMOKE',
  3: 'S_BINARY',
  4: 'S_DIMMER',
  5: 'S_COVER',
  6: 'S_TEMP',
  7: 'S_HUM',
  8: 'S_BARO',
  9: 'S_WIND',
  10: 'S_RAIN',
  11: 'S_UV',
  12: 'S_WEIGHT',
  13: 'S_POWER',
  14: 'S_HEATER',
  15: 'S_DISTANCE',
  16: 'S_LIGHT_LEVEL',
  17: 'S_ARDUINO_NODE',
  18: 'S_ARDUINO_REPEATER_NODE',
  19: 'S_LOCK',
  20: 'S_IR',
  21: 'S_WATER',
  22: 'S_AIR_QUALITY',
  23: 'S_CUSTOM',
  24: 'S_DUST',
  25: 'S_SCENE_CONTROLLER',
  26: 'S_RGB_LIGHT',
  27: 'S_RGBW_LIGHT',
  28: 'S_COLOR_SENSOR',
  29: 'S_HVAC',
  30: 'S_MULTIMETER',
  31: 'S_SPRINKLER',
  32: 'S_WATER_LEAK',
  33: 'S_SOUND',
  34: 'S_VIBRATION',
  35: 'S_MOISTURE',
  36: 'S_INFO',
  37: 'S_GAS',
  38: 'S_GPS',
  39: 'S_WATER_QUALITY',
};

const valueTypeNames: Record<number, string> = {
  0: 'V_TEMP',
  1: 'V_HUM',
  2: 'V_STATUS',
  3: 'V_PERCENTAGE',
  4: 'V_PRESSURE',
  5: 'V_FORECAST',
  6: 'V_RAIN',
  7: 'V_RAINRATE',
  8: 'V_WIND',
  9: 'V_GUST',
  10: 'V_DIRECTION',
  11: 'V_UV',
  12: 'V_WEIGHT',
  13: 'V_DISTANCE',
  14: 'V_IMPEDANCE',
  15: 'V_ARMED',
  16: 'V_TRIPPED',
  17: 'V_WATT',
  18: 'V_KWH',
  19: 'V_SCENE_ON',
  20: 'V_SCENE_OFF',
  21: 'V_HVAC_FLOW_STATE',
  22: 'V_HVAC_SPEED',
  23: 'V_LIGHT_LEVEL',
  24: 'V_VAR1',
  25: 'V_VAR2',
  26: 'V_VAR3',
  27: 'V_VAR4',
  28: 'V_VAR5',
  29: 'V_UP',
  30: 'V_DOWN',
  31: 'V_STOP',
  32: 'V_IR_SEND',
  33: 'V_IR_RECEIVE',
  34: 'V_FLOW',
  35: 'V_VOLUME',
  36: 'V_LOCK_STATUS',
  37: 'V_LEVEL',
  38: 'V_VOLTAGE',
  39: 'V_CURRENT',
  40: 'V_RGB',
  41: 'V_RGBW',
  42: 'V_ID',
  43: 'V_UNIT_PREFIX',
  44: 'V_HVAC_SETPOINT_COOL',
  45: 'V_HVAC_SETPOINT_HEAT',
  46: 'V_HVAC_FLOW_MODE',
  47: 'V_TEXT',
  48: 'V_CUSTOM',
  49: 'V_POSITION',
  50: 'V_IR_RECORD',
  51: 'V_PH',
  52: 'V_ORP',
  53: 'V_EC',
  54: 'V_VAR',
  55: 'V_VA',
  56: 'V_POWER_FACTOR',
};

const valueTypeIds = Object.fromEntries(Object.entries(valueTypeNames).map(([keyArg, valueArg]) => [valueArg, Number(keyArg)]));

export class MysensorsClient extends SimpleLocalClient<IMysensorsConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IMysensorsConfig) {
    super(mysensorsProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.cachedSnapshot) {
      return clone(this.cachedSnapshot);
    }
    if (this.configArg.client || this.configArg.snapshotProvider || (!forceRefreshArg && this.configArg.snapshot)) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }
    if (!this.endpoint()) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }
    if (this.gatewayType() !== 'TCP') {
      this.cachedSnapshot = this.unsupportedGatewaySnapshot();
      return clone(this.cachedSnapshot);
    }

    try {
      const messages = await this.collectTcpMessages();
      this.cachedSnapshot = this.snapshotFromMessages(messages);
    } catch (errorArg) {
      this.cachedSnapshot = MysensorsMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
    }
    return clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor) {
      return super.execute(requestArg);
    }
    if (this.gatewayType() !== 'TCP') {
      return { success: false, error: 'MySensors native control supports documented TCP/Ethernet gateways only; use an injected executor for Serial or MQTT gateways.' };
    }
    if (!this.endpoint()) {
      return { success: false, error: 'MySensors TCP control requires config.host/config.url plus nodeId, childId, valueType, and value, or an injected executor.' };
    }

    try {
      const command = await this.commandForRequest(requestArg);
      await this.sendTcpLine(command.line);
      this.cachedSnapshot = undefined;
      return { success: true, data: command };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await this.configArg.client?.destroy?.();
  }

  private async collectTcpMessages(): Promise<IMysensorsMessage[]> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('MySensors TCP snapshot requires config.host or config.url.');
    }
    const timeoutMs = this.configArg.timeoutMs || mysensorsDefaultTimeoutMs;
    const collectMs = this.configArg.collectMs || mysensorsDefaultCollectMs;
    const initialCommand = this.configArg.tcpCommand ?? mysensorsProfile.defaultTcpCommand;

    return new Promise<IMysensorsMessage[]>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });
      const messages: IMysensorsMessage[] = [];
      let buffer = '';
      let settled = false;
      let collectTimer: NodeJS.Timeout | undefined;

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        if (collectTimer) {
          clearTimeout(collectTimer);
        }
        socket.removeAllListeners();
        socket.end();
        socket.destroy();
        errorArg ? reject(errorArg) : resolve(messages);
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`MySensors TCP gateway ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => {
        if (initialCommand) {
          socket.write(initialCommand.endsWith('\n') ? initialCommand : `${initialCommand}\n`);
        }
        collectTimer = setTimeout(() => finish(), collectMs);
      });
      socket.on('data', (chunkArg) => {
        buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : Buffer.from(chunkArg).toString('utf8');
        const lines = buffer.split(/\r?\n/);
        buffer = lines.pop() || '';
        for (const line of lines) {
          const message = parseMessage(line);
          if (message) {
            messages.push(message);
          }
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('end', () => finish());
      socket.on('close', () => finish());
    });
  }

  private snapshotFromMessages(messagesArg: IMysensorsMessage[]): ISimpleLocalSnapshot {
    const endpoint = this.endpoint();
    const presentations = new Map<string, IMysensorsPresentation>();
    const nodeNames = new Map<number, string>();

    for (const message of messagesArg) {
      if (message.command === 0) {
        presentations.set(childKey(message.nodeId, message.childId), {
          type: message.subType,
          typeName: presentationTypeNames[message.subType] || `S_${message.subType}`,
          description: message.payload || undefined,
        });
      } else if (message.command === 3 && message.subType === 11 && message.payload) {
        nodeNames.set(message.nodeId, message.payload);
      }
    }

    const latestValues = new Map<string, IMysensorsMessage>();
    for (const message of messagesArg) {
      if (message.command === 1) {
        latestValues.set(`${childKey(message.nodeId, message.childId)}:${message.subType}`, message);
      }
    }

    const baseId = this.configArg.uniqueId || this.configArg.deviceId || (endpoint ? `${endpoint.host}:${endpoint.port}` : mysensorsProfile.domain);
    const entities: ISimpleLocalEntitySnapshot[] = [...latestValues.values()].map((messageArg) => {
      const presentation = presentations.get(childKey(messageArg.nodeId, messageArg.childId));
      const platform = platformForMessage(messageArg, presentation);
      const valueTypeName = valueTypeNames[messageArg.subType] || `V_${messageArg.subType}`;
      const name = presentation?.description || `${nodeNames.get(messageArg.nodeId) || `Node ${messageArg.nodeId}`} ${valueTypeName.replace(/^V_/, '').replace(/_/g, ' ').toLowerCase()}`;
      return {
        id: `node_${messageArg.nodeId}_child_${messageArg.childId}_${valueTypeName.toLowerCase()}`,
        uniqueId: `${baseId}_node_${messageArg.nodeId}_child_${messageArg.childId}_${messageArg.subType}`,
        name: titleCase(name),
        platform,
        state: coercePayload(messageArg.payload),
        available: true,
        writable: isWritableValue(messageArg.subType, platform),
        unit: unitForValueType(messageArg.subType),
        deviceClass: deviceClassForValueType(messageArg.subType),
        attributes: cleanAttributes({
          nodeId: messageArg.nodeId,
          childId: messageArg.childId,
          valueType: messageArg.subType,
          valueTypeName,
          presentationType: presentation?.type,
          presentationTypeName: presentation?.typeName,
          raw: messageArg.raw,
        }),
      };
    });

    if (!entities.length) {
      entities.push({
        id: 'gateway_online',
        uniqueId: `${baseId}_gateway_online`,
        name: 'Gateway Online',
        platform: 'binary_sensor',
        state: true,
        available: true,
        writable: false,
        attributes: { capturedMessages: messagesArg.length },
      });
    }

    return MysensorsMapper.toSnapshot({
      config: this.configArg,
      rawData: {
        device: {
          id: baseId,
          name: this.configArg.name || mysensorsProfile.defaultName,
          manufacturer: mysensorsProfile.manufacturer,
          model: 'MySensors TCP/Ethernet Gateway',
          host: endpoint?.host,
          port: endpoint?.port,
          protocol: 'tcp',
          attributes: cleanAttributes({ gatewayType: 'TCP', protocolVersion: this.configArg.protocolVersion, capturedMessages: messagesArg.length }),
        },
        messages: messagesArg,
        entities,
        online: true,
        updatedAt: new Date().toISOString(),
        source: 'tcp',
      },
      online: true,
      source: 'tcp',
    });
  }

  private async commandForRequest(requestArg: IServiceCallRequest): Promise<{ line: string; nodeId: number; childId: number; valueType: number; value: string; ack: 0 | 1 }> {
    const data = requestArg.data || {};
    const target = await this.commandTarget(requestArg);
    const valueType = this.valueTypeForRequest(requestArg);
    const value = this.valueForRequest(requestArg, valueType);
    const ack = boolish(data.ack ?? this.configArg.ack) ? 1 : 0;
    const line = `${target.nodeId};${target.childId};1;${ack};${valueType};${value}`;
    return { line, nodeId: target.nodeId, childId: target.childId, valueType, value, ack };
  }

  private async commandTarget(requestArg: IServiceCallRequest): Promise<{ nodeId: number; childId: number }> {
    const data = requestArg.data || {};
    const directNodeId = numberValue(data.nodeId ?? data.node_id ?? this.configArg.nodeId);
    const directChildId = numberValue(data.childId ?? data.child_id ?? this.configArg.childId);
    if (directNodeId !== undefined && directChildId !== undefined) {
      return { nodeId: directNodeId, childId: directChildId };
    }

    const fromText = targetFromText(requestArg.target.entityId) || targetFromText(requestArg.target.deviceId);
    if (fromText) {
      return fromText;
    }

    const snapshot = await this.getSnapshot();
    const writable = snapshot.entities.filter((entityArg) => entityArg.writable && numberValue(entityArg.attributes?.nodeId) !== undefined && numberValue(entityArg.attributes?.childId) !== undefined);
    if (writable.length === 1) {
      return { nodeId: numberValue(writable[0].attributes?.nodeId)!, childId: numberValue(writable[0].attributes?.childId)! };
    }
    throw new Error('MySensors TCP control requires data.nodeId and data.childId unless exactly one writable captured entity is available.');
  }

  private valueTypeForRequest(requestArg: IServiceCallRequest): number {
    const data = requestArg.data || {};
    const direct = valueTypeId(data.valueType ?? data.value_type ?? this.configArg.valueType);
    if (direct !== undefined) {
      return direct;
    }
    if (requestArg.service === 'turn_on' || requestArg.service === 'turn_off' || requestArg.service === 'toggle') {
      return 2;
    }
    if (requestArg.service === 'open_cover') {
      return 29;
    }
    if (requestArg.service === 'close_cover') {
      return 30;
    }
    if (requestArg.service === 'stop_cover') {
      return 31;
    }
    if (requestArg.service === 'set_temperature') {
      return stringValue(data.hvac_mode)?.toLowerCase() === 'cool' ? 44 : 45;
    }
    if (requestArg.service === 'set_hvac_mode') {
      return 21;
    }
    if (requestArg.service === 'press') {
      return 19;
    }
    throw new Error('MySensors TCP control requires data.valueType for this service.');
  }

  private valueForRequest(requestArg: IServiceCallRequest, valueTypeArg: number): string {
    const data = requestArg.data || {};
    const direct = data.value ?? data.payload;
    if (direct !== undefined) {
      return String(direct);
    }
    if (requestArg.service === 'turn_on') {
      return '1';
    }
    if (requestArg.service === 'turn_off') {
      return '0';
    }
    if (requestArg.service === 'toggle') {
      throw new Error('MySensors toggle requires explicit data.value because the TCP serial API has no generic toggle command.');
    }
    if (requestArg.service === 'open_cover' || requestArg.service === 'close_cover' || requestArg.service === 'stop_cover' || requestArg.service === 'press') {
      return '1';
    }
    if (requestArg.service === 'set_temperature') {
      const temperature = numberValue(data.temperature ?? data.targetTemperature);
      if (temperature === undefined) {
        throw new Error('MySensors set_temperature requires data.temperature.');
      }
      return String(temperature);
    }
    if (requestArg.service === 'set_hvac_mode') {
      const mode = stringValue(data.hvac_mode ?? data.mode)?.toLowerCase();
      const mapped: Record<string, string> = { off: 'Off', heat: 'HeatOn', cool: 'CoolOn', auto: 'AutoChangeOver' };
      if (!mode || !mapped[mode]) {
        throw new Error('MySensors set_hvac_mode requires data.hvac_mode off, heat, cool, or auto.');
      }
      return mapped[mode];
    }
    if (valueTypeArg === 2) {
      return '1';
    }
    throw new Error('MySensors TCP control requires data.value for this service.');
  }

  private async sendTcpLine(lineArg: string): Promise<void> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('MySensors TCP command requires config.host or config.url.');
    }
    const timeoutMs = this.configArg.timeoutMs || mysensorsDefaultTimeoutMs;
    await new Promise<void>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });
      let settled = false;
      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.end();
        socket.destroy();
        errorArg ? reject(errorArg) : resolve();
      };
      socket.setTimeout(timeoutMs, () => finish(new Error(`MySensors TCP command to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => socket.write(lineArg.endsWith('\n') ? lineArg : `${lineArg}\n`, () => finish()));
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private unsupportedGatewaySnapshot(): ISimpleLocalSnapshot {
    return MysensorsMapper.toSnapshot({
      config: this.configArg,
      online: false,
      source: 'runtime',
      error: `MySensors native live support is limited to documented TCP/Ethernet gateways; ${this.gatewayType()} requires an injected client, commandExecutor, snapshotProvider, or rawData.`,
    });
  }

  private gatewayType(): TMysensorsGatewayType {
    return this.configArg.gatewayType || 'TCP';
  }

  private endpoint(): IMysensorsEndpoint | undefined {
    const parsed = parseEndpoint(this.configArg.url);
    if (parsed) {
      return parsed;
    }
    if (!this.configArg.host) {
      return undefined;
    }
    return { host: this.configArg.host, port: this.configArg.port || mysensorsDefaultTcpPort };
  }
}

function parseMessage(lineArg: string): IMysensorsMessage | undefined {
  const line = lineArg.trim();
  if (!line) {
    return undefined;
  }
  const parts = line.split(';');
  if (parts.length < 6) {
    return undefined;
  }
  const nodeId = numberValue(parts[0]);
  const childId = numberValue(parts[1]);
  const command = numberValue(parts[2]);
  const ack = numberValue(parts[3]);
  const subType = numberValue(parts[4]);
  if (nodeId === undefined || childId === undefined || command === undefined || ack === undefined || subType === undefined) {
    return undefined;
  }
  const payload = parts.slice(5).join(';');
  return {
    nodeId,
    childId,
    command,
    commandName: messageTypeNames[command] || `command_${command}`,
    ack: ack === 1,
    subType,
    subTypeName: command === 0 ? presentationTypeNames[subType] || `S_${subType}` : valueTypeNames[subType] || `V_${subType}`,
    payload,
    raw: line,
  };
}

function platformForMessage(messageArg: IMysensorsMessage, presentationArg?: IMysensorsPresentation): ISimpleLocalEntitySnapshot['platform'] {
  const presentationType = presentationArg?.typeName;
  if (presentationType && ['S_BINARY', 'S_DIMMER', 'S_RGB_LIGHT', 'S_RGBW_LIGHT'].includes(presentationType)) {
    return presentationType === 'S_BINARY' ? 'switch' : 'light';
  }
  if (presentationType === 'S_COVER') {
    return 'cover';
  }
  if (presentationType === 'S_HVAC' || presentationType === 'S_HEATER') {
    return 'climate';
  }
  if (messageArg.subType === 15 || messageArg.subType === 16) {
    return 'binary_sensor';
  }
  if ([2, 3, 29, 30, 31, 36, 40, 41].includes(messageArg.subType)) {
    return messageArg.subType === 2 || messageArg.subType === 36 ? 'switch' : 'light';
  }
  if (messageArg.subType === 47) {
    return 'text';
  }
  return 'sensor';
}

function isWritableValue(valueTypeArg: number, platformArg?: ISimpleLocalEntitySnapshot['platform']): boolean {
  return Boolean(platformArg && ['button', 'climate', 'cover', 'light', 'switch', 'text'].includes(platformArg)) || [2, 3, 19, 20, 21, 22, 29, 30, 31, 32, 36, 40, 41, 44, 45, 47].includes(valueTypeArg);
}

function unitForValueType(valueTypeArg: number): string | undefined {
  const units: Record<number, string> = {
    0: 'C',
    1: '%',
    3: '%',
    4: 'hPa',
    8: 'm/s',
    10: 'deg',
    11: 'UV index',
    12: 'kg',
    13: 'cm',
    17: 'W',
    18: 'kWh',
    23: '%',
    37: 'level',
    38: 'V',
    39: 'A',
    44: 'C',
    45: 'C',
    51: 'pH',
    52: 'mV',
    53: 'uS/cm',
    54: 'var',
    55: 'VA',
  };
  return units[valueTypeArg];
}

function deviceClassForValueType(valueTypeArg: number): string | undefined {
  const classes: Record<number, string> = {
    0: 'temperature',
    1: 'humidity',
    4: 'pressure',
    17: 'power',
    18: 'energy',
    38: 'voltage',
    39: 'current',
    44: 'temperature',
    45: 'temperature',
  };
  return classes[valueTypeArg];
}

function coercePayload(valueArg: string): unknown {
  if (valueArg === '0' || valueArg === '1') {
    return valueArg === '1';
  }
  const numeric = Number(valueArg);
  if (valueArg.trim() && Number.isFinite(numeric)) {
    return numeric;
  }
  return valueArg;
}

function valueTypeId(valueArg: unknown): number | undefined {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg !== 'string' || !valueArg.trim()) {
    return undefined;
  }
  const normalized = valueArg.trim().toUpperCase();
  const withPrefix = normalized.startsWith('V_') ? normalized : `V_${normalized}`;
  return valueTypeIds[withPrefix] ?? numberValue(normalized);
}

function targetFromText(valueArg: unknown): { nodeId: number; childId: number } | undefined {
  if (typeof valueArg !== 'string') {
    return undefined;
  }
  const match = /node[_-]?(\d+).*child[_-]?(\d+)/i.exec(valueArg);
  return match ? { nodeId: Number(match[1]), childId: Number(match[2]) } : undefined;
}

function childKey(nodeIdArg: number, childIdArg: number): string {
  return `${nodeIdArg}:${childIdArg}`;
}

function parseEndpoint(valueArg: unknown): IMysensorsEndpoint | undefined {
  if (typeof valueArg !== 'string' || !valueArg.trim()) {
    return undefined;
  }
  try {
    const parsed = new URL(valueArg.includes('://') ? valueArg : `tcp://${valueArg}`);
    return { host: parsed.hostname, port: parsed.port ? Number(parsed.port) : mysensorsDefaultTcpPort };
  } catch {
    return undefined;
  }
}

function numberValue(valueArg: unknown): number | undefined {
  const value = typeof valueArg === 'number' ? valueArg : typeof valueArg === 'string' && valueArg.trim() ? Number(valueArg) : undefined;
  return value !== undefined && Number.isFinite(value) ? value : undefined;
}

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

function boolish(valueArg: unknown): boolean {
  if (typeof valueArg === 'string') {
    return valueArg === '1' || valueArg.toLowerCase() === 'true';
  }
  return valueArg === true || valueArg === 1;
}

function titleCase(valueArg: string): string {
  return valueArg.replace(/[_-]+/g, ' ').replace(/\b\w/g, (charArg) => charArg.toUpperCase());
}

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

function clone<TValue>(valueArg: TValue): TValue {
  return JSON.parse(JSON.stringify(valueArg)) as TValue;
}

function errorMessage(errorArg: unknown): string {
  return errorArg instanceof Error ? errorArg.message : String(errorArg);
}
