import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { OsramlightifyMapper } from './osramlightify.mapper.js';
import type { IOsramlightifyConfig, IOsramlightifyGroupRaw, IOsramlightifyLightRaw, IOsramlightifyRawApiData, IOsramlightifySceneRaw } from './osramlightify.types.js';
import { osramlightifyDefaultPort, osramlightifyDefaultTimeoutMs, osramlightifyProfile } from './osramlightify.types.js';

const commandAllLightStatus = 0x13;
const commandGroupList = 0x1e;
const commandSceneList = 0x1f;
const commandLuminance = 0x31;
const commandOnOff = 0x32;
const commandTemperature = 0x33;
const commandColour = 0x36;
const commandActivateScene = 0x52;
const flagLight = 0x00;
const flagGlobal = 0x02;
const defaultAlpha = 0xff;
const noRgbValues = [1, 0, 0] as const;

interface IOsramlightifyDeviceTypeDefinition {
  deviceType: 'LIGHT' | 'PLUG' | 'SENSOR' | 'SWITCH';
  deviceSubType: string;
  deviceName: string;
  supportedFeatures: string[];
  minTemp?: number;
  maxTemp?: number;
}

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

interface IOsramlightifyCommandTarget {
  address?: string;
  groupId?: number;
}

type TOsramBuffer = Buffer<ArrayBufferLike>;

const deviceTypes: Record<number, IOsramlightifyDeviceTypeDefinition> = {
  1: { deviceType: 'LIGHT', deviceSubType: 'LIGHT_FIXED_WHITE', deviceName: 'light non softswitch', supportedFeatures: ['on', 'lum'] },
  2: { deviceType: 'LIGHT', deviceSubType: 'LIGHT_TUNABLE_WHITE', deviceName: 'light tunable white', supportedFeatures: ['on', 'lum', 'temp'], minTemp: 2700, maxTemp: 6500 },
  4: { deviceType: 'LIGHT', deviceSubType: 'LIGHT_FIXED_WHITE', deviceName: 'light fixed white', supportedFeatures: ['on', 'lum'] },
  8: { deviceType: 'LIGHT', deviceSubType: 'LIGHT_RGB', deviceName: 'light rgb', supportedFeatures: ['on', 'lum', 'rgb'] },
  10: { deviceType: 'LIGHT', deviceSubType: 'LIGHT_RGBW', deviceName: 'light rgbw', supportedFeatures: ['on', 'lum', 'temp', 'rgb'], minTemp: 1900, maxTemp: 6500 },
  16: { deviceType: 'PLUG', deviceSubType: 'PLUG', deviceName: 'plug', supportedFeatures: ['on'] },
  31: { deviceType: 'SENSOR', deviceSubType: 'CONTACT_SENSOR', deviceName: 'contact sensor', supportedFeatures: [] },
  32: { deviceType: 'SENSOR', deviceSubType: 'MOTION_SENSOR', deviceName: 'motion sensor', supportedFeatures: [] },
  64: { deviceType: 'SWITCH', deviceSubType: 'SWITCH', deviceName: '2 button switch', supportedFeatures: [] },
  65: { deviceType: 'SWITCH', deviceSubType: 'SWITCH', deviceName: '4 button switch', supportedFeatures: [] },
  66: { deviceType: 'SWITCH', deviceSubType: 'SWITCH', deviceName: '3 button switch', supportedFeatures: [] },
  67: { deviceType: 'SWITCH', deviceSubType: 'SWITCH', deviceName: 'unknown switch', supportedFeatures: [] },
  68: { deviceType: 'SWITCH', deviceSubType: 'SWITCH', deviceName: 'unknown switch', supportedFeatures: [] },
  128: { deviceType: 'LIGHT', deviceSubType: 'LIGHT_TUNABLE_WHITE', deviceName: 'tradfri tunable white', supportedFeatures: ['on', 'lum', 'temp'], minTemp: 2200, maxTemp: 4000 },
};

export class OsramlightifyClient extends SimpleLocalClient<IOsramlightifyConfig> {
  private currentOsramSnapshot?: ISimpleLocalSnapshot;
  private sequence = 0;

  constructor(private readonly configArg: IOsramlightifyConfig) {
    super(osramlightifyProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.currentOsramSnapshot) {
      return clone(this.currentOsramSnapshot);
    }

    if (!forceRefreshArg && this.configArg.snapshot) {
      this.currentOsramSnapshot = OsramlightifyMapper.toSnapshot({ config: this.configArg, source: 'snapshot', online: this.configArg.snapshot.online });
      return clone(this.currentOsramSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        const rawData = await this.fetchRawData();
        this.currentOsramSnapshot = OsramlightifyMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'tcp' });
      } catch (errorArg) {
        this.currentOsramSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentOsramSnapshot);
    }

    if (this.configArg.client || this.configArg.snapshotProvider) {
      this.currentOsramSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.currentOsramSnapshot);
    }

    if (this.hasManualData()) {
      this.currentOsramSnapshot = OsramlightifyMapper.toSnapshot({ config: this.configArg, rawData: this.configArg.rawData ?? this.configArg.rawText, online: this.configArg.online ?? true, source: 'manual' });
      return clone(this.currentOsramSnapshot);
    }

    this.currentOsramSnapshot = this.createOfflineSnapshot('OSRAM Lightify snapshots require config.host/config.url for the local TCP gateway, injected client, snapshotProvider, snapshot, entities, or rawData.');
    return clone(this.currentOsramSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentOsramSnapshot = undefined;
    const liveCapable = this.hasEndpoint() || Boolean(this.configArg.client || this.configArg.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData() || Boolean(this.configArg.snapshot));
    return { success, error: success ? undefined : snapshot.error || 'OSRAM Lightify refresh requires a TCP gateway endpoint, injected client, snapshotProvider, or manual snapshot data.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.commandExecutor) {
      return normalizeServiceResult(await this.configArg.commandExecutor.execute(requestArg));
    }
    if (this.configArg.client?.execute) {
      return normalizeServiceResult(await this.configArg.client.execute(requestArg));
    }
    if (!this.hasEndpoint()) {
      return { success: false, error: `OSRAM Lightify service ${requestArg.domain}.${requestArg.service} requires config.host/config.url for the TCP gateway or an injected client.execute/commandExecutor.` };
    }

    try {
      const sent = await this.executeTcpCommand(requestArg);
      this.currentOsramSnapshot = undefined;
      return { success: true, data: { sent } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  private async fetchRawData(): Promise<IOsramlightifyRawApiData> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('OSRAM Lightify TCP polling requires config.host or config.url.');
    }

    const socket = await this.connect(endpoint);
    try {
      const lights = this.parseLights(await this.send(socket, this.buildGlobalCommand(commandAllLightStatus, Buffer.from([0x01]))));
      const scenes = this.shouldIncludeGroups() ? this.parseScenes(await this.send(socket, this.buildGlobalCommand(commandSceneList))) : [];
      const groups = this.shouldIncludeGroups() ? this.buildGroups(this.parseGroupRecords(await this.send(socket, this.buildGlobalCommand(commandGroupList))), lights, scenes) : [];
      const entities = this.entitiesFromRaw(lights, groups);

      return {
        device: {
          id: this.configArg.uniqueId || `${endpoint.host}:${endpoint.port}`,
          name: this.configArg.name || 'OSRAM Lightify Gateway',
          manufacturer: 'OSRAM',
          model: 'Lightify Gateway',
          host: endpoint.host,
          port: endpoint.port,
          protocol: 'tcp',
        },
        lights,
        groups,
        scenes,
        entities,
        fetchedAt: new Date().toISOString(),
      };
    } finally {
      socket.destroy();
    }
  }

  private async executeTcpCommand(requestArg: IServiceCallRequest): Promise<string[]> {
    const target = await this.commandTarget(requestArg);
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('OSRAM Lightify TCP command requires config.host or config.url.');
    }
    if (!target.address && target.groupId === undefined) {
      throw new Error('OSRAM Lightify command requires data.lightAddress/address, data.groupId, target.entityId, or a snapshot with one light target.');
    }

    const socket = await this.connect(endpoint);
    const sent: string[] = [];
    try {
      const transition = transitionTenths(requestArg.data?.transition);
      if (requestArg.service === 'turn_off') {
        await this.send(socket, this.buildOnOffCommand(target, false));
        sent.push('onoff:false');
        return sent;
      }

      if (requestArg.service === 'toggle') {
        const current = await this.currentTargetState(target);
        await this.send(socket, this.buildOnOffCommand(target, !current));
        sent.push(`onoff:${!current}`);
        return sent;
      }

      if (requestArg.service !== 'turn_on') {
        throw new Error(`Unsupported OSRAM Lightify service: ${requestArg.service}`);
      }

      if (requestArg.data?.effect && typeof requestArg.data.effect === 'string') {
        const sceneId = numberValue(requestArg.data.sceneId) ?? numberValue(requestArg.data.scene_id);
        if (sceneId === undefined) {
          throw new Error('OSRAM Lightify scene activation requires data.sceneId.');
        }
        await this.send(socket, this.buildGroupCommand(commandActivateScene, sceneId));
        sent.push(`scene:${sceneId}`);
        return sent;
      }

      const brightness = brightnessPercent(requestArg.data);
      const kelvin = numberValue(requestArg.data?.colorTempKelvin) ?? numberValue(requestArg.data?.color_temp_kelvin) ?? numberValue(requestArg.data?.kelvin);
      const rgb = rgbValue(requestArg.data?.rgbColor ?? requestArg.data?.rgb_color);

      await this.send(socket, this.buildOnOffCommand(target, true));
      sent.push('onoff:true');
      if (brightness !== undefined) {
        await this.send(socket, this.buildTargetCommand(target, commandLuminance, packLuminance(brightness, transition)));
        sent.push(`luminance:${brightness}`);
      }
      if (kelvin !== undefined) {
        await this.send(socket, this.buildTargetCommand(target, commandTemperature, packTemperature(kelvin, transition)));
        sent.push(`temperature:${kelvin}`);
      }
      if (rgb) {
        await this.send(socket, this.buildTargetCommand(target, commandColour, packColour(rgb, transition)));
        sent.push(`rgb:${rgb.join(',')}`);
      }
      return sent;
    } finally {
      socket.destroy();
    }
  }

  private parseLights(dataArg: TOsramBuffer): IOsramlightifyLightRaw[] {
    const count = dataArg.length >= 9 ? dataArg.readUInt16LE(7) : 0;
    const lights: IOsramlightifyLightRaw[] = [];
    if (dataArg.length < 9 + count * 50) {
      throw new Error(`OSRAM Lightify light status response too short for ${count} records.`);
    }

    for (let index = 0; index < count; index++) {
      const offset = 9 + index * 50;
      const address = dataArg.readBigUInt64LE(offset + 2).toString(16).padStart(16, '0');
      const stat = dataArg.subarray(offset + 10, offset + 26);
      const typeId = stat[0];
      const version = stat.subarray(1, 5).toString('hex').toUpperCase();
      const reachable = Boolean(stat[5]);
      const groups = groupsFromMask(stat.readUInt16LE(6));
      const on = Boolean(stat[8]);
      const luminance = stat[9];
      const temperatureKelvin = stat.readUInt16LE(10);
      const rgb: [number, number, number] = [stat[12], stat[13], stat[14]];
      const alpha = stat[15];
      const name = trimNulls(dataArg.subarray(offset + 26, offset + 42).toString('utf8')) || `Light ${index + 1}`;
      const lastSeenMinutes = dataArg.readUInt32LE(offset + 42) * 5;
      const definition = this.deviceTypeDefinition(typeId, rgb);

      lights.push({
        address,
        typeId,
        deviceType: definition.deviceType,
        deviceSubType: definition.deviceSubType,
        deviceName: definition.deviceName,
        name,
        reachable,
        lastSeenMinutes,
        on,
        luminance,
        temperatureKelvin,
        rgb,
        alpha,
        groups,
        firmwareVersion: version,
        supportedFeatures: [...definition.supportedFeatures],
        index,
      });
    }
    return lights;
  }

  private parseGroupRecords(dataArg: TOsramBuffer): Array<{ id: number; name: string }> {
    const count = dataArg.length >= 9 ? dataArg.readUInt16LE(7) : 0;
    if (dataArg.length < 9 + count * 18) {
      throw new Error(`OSRAM Lightify group response too short for ${count} records.`);
    }
    return Array.from({ length: count }, (_unusedArg, indexArg) => {
      const offset = 9 + indexArg * 18;
      return { id: dataArg.readUInt16LE(offset), name: trimNulls(dataArg.subarray(offset + 2, offset + 18).toString('utf8')) || `Group ${dataArg.readUInt16LE(offset)}` };
    });
  }

  private parseScenes(dataArg: TOsramBuffer): IOsramlightifySceneRaw[] {
    const count = dataArg.length >= 9 ? dataArg.readUInt16LE(7) : 0;
    if (dataArg.length < 9 + count * 20) {
      throw new Error(`OSRAM Lightify scene response too short for ${count} records.`);
    }
    return Array.from({ length: count }, (_unusedArg, indexArg) => {
      const offset = 9 + indexArg * 20;
      return {
        id: dataArg[offset],
        name: trimNulls(dataArg.subarray(offset + 2, offset + 18).toString('utf8')) || `Scene ${dataArg[offset]}`,
        groupId: groupFromMask(dataArg.readUInt16LE(offset + 18)),
      };
    });
  }

  private buildGroups(groupRecordsArg: Array<{ id: number; name: string }>, lightsArg: IOsramlightifyLightRaw[], scenesArg: IOsramlightifySceneRaw[]): IOsramlightifyGroupRaw[] {
    return groupRecordsArg.map((groupArg) => {
      const lights = lightsArg.filter((lightArg) => lightArg.groups.includes(groupArg.id));
      const supportedFeatures = unique(lights.flatMap((lightArg) => lightArg.supportedFeatures));
      const featureLight = (featureArg: string) => lights.find((lightArg) => lightArg.supportedFeatures.includes(featureArg)) || lights[0];
      return {
        id: groupArg.id,
        name: groupArg.name,
        lights: lights.map((lightArg) => lightArg.address),
        lightNames: lights.map((lightArg) => lightArg.name),
        scenes: scenesArg.filter((sceneArg) => sceneArg.groupId === groupArg.id).map((sceneArg) => sceneArg.name),
        reachable: lights.some((lightArg) => lightArg.reachable),
        on: lights.some((lightArg) => lightArg.on),
        luminance: featureLight('lum')?.luminance ?? 0,
        temperatureKelvin: featureLight('temp')?.temperatureKelvin ?? 0,
        rgb: featureLight('rgb')?.rgb ?? [0, 0, 0],
        supportedFeatures,
      };
    });
  }

  private entitiesFromRaw(lightsArg: IOsramlightifyLightRaw[], groupsArg: IOsramlightifyGroupRaw[]): ISimpleLocalSnapshot['entities'] {
    const lightEntities = lightsArg
      .filter((lightArg) => this.shouldExposeLight(lightArg))
      .map((lightArg) => ({
        id: `light_${lightArg.address}`,
        uniqueId: `osramlightify_light_${lightArg.address}`,
        name: lightArg.name,
        platform: 'light' as const,
        state: lightArg.on,
        available: lightArg.reachable,
        writable: true,
        attributes: {
          address: lightArg.address,
          brightness: Math.floor(lightArg.luminance * 2.55),
          colorTempKelvin: lightArg.supportedFeatures.includes('temp') ? lightArg.temperatureKelvin : undefined,
          rgb: lightArg.supportedFeatures.includes('rgb') ? lightArg.rgb : undefined,
          groups: lightArg.groups,
          supportedFeatures: lightArg.supportedFeatures,
          typeId: lightArg.typeId,
          deviceType: lightArg.deviceType,
          deviceName: lightArg.deviceName,
          firmwareVersion: lightArg.firmwareVersion,
          lastSeenMinutes: lightArg.lastSeenMinutes,
        },
      }));
    const groupEntities = groupsArg.map((groupArg) => ({
      id: `group_${groupArg.id}`,
      uniqueId: `osramlightify_group_${groupArg.id}`,
      name: groupArg.name,
      platform: 'light' as const,
      state: groupArg.on,
      available: groupArg.reachable,
      writable: true,
      attributes: {
        groupId: groupArg.id,
        lights: groupArg.lightNames,
        lightAddresses: groupArg.lights,
        scenes: groupArg.scenes,
        brightness: Math.floor(groupArg.luminance * 2.55),
        colorTempKelvin: groupArg.supportedFeatures.includes('temp') ? groupArg.temperatureKelvin : undefined,
        rgb: groupArg.supportedFeatures.includes('rgb') ? groupArg.rgb : undefined,
        supportedFeatures: groupArg.supportedFeatures,
      },
    }));
    return [...lightEntities, ...groupEntities];
  }

  private async commandTarget(requestArg: IServiceCallRequest): Promise<IOsramlightifyCommandTarget> {
    const data = requestArg.data || {};
    const entityId = stringValue(requestArg.target.entityId);
    const groupId = numberValue(data.groupId) ?? numberValue(data.group_id) ?? groupIdFromEntity(entityId);
    const address = normalizeAddress(data.address) ?? normalizeAddress(data.lightAddress) ?? normalizeAddress(data.light_address) ?? addressFromEntity(entityId);
    if (groupId !== undefined || address) {
      return { groupId, address };
    }

    const snapshot = await this.getSnapshot(false);
    const lightTargets = snapshot.entities.filter((entityArg) => typeof entityArg.attributes?.address === 'string');
    if (lightTargets.length === 1) {
      return { address: lightTargets[0].attributes!.address as string };
    }
    return {};
  }

  private async currentTargetState(targetArg: IOsramlightifyCommandTarget): Promise<boolean> {
    const snapshot = await this.getSnapshot(false);
    const entity = snapshot.entities.find((entityArg) => targetArg.groupId !== undefined ? entityArg.id === `group_${targetArg.groupId}` : entityArg.attributes?.address === targetArg.address);
    return Boolean(entity?.state);
  }

  private buildOnOffCommand(targetArg: IOsramlightifyCommandTarget, onArg: boolean): TOsramBuffer {
    return this.buildTargetCommand(targetArg, commandOnOff, Buffer.from([onArg ? 1 : 0]));
  }

  private buildTargetCommand(targetArg: IOsramlightifyCommandTarget, commandArg: number, dataArg: TOsramBuffer): TOsramBuffer {
    if (targetArg.groupId !== undefined) {
      return this.buildGroupCommand(commandArg, targetArg.groupId, dataArg);
    }
    if (!targetArg.address) {
      throw new Error('OSRAM Lightify command target is missing a light address.');
    }
    return this.buildLightCommand(commandArg, targetArg.address, dataArg);
  }

  private buildGlobalCommand(commandArg: number, dataArg: TOsramBuffer = Buffer.alloc(0)): TOsramBuffer {
    return this.buildBasicCommand(flagGlobal, commandArg, Buffer.alloc(0), dataArg);
  }

  private buildGroupCommand(commandArg: number, groupIdArg: number, dataArg: TOsramBuffer = Buffer.alloc(0)): TOsramBuffer {
    const address = Buffer.alloc(8);
    address[0] = groupIdArg & 0xff;
    return this.buildBasicCommand(flagGlobal, commandArg, address, dataArg);
  }

  private buildLightCommand(commandArg: number, addressArg: string, dataArg: TOsramBuffer = Buffer.alloc(0)): TOsramBuffer {
    const address = Buffer.alloc(8);
    address.writeBigUInt64LE(BigInt(`0x${normalizeAddress(addressArg)}`));
    return this.buildBasicCommand(flagLight, commandArg, address, dataArg);
  }

  private buildBasicCommand(flagArg: number, commandArg: number, addressArg: TOsramBuffer, dataArg: TOsramBuffer): TOsramBuffer {
    const length = 6 + addressArg.length + dataArg.length;
    const buffer = Buffer.alloc(2 + length);
    buffer.writeUInt16LE(length, 0);
    buffer[2] = flagArg;
    buffer[3] = commandArg;
    buffer[4] = 0;
    buffer[5] = 0;
    buffer[6] = 0x07;
    buffer[7] = this.nextSequence();
    addressArg.copy(buffer, 8);
    dataArg.copy(buffer, 8 + addressArg.length);
    return buffer;
  }

  private async connect(endpointArg: IOsramlightifyEndpoint): Promise<plugins.net.Socket> {
    const timeoutMs = this.configArg.timeoutMs || osramlightifyDefaultTimeoutMs;
    return new Promise<plugins.net.Socket>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: endpointArg.host, port: endpointArg.port });
      const timer = setTimeout(() => {
        socket.destroy();
        reject(new Error(`OSRAM Lightify TCP connection to ${endpointArg.host}:${endpointArg.port} timed out after ${timeoutMs}ms.`));
      }, timeoutMs);
      socket.once('connect', () => {
        clearTimeout(timer);
        socket.setNoDelay(true);
        resolve(socket);
      });
      socket.once('error', (errorArg) => {
        clearTimeout(timer);
        reject(errorArg);
      });
    });
  }

  private async send(socketArg: plugins.net.Socket, commandArg: TOsramBuffer): Promise<TOsramBuffer> {
    const timeoutMs = this.configArg.timeoutMs || osramlightifyDefaultTimeoutMs;
    return new Promise<TOsramBuffer>((resolve, reject) => {
      let buffer = Buffer.alloc(0);
      let expectedLength: number | undefined;
      let settled = false;
      const cleanup = () => {
        clearTimeout(timer);
        socketArg.off('data', onData);
        socketArg.off('error', onError);
        socketArg.off('close', onClose);
      };
      const finish = (errorArg?: Error, valueArg?: TOsramBuffer) => {
        if (settled) {
          return;
        }
        settled = true;
        cleanup();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(valueArg || Buffer.alloc(0));
      };
      const onData = (chunkArg: TOsramBuffer) => {
        buffer = Buffer.concat([buffer, chunkArg]);
        if (expectedLength === undefined && buffer.length >= 2) {
          expectedLength = 2 + buffer.readUInt16LE(0);
        }
        if (expectedLength !== undefined && buffer.length >= expectedLength) {
          finish(undefined, buffer.subarray(2, expectedLength));
        }
      };
      const onError = (errorArg: Error) => finish(errorArg);
      const onClose = () => finish(new Error('OSRAM Lightify TCP connection closed before a complete response was received.'));
      const timer = setTimeout(() => finish(new Error(`OSRAM Lightify TCP request timed out after ${timeoutMs}ms.`)), timeoutMs);

      socketArg.on('data', onData);
      socketArg.once('error', onError);
      socketArg.once('close', onClose);
      socketArg.write(commandArg, (errorArg) => {
        if (errorArg) {
          finish(errorArg);
        }
      });
    });
  }

  private deviceTypeDefinition(typeIdArg: number, rgbArg: [number, number, number]): IOsramlightifyDeviceTypeDefinition {
    const known = deviceTypes[typeIdArg];
    if (known) {
      return known;
    }
    const assumeTunable = rgbArg[0] === noRgbValues[0] && rgbArg[1] === noRgbValues[1] && rgbArg[2] === noRgbValues[2];
    return assumeTunable
      ? { ...deviceTypes[2], deviceName: 'unknown device' }
      : { ...deviceTypes[10], deviceName: 'unknown device' };
  }

  private endpoint(): IOsramlightifyEndpoint | undefined {
    const configured = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!configured) {
      return undefined;
    }
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(configured) ? configured : `tcp://${configured}`);
      return { host: url.hostname, port: this.configArg.port || (url.port ? Number(url.port) : osramlightifyDefaultPort) };
    } catch {
      return { host: configured, port: this.configArg.port || osramlightifyDefaultPort };
    }
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private hasManualData(): boolean {
    return this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || Boolean(this.configArg.entities?.length || this.configArg.snapshotProvider);
  }

  private shouldIncludeGroups(): boolean {
    return this.configArg.includeGroups ?? this.configArg.allowLightifyGroups ?? this.configArg.allow_lightify_groups ?? true;
  }

  private shouldExposeLight(lightArg: IOsramlightifyLightRaw): boolean {
    if (lightArg.deviceType === 'SENSOR') {
      return Boolean(this.configArg.allowLightifySensors ?? this.configArg.allow_lightify_sensors);
    }
    if (lightArg.deviceType === 'SWITCH') {
      return Boolean(this.configArg.allowLightifySwitches ?? this.configArg.allow_lightify_switches);
    }
    return true;
  }

  private nextSequence(): number {
    this.sequence = (this.sequence + 1) % 256;
    return this.sequence;
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return OsramlightifyMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorArg });
  }
}

const groupsFromMask = (maskArg: number): number[] => {
  const groups: number[] = [];
  for (let group = 1; group <= 16; group++) {
    if (maskArg & (1 << (group - 1))) {
      groups.push(group);
    }
  }
  return groups;
};

const groupFromMask = (maskArg: number): number | undefined => {
  for (let group = 16; group >= 1; group--) {
    if (maskArg & (1 << (group - 1))) {
      return group;
    }
  }
  return undefined;
};

const packLuminance = (luminanceArg: number, transitionArg: number): Buffer => {
  const buffer = Buffer.alloc(3);
  buffer[0] = clamp(Math.round(luminanceArg), 0, 100);
  buffer.writeUInt16LE(clamp(Math.round(transitionArg), 0, 0xffff), 1);
  return buffer;
};

const packTemperature = (kelvinArg: number, transitionArg: number): Buffer => {
  const buffer = Buffer.alloc(4);
  buffer.writeUInt16LE(clamp(Math.round(kelvinArg), 0, 0xffff), 0);
  buffer.writeUInt16LE(clamp(Math.round(transitionArg), 0, 0xffff), 2);
  return buffer;
};

const packColour = (rgbArg: [number, number, number], transitionArg: number): Buffer => {
  const buffer = Buffer.alloc(6);
  buffer[0] = clamp(rgbArg[0], 0, 255);
  buffer[1] = clamp(rgbArg[1], 0, 255);
  buffer[2] = clamp(rgbArg[2], 0, 255);
  buffer[3] = defaultAlpha;
  buffer.writeUInt16LE(clamp(Math.round(transitionArg), 0, 0xffff), 4);
  return buffer;
};

const brightnessPercent = (dataArg: Record<string, unknown> | undefined): number | undefined => {
  const luminance = numberValue(dataArg?.luminance) ?? numberValue(dataArg?.brightnessPercent) ?? numberValue(dataArg?.brightness_percent);
  if (luminance !== undefined) {
    return clamp(Math.round(luminance), 0, 100);
  }
  const brightness = numberValue(dataArg?.brightness);
  return brightness === undefined ? undefined : clamp(Math.round(brightness / 2.55), 0, 100);
};

const rgbValue = (valueArg: unknown): [number, number, number] | undefined => {
  if (!Array.isArray(valueArg) || valueArg.length < 3) {
    return undefined;
  }
  const values = valueArg.slice(0, 3).map(numberValue);
  if (values.some((value) => value === undefined)) {
    return undefined;
  }
  return [clamp(values[0]!, 0, 255), clamp(values[1]!, 0, 255), clamp(values[2]!, 0, 255)];
};

const transitionTenths = (valueArg: unknown): number => {
  const seconds = numberValue(valueArg) || 0;
  return Math.round(seconds * 10);
};

const normalizeAddress = (valueArg: unknown): string | undefined => {
  if (typeof valueArg === 'bigint') {
    return valueArg.toString(16).padStart(16, '0');
  }
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return BigInt(Math.round(valueArg)).toString(16).padStart(16, '0');
  }
  if (typeof valueArg !== 'string') {
    return undefined;
  }
  const hex = valueArg.replace(/[^a-fA-F0-9]/g, '');
  return hex.length ? hex.padStart(16, '0').slice(-16).toLowerCase() : undefined;
};

const addressFromEntity = (entityIdArg: string | undefined): string | undefined => {
  const match = entityIdArg?.match(/([a-fA-F0-9]{16})/);
  return match ? normalizeAddress(match[1]) : undefined;
};

const groupIdFromEntity = (entityIdArg: string | undefined): number | undefined => {
  const match = entityIdArg?.match(/group_(\d+)/);
  return match ? Number(match[1]) : undefined;
};

const trimNulls = (valueArg: string): string => valueArg.replace(/\0/g, '').trim();

const unique = (valuesArg: string[]): string[] => [...new Set(valuesArg)];

const clamp = (valueArg: number, minArg: number, maxArg: number): number => Math.min(maxArg, Math.max(minArg, valueArg));

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

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

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

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

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
