import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  IGo2rtcCamera,
  IGo2rtcClientCommand,
  IGo2rtcConfig,
  IGo2rtcEndpointSet,
  IGo2rtcProducerConsumer,
  IGo2rtcRawProducerConsumer,
  IGo2rtcRawStreamInfo,
  IGo2rtcRawStreamsResponse,
  IGo2rtcSensor,
  IGo2rtcSnapshot,
  IGo2rtcStream,
  IGo2rtcTrack,
  TGo2rtcProtocol,
  TGo2rtcSnapshotSource,
  TGo2rtcStreamProtocol,
  TGo2rtcTrackKind,
} from './go2rtc.types.js';
import { go2rtcDefaultApiPort, go2rtcDefaultRtspPort } from './go2rtc.types.js';

const cameraStreamServices = new Set(['stream', 'stream_source', 'get_stream', 'stream_metadata']);
const cameraSnapshotServices = new Set(['snapshot', 'camera_image', 'camera_snapshot', 'snapshot_metadata']);
const endpointKeys = new Set<keyof IGo2rtcEndpointSet>(['apiUrl', 'snapshotUrl', 'mjpegUrl', 'mp4Url', 'hlsUrl', 'whepUrl', 'websocketUrl', 'rtspUrl']);

export class Go2rtcMapper {
  public static toSnapshot(configArg: IGo2rtcConfig, connectedArg?: boolean, sourceArg: TGo2rtcSnapshotSource = 'manual', errorArg?: string): IGo2rtcSnapshot {
    const configuredEndpoint = this.endpointFromConfig(configArg);
    const connected = connectedArg ?? configArg.snapshot?.connected ?? configArg.connected ?? false;
    const endpoints = this.endpointSet(configArg, undefined, configuredEndpoint);
    const streamMap = new Map<string, IGo2rtcStream>();

    for (const stream of [...(configArg.snapshot?.streams || []), ...(configArg.streams || [])]) {
      const normalized = this.normalizeStream(stream, configArg, configuredEndpoint);
      streamMap.set(normalized.id, { ...streamMap.get(normalized.id), ...normalized });
    }

    const streams = [...streamMap.values()].sort((leftArg, rightArg) => leftArg.name.localeCompare(rightArg.name));
    const cameras = this.uniqueCameras([
      ...(configArg.snapshot?.cameras || []),
      ...(configArg.cameras || []),
      ...streams.map((streamArg) => this.cameraFromStream(streamArg, configArg, connected, configuredEndpoint)),
    ]);
    const sensors = this.defaultSensors(configArg, connected, streams, errorArg);

    return {
      id: configArg.uniqueId || configArg.snapshot?.id || configuredEndpoint.host || configuredEndpoint.url || 'go2rtc',
      name: configArg.name || configArg.snapshot?.name || configuredEndpoint.host || 'go2rtc',
      connected,
      configured: Boolean(configArg.url || configArg.host || configArg.snapshot || streams.length || configArg.commandExecutor),
      source: configArg.snapshot?.source && !errorArg ? configArg.snapshot.source : sourceArg,
      serverInfo: {
        ...configArg.snapshot?.serverInfo,
        ...configArg.serverInfo,
        rtsp: {
          ...configArg.snapshot?.serverInfo?.rtsp,
          ...configArg.serverInfo?.rtsp,
          defaultQuery: configArg.rtsp?.defaultQuery || configArg.serverInfo?.rtsp?.defaultQuery || configArg.snapshot?.serverInfo?.rtsp?.defaultQuery,
        },
      },
      schemes: [...new Set([...(configArg.snapshot?.schemes || []), ...(configArg.schemes || [])])].sort(),
      streams,
      cameras,
      sensors: [...sensors, ...(configArg.snapshot?.sensors || []), ...(configArg.sensors || [])],
      endpoints,
      rtsp: {
        ...configArg.snapshot?.rtsp,
        ...configArg.rtsp,
      },
      webrtc: {
        ...configArg.snapshot?.webrtc,
        ...configArg.webrtc,
      },
      updatedAt: new Date().toISOString(),
      error: errorArg || configArg.snapshot?.error,
      metadata: {
        ...configArg.snapshot?.metadata,
        ...configArg.metadata,
        recommendedVersion: '1.9.14',
        homeAssistantManagedServer: configArg.url === 'http://localhost:11984/',
      },
    };
  }

  public static streamsFromRaw(rawArg: IGo2rtcRawStreamsResponse, configArg: IGo2rtcConfig): IGo2rtcStream[] {
    const endpoint = this.endpointFromConfig(configArg);
    return Object.entries(rawArg || {})
      .filter((entryArg): entryArg is [string, IGo2rtcRawStreamInfo] => Boolean(entryArg[0] && entryArg[1]))
      .map(([nameArg, infoArg]) => this.streamFromRaw(nameArg, infoArg, configArg, endpoint));
  }

  public static toDevices(snapshotArg: IGo2rtcSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    return snapshotArg.cameras.map((cameraArg) => {
      const stream = snapshotArg.streams.find((streamArg) => streamArg.id === cameraArg.streamId || streamArg.name === cameraArg.streamName);
      const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
        { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
        { id: 'camera', capability: 'camera', name: cameraArg.name, readable: true, writable: false },
        { id: 'status', capability: 'sensor', name: 'Stream status', readable: true, writable: false },
        { id: 'producers', capability: 'sensor', name: 'Producer count', readable: true, writable: false },
        { id: 'consumers', capability: 'sensor', name: 'Consumer count', readable: true, writable: false },
      ];
      const state: plugins.shxInterfaces.data.IDeviceState[] = [
        { featureId: 'connectivity', value: snapshotArg.connected && cameraArg.available ? 'online' : snapshotArg.configured ? 'configured' : 'offline', updatedAt },
        { featureId: 'camera', value: this.deviceStateValue(cameraArg.endpoints), updatedAt },
        { featureId: 'status', value: this.streamStatus(snapshotArg, stream), updatedAt },
        { featureId: 'producers', value: stream?.producerCount ?? 0, updatedAt },
        { featureId: 'consumers', value: stream?.consumerCount ?? 0, updatedAt },
      ];

      return {
        id: this.deviceId(snapshotArg, cameraArg),
        integrationDomain: 'go2rtc',
        name: cameraArg.name,
        protocol: 'http',
        manufacturer: 'go2rtc',
        model: snapshotArg.serverInfo?.version ? `go2rtc ${snapshotArg.serverInfo.version}` : 'go2rtc stream',
        online: snapshotArg.connected && cameraArg.available,
        features,
        state,
        metadata: this.cleanAttributes({
          streamId: cameraArg.streamId,
          streamName: cameraArg.streamName,
          sourceUrls: cameraArg.sourceUrls,
          protocols: cameraArg.protocols,
          tracks: cameraArg.tracks,
          endpoints: cameraArg.endpoints,
          serverVersion: snapshotArg.serverInfo?.version,
          schemes: snapshotArg.schemes,
          ...cameraArg.metadata,
        }),
      };
    });
  }

  public static toEntities(snapshotArg: IGo2rtcSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const usedIds = new Map<string, number>();
    const serverDeviceId = `go2rtc.server.${this.uniqueBase(snapshotArg)}`;

    entities.push(this.entity('sensor', `${snapshotArg.name} Status`, serverDeviceId, `go2rtc_${this.uniqueBase(snapshotArg)}_status`, snapshotArg.connected ? 'online' : snapshotArg.configured ? 'configured' : 'offline', usedIds, {
      version: snapshotArg.serverInfo?.version,
      configPath: snapshotArg.serverInfo?.configPath,
      host: snapshotArg.serverInfo?.host,
      source: snapshotArg.source,
      error: snapshotArg.error,
      endpoints: snapshotArg.endpoints,
    }, true));

    entities.push(this.entity('sensor', `${snapshotArg.name} Streams`, serverDeviceId, `go2rtc_${this.uniqueBase(snapshotArg)}_stream_count`, snapshotArg.streams.length, usedIds, {
      schemes: snapshotArg.schemes,
      streamNames: snapshotArg.streams.map((streamArg) => streamArg.name),
    }, snapshotArg.configured));

    for (const camera of snapshotArg.cameras) {
      const stream = snapshotArg.streams.find((streamArg) => streamArg.id === camera.streamId || streamArg.name === camera.streamName);
      const deviceId = this.deviceId(snapshotArg, camera);
      entities.push(this.entity('camera' as TEntityPlatform, camera.name, deviceId, `go2rtc_${this.uniqueBase(snapshotArg)}_camera_${this.slug(camera.streamId)}`, snapshotArg.connected && camera.available ? 'idle' : 'unavailable', usedIds, {
        streamId: camera.streamId,
        streamName: camera.streamName,
        sourceUrls: camera.sourceUrls,
        protocols: camera.protocols,
        tracks: camera.tracks,
        endpoints: camera.endpoints,
        snapshotUrl: camera.endpoints.snapshotUrl,
        streamSource: camera.endpoints.rtspUrl || camera.endpoints.mp4Url || camera.endpoints.mjpegUrl,
        rtspUrl: camera.endpoints.rtspUrl,
        webRtcUrl: camera.endpoints.whepUrl,
        websocketUrl: camera.endpoints.websocketUrl,
        supportedFeatures: this.supportedCameraFeatures(camera),
        serviceMappings: {
          snapshot: 'camera.snapshot',
          streamSource: 'camera.stream_source',
          refresh: 'go2rtc.refresh',
          addStream: 'go2rtc.add_stream',
          updateStream: 'go2rtc.update_stream',
          deleteStream: 'go2rtc.delete_stream',
          streamToCamera: 'go2rtc.stream_to_camera',
        },
        producerCount: stream?.producerCount ?? 0,
        consumerCount: stream?.consumerCount ?? 0,
      }, snapshotArg.connected && camera.available));

      entities.push(this.entity('sensor', `${camera.name} Status`, deviceId, `go2rtc_${this.uniqueBase(snapshotArg)}_${this.slug(camera.streamId)}_status`, this.streamStatus(snapshotArg, stream), usedIds, {
        streamId: camera.streamId,
        producerCount: stream?.producerCount ?? 0,
        consumerCount: stream?.consumerCount ?? 0,
        sourceUrls: camera.sourceUrls,
      }, snapshotArg.configured));

      entities.push(this.entity('sensor', `${camera.name} Producers`, deviceId, `go2rtc_${this.uniqueBase(snapshotArg)}_${this.slug(camera.streamId)}_producers`, stream?.producerCount ?? 0, usedIds, {
        streamId: camera.streamId,
        producers: stream?.producers,
      }, snapshotArg.configured));

      entities.push(this.entity('sensor', `${camera.name} Consumers`, deviceId, `go2rtc_${this.uniqueBase(snapshotArg)}_${this.slug(camera.streamId)}_consumers`, stream?.consumerCount ?? 0, usedIds, {
        streamId: camera.streamId,
        consumers: stream?.consumers,
      }, snapshotArg.configured));
    }

    for (const sensor of snapshotArg.sensors) {
      entities.push(this.entity('sensor', sensor.name, serverDeviceId, `go2rtc_${this.uniqueBase(snapshotArg)}_${this.slug(sensor.id)}`, sensor.value ?? 'unknown', usedIds, {
        unit: sensor.unit,
        deviceClass: sensor.deviceClass,
        entityCategory: sensor.entityCategory,
        ...sensor.attributes,
      }, sensor.available !== false));
    }

    return entities;
  }

  public static commandForService(snapshotArg: IGo2rtcSnapshot, requestArg: IServiceCallRequest): IGo2rtcClientCommand | undefined {
    if (requestArg.domain === 'camera' && cameraStreamServices.has(requestArg.service)) {
      const camera = this.findCamera(snapshotArg, requestArg);
      return {
        type: 'stream_source',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        streamId: camera?.streamId,
        streamSource: {
          streamId: camera?.streamId,
          endpoint: this.endpointKey(requestArg.data?.endpoint),
        },
      };
    }

    if (requestArg.domain === 'camera' && cameraSnapshotServices.has(requestArg.service)) {
      const camera = this.findCamera(snapshotArg, requestArg);
      return {
        type: 'snapshot_image',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        streamId: camera?.streamId,
        snapshot: {
          streamId: camera?.streamId,
          width: this.numberValue(requestArg.data?.width ?? requestArg.data?.w),
          height: this.numberValue(requestArg.data?.height ?? requestArg.data?.h),
          filename: this.stringValue(requestArg.data?.filename),
        },
        httpCommands: camera?.streamId ? [{ label: 'snapshot', method: 'GET', path: '/api/frame.jpeg', query: { src: camera.streamId }, expect: 'image' }] : undefined,
      };
    }

    if (requestArg.domain !== 'go2rtc') {
      return undefined;
    }

    if (requestArg.service === 'refresh') {
      return { type: 'refresh', service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }

    if (requestArg.service === 'reload' || requestArg.service === 'restart') {
      return {
        type: 'restart',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        httpCommands: [{ label: 'restart', method: 'POST', path: '/api/restart', expect: 'empty' }],
      };
    }

    if (cameraStreamServices.has(requestArg.service) || cameraSnapshotServices.has(requestArg.service)) {
      return this.commandForService(snapshotArg, { ...requestArg, domain: 'camera' });
    }

    if (requestArg.service === 'add_stream' || requestArg.service === 'create_stream') {
      const streamName = this.stringValue(requestArg.data?.name ?? requestArg.data?.streamId ?? requestArg.data?.stream_id ?? requestArg.data?.stream);
      const src = this.stringValue(requestArg.data?.src ?? requestArg.data?.source ?? requestArg.data?.url);
      return streamName && src ? {
        type: 'add_stream',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        streamId: streamName,
        streamMutation: { name: streamName, src },
        httpCommands: [{ label: 'add_stream', method: 'PUT', path: '/api/streams', query: { name: streamName, src }, expect: 'empty' }],
      } : undefined;
    }

    if (requestArg.service === 'update_stream') {
      const streamName = this.stringValue(requestArg.data?.name ?? requestArg.data?.streamId ?? requestArg.data?.stream_id ?? requestArg.data?.stream) || this.findCamera(snapshotArg, requestArg)?.streamId;
      const src = this.stringValue(requestArg.data?.src ?? requestArg.data?.source ?? requestArg.data?.url);
      return streamName && src ? {
        type: 'update_stream',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        streamId: streamName,
        streamMutation: { name: streamName, src },
        httpCommands: [{ label: 'update_stream', method: 'PATCH', path: '/api/streams', query: { name: streamName, src }, expect: 'empty' }],
      } : undefined;
    }

    if (requestArg.service === 'delete_stream' || requestArg.service === 'remove_stream') {
      const streamName = this.stringValue(requestArg.data?.name ?? requestArg.data?.streamId ?? requestArg.data?.stream_id ?? requestArg.data?.stream) || this.findCamera(snapshotArg, requestArg)?.streamId;
      return streamName ? {
        type: 'delete_stream',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        streamId: streamName,
        streamMutation: { name: streamName },
        httpCommands: [{ label: 'delete_stream', method: 'DELETE', path: '/api/streams', query: { src: streamName }, expect: 'empty' }],
      } : undefined;
    }

    if (requestArg.service === 'stream_to_camera' || requestArg.service === 'play_stream' || requestArg.service === 'stop_stream_to_camera') {
      const camera = this.findCamera(snapshotArg, requestArg);
      const dst = this.stringValue(requestArg.data?.dst ?? requestArg.data?.destination ?? requestArg.data?.streamId ?? requestArg.data?.stream_id) || camera?.streamId;
      const src = requestArg.service === 'stop_stream_to_camera' ? '' : this.stringValue(requestArg.data?.src ?? requestArg.data?.source ?? requestArg.data?.url);
      return dst && src !== undefined ? {
        type: 'stream_to_camera',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        streamId: dst,
        streamMutation: { dst, src },
        httpCommands: [{ label: 'stream_to_camera', method: 'POST', path: '/api/streams', query: { dst, src }, expect: 'empty' }],
      } : undefined;
    }

    return undefined;
  }

  public static deviceId(snapshotArg: IGo2rtcSnapshot, cameraArg: IGo2rtcCamera): string {
    return `go2rtc.stream.${this.uniqueBase(snapshotArg)}.${this.slug(cameraArg.streamId || cameraArg.streamName)}`;
  }

  public static endpointSet(configArg: IGo2rtcConfig, streamNameArg?: string, endpointArg = this.endpointFromConfig(configArg)): IGo2rtcEndpointSet {
    const baseUrl = endpointArg.url;
    const endpoints: IGo2rtcEndpointSet = { apiUrl: baseUrl };
    if (!baseUrl) {
      return endpoints;
    }
    if (!streamNameArg) {
      return endpoints;
    }
    endpoints.snapshotUrl = this.urlWithQuery(baseUrl, '/api/frame.jpeg', { src: streamNameArg });
    endpoints.mjpegUrl = this.urlWithQuery(baseUrl, '/api/stream.mjpeg', { src: streamNameArg });
    endpoints.mp4Url = this.urlWithQuery(baseUrl, '/api/stream.mp4', { src: streamNameArg });
    endpoints.hlsUrl = this.urlWithQuery(baseUrl, '/api/stream.m3u8', { src: streamNameArg });
    endpoints.whepUrl = this.urlWithQuery(baseUrl, '/api/webrtc', { src: streamNameArg });
    endpoints.websocketUrl = this.webSocketUrl(baseUrl, streamNameArg);
    endpoints.rtspUrl = this.rtspUrl(configArg, streamNameArg, endpointArg);
    return endpoints;
  }

  public static endpointFromConfig(configArg: IGo2rtcConfig): { protocol: TGo2rtcProtocol; host?: string; port: number; url?: string } {
    const parsed = safeUrl(configArg.url || configArg.host);
    if (parsed) {
      const protocol = parsed.protocol === 'https:' ? 'https' : 'http';
      const port = parsed.port ? Number(parsed.port) : protocol === 'https' ? 443 : go2rtcDefaultApiPort;
      const path = parsed.pathname && parsed.pathname !== '/' ? parsed.pathname.replace(/\/$/, '') : '';
      return { protocol, host: parsed.hostname, port, url: `${protocol}://${hostForUrl(parsed.hostname)}:${port}${path}` };
    }
    const protocol = configArg.protocol || 'http';
    const port = configArg.port || go2rtcDefaultApiPort;
    return { protocol, host: configArg.host, port, url: configArg.host ? `${protocol}://${hostForUrl(configArg.host)}:${port}` : undefined };
  }

  private static streamFromRaw(nameArg: string, infoArg: IGo2rtcRawStreamInfo, configArg: IGo2rtcConfig, endpointArg: ReturnType<typeof Go2rtcMapper.endpointFromConfig>): IGo2rtcStream {
    const producers = (infoArg.producers || []).map((producerArg) => this.producerConsumerFromRaw(producerArg));
    const consumers = (infoArg.consumers || []).map((consumerArg) => this.producerConsumerFromRaw(consumerArg));
    const sourceUrls = [...new Set(producers.map((producerArg) => producerArg.url).filter((valueArg): valueArg is string => Boolean(valueArg)))];
    const tracks = this.uniqueTracks(producers.flatMap((producerArg) => producerArg.tracks || []));
    return {
      id: nameArg,
      name: nameArg,
      sourceUrls,
      producerCount: producers.length,
      consumerCount: consumers.length,
      producers,
      consumers,
      online: producers.length > 0,
      configured: true,
      endpoints: this.endpointSet(configArg, nameArg, endpointArg),
      protocols: this.protocolsForUrls(sourceUrls),
      tracks,
      metadata: {
        raw: infoArg,
      },
    };
  }

  private static normalizeStream(streamArg: IGo2rtcStream, configArg: IGo2rtcConfig, endpointArg: ReturnType<typeof Go2rtcMapper.endpointFromConfig>): IGo2rtcStream {
    const producers = (streamArg.producers || []).map((producerArg) => this.normalizeProducerConsumer(producerArg));
    const consumers = (streamArg.consumers || []).map((consumerArg) => this.normalizeProducerConsumer(consumerArg));
    const sourceUrls = [...new Set([...(streamArg.sourceUrls || []), ...producers.map((producerArg) => producerArg.url).filter((valueArg): valueArg is string => Boolean(valueArg))])];
    const tracks = this.uniqueTracks([...(streamArg.tracks || []), ...producers.flatMap((producerArg) => producerArg.tracks || [])]);
    return {
      ...streamArg,
      id: streamArg.id || streamArg.name,
      name: streamArg.name || streamArg.id,
      sourceUrls,
      producerCount: streamArg.producerCount ?? producers.length,
      consumerCount: streamArg.consumerCount ?? consumers.length,
      producers,
      consumers,
      online: streamArg.online ?? producers.length > 0,
      configured: streamArg.configured ?? Boolean(sourceUrls.length || streamArg.id || streamArg.name),
      endpoints: {
        ...this.endpointSet(configArg, streamArg.name || streamArg.id, endpointArg),
        ...streamArg.endpoints,
      },
      protocols: streamArg.protocols || this.protocolsForUrls(sourceUrls),
      tracks,
    };
  }

  private static producerConsumerFromRaw(rawArg: IGo2rtcRawProducerConsumer): IGo2rtcProducerConsumer {
    const url = this.stringValue(rawArg.url);
    return {
      id: this.stringValue(rawArg.id),
      url,
      remoteAddress: this.stringValue(rawArg.remote_addr ?? rawArg.remoteAddr ?? rawArg.remoteAddress),
      protocol: this.protocolForUrl(url),
      userAgent: this.stringValue(rawArg.user_agent ?? rawArg.userAgent),
      state: this.stringValue(rawArg.state),
      tracks: this.tracksFromRaw(rawArg.tracks || rawArg.medias),
      raw: rawArg,
    };
  }

  private static normalizeProducerConsumer(valueArg: IGo2rtcProducerConsumer): IGo2rtcProducerConsumer {
    return {
      ...valueArg,
      protocol: valueArg.protocol || this.protocolForUrl(valueArg.url),
      tracks: valueArg.tracks || [],
    };
  }

  private static tracksFromRaw(valueArg: unknown): IGo2rtcTrack[] {
    if (!Array.isArray(valueArg)) {
      return [];
    }
    return valueArg.map((itemArg) => {
      const item = record(itemArg) || {};
      const codec = this.stringValue(item.codec || item.name || item.format);
      const media = this.stringValue(item.media || item.kind || item.type);
      return {
        kind: this.trackKind(media || codec),
        codec,
        media,
        direction: this.stringValue(item.direction),
        raw: item,
      };
    });
  }

  private static cameraFromStream(streamArg: IGo2rtcStream, configArg: IGo2rtcConfig, connectedArg: boolean, endpointArg: ReturnType<typeof Go2rtcMapper.endpointFromConfig>): IGo2rtcCamera {
    return {
      id: streamArg.id,
      name: streamArg.name,
      streamId: streamArg.id,
      streamName: streamArg.name,
      available: connectedArg && streamArg.configured,
      endpoints: {
        ...this.endpointSet(configArg, streamArg.name, endpointArg),
        ...streamArg.endpoints,
      },
      sourceUrls: streamArg.sourceUrls,
      protocols: streamArg.protocols,
      tracks: streamArg.tracks,
      metadata: streamArg.metadata,
    };
  }

  private static uniqueCameras(camerasArg: IGo2rtcCamera[]): IGo2rtcCamera[] {
    const seen = new Set<string>();
    const cameras: IGo2rtcCamera[] = [];
    for (const camera of camerasArg) {
      const key = this.slug(camera.streamId || camera.id || camera.name);
      if (seen.has(key)) {
        continue;
      }
      seen.add(key);
      cameras.push(camera);
    }
    return cameras;
  }

  private static defaultSensors(configArg: IGo2rtcConfig, connectedArg: boolean, streamsArg: IGo2rtcStream[], errorArg?: string): IGo2rtcSensor[] {
    const sensors: IGo2rtcSensor[] = [
      { id: 'connected', name: 'go2rtc Connected', value: connectedArg, deviceClass: 'connectivity', available: true },
      { id: 'stream_count', name: 'go2rtc Stream Count', value: streamsArg.length, available: true },
    ];
    const version = configArg.serverInfo?.version || configArg.snapshot?.serverInfo?.version;
    if (version) {
      sensors.push({ id: 'version', name: 'go2rtc Version', value: version, entityCategory: 'diagnostic', available: true });
    }
    if (errorArg) {
      sensors.push({ id: 'last_error', name: 'go2rtc Last Error', value: errorArg, entityCategory: 'diagnostic', available: true });
    }
    return sensors;
  }

  private static supportedCameraFeatures(cameraArg: IGo2rtcCamera): string[] {
    const features = ['snapshot', 'stream'];
    if (cameraArg.endpoints.whepUrl || cameraArg.endpoints.websocketUrl) {
      features.push('webrtc');
    }
    if (cameraArg.endpoints.rtspUrl) {
      features.push('rtsp');
    }
    if (cameraArg.endpoints.mjpegUrl) {
      features.push('mjpeg');
    }
    return features;
  }

  private static streamStatus(snapshotArg: IGo2rtcSnapshot, streamArg?: IGo2rtcStream): string {
    if (!streamArg) {
      return snapshotArg.connected ? 'missing' : 'offline';
    }
    if (!snapshotArg.connected) {
      return streamArg.configured ? 'configured' : 'offline';
    }
    if (streamArg.online) {
      return streamArg.consumerCount > 0 ? 'streaming' : 'available';
    }
    return streamArg.configured ? 'configured' : 'offline';
  }

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

  private static findCamera(snapshotArg: IGo2rtcSnapshot, requestArg: IServiceCallRequest): IGo2rtcCamera | undefined {
    const target = requestArg.target.entityId
      || requestArg.target.deviceId
      || this.stringValue(requestArg.data?.streamId ?? requestArg.data?.stream_id ?? requestArg.data?.stream ?? requestArg.data?.name ?? requestArg.data?.cameraId ?? requestArg.data?.camera_id);
    if (!target) {
      return snapshotArg.cameras[0];
    }
    const cameraEntities = this.toEntities(snapshotArg).filter((entityArg) => entityArg.platform === ('camera' as TEntityPlatform));
    const entity = cameraEntities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || entityArg.deviceId === target || entityArg.attributes?.streamId === target || entityArg.attributes?.streamName === target);
    const streamId = this.stringValue(entity?.attributes?.streamId) || target;
    return snapshotArg.cameras.find((cameraArg) => cameraArg.streamId === streamId || cameraArg.streamName === streamId || cameraArg.id === streamId || this.deviceId(snapshotArg, cameraArg) === target) || snapshotArg.cameras[0];
  }

  private static endpointKey(valueArg: unknown): keyof IGo2rtcEndpointSet | undefined {
    const value = this.stringValue(valueArg);
    if (!value) {
      return undefined;
    }
    const aliases: Record<string, keyof IGo2rtcEndpointSet> = {
      snapshot: 'snapshotUrl',
      jpeg: 'snapshotUrl',
      mjpeg: 'mjpegUrl',
      mp4: 'mp4Url',
      hls: 'hlsUrl',
      webrtc: 'whepUrl',
      whep: 'whepUrl',
      websocket: 'websocketUrl',
      ws: 'websocketUrl',
      rtsp: 'rtspUrl',
    };
    const key = aliases[value.toLowerCase()] || value;
    return endpointKeys.has(key as keyof IGo2rtcEndpointSet) ? key as keyof IGo2rtcEndpointSet : undefined;
  }

  private static rtspUrl(configArg: IGo2rtcConfig, streamNameArg: string, endpointArg: ReturnType<typeof Go2rtcMapper.endpointFromConfig>): string | undefined {
    if (configArg.rtsp?.enabled === false) {
      return undefined;
    }
    const host = configArg.rtsp?.host || endpointArg.host;
    if (!host) {
      return undefined;
    }
    const port = configArg.rtsp?.port || this.portFromRtspListen(configArg.serverInfo?.rtsp?.listen) || go2rtcDefaultRtspPort;
    const query = configArg.rtsp?.defaultQuery || configArg.serverInfo?.rtsp?.defaultQuery;
    return `rtsp://${hostForUrl(host)}:${port}/${encodeURIComponent(streamNameArg)}${query ? `?${query}` : ''}`;
  }

  private static webSocketUrl(baseUrlArg: string, streamNameArg: string): string | undefined {
    try {
      const url = new URL('/api/ws', baseUrlArg.endsWith('/') ? baseUrlArg : `${baseUrlArg}/`);
      url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
      url.searchParams.set('src', streamNameArg);
      return url.toString();
    } catch {
      return undefined;
    }
  }

  private static urlWithQuery(baseUrlArg: string, pathArg: string, queryArg: Record<string, string | number | boolean | undefined>): string | undefined {
    try {
      const url = new URL(pathArg, baseUrlArg.endsWith('/') ? baseUrlArg : `${baseUrlArg}/`);
      for (const [key, value] of Object.entries(queryArg)) {
        if (value !== undefined) {
          url.searchParams.set(key, String(value));
        }
      }
      return url.toString();
    } catch {
      return undefined;
    }
  }

  private static portFromRtspListen(valueArg?: string): number | undefined {
    if (!valueArg) {
      return undefined;
    }
    const match = valueArg.match(/:(\d+)(?:\/|$)/) || valueArg.match(/^(\d+)$/);
    if (!match) {
      return undefined;
    }
    const port = Number(match[1]);
    return Number.isInteger(port) && port > 0 && port < 65536 ? port : undefined;
  }

  private static protocolsForUrls(urlsArg: string[]): TGo2rtcStreamProtocol[] {
    return [...new Set(urlsArg.map((urlArg) => this.protocolForUrl(urlArg)))];
  }

  private static protocolForUrl(urlArg?: string): TGo2rtcStreamProtocol {
    if (!urlArg) {
      return 'unknown';
    }
    const protocol = urlArg.split(':', 1)[0]?.toLowerCase();
    if (protocol === 'rtsp' || protocol === 'rtsps' || protocol === 'http' || protocol === 'https' || protocol === 'webrtc' || protocol === 'ffmpeg' || protocol === 'exec') {
      return protocol;
    }
    return 'unknown';
  }

  private static trackKind(valueArg?: string): TGo2rtcTrackKind {
    const value = (valueArg || '').toLowerCase();
    if (value.includes('video') || value.includes('h264') || value.includes('h265') || value.includes('jpeg')) {
      return 'video';
    }
    if (value.includes('microphone')) {
      return 'microphone';
    }
    if (value.includes('audio') || value.includes('aac') || value.includes('opus') || value.includes('pcma') || value.includes('pcmu')) {
      return 'audio';
    }
    if (value.includes('application')) {
      return 'application';
    }
    return 'unknown';
  }

  private static uniqueTracks(tracksArg: IGo2rtcTrack[]): IGo2rtcTrack[] {
    const seen = new Set<string>();
    return tracksArg.filter((trackArg) => {
      const key = `${trackArg.kind}:${trackArg.codec || ''}:${trackArg.media || ''}`;
      if (seen.has(key)) {
        return false;
      }
      seen.add(key);
      return true;
    });
  }

  private static uniqueBase(snapshotArg: IGo2rtcSnapshot): string {
    return this.slug(snapshotArg.id || snapshotArg.serverInfo?.host || snapshotArg.endpoints.apiUrl || snapshotArg.name || 'go2rtc');
  }

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

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

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

  private static stringValue(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'string' && valueArg.trim()) {
      return valueArg.trim();
    }
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return String(valueArg);
    }
    return undefined;
  }

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

const safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const record = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
};

const hostForUrl = (hostArg: string): string => hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
