import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  IMotionEyeCamera,
  IMotionEyeClientCommand,
  IMotionEyeSnapshot,
  IMotionEyeSwitch,
} from './motioneye.types.js';
import { motionEyeKnownActions, motionEyeSwitchDescriptions } from './motioneye.types.js';

const cameraStreamServices = new Set(['stream', 'stream_source', 'get_stream']);
const cameraSnapshotServices = new Set(['snapshot', 'camera_image', 'camera_snapshot']);
const serviceBooleanKeys = ['enabled', 'enable', 'on', 'state', 'value'];
const recordStartServices = new Set(['record_start', 'start_recording', 'enable_recording']);
const recordStopServices = new Set(['record_stop', 'stop_recording', 'disable_recording']);

export class MotionEyeMapper {
  public static toDevices(snapshotArg: IMotionEyeSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    return snapshotArg.cameras.map((cameraArg) => {
      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: true },
        { id: 'motion_detection', capability: 'switch', name: 'Motion detection', readable: true, writable: true },
        { id: 'recording', capability: 'switch', name: 'Recording', readable: false, writable: true },
      ];
      const state: plugins.shxInterfaces.data.IDeviceState[] = [
        { featureId: 'connectivity', value: snapshotArg.connected && cameraArg.available !== false ? 'online' : 'offline', updatedAt },
        { featureId: 'camera', value: { mjpegUrl: cameraArg.mjpegUrl || null, snapshotUrl: cameraArg.snapshotUrl || null, isStreaming: cameraArg.isStreaming }, updatedAt },
        { featureId: 'motion_detection', value: cameraArg.motionDetectionEnabled, updatedAt },
        { featureId: 'recording', value: 'action', updatedAt },
      ];

      for (const switchArg of snapshotArg.switches.filter((switchArg) => switchArg.cameraId === cameraArg.id)) {
        if (!features.some((featureArg) => featureArg.id === `switch_${this.slug(switchArg.key)}`)) {
          features.push({ id: `switch_${this.slug(switchArg.key)}`, capability: 'switch', name: switchArg.name, readable: true, writable: true });
        }
        state.push({ featureId: `switch_${this.slug(switchArg.key)}`, value: switchArg.isOn, updatedAt });
      }

      return {
        id: this.deviceId(snapshotArg, cameraArg),
        integrationDomain: 'motioneye',
        name: cameraArg.name,
        protocol: 'http',
        manufacturer: snapshotArg.deviceInfo.manufacturer || 'motionEye',
        model: snapshotArg.deviceInfo.model || 'motionEye camera',
        online: snapshotArg.connected && cameraArg.available !== false,
        features,
        state,
        metadata: {
          motionEyeUrl: snapshotArg.deviceInfo.url,
          host: snapshotArg.deviceInfo.host,
          port: snapshotArg.deviceInfo.port,
          protocol: snapshotArg.deviceInfo.protocol,
          cameraId: cameraArg.id,
          numericId: cameraArg.numericId,
          streamingPort: cameraArg.streamingPort,
          streamingAuthMode: cameraArg.streamingAuthMode,
          rootDirectory: cameraArg.rootDirectory,
          actions: cameraArg.actions,
        },
      };
    });
  }

  public static toEntities(snapshotArg: IMotionEyeSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const usedIds = new Map<string, number>();

    for (const camera of snapshotArg.cameras) {
      const deviceId = this.deviceId(snapshotArg, camera);
      entities.push(this.entity('camera' as TEntityPlatform, camera.name, deviceId, `motioneye_${this.uniqueBase(snapshotArg)}_camera_${this.slug(camera.id)}`, snapshotArg.connected && camera.available !== false ? 'idle' : 'unavailable', usedIds, {
        cameraId: camera.id,
        numericId: camera.numericId,
        mjpegUrl: camera.mjpegUrl,
        streamSource: camera.mjpegUrl,
        snapshotUrl: camera.snapshotUrl,
        stillImageUrl: camera.snapshotUrl,
        streamingPort: camera.streamingPort,
        streamingAuthMode: camera.streamingAuthMode,
        motionDetectionEnabled: camera.motionDetectionEnabled,
        actions: camera.actions,
        rootDirectory: camera.rootDirectory,
        supportedFeatures: this.supportedCameraFeatures(camera),
        serviceMappings: {
          snapshot: 'camera.snapshot',
          streamSource: 'camera.stream_source',
          motionDetection: 'camera.enable_motion_detection',
          action: 'motioneye.action',
          recordStart: 'motioneye.record_start',
          recordStop: 'motioneye.record_stop',
        },
        ...camera.attributes,
      }, snapshotArg.connected && camera.available !== false));
    }

    for (const switchArg of snapshotArg.switches) {
      const camera = this.cameraById(snapshotArg, switchArg.cameraId);
      const deviceId = camera ? this.deviceId(snapshotArg, camera) : `motioneye.device.${this.uniqueBase(snapshotArg)}`;
      entities.push(this.entity('switch', switchArg.name, deviceId, `motioneye_${this.uniqueBase(snapshotArg)}_${this.slug(switchArg.cameraId)}_${this.slug(switchArg.key)}`, switchArg.isOn ? 'on' : 'off', usedIds, {
        key: switchArg.key,
        cameraId: switchArg.cameraId,
        entityCategory: switchArg.entityCategory,
        ...switchArg.attributes,
      }, snapshotArg.connected && switchArg.available !== false));
    }

    for (const sensor of snapshotArg.sensors) {
      const camera = sensor.cameraId ? this.cameraById(snapshotArg, sensor.cameraId) : undefined;
      const deviceId = camera ? this.deviceId(snapshotArg, camera) : `motioneye.device.${this.uniqueBase(snapshotArg)}`;
      entities.push(this.entity('sensor', sensor.name, deviceId, `motioneye_${this.uniqueBase(snapshotArg)}_${this.slug(sensor.cameraId || 'hub')}_${this.slug(sensor.key)}`, sensor.value ?? 'unknown', usedIds, {
        key: sensor.key,
        cameraId: sensor.cameraId,
        unit: sensor.unit,
        deviceClass: sensor.deviceClass,
        entityCategory: sensor.entityCategory,
        ...sensor.attributes,
      }, snapshotArg.connected && sensor.available !== false));
    }

    return entities;
  }

  public static commandForService(snapshotArg: IMotionEyeSnapshot, requestArg: IServiceCallRequest): IMotionEyeClientCommand | 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, cameraId: camera?.id };
    }
    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,
        cameraId: camera?.id,
        filename: this.stringValue(requestArg.data?.filename),
        httpCommands: camera?.numericId === undefined ? undefined : [{ label: 'snapshot', method: 'GET', path: `/picture/${camera.numericId}/current/`, admin: false }],
      };
    }
    if (requestArg.domain === 'camera' && (requestArg.service === 'enable_motion_detection' || requestArg.service === 'disable_motion_detection')) {
      const camera = this.findCamera(snapshotArg, requestArg);
      const enabled = requestArg.service === 'enable_motion_detection';
      return this.switchCommand(camera, 'motion_detection', enabled, requestArg);
    }
    if (requestArg.domain === 'switch' && ['turn_on', 'turn_off', 'toggle'].includes(requestArg.service)) {
      const switchEntity = this.findSwitch(snapshotArg, requestArg);
      if (!switchEntity) {
        return undefined;
      }
      const enabled = requestArg.service === 'turn_on' ? true : requestArg.service === 'turn_off' ? false : !switchEntity.isOn;
      return this.switchCommand(this.cameraById(snapshotArg, switchEntity.cameraId), switchEntity.key, enabled, requestArg);
    }
    if (requestArg.domain === 'motioneye') {
      return this.motionEyeCommand(snapshotArg, requestArg);
    }
    return undefined;
  }

  public static deviceId(snapshotArg: IMotionEyeSnapshot, cameraArg: IMotionEyeCamera): string {
    return `motioneye.camera.${this.uniqueBase(snapshotArg)}.${this.slug(cameraArg.id)}`;
  }

  private static motionEyeCommand(snapshotArg: IMotionEyeSnapshot, requestArg: IServiceCallRequest): IMotionEyeClientCommand | undefined {
    if (requestArg.service === 'refresh') {
      return { type: 'refresh', service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }
    if (cameraStreamServices.has(requestArg.service) || cameraSnapshotServices.has(requestArg.service)) {
      return this.commandForService(snapshotArg, { ...requestArg, domain: 'camera' });
    }
    const camera = this.findCamera(snapshotArg, requestArg);
    if (requestArg.service === 'action') {
      const action = this.stringValue(requestArg.data?.action);
      return action ? this.actionCommand(camera, action, requestArg) : undefined;
    }
    if (requestArg.service === 'snapshot') {
      return this.actionCommand(camera, 'snapshot', requestArg);
    }
    if (recordStartServices.has(requestArg.service)) {
      return this.actionCommand(camera, 'record_start', requestArg);
    }
    if (recordStopServices.has(requestArg.service)) {
      return this.actionCommand(camera, 'record_stop', requestArg);
    }
    if (requestArg.service === 'set_text_overlay') {
      const leftText = this.stringValue(requestArg.data?.left_text ?? requestArg.data?.leftText);
      const rightText = this.stringValue(requestArg.data?.right_text ?? requestArg.data?.rightText);
      const customLeftText = this.stringValue(requestArg.data?.custom_left_text ?? requestArg.data?.customLeftText);
      const customRightText = this.stringValue(requestArg.data?.custom_right_text ?? requestArg.data?.customRightText);
      if ([leftText, rightText, customLeftText, customRightText].every((valueArg) => valueArg === undefined)) {
        return undefined;
      }
      return {
        type: 'set_text_overlay',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        cameraId: camera?.id,
        leftText,
        rightText,
        customLeftText,
        customRightText,
        httpCommands: camera?.numericId === undefined ? undefined : [{ label: 'set_text_overlay', method: 'POST', path: `/config/${camera.numericId}/set`, admin: true }],
      };
    }
    if (requestArg.service === 'set_switch' || requestArg.service === 'set_camera_setting') {
      const key = this.switchKey(requestArg.data?.key ?? requestArg.data?.setting);
      const enabled = this.booleanFromData(requestArg.data);
      return key && enabled !== undefined ? this.switchCommand(camera, key, enabled, requestArg) : undefined;
    }
    if (requestArg.service === 'media_list') {
      const kind = requestArg.data?.kind === 'movies' || requestArg.data?.mediaKind === 'movies' ? 'movies' : 'images';
      return { type: 'media_list', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, mediaKind: kind, prefix: this.stringValue(requestArg.data?.prefix) };
    }

    const switchKey = requestArg.service.startsWith('set_') ? this.switchKey(requestArg.service.slice(4)) : this.switchKey(requestArg.service);
    if (switchKey) {
      const enabled = this.booleanFromData(requestArg.data) ?? true;
      return this.switchCommand(camera, switchKey, enabled, requestArg);
    }
    const action = motionEyeKnownActions.includes(requestArg.service as typeof motionEyeKnownActions[number]) ? requestArg.service : undefined;
    return action ? this.actionCommand(camera, action, requestArg) : undefined;
  }

  private static actionCommand(cameraArg: IMotionEyeCamera | undefined, actionArg: string, requestArg: IServiceCallRequest): IMotionEyeClientCommand | undefined {
    if (!actionArg.trim()) {
      return undefined;
    }
    return {
      type: 'action',
      service: requestArg.service,
      target: requestArg.target,
      data: requestArg.data,
      cameraId: cameraArg?.id,
      action: actionArg,
      httpCommands: cameraArg?.numericId === undefined ? undefined : [{ label: `action:${actionArg}`, method: 'POST', path: `/action/${cameraArg.numericId}/${encodeURIComponent(actionArg)}`, admin: true, data: {} }],
    };
  }

  private static switchCommand(cameraArg: IMotionEyeCamera | undefined, keyArg: string, enabledArg: boolean, requestArg: IServiceCallRequest): IMotionEyeClientCommand | undefined {
    if (!cameraArg) {
      return undefined;
    }
    return {
      type: 'set_switch',
      service: requestArg.service,
      target: requestArg.target,
      data: requestArg.data,
      cameraId: cameraArg.id,
      key: keyArg,
      enabled: enabledArg,
      httpCommands: cameraArg.numericId === undefined ? undefined : [{ label: keyArg, method: 'POST', path: `/config/${cameraArg.numericId}/set`, admin: true }],
    };
  }

  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) || this.slug(uniqueIdArg)}`;
    const seen = usedIdsArg.get(baseId) || 0;
    usedIdsArg.set(baseId, seen + 1);
    return {
      id: seen ? `${baseId}_${seen + 1}` : baseId,
      uniqueId: uniqueIdArg,
      integrationDomain: 'motioneye',
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes(attributesArg),
      available: availableArg,
    };
  }

  private static findCamera(snapshotArg: IMotionEyeSnapshot, requestArg: IServiceCallRequest): IMotionEyeCamera | undefined {
    const target = requestArg.target.entityId || requestArg.target.deviceId || this.stringValue(requestArg.data?.cameraId ?? requestArg.data?.camera_id ?? requestArg.data?.camera);
    if (!target) {
      return snapshotArg.cameras[0];
    }
    const entities = this.toEntities(snapshotArg).filter((entityArg) => entityArg.platform === ('camera' as TEntityPlatform));
    const entity = entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || entityArg.attributes?.cameraId === target || entityArg.attributes?.numericId === target);
    const cameraId = this.stringValue(entity?.attributes?.cameraId) || target;
    return snapshotArg.cameras.find((cameraArg) => cameraArg.id === cameraId || String(cameraArg.numericId) === cameraId || this.deviceId(snapshotArg, cameraArg) === target) || snapshotArg.cameras[0];
  }

  private static findSwitch(snapshotArg: IMotionEyeSnapshot, requestArg: IServiceCallRequest): IMotionEyeSwitch | undefined {
    const target = requestArg.target.entityId || requestArg.target.deviceId || this.stringValue(requestArg.data?.entity_id ?? requestArg.data?.entityId ?? requestArg.data?.key ?? requestArg.data?.setting);
    if (!target) {
      return snapshotArg.switches[0];
    }
    const entities = this.toEntities(snapshotArg).filter((entityArg) => entityArg.platform === 'switch');
    const entity = entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || entityArg.attributes?.key === target);
    const key = this.stringValue(entity?.attributes?.key) || this.switchKey(target);
    const cameraId = this.stringValue(entity?.attributes?.cameraId);
    return snapshotArg.switches.find((switchArg) => (key ? switchArg.key === key : false) && (!cameraId || switchArg.cameraId === cameraId))
      || snapshotArg.switches.find((switchArg) => switchArg.key === target || switchArg.name === target);
  }

  private static cameraById(snapshotArg: IMotionEyeSnapshot, cameraIdArg: string): IMotionEyeCamera | undefined {
    return snapshotArg.cameras.find((cameraArg) => cameraArg.id === cameraIdArg || String(cameraArg.numericId) === cameraIdArg);
  }

  private static supportedCameraFeatures(cameraArg: IMotionEyeCamera): string[] {
    const features = ['stream', 'snapshot', 'motion_detection'];
    if (cameraArg.actions.includes('record_start') || cameraArg.actions.includes('record_stop')) {
      features.push('recording');
    }
    if (cameraArg.actions.length) {
      features.push('actions');
    }
    return features;
  }

  private static switchKey(valueArg: unknown): string | undefined {
    const value = this.stringValue(valueArg);
    return value && motionEyeSwitchDescriptions.some((descriptionArg) => descriptionArg.key === value) ? value : undefined;
  }

  private static deviceName(snapshotArg: IMotionEyeSnapshot): string {
    return snapshotArg.deviceInfo.name || snapshotArg.deviceInfo.host || 'motionEye';
  }

  private static uniqueBase(snapshotArg: IMotionEyeSnapshot): string {
    return this.slug(snapshotArg.deviceInfo.id || snapshotArg.deviceInfo.host || snapshotArg.deviceInfo.url || this.deviceName(snapshotArg));
  }

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

  private static booleanFromData(dataArg: Record<string, unknown> | undefined): boolean | undefined {
    for (const key of serviceBooleanKeys) {
      const value = this.booleanValue(dataArg?.[key]);
      if (value !== undefined) {
        return value;
      }
    }
    return undefined;
  }

  private static booleanValue(valueArg: unknown): boolean | undefined {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'string') {
      if (['on', 'true', 'yes', '1'].includes(valueArg.toLowerCase())) {
        return true;
      }
      if (['off', 'false', 'no', '0'].includes(valueArg.toLowerCase())) {
        return false;
      }
    }
    return undefined;
  }

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

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