import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  IHyperionCommandRequest,
  IHyperionComponentState,
  IHyperionConfig,
  IHyperionEffectDefinition,
  IHyperionInstanceInfo,
  IHyperionInstanceSnapshot,
  IHyperionPriority,
  IHyperionRawData,
  IHyperionServerInfo,
  IHyperionSnapshot,
  THyperionComponentId,
  THyperionImageFormat,
  THyperionSnapshotSource,
  THyperionTransport,
} from './hyperion.types.js';
import {
  hyperionComponentIds,
  hyperionDefaultJsonPort,
  hyperionDefaultOrigin,
  hyperionDefaultPriority,
  hyperionDefaultWebPort,
  hyperionDisplayName,
  hyperionDomain,
  hyperionSolidEffect,
} from './hyperion.types.js';

interface IHyperionSnapshotOptions {
  config: IHyperionConfig;
  rawData?: IHyperionRawData;
  online?: boolean;
  source?: THyperionSnapshotSource;
  error?: string;
}

interface IHyperionEntityDefinition {
  key: string;
  name: string;
  platform: TEntityPlatform;
  state: unknown;
  attributes?: Record<string, unknown>;
  available?: boolean;
}

const componentLabels: Record<string, string> = {
  ALL: 'All',
  SMOOTHING: 'Smoothing',
  BLACKBORDER: 'Blackbar detection',
  FORWARDER: 'Forwarder',
  BOBLIGHTSERVER: 'Boblight server',
  GRABBER: 'Platform capture',
  LEDDEVICE: 'LED device',
  V4L: 'USB capture',
  AUDIO: 'Audio capture',
};

export class HyperionMapper {
  public static toSnapshot(optionsArg: IHyperionSnapshotOptions): IHyperionSnapshot {
    if (optionsArg.config.snapshot && !optionsArg.rawData) {
      return this.normalizeSnapshot(this.clone(optionsArg.config.snapshot), optionsArg.config, optionsArg.source || 'snapshot');
    }

    const rawData = optionsArg.rawData || optionsArg.config.rawData || {};
    const sysInfo = rawData.sysInfo || {};
    const serverInfo = rawData.serverInfo || {};
    const endpoint = parseEndpoint(optionsArg.config.url);
    const transport = optionsArg.config.transport || endpoint?.transport || 'tcp';
    const host = optionsArg.config.host || endpoint?.host;
    const ssl = optionsArg.config.ssl ?? optionsArg.config.tls ?? endpoint?.ssl ?? false;
    const port = optionsArg.config.port || endpoint?.port || this.defaultPort(transport, ssl, optionsArg.config);
    const webPort = optionsArg.config.webPort || (transport === 'http' || transport === 'websocket' ? port : undefined) || endpoint?.port || hyperionDefaultWebPort;
    const jsonPort = optionsArg.config.jsonPort || (transport === 'tcp' ? port : undefined) || hyperionDefaultJsonPort;
    const hyperionInfo = sysInfo.hyperion || {};
    const deviceId = optionsArg.config.uniqueId || hyperionInfo.id || (host ? `${host}:${port}` : undefined) || optionsArg.config.name || hyperionDomain;
    const deviceName = optionsArg.config.name || this.firstInstanceName(serverInfo) || hyperionDisplayName;
    const instances = this.instances(serverInfo, rawData, optionsArg.config, optionsArg.online ?? optionsArg.config.online ?? Boolean(rawData.serverInfo));
    const effectNames = new Set<string>();
    for (const instance of instances) {
      for (const effectName of instance.effectNames) {
        effectNames.add(effectName);
      }
    }

    const snapshot: IHyperionSnapshot = {
      device: {
        id: deviceId,
        name: deviceName,
        manufacturer: optionsArg.config.manufacturer || 'Hyperion',
        model: optionsArg.config.model || 'Hyperion-NG',
        version: hyperionInfo.version,
        host,
        port,
        jsonPort,
        webPort,
        ssl,
        transport,
        hostname: serverInfo.hostname || sysInfo.system?.hostName,
      },
      instances,
      capabilities: {
        localControl: Boolean(optionsArg.config.host || optionsArg.config.url || optionsArg.config.commandExecutor || optionsArg.config.client?.execute),
        effects: effectNames.size > 0,
        components: instances.some((instanceArg) => instanceArg.components.length > 0),
        imageSnapshot: Boolean(optionsArg.config.host || optionsArg.config.url || optionsArg.config.commandExecutor || optionsArg.config.client?.execute),
        liveStreaming: Boolean((transport === 'tcp' || transport === 'websocket') && (optionsArg.config.host || optionsArg.config.url || optionsArg.config.commandExecutor || optionsArg.config.client?.execute)),
        multiInstance: instances.length > 1,
        authRequired: rawData.authRequired,
      },
      settings: {
        priority: this.priority(optionsArg.config.priority),
        origin: optionsArg.config.origin || hyperionDefaultOrigin,
        effectHideList: optionsArg.config.effectHideList || [],
      },
      rawData: Object.keys(rawData).length ? rawData : undefined,
      online: optionsArg.online ?? optionsArg.config.online ?? Boolean(rawData.serverInfo),
      updatedAt: rawData.fetchedAt || new Date().toISOString(),
      source: optionsArg.source || (rawData.serverInfo ? transport : 'runtime'),
      error: optionsArg.error,
    };

    return this.normalizeSnapshot(snapshot, optionsArg.config, snapshot.source);
  }

  public static toDevices(snapshotArg: IHyperionSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    return snapshotArg.instances.map((instanceArg) => {
      const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
        { id: 'power', capability: 'light', name: 'Power', readable: true, writable: snapshotArg.capabilities.localControl },
        { id: 'brightness', capability: 'light', name: 'Brightness', readable: true, writable: snapshotArg.capabilities.localControl, unit: '%' },
        { id: 'visible_priority', capability: 'sensor', name: 'Visible priority', readable: true, writable: false },
        { id: 'image_snapshot', capability: 'sensor', name: 'Image snapshot', readable: true, writable: false },
      ];
      for (const component of hyperionComponentIds) {
        features.push({ id: `component_${this.slug(component)}`, capability: 'switch', name: `Component ${componentLabels[component] || component}`, readable: true, writable: snapshotArg.capabilities.localControl });
      }
      return {
        id: this.deviceId(snapshotArg, instanceArg.instance),
        integrationDomain: hyperionDomain,
        name: instanceArg.name,
        protocol: (snapshotArg.device.transport || (snapshotArg.device.host ? 'hyperion-json' : 'unknown')) as plugins.shxInterfaces.data.TDeviceProtocol,
        manufacturer: snapshotArg.device.manufacturer,
        model: snapshotArg.device.model,
        online: snapshotArg.online && instanceArg.running,
        features,
        state: [
          { featureId: 'power', value: this.lightIsOn(instanceArg, snapshotArg.settings.priority), updatedAt },
          { featureId: 'brightness', value: this.brightnessPercent(instanceArg) ?? null, updatedAt },
          { featureId: 'visible_priority', value: this.deviceStateValue(this.visiblePriorityValue(instanceArg)), updatedAt },
          { featureId: 'image_snapshot', value: instanceArg.imageSnapshot ? `${instanceArg.imageSnapshot.format || 'PNG'}:${instanceArg.imageSnapshot.width || '?'}x${instanceArg.imageSnapshot.height || '?'}` : null, updatedAt },
        ],
        metadata: this.cleanAttributes({
          serverId: snapshotArg.device.id,
          instance: instanceArg.instance,
          running: instanceArg.running,
          host: snapshotArg.device.host,
          port: snapshotArg.device.port,
          jsonPort: snapshotArg.device.jsonPort,
          webPort: snapshotArg.device.webPort,
          ssl: snapshotArg.device.ssl,
          version: snapshotArg.device.version,
          source: snapshotArg.source,
          error: snapshotArg.error,
        }),
      };
    });
  }

  public static toEntities(snapshotArg: IHyperionSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const uniqueBase = this.uniqueBase(snapshotArg);
    for (const instance of snapshotArg.instances) {
      const deviceId = this.deviceId(snapshotArg, instance.instance);
      const available = snapshotArg.online && instance.running;
      const lightPriority = this.priorityEntry(instance, snapshotArg.settings.priority);
      entities.push(this.entity(snapshotArg, instance, {
        key: 'light',
        name: instance.name,
        platform: 'light',
        state: lightPriority ? 'on' : 'off',
        available,
        attributes: {
          brightness: this.brightness255(instance),
          brightnessPercent: this.brightnessPercent(instance),
          rgbColor: this.rgbColor(lightPriority),
          effect: this.effectName(lightPriority),
          effectList: [hyperionSolidEffect, ...instance.effectNames.filter((effectArg) => !snapshotArg.settings.effectHideList.includes(effectArg))],
          priority: snapshotArg.settings.priority,
          instance: instance.instance,
          key: 'light',
        },
      }, deviceId, uniqueBase));

      entities.push(this.entity(snapshotArg, instance, {
        key: 'visible_priority',
        name: `${instance.name} Visible priority`,
        platform: 'sensor',
        state: this.visiblePriorityValue(instance),
        available,
        attributes: this.cleanAttributes({
          component_id: instance.visiblePriority?.componentId,
          origin: instance.visiblePriority?.origin,
          priority: instance.visiblePriority?.priority,
          owner: instance.visiblePriority?.owner,
          color: instance.visiblePriority?.value,
          instance: instance.instance,
          key: 'visible_priority',
        }),
      }, deviceId, uniqueBase));

      entities.push(this.entity(snapshotArg, instance, {
        key: 'camera',
        name: `${instance.name} Image`,
        platform: 'camera' as TEntityPlatform,
        state: available ? 'idle' : 'unavailable',
        available,
        attributes: this.cleanAttributes({
          format: instance.imageSnapshot?.format,
          width: instance.imageSnapshot?.width,
          height: instance.imageSnapshot?.height,
          hasImage: Boolean(instance.imageSnapshot?.data),
          instance: instance.instance,
          key: 'camera',
        }),
      }, deviceId, uniqueBase));

      for (const component of hyperionComponentIds) {
        const state = this.componentState(instance, component);
        entities.push(this.entity(snapshotArg, instance, {
          key: `component_${this.slug(component)}`,
          name: `${instance.name} Component ${componentLabels[component] || component}`,
          platform: 'switch',
          state: state?.enabled ? 'on' : 'off',
          available,
          attributes: {
            component,
            instance: instance.instance,
            key: 'component',
          },
        }, deviceId, uniqueBase));
      }
    }
    return entities;
  }

  public static commandsForService(snapshotArg: IHyperionSnapshot, requestArg: IServiceCallRequest): IHyperionCommandRequest[] {
    if (requestArg.domain === hyperionDomain) {
      const command = this.hyperionServiceCommand(snapshotArg, requestArg);
      return command ? [command] : [];
    }

    if (requestArg.domain === 'light') {
      const instance = this.instanceForTarget(snapshotArg, requestArg);
      if (requestArg.service === 'turn_off') {
        return [{ action: 'clear', instance, priority: snapshotArg.settings.priority, service: requestArg.service, target: requestArg.target, data: requestArg.data }];
      }
      if (requestArg.service !== 'turn_on') {
        return [];
      }
      const commands: IHyperionCommandRequest[] = [];
      const brightness = this.numberData(requestArg, 'brightness');
      if (brightness !== undefined) {
        commands.push({ action: 'adjust_brightness', instance, brightness, service: requestArg.service, target: requestArg.target, data: requestArg.data });
      }
      const effect = this.stringData(requestArg, 'effect');
      if (effect && effect !== hyperionSolidEffect) {
        commands.push({ action: 'set_effect', instance, effect, priority: snapshotArg.settings.priority, origin: snapshotArg.settings.origin, duration: this.numberData(requestArg, 'duration'), service: requestArg.service, target: requestArg.target, data: requestArg.data });
      } else {
        const color = this.colorData(requestArg) || this.currentOrDefaultColor(snapshotArg, instance);
        commands.push({ action: 'set_color', instance, color, priority: snapshotArg.settings.priority, origin: snapshotArg.settings.origin, duration: this.numberData(requestArg, 'duration'), service: requestArg.service, target: requestArg.target, data: requestArg.data });
      }
      return commands;
    }

    if (requestArg.domain === 'switch') {
      const enabled = requestArg.service === 'turn_on' ? true : requestArg.service === 'turn_off' ? false : undefined;
      if (enabled === undefined) {
        return [];
      }
      const component = this.componentForTarget(snapshotArg, requestArg);
      return component ? [{ action: 'set_component', instance: this.instanceForTarget(snapshotArg, requestArg), component, state: enabled, service: requestArg.service, target: requestArg.target, data: requestArg.data }] : [];
    }

    if (requestArg.domain === 'camera' && (requestArg.service === 'snapshot' || requestArg.service === 'get_image_snapshot')) {
      return [{ action: 'get_image_snapshot', instance: this.instanceForTarget(snapshotArg, requestArg), format: this.imageFormat(requestArg.data?.format) }];
    }

    return [];
  }

  public static commandForService(snapshotArg: IHyperionSnapshot, requestArg: IServiceCallRequest): IHyperionCommandRequest | undefined {
    return this.commandsForService(snapshotArg, requestArg)[0];
  }

  public static deviceId(snapshotArg: IHyperionSnapshot, instanceArg: number): string {
    return `${hyperionDomain}.instance.${this.uniqueBase(snapshotArg)}.${instanceArg}`;
  }

  public static uniqueBase(snapshotArg: IHyperionSnapshot): string {
    return this.slug(snapshotArg.device.id || snapshotArg.device.host || snapshotArg.device.name || hyperionDomain);
  }

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

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

  private static instances(serverInfoArg: IHyperionServerInfo, rawDataArg: IHyperionRawData, configArg: IHyperionConfig, onlineArg: boolean): IHyperionInstanceSnapshot[] {
    const rawInstances = Array.isArray(serverInfoArg.instance) && serverInfoArg.instance.length ? serverInfoArg.instance : [{ instance: configArg.instance ?? 0, friendly_name: configArg.name || hyperionDisplayName, running: onlineArg }];
    const seen = new Set<number>();
    return rawInstances.map((rawInstanceArg) => this.instanceSnapshot(rawInstanceArg, rawDataArg, serverInfoArg, configArg, onlineArg)).filter((instanceArg) => {
      if (seen.has(instanceArg.instance)) {
        return false;
      }
      seen.add(instanceArg.instance);
      return true;
    });
  }

  private static instanceSnapshot(rawInstanceArg: IHyperionInstanceInfo, rawDataArg: IHyperionRawData, fallbackInfoArg: IHyperionServerInfo, configArg: IHyperionConfig, onlineArg: boolean): IHyperionInstanceSnapshot {
    const instance = Number.isInteger(rawInstanceArg.instance) ? rawInstanceArg.instance : configArg.instance ?? 0;
    const info = rawDataArg.instanceServerInfo?.[instance] || fallbackInfoArg;
    const effects = this.effects(info);
    const priorities = Array.isArray(info.priorities) ? info.priorities : [];
    return {
      instance,
      name: rawInstanceArg.friendly_name || configArg.name || `${hyperionDisplayName} ${instance}`,
      running: rawInstanceArg.running ?? onlineArg,
      serverInfo: info,
      components: Array.isArray(info.components) ? info.components : [],
      effects,
      effectNames: effects.map((effectArg) => effectArg.name).filter(Boolean),
      activeEffectNames: Array.isArray(info.activeEffects) ? info.activeEffects.map((effectArg) => effectArg.name).filter((effectArg): effectArg is string => Boolean(effectArg)) : [],
      adjustments: Array.isArray(info.adjustment) ? info.adjustment : [],
      priorities,
      visiblePriority: priorities.find((priorityArg) => priorityArg.visible === true),
      imageSnapshot: rawDataArg.imageSnapshots?.[instance],
    };
  }

  private static effects(serverInfoArg: IHyperionServerInfo): IHyperionEffectDefinition[] {
    return Array.isArray(serverInfoArg.effects) ? serverInfoArg.effects.filter((effectArg) => typeof effectArg.name === 'string') : [];
  }

  private static normalizeSnapshot(snapshotArg: IHyperionSnapshot, configArg: IHyperionConfig, sourceArg: THyperionSnapshotSource): IHyperionSnapshot {
    return {
      ...snapshotArg,
      instances: snapshotArg.instances.length ? snapshotArg.instances : this.instances({}, {}, configArg, snapshotArg.online),
      settings: {
        priority: this.priority(snapshotArg.settings?.priority ?? configArg.priority),
        origin: snapshotArg.settings?.origin || configArg.origin || hyperionDefaultOrigin,
        effectHideList: snapshotArg.settings?.effectHideList || configArg.effectHideList || [],
      },
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
      source: sourceArg,
    };
  }

  private static entity(snapshotArg: IHyperionSnapshot, instanceArg: IHyperionInstanceSnapshot, definitionArg: IHyperionEntityDefinition, deviceIdArg: string, uniqueBaseArg: string): IIntegrationEntity {
    const entityId = `${definitionArg.platform}.${this.slug(`${instanceArg.name}_${definitionArg.key}`)}`;
    return {
      id: entityId,
      uniqueId: `${hyperionDomain}_${uniqueBaseArg}_${instanceArg.instance}_${definitionArg.key}`,
      integrationDomain: hyperionDomain,
      deviceId: deviceIdArg,
      platform: definitionArg.platform,
      name: definitionArg.name,
      state: definitionArg.state,
      available: definitionArg.available ?? (snapshotArg.online && instanceArg.running),
      attributes: this.cleanAttributes(definitionArg.attributes || {}),
    };
  }

  private static hyperionServiceCommand(snapshotArg: IHyperionSnapshot, requestArg: IServiceCallRequest): IHyperionCommandRequest | undefined {
    if (requestArg.service === 'refresh') {
      return { action: 'refresh', service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }
    if (requestArg.service === 'set_color') {
      const color = this.colorData(requestArg);
      return color ? { action: 'set_color', instance: this.instanceForTarget(snapshotArg, requestArg), color, priority: this.priority(this.numberData(requestArg, 'priority') ?? snapshotArg.settings.priority), origin: this.stringData(requestArg, 'origin') || snapshotArg.settings.origin, duration: this.numberData(requestArg, 'duration'), service: requestArg.service, target: requestArg.target, data: requestArg.data } : undefined;
    }
    if (requestArg.service === 'set_effect') {
      const effect = this.stringData(requestArg, 'effect') || this.stringData(requestArg, 'name');
      return effect ? { action: 'set_effect', instance: this.instanceForTarget(snapshotArg, requestArg), effect, effectArgs: this.objectData(requestArg, 'args'), priority: this.priority(this.numberData(requestArg, 'priority') ?? snapshotArg.settings.priority), origin: this.stringData(requestArg, 'origin') || snapshotArg.settings.origin, duration: this.numberData(requestArg, 'duration'), service: requestArg.service, target: requestArg.target, data: requestArg.data } : undefined;
    }
    if (requestArg.service === 'clear') {
      return { action: 'clear', instance: this.instanceForTarget(snapshotArg, requestArg), priority: this.priority(this.numberData(requestArg, 'priority') ?? snapshotArg.settings.priority), service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }
    if (requestArg.service === 'set_component') {
      const component = this.stringData(requestArg, 'component') as THyperionComponentId | undefined;
      const state = this.booleanData(requestArg, 'state') ?? this.booleanData(requestArg, 'enabled');
      return component && state !== undefined ? { action: 'set_component', instance: this.instanceForTarget(snapshotArg, requestArg), component, state, service: requestArg.service, target: requestArg.target, data: requestArg.data } : undefined;
    }
    if (requestArg.service === 'get_image_snapshot' || requestArg.service === 'image_snapshot') {
      return { action: 'get_image_snapshot', instance: this.instanceForTarget(snapshotArg, requestArg), format: this.imageFormat(requestArg.data?.format), service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }
    if (requestArg.service === 'image_stream_start') {
      return { action: 'image_stream_start', instance: this.instanceForTarget(snapshotArg, requestArg), service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }
    if (requestArg.service === 'image_stream_stop') {
      return { action: 'image_stream_stop', instance: this.instanceForTarget(snapshotArg, requestArg), service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }
    if (requestArg.service === 'raw_command' || requestArg.service === 'command') {
      const payload = this.objectData(requestArg, 'payload') || this.objectData(requestArg, 'command');
      return payload && typeof payload.command === 'string' ? { action: 'raw_command', payload: payload as IHyperionCommandRequest['payload'], service: requestArg.service, target: requestArg.target, data: requestArg.data } : undefined;
    }
    if (requestArg.service === 'restore') {
      return { action: 'restore', snapshot: requestArg.data?.snapshot as IHyperionSnapshot | undefined, service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }
    return undefined;
  }

  private static firstInstanceName(serverInfoArg: IHyperionServerInfo): string | undefined {
    return Array.isArray(serverInfoArg.instance) ? serverInfoArg.instance.find((instanceArg) => instanceArg.friendly_name)?.friendly_name : undefined;
  }

  private static defaultPort(transportArg: THyperionTransport, sslArg: boolean, configArg: IHyperionConfig): number {
    if (transportArg === 'tcp') {
      return configArg.jsonPort || hyperionDefaultJsonPort;
    }
    return configArg.webPort || (sslArg ? 8092 : hyperionDefaultWebPort);
  }

  private static priority(valueArg: unknown): number {
    const value = typeof valueArg === 'number' && Number.isFinite(valueArg) ? Math.round(valueArg) : hyperionDefaultPriority;
    return Math.max(1, Math.min(253, value));
  }

  private static priorityEntry(instanceArg: IHyperionInstanceSnapshot, priorityArg: number): IHyperionPriority | undefined {
    return instanceArg.priorities.find((priorityEntryArg) => priorityEntryArg.priority === priorityArg);
  }

  private static lightIsOn(instanceArg: IHyperionInstanceSnapshot, priorityArg: number): boolean {
    return Boolean(this.priorityEntry(instanceArg, priorityArg));
  }

  private static componentState(instanceArg: IHyperionInstanceSnapshot, componentArg: string): IHyperionComponentState | undefined {
    return instanceArg.components.find((componentStateArg) => componentStateArg.name === componentArg);
  }

  private static brightnessPercent(instanceArg: IHyperionInstanceSnapshot): number | undefined {
    const brightness = instanceArg.adjustments[0]?.brightness;
    return typeof brightness === 'number' ? Math.max(0, Math.min(100, Math.round(brightness))) : undefined;
  }

  private static brightness255(instanceArg: IHyperionInstanceSnapshot): number | undefined {
    const brightness = this.brightnessPercent(instanceArg);
    return brightness === undefined ? undefined : Math.round((brightness * 255) / 100);
  }

  private static rgbColor(priorityArg: IHyperionPriority | undefined): number[] | undefined {
    const rgb = priorityArg?.value?.RGB;
    return Array.isArray(rgb) && rgb.length >= 3 ? rgb.slice(0, 3).map((valueArg) => this.colorByte(valueArg)) : undefined;
  }

  private static effectName(priorityArg: IHyperionPriority | undefined): string {
    return priorityArg?.componentId === 'EFFECT' && priorityArg.owner ? priorityArg.owner : hyperionSolidEffect;
  }

  private static visiblePriorityValue(instanceArg: IHyperionInstanceSnapshot): unknown {
    const priority = instanceArg.visiblePriority;
    if (!priority) {
      return null;
    }
    if (priority.componentId === 'COLOR' && priority.value?.RGB) {
      return priority.value.RGB;
    }
    return priority.owner || priority.componentId || (priority.priority ?? null);
  }

  private static currentOrDefaultColor(snapshotArg: IHyperionSnapshot, instanceArg: number | undefined): number[] {
    const instance = snapshotArg.instances.find((instanceEntryArg) => instanceEntryArg.instance === instanceArg) || snapshotArg.instances[0];
    return this.rgbColor(this.priorityEntry(instance, snapshotArg.settings.priority)) || [255, 255, 255];
  }

  private static instanceForTarget(snapshotArg: IHyperionSnapshot, requestArg: IServiceCallRequest): number | undefined {
    const dataInstance = this.numberData(requestArg, 'instance');
    if (dataInstance !== undefined) {
      return Math.max(0, Math.round(dataInstance));
    }
    const target = `${requestArg.target.entityId || ''} ${requestArg.target.deviceId || ''}`;
    for (const instance of snapshotArg.instances) {
      const slug = this.slug(instance.name);
      if (target.includes(`_${instance.instance}`) || target.includes(`.${slug}_`) || target.includes(`${slug}`)) {
        return instance.instance;
      }
    }
    return snapshotArg.instances.length === 1 ? snapshotArg.instances[0].instance : undefined;
  }

  private static componentForTarget(snapshotArg: IHyperionSnapshot, requestArg: IServiceCallRequest): THyperionComponentId | undefined {
    const explicit = this.stringData(requestArg, 'component');
    if (explicit) {
      return explicit.toUpperCase();
    }
    const target = `${requestArg.target.entityId || ''} ${requestArg.target.deviceId || ''}`.toLowerCase();
    for (const component of hyperionComponentIds) {
      const slug = this.slug(component);
      const labelSlug = this.slug(componentLabels[component] || component);
      if (target.includes(`component_${slug}`) || target.includes(labelSlug)) {
        return component;
      }
    }
    void snapshotArg;
    return undefined;
  }

  private static colorData(requestArg: IServiceCallRequest): number[] | undefined {
    const color = requestArg.data?.color || requestArg.data?.rgb_color || requestArg.data?.rgbColor;
    if (Array.isArray(color) && color.length >= 3) {
      return color.slice(0, 3).map((valueArg) => this.colorByte(valueArg));
    }
    const hsColor = requestArg.data?.hs_color || requestArg.data?.hsColor;
    if (Array.isArray(hsColor) && hsColor.length >= 2) {
      return this.hsToRgb(Number(hsColor[0]), Number(hsColor[1]));
    }
    return undefined;
  }

  private static hsToRgb(hueArg: number, saturationArg: number): number[] {
    const hue = ((hueArg % 360) + 360) % 360;
    const saturation = Math.max(0, Math.min(100, saturationArg)) / 100;
    const chroma = saturation;
    const x = chroma * (1 - Math.abs((hue / 60) % 2 - 1));
    const match = 1 - chroma;
    const [r, g, b] = hue < 60 ? [chroma, x, 0]
      : hue < 120 ? [x, chroma, 0]
        : hue < 180 ? [0, chroma, x]
          : hue < 240 ? [0, x, chroma]
            : hue < 300 ? [x, 0, chroma]
              : [chroma, 0, x];
    return [r, g, b].map((valueArg) => this.colorByte((valueArg + match) * 255));
  }

  private static colorByte(valueArg: unknown): number {
    const value = typeof valueArg === 'number' ? valueArg : Number(valueArg);
    return Math.max(0, Math.min(255, Math.round(Number.isFinite(value) ? value : 0)));
  }

  private static imageFormat(valueArg: unknown): THyperionImageFormat {
    return valueArg === 'BMP' || valueArg === 'JPG' || valueArg === 'PNG' ? valueArg : 'PNG';
  }

  private static stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
    const value = requestArg.data?.[keyArg];
    return typeof value === 'string' && value.trim() ? value.trim() : undefined;
  }

  private static numberData(requestArg: IServiceCallRequest, keyArg: string): number | undefined {
    const value = requestArg.data?.[keyArg];
    return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
  }

  private static booleanData(requestArg: IServiceCallRequest, keyArg: string): boolean | undefined {
    const value = requestArg.data?.[keyArg];
    return typeof value === 'boolean' ? value : undefined;
  }

  private static objectData(requestArg: IServiceCallRequest, keyArg: string): Record<string, unknown> | undefined {
    const value = requestArg.data?.[keyArg];
    return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
  }

  private static cleanAttributes(attributesArg: Record<string, unknown>): Record<string, unknown> {
    const result: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(attributesArg)) {
      if (value !== undefined) {
        result[key] = value;
      }
    }
    return result;
  }

  private static deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
    if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean' || valueArg === null) {
      return valueArg;
    }
    return JSON.stringify(valueArg);
  }
}

const parseEndpoint = (valueArg: string | undefined): { host: string; port?: number; ssl: boolean; transport: THyperionTransport } | undefined => {
  if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    const url = new URL(valueArg);
    const protocol = url.protocol.replace(':', '').toLowerCase();
    const transport: THyperionTransport = protocol === 'ws' || protocol === 'wss' ? 'websocket' : protocol === 'http' || protocol === 'https' ? 'http' : 'tcp';
    return { host: url.hostname, port: url.port ? Number(url.port) : undefined, ssl: protocol === 'https' || protocol === 'wss', transport };
  } catch {
    return undefined;
  }
};
