import {
  homematicDefaultInterfacePort,
  homematicDefaultJsonPort,
  type IHomematicChannel,
  type IHomematicCommand,
  type IHomematicCommandResult,
  type IHomematicConfig,
  type IHomematicCcu,
  type IHomematicDatapoint,
  type IHomematicDevice,
  type IHomematicEvent,
  type IHomematicInterface,
  type IHomematicInterfaceConfig,
  type IHomematicSnapshot,
  type IHomematicValue,
  type IHomematicXmlRpcCommandShape,
  type IHomematicXmlRpcDeviceDescription,
  type IHomematicXmlRpcParamsetDescription,
  type THomematicDatapointKind,
  type THomematicParamsetKey,
  type THomematicProtocol,
  type THomematicValueType,
} from './homematic.types.js';

const defaultTimeoutMs = 10000;
const defaultInterfaceName = 'default';
const defaultInterfaceId = 'smarthome-exchange';

type THomematicEventHandler = (eventArg: IHomematicEvent) => void;

interface IXmlNode {
  name: string;
  text: string;
  children: IXmlNode[];
}

export class HomematicXmlRpcError extends Error {
  constructor(messageArg: string, public readonly status?: number, public readonly fault?: unknown) {
    super(messageArg);
    this.name = 'HomematicXmlRpcError';
  }
}

export class HomematicXmlRpcClient {
  constructor(private readonly config: IHomematicInterfaceConfig, private readonly timeoutMs = defaultTimeoutMs) {}

  public endpoint(): string {
    const protocol = this.protocol();
    const host = this.config.host || '127.0.0.1';
    const port = this.config.port || homematicDefaultInterfacePort;
    const path = this.config.path ? (this.config.path.startsWith('/') ? this.config.path : `/${this.config.path}`) : '';
    return `${protocol}://${host}:${port}${path}`;
  }

  public async call(methodArg: string, paramsArg: unknown[] = []): Promise<unknown> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
    try {
      const response = await fetch(this.endpoint(), {
        method: 'POST',
        headers: {
          'content-type': 'text/xml',
          accept: 'text/xml',
        },
        body: this.encodeMethodCall(methodArg, paramsArg),
        signal: controller.signal,
      });
      const body = await response.text();
      if (!response.ok) {
        throw new HomematicXmlRpcError(`Homematic XML-RPC ${methodArg} failed with HTTP ${response.status}.`, response.status, body);
      }
      return this.decodeMethodResponse(body);
    } finally {
      clearTimeout(timeout);
    }
  }

  private protocol(): THomematicProtocol {
    return this.config.protocol || (this.config.ssl ? 'https' : 'http');
  }

  private encodeMethodCall(methodArg: string, paramsArg: unknown[]): string {
    const params = paramsArg.map((paramArg) => `<param>${this.encodeValue(paramArg)}</param>`).join('');
    return `<?xml version="1.0"?><methodCall><methodName>${escapeXml(methodArg)}</methodName><params>${params}</params></methodCall>`;
  }

  private encodeValue(valueArg: unknown): string {
    if (valueArg === null || valueArg === undefined) {
      return '<value><nil/></value>';
    }
    if (typeof valueArg === 'boolean') {
      return `<value><boolean>${valueArg ? '1' : '0'}</boolean></value>`;
    }
    if (typeof valueArg === 'number') {
      return Number.isInteger(valueArg) ? `<value><int>${valueArg}</int></value>` : `<value><double>${valueArg}</double></value>`;
    }
    if (typeof valueArg === 'string') {
      return `<value><string>${escapeXml(valueArg)}</string></value>`;
    }
    if (Array.isArray(valueArg)) {
      return `<value><array><data>${valueArg.map((itemArg) => this.encodeValue(itemArg)).join('')}</data></array></value>`;
    }
    if (typeof valueArg === 'object') {
      const members = Object.entries(valueArg as Record<string, unknown>)
        .map(([keyArg, memberValueArg]) => `<member><name>${escapeXml(keyArg)}</name>${this.encodeValue(memberValueArg)}</member>`)
        .join('');
      return `<value><struct>${members}</struct></value>`;
    }
    return `<value><string>${escapeXml(String(valueArg))}</string></value>`;
  }

  private decodeMethodResponse(xmlArg: string): unknown {
    const root = parseXml(xmlArg);
    const response = firstChild(root, 'methodResponse') || root;
    const faultValue = firstChild(firstChild(response, 'fault'), 'value');
    if (faultValue) {
      const fault = this.decodeValue(faultValue);
      const message = typeof fault === 'object' && fault && 'faultString' in fault ? String((fault as { faultString?: unknown }).faultString) : 'Homematic XML-RPC fault.';
      throw new HomematicXmlRpcError(message, undefined, fault);
    }
    const valueNode = firstChild(firstChild(firstChild(response, 'params'), 'param'), 'value');
    return valueNode ? this.decodeValue(valueNode) : undefined;
  }

  private decodeValue(valueNodeArg: IXmlNode): unknown {
    const typedNode = valueNodeArg.children.find((childArg) => childArg.name !== '#text');
    if (!typedNode) {
      return decodeXml(valueNodeArg.text.trim());
    }
    if (typedNode.name === 'value') {
      return this.decodeValue(typedNode);
    }
    const text = decodeXml(typedNode.text.trim());
    if (typedNode.name === 'int' || typedNode.name === 'i4' || typedNode.name === 'i8') {
      return Number.parseInt(text, 10);
    }
    if (typedNode.name === 'double') {
      return Number.parseFloat(text);
    }
    if (typedNode.name === 'boolean') {
      return text === '1' || text.toLowerCase() === 'true';
    }
    if (typedNode.name === 'string' || typedNode.name === 'dateTime.iso8601' || typedNode.name === 'base64') {
      return text;
    }
    if (typedNode.name === 'nil') {
      return null;
    }
    if (typedNode.name === 'array') {
      const dataNode = firstChild(typedNode, 'data');
      return (dataNode?.children || []).filter((childArg) => childArg.name === 'value').map((childArg) => this.decodeValue(childArg));
    }
    if (typedNode.name === 'struct') {
      const result: Record<string, unknown> = {};
      for (const member of typedNode.children.filter((childArg) => childArg.name === 'member')) {
        const name = decodeXml(firstChild(member, 'name')?.text.trim() || '');
        const value = firstChild(member, 'value');
        if (name && value) {
          result[name] = this.decodeValue(value);
        }
      }
      return result;
    }
    return text;
  }
}

export class HomematicClient {
  private snapshot?: IHomematicSnapshot;
  private readonly events: IHomematicEvent[] = [];
  private readonly eventHandlers = new Set<THomematicEventHandler>();

  constructor(private readonly config: IHomematicConfig) {
    this.snapshot = config.snapshot ? this.normalizeSnapshot(clone(config.snapshot), 'snapshot') : undefined;
  }

  public async getSnapshot(): Promise<IHomematicSnapshot> {
    if (this.config.snapshot) {
      this.snapshot = this.normalizeSnapshot(clone(this.config.snapshot), 'snapshot');
      return this.snapshot;
    }
    if (this.hasLiveEndpoint()) {
      try {
        this.snapshot = this.normalizeSnapshot(await this.fetchSnapshot(), 'xmlrpc');
        return this.snapshot;
      } catch (errorArg) {
        this.emit({ type: 'error', data: errorArg instanceof Error ? errorArg.message : String(errorArg), timestamp: Date.now() });
        this.snapshot = this.normalizeSnapshot(this.snapshotFromConfig(false), 'runtime');
        return this.snapshot;
      }
    }
    this.snapshot = this.normalizeSnapshot(this.snapshotFromConfig(this.config.connected ?? true), this.hasManualSnapshotData() ? 'manual' : 'runtime');
    return this.snapshot;
  }

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

  public async sendCommand(commandArg: IHomematicCommand): Promise<IHomematicCommandResult> {
    const shape = commandArg.type === 'refresh' ? undefined : this.xmlRpcCommandShape(commandArg);
    this.emit({ type: 'command_mapped', command: commandArg, data: shape, entityId: commandArg.type === 'refresh' ? undefined : commandArg.entityId, deviceId: commandArg.type === 'refresh' ? undefined : commandArg.deviceId, timestamp: Date.now() });

    try {
      if (this.config.commandExecutor) {
        const result = this.commandResult(await this.config.commandExecutor(commandArg), commandArg, shape);
        if (result.success) {
          await this.patchSnapshot(commandArg);
        }
        this.emit({ type: result.success ? 'command_executed' : 'command_failed', command: commandArg, data: result, timestamp: Date.now() });
        return result;
      }

      if (commandArg.type === 'refresh') {
        this.snapshot = undefined;
        const snapshot = await this.getSnapshot();
        this.emit({ type: 'snapshot_refreshed', data: snapshot, timestamp: Date.now() });
        return { success: true, data: snapshot };
      }

      let xmlRpcResult: unknown;
      if (shape && this.hasLiveEndpoint(commandArg.interfaceName)) {
        xmlRpcResult = await this.xmlRpcClient(commandArg.interfaceName).call(shape.xmlRpcMethod, shape.params);
      }
      await this.patchSnapshot(commandArg);
      const result = { success: true, data: { shape, xmlRpcResult } };
      this.emit({ type: 'command_executed', command: commandArg, data: result, timestamp: Date.now() });
      return result;
    } catch (errorArg) {
      const result = { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg), data: { command: commandArg, shape } };
      this.emit({ type: 'command_failed', command: commandArg, data: result, timestamp: Date.now() });
      return result;
    }
  }

  public xmlRpcCommandShape(commandArg: Exclude<IHomematicCommand, { type: 'refresh' }>): IHomematicXmlRpcCommandShape {
    const interfaceConfig = this.interfaceConfig(commandArg.interfaceName);
    const endpoint = new HomematicXmlRpcClient(interfaceConfig, this.config.timeoutMs || defaultTimeoutMs).endpoint();
    const params = this.xmlRpcParams(commandArg);
    return {
      protocol: 'xmlrpc',
      endpoint,
      method: 'POST',
      contentType: 'text/xml',
      xmlRpcMethod: commandArg.type === 'raw_xmlrpc' ? commandArg.method : 'setValue',
      params,
      interfaceName: commandArg.interfaceName || interfaceConfig.name || defaultInterfaceName,
      host: interfaceConfig.host,
      port: interfaceConfig.port,
      path: interfaceConfig.path,
    };
  }

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

  private xmlRpcParams(commandArg: Exclude<IHomematicCommand, { type: 'refresh' }>): unknown[] {
    if (commandArg.type === 'raw_xmlrpc') {
      return commandArg.params || [];
    }
    return [this.channelAddress(commandArg.address, commandArg.channel), commandArg.datapoint, this.commandValue(commandArg)];
  }

  private commandValue(commandArg: Exclude<IHomematicCommand, { type: 'refresh' | 'raw_xmlrpc' }>): IHomematicValue {
    if (commandArg.type === 'press') {
      return commandArg.value ?? true;
    }
    return commandArg.value;
  }

  private async fetchSnapshot(): Promise<IHomematicSnapshot> {
    const updatedAt = new Date().toISOString();
    const interfaces = this.configInterfaces().filter((interfaceArg) => interfaceArg.connect !== false);
    const allDevices: IHomematicDevice[] = [];
    const raw: Record<string, unknown> = {};

    for (const interfaceConfig of interfaces) {
      const interfaceName = interfaceConfig.name || interfaceConfig.id || defaultInterfaceName;
      const client = new HomematicXmlRpcClient(interfaceConfig, this.config.timeoutMs || defaultTimeoutMs);
      const interfaceId = `${this.config.interfaceId || defaultInterfaceId}-${interfaceName}`;
      const descriptions = await this.listDevices(client, interfaceId);
      const channelDescriptions = descriptions.filter((descriptionArg) => Boolean(descriptionArg.ADDRESS && descriptionArg.PARENT));
      const paramsets: Record<string, Record<string, IHomematicValue>> = {};
      const paramsetDescriptions: Record<string, Record<string, IHomematicXmlRpcParamsetDescription>> = {};

      for (const description of channelDescriptions) {
        const address = description.ADDRESS;
        if (!address || !description.PARAMSETS?.includes('VALUES')) {
          continue;
        }
        const valueSet = await optionalRecord(() => client.call('getParamset', [address, 'VALUES']));
        const descriptionSet = await optionalRecord(() => client.call('getParamsetDescription', [address, 'VALUES']));
        paramsets[address] = valueSet as Record<string, IHomematicValue>;
        paramsetDescriptions[address] = descriptionSet as Record<string, IHomematicXmlRpcParamsetDescription>;
      }

      allDevices.push(...this.devicesFromDescriptions(interfaceName, descriptions, paramsets, paramsetDescriptions));
      raw[interfaceName] = { descriptions, paramsets, paramsetDescriptions };
    }

    return {
      ccu: this.ccuInfo(true),
      interfaces: interfaces.map((interfaceArg) => this.interfaceInfo(interfaceArg, true)),
      devices: allDevices,
      events: [...(this.config.events || []), ...this.events],
      connected: true,
      updatedAt,
      source: 'xmlrpc',
      raw,
    };
  }

  private async listDevices(clientArg: HomematicXmlRpcClient, interfaceIdArg: string): Promise<IHomematicXmlRpcDeviceDescription[]> {
    const noArgResult = await optionalArray(() => clientArg.call('listDevices', []));
    if (noArgResult.length) {
      return noArgResult as IHomematicXmlRpcDeviceDescription[];
    }
    const interfaceResult = await optionalArray(() => clientArg.call('listDevices', [interfaceIdArg]));
    return interfaceResult as IHomematicXmlRpcDeviceDescription[];
  }

  private devicesFromDescriptions(interfaceNameArg: string, descriptionsArg: IHomematicXmlRpcDeviceDescription[], paramsetsArg: Record<string, Record<string, IHomematicValue>>, paramsetDescriptionsArg: Record<string, Record<string, IHomematicXmlRpcParamsetDescription>>): IHomematicDevice[] {
    const parents = descriptionsArg.filter((descriptionArg) => descriptionArg.ADDRESS && !descriptionArg.PARENT);
    const channels = descriptionsArg.filter((descriptionArg) => descriptionArg.ADDRESS && descriptionArg.PARENT);
    const devices = new Map<string, IHomematicDevice>();

    for (const parent of parents) {
      const address = parent.ADDRESS || 'unknown';
      devices.set(address, {
        address,
        name: stringValue(parent.NAME) || address,
        type: parent.TYPE,
        interfaceName: interfaceNameArg,
        manufacturer: 'eQ-3',
        model: parent.TYPE,
        firmwareVersion: parent.FIRMWARE || parent.AVAILABLE_FIRMWARE,
        available: true,
        channels: [],
        paramsets: (parent.PARAMSETS || []) as THomematicParamsetKey[],
        raw: parent,
      });
    }

    for (const channel of channels) {
      const address = channel.ADDRESS || 'unknown:0';
      const parentAddress = channel.PARENT || address.split(':')[0];
      const parent = devices.get(parentAddress) || {
        address: parentAddress,
        name: parentAddress,
        type: channel.PARENT_TYPE,
        interfaceName: interfaceNameArg,
        manufacturer: 'eQ-3',
        model: channel.PARENT_TYPE,
        available: true,
        channels: [],
      };
      const index = numberValue(channel.INDEX) ?? channelIndex(address);
      const datapoints = this.datapointsFromParamset(address, index, paramsetsArg[address] || {}, paramsetDescriptionsArg[address] || {});
      parent.channels.push({
        address,
        index,
        name: stringValue(channel.NAME) || `${parent.name || parentAddress} ${index}`,
        type: channel.TYPE || channel.PARENT_TYPE,
        parentAddress,
        datapoints,
        paramsets: (channel.PARAMSETS || []) as THomematicParamsetKey[],
        raw: channel,
      });
      parent.available = !datapoints.some((datapointArg) => datapointArg.name === 'UNREACH' && Boolean(datapointArg.value));
      devices.set(parentAddress, parent);
    }

    return [...devices.values()];
  }

  private datapointsFromParamset(addressArg: string, channelArg: number, valuesArg: Record<string, IHomematicValue>, descriptionsArg: Record<string, IHomematicXmlRpcParamsetDescription>): IHomematicDatapoint[] {
    const keys = new Set([...Object.keys(valuesArg), ...Object.keys(descriptionsArg)]);
    return [...keys].sort().map((keyArg) => {
      const description = descriptionsArg[keyArg] || {};
      const operations = numberValue(description.OPERATIONS);
      const value = valuesArg[keyArg] ?? null;
      const valueType = this.valueType(description.TYPE, value);
      return {
        name: keyArg,
        value,
        valueType,
        channel: channelArg,
        address: addressArg,
        kind: this.datapointKind(keyArg, valueType, operations),
        paramsetKey: 'VALUES',
        operations,
        readable: operations === undefined ? true : Boolean(operations & 1),
        writable: operations === undefined ? undefined : Boolean(operations & 2),
        eventable: operations === undefined ? undefined : Boolean(operations & 4),
        unit: stringValue(description.UNIT),
        min: numberValue(description.MIN),
        max: numberValue(description.MAX),
        valueList: Array.isArray(description.VALUE_LIST) ? description.VALUE_LIST.map(String) : undefined,
        attributes: cleanRecord(description),
      };
    });
  }

  private snapshotFromConfig(connectedArg: boolean): IHomematicSnapshot {
    const updatedAt = new Date().toISOString();
    const interfaces = this.configInterfaces().map((interfaceArg) => this.interfaceInfo(interfaceArg, connectedArg && interfaceArg.connect !== false));
    return {
      ccu: this.ccuInfo(connectedArg),
      interfaces,
      devices: clone(this.config.devices || this.config.snapshot?.devices || []),
      events: [...(this.config.events || []), ...this.events],
      connected: connectedArg,
      updatedAt,
      source: this.hasManualSnapshotData() ? 'manual' : 'runtime',
    };
  }

  private normalizeSnapshot(snapshotArg: IHomematicSnapshot, sourceArg: IHomematicSnapshot['source']): IHomematicSnapshot {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const ccu = {
      manufacturer: 'eQ-3',
      model: 'Homematic CCU',
      name: this.config.name || 'Homematic CCU',
      ...snapshotArg.ccu,
    };
    ccu.id = ccu.id || this.config.uniqueId || ccu.serialNumber || ccu.host || this.config.host || 'homematic-ccu';
    ccu.host = ccu.host || this.config.host;
    ccu.port = ccu.port || this.config.port || homematicDefaultInterfacePort;
    ccu.online = snapshotArg.connected && ccu.online !== false;
    const interfaces = snapshotArg.interfaces.length ? snapshotArg.interfaces : this.configInterfaces().map((interfaceArg) => this.interfaceInfo(interfaceArg, snapshotArg.connected));
    const devices = snapshotArg.devices.map((deviceArg) => this.normalizeDevice(deviceArg, updatedAt));
    return {
      ...snapshotArg,
      ccu,
      interfaces,
      devices,
      events: [...(snapshotArg.events || []), ...this.events],
      connected: snapshotArg.connected,
      updatedAt,
      source: sourceArg,
    };
  }

  private normalizeDevice(deviceArg: IHomematicDevice, updatedAtArg: string): IHomematicDevice {
    const address = deviceArg.address;
    const channels: IHomematicChannel[] = (deviceArg.channels || []).map((channelArg) => {
      const index = numberValue(channelArg.index) ?? channelIndex(channelArg.address);
      const channelAddress = channelArg.address || this.channelAddress(address, index);
      return {
        ...channelArg,
        address: channelAddress,
        index,
        parentAddress: channelArg.parentAddress || address,
        name: channelArg.name || `${deviceArg.name || address} ${index}`,
        type: channelArg.type || deviceArg.type,
        datapoints: (channelArg.datapoints || []).map((datapointArg) => this.normalizeDatapoint(datapointArg, channelAddress, index, updatedAtArg)),
      };
    });

    if (deviceArg.datapoints?.length) {
      const byIndex = new Map<number, IHomematicChannel>();
      for (const channel of channels) {
        byIndex.set(channel.index, channel);
      }
      for (const datapoint of deviceArg.datapoints) {
        const index = datapoint.channel ?? channelIndex(datapoint.address || '') ?? 0;
        const channel = byIndex.get(index) || {
          address: this.channelAddress(address, index),
          index,
          parentAddress: address,
          name: `${deviceArg.name || address} ${index}`,
          type: deviceArg.type,
          datapoints: [],
        };
        channel.datapoints.push(this.normalizeDatapoint(datapoint, channel.address, index, updatedAtArg));
        byIndex.set(index, channel);
      }
      channels.splice(0, channels.length, ...[...byIndex.values()].sort((leftArg, rightArg) => leftArg.index - rightArg.index));
    }

    return {
      ...deviceArg,
      name: deviceArg.name || address,
      manufacturer: deviceArg.manufacturer || 'eQ-3',
      model: deviceArg.model || deviceArg.type,
      available: deviceArg.available ?? !deviceArg.unreach,
      channels,
    };
  }

  private normalizeDatapoint(datapointArg: IHomematicDatapoint, addressArg: string, channelArg: number, updatedAtArg: string): IHomematicDatapoint {
    const valueType = datapointArg.valueType || this.valueType(undefined, datapointArg.value);
    return {
      ...datapointArg,
      name: datapointArg.name.toUpperCase(),
      value: datapointArg.value ?? null,
      valueType,
      channel: datapointArg.channel ?? channelArg,
      address: datapointArg.address || addressArg,
      kind: datapointArg.kind || this.datapointKind(datapointArg.name, valueType, datapointArg.operations),
      readable: datapointArg.readable ?? true,
      updatedAt: datapointArg.updatedAt || updatedAtArg,
    };
  }

  private async patchSnapshot(commandArg: IHomematicCommand): Promise<void> {
    if (commandArg.type === 'refresh' || commandArg.type === 'raw_xmlrpc') {
      return;
    }
    const snapshot = this.snapshot || await this.getSnapshot();
    if (commandArg.type === 'press') {
      this.emit({ type: 'keypress', command: commandArg, address: commandArg.address, channel: commandArg.channel, datapoint: commandArg.datapoint, value: commandArg.value ?? true, timestamp: Date.now() });
      return;
    }
    const targetAddress = this.channelAddress(commandArg.address, commandArg.channel);
    for (const device of snapshot.devices) {
      for (const channel of device.channels) {
        if (channel.address !== targetAddress && !(device.address === commandArg.address && channel.index === commandArg.channel)) {
          continue;
        }
        const datapoint = channel.datapoints.find((datapointArg) => datapointArg.name === commandArg.datapoint);
        if (datapoint) {
          datapoint.value = commandArg.value;
          datapoint.updatedAt = new Date().toISOString();
          this.emit({ type: 'datapoint_changed', command: commandArg, address: device.address, channel: channel.index, datapoint: datapoint.name, value: datapoint.value, timestamp: Date.now() });
          return;
        }
      }
    }
  }

  private ccuInfo(onlineArg: boolean): IHomematicCcu {
    return {
      id: this.config.ccu?.id || this.config.uniqueId || this.config.host || 'homematic-ccu',
      name: this.config.ccu?.name || this.config.name || 'Homematic CCU',
      host: this.config.ccu?.host || this.config.host,
      port: this.config.ccu?.port || this.config.port || homematicDefaultInterfacePort,
      manufacturer: this.config.ccu?.manufacturer || 'eQ-3',
      model: this.config.ccu?.model || 'Homematic CCU',
      serialNumber: this.config.ccu?.serialNumber,
      firmwareVersion: this.config.ccu?.firmwareVersion,
      online: onlineArg,
      serviceMessages: this.config.ccu?.serviceMessages || [],
      variables: this.config.ccu?.variables || {},
      attributes: this.config.ccu?.attributes,
    };
  }

  private configInterfaces(): IHomematicInterfaceConfig[] {
    if (this.config.interfaces && Object.keys(this.config.interfaces).length) {
      return Object.entries(this.config.interfaces).map(([nameArg, interfaceArg]) => ({ name: interfaceArg.name || nameArg, ...interfaceArg }));
    }
    return [{
      name: this.config.interfaceName || defaultInterfaceName,
      host: this.config.host,
      port: this.config.port || homematicDefaultInterfacePort,
      path: this.config.path,
      protocol: this.config.ssl ? 'https' : 'http',
      ssl: this.config.ssl,
      verifySsl: this.config.verifySsl,
      username: this.config.username,
      password: this.config.password,
      jsonPort: this.config.jsonPort || homematicDefaultJsonPort,
      resolvenames: this.config.resolvenames,
      connect: true,
      callbackIp: this.config.callbackIp,
      callbackPort: this.config.callbackPort,
    }];
  }

  private interfaceInfo(interfaceArg: IHomematicInterfaceConfig, onlineArg: boolean): IHomematicInterface {
    const name = interfaceArg.name || interfaceArg.id || defaultInterfaceName;
    return {
      id: interfaceArg.id || name,
      name,
      host: interfaceArg.host,
      port: interfaceArg.port || homematicDefaultInterfacePort,
      path: interfaceArg.path,
      protocol: interfaceArg.protocol || (interfaceArg.ssl ? 'https' : 'http'),
      family: interfaceArg.family || this.interfaceFamily(interfaceArg.port),
      online: onlineArg,
      attributes: interfaceArg.attributes,
    };
  }

  private interfaceConfig(interfaceNameArg?: string): IHomematicInterfaceConfig {
    const interfaces = this.configInterfaces();
    const found = interfaces.find((interfaceArg) => interfaceArg.name === interfaceNameArg || interfaceArg.id === interfaceNameArg) || interfaces[0];
    return {
      ...found,
      name: found.name || found.id || interfaceNameArg || defaultInterfaceName,
      port: found.port || homematicDefaultInterfacePort,
      jsonPort: found.jsonPort || homematicDefaultJsonPort,
    };
  }

  private xmlRpcClient(interfaceNameArg?: string): HomematicXmlRpcClient {
    return new HomematicXmlRpcClient(this.interfaceConfig(interfaceNameArg), this.config.timeoutMs || defaultTimeoutMs);
  }

  private hasLiveEndpoint(interfaceNameArg?: string): boolean {
    const config = this.interfaceConfig(interfaceNameArg);
    return Boolean(config.host && config.connect !== false);
  }

  private hasManualSnapshotData(): boolean {
    return Boolean(this.config.ccu || this.config.devices?.length || this.config.interfaces && Object.keys(this.config.interfaces).length);
  }

  private valueType(typeArg: unknown, valueArg: unknown): THomematicValueType {
    const type = typeof typeArg === 'string' ? typeArg.toUpperCase() : '';
    if (['BOOL', 'BOOLEAN'].includes(type)) {
      return 'boolean';
    }
    if (['INTEGER', 'ENUM'].includes(type)) {
      return 'integer';
    }
    if (['FLOAT', 'DOUBLE'].includes(type)) {
      return 'double';
    }
    if (type === 'STRING') {
      return 'string';
    }
    if (typeof valueArg === 'boolean') {
      return 'boolean';
    }
    if (typeof valueArg === 'number') {
      return Number.isInteger(valueArg) ? 'integer' : 'double';
    }
    if (typeof valueArg === 'string') {
      return 'string';
    }
    if (Array.isArray(valueArg)) {
      return 'array';
    }
    if (valueArg && typeof valueArg === 'object') {
      return 'struct';
    }
    return 'unknown';
  }

  private datapointKind(nameArg: string, typeArg: THomematicValueType, operationsArg?: number): THomematicDatapointKind {
    const name = nameArg.toUpperCase();
    if (name.startsWith('PRESS_') || name === 'PRESS' || name === 'STOP' || name.endsWith('_MODE')) {
      return 'action';
    }
    if (operationsArg !== undefined && Boolean(operationsArg & 2)) {
      return 'write';
    }
    if (operationsArg !== undefined && Boolean(operationsArg & 4) && !Boolean(operationsArg & 1)) {
      return 'event';
    }
    if (['LOWBAT', 'LOW_BAT', 'UNREACH', 'MOTION', 'STATE', 'RAINING', 'DUTY_CYCLE'].includes(name) || typeArg === 'boolean') {
      return 'binary';
    }
    if (['RSSI_PEER', 'RSSI_DEVICE', 'ERROR', 'SABOTAGE', 'CONTROL_MODE', 'BATTERY_STATE'].includes(name)) {
      return 'attribute';
    }
    return 'sensor';
  }

  private interfaceFamily(portArg?: number): IHomematicInterface['family'] {
    if (portArg === 2010 || portArg === 32010 || portArg === 42010) {
      return 'hmip-rf';
    }
    if (portArg === 2000) {
      return 'bidcos-wired';
    }
    if (portArg === 9292) {
      return 'homegear';
    }
    return portArg ? 'bidcos-rf' : 'unknown';
  }

  private channelAddress(addressArg: string, channelArg?: number): string {
    if (addressArg.includes(':') || channelArg === undefined) {
      return addressArg;
    }
    return `${addressArg}:${channelArg}`;
  }

  private commandResult(resultArg: IHomematicCommandResult | unknown, commandArg: IHomematicCommand, shapeArg?: IHomematicXmlRpcCommandShape): IHomematicCommandResult {
    if (resultArg && typeof resultArg === 'object' && 'success' in resultArg && typeof (resultArg as { success?: unknown }).success === 'boolean') {
      return resultArg as IHomematicCommandResult;
    }
    return { success: true, data: { result: resultArg, command: commandArg, shape: shapeArg } };
  }

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

const optionalArray = async (factoryArg: () => Promise<unknown>): Promise<unknown[]> => {
  try {
    const value = await factoryArg();
    return Array.isArray(value) ? value : [];
  } catch {
    return [];
  }
};

const optionalRecord = async (factoryArg: () => Promise<unknown>): Promise<Record<string, unknown>> => {
  try {
    const value = await factoryArg();
    return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
  } catch {
    return {};
  }
};

const channelIndex = (addressArg: string): number => {
  const part = addressArg.split(':')[1];
  return part && Number.isFinite(Number(part)) ? Number(part) : 0;
};

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg)) ? Number(valueArg) : undefined;

const cleanRecord = (recordArg: Record<string, unknown>): Record<string, unknown> => Object.fromEntries(Object.entries(recordArg).filter(([, valueArg]) => valueArg !== undefined));

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const parseXml = (xmlArg: string): IXmlNode => {
  const root: IXmlNode = { name: 'root', text: '', children: [] };
  const stack: IXmlNode[] = [root];
  const tagRegex = /<([^>]+)>/g;
  let lastIndex = 0;
  let match: RegExpExecArray | null;
  while ((match = tagRegex.exec(xmlArg))) {
    const text = xmlArg.slice(lastIndex, match.index);
    if (text) {
      stack[stack.length - 1].text += text;
    }
    const tag = match[1].trim();
    lastIndex = tagRegex.lastIndex;
    if (!tag || tag.startsWith('?') || tag.startsWith('!')) {
      continue;
    }
    if (tag.startsWith('/')) {
      if (stack.length > 1) {
        stack.pop();
      }
      continue;
    }
    const selfClosing = tag.endsWith('/');
    const name = tag.replace(/\/$/, '').split(/\s+/)[0];
    const node: IXmlNode = { name, text: '', children: [] };
    stack[stack.length - 1].children.push(node);
    if (!selfClosing) {
      stack.push(node);
    }
  }
  const rest = xmlArg.slice(lastIndex);
  if (rest) {
    stack[stack.length - 1].text += rest;
  }
  return root;
};

const firstChild = (nodeArg: IXmlNode | undefined, nameArg: string): IXmlNode | undefined => nodeArg?.children.find((childArg) => childArg.name === nameArg);

const escapeXml = (valueArg: string): string => valueArg
  .replace(/&/g, '&amp;')
  .replace(/</g, '&lt;')
  .replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;')
  .replace(/'/g, '&apos;');

const decodeXml = (valueArg: string): string => valueArg
  .replace(/&lt;/g, '<')
  .replace(/&gt;/g, '>')
  .replace(/&quot;/g, '"')
  .replace(/&apos;/g, "'")
  .replace(/&amp;/g, '&');
