import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  IAmcrestCamera,
  IAmcrestClientCommand,
  IAmcrestEvent,
  IAmcrestSnapshot,
  IAmcrestSwitch,
  TAmcrestColorBw,
  TAmcrestPtzMovement,
  TAmcrestResolution,
  TAmcrestStreamSource,
} from './amcrest.types.js';
import { amcrestColorModes, amcrestPtzMovements, amcrestResolutionSubtype, amcrestSubtypeStream } from './amcrest.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'];

export class AmcrestMapper {
  public static toDevices(snapshotArg: IAmcrestSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
      { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
    ];
    const state: plugins.shxInterfaces.data.IDeviceState[] = [
      { featureId: 'connectivity', value: snapshotArg.connected ? 'online' : 'offline', updatedAt },
    ];

    for (const camera of snapshotArg.cameras) {
      features.push({ id: `camera_${this.slug(camera.id)}`, capability: 'camera', name: camera.name || `Camera ${camera.channel}`, readable: true, writable: Boolean(camera.supportsPtz) });
      state.push({
        featureId: `camera_${this.slug(camera.id)}`,
        value: {
          snapshotUrl: camera.snapshotUrl || null,
          mjpegUrl: camera.mjpegUrl || null,
          rtspUrl: camera.rtspUrl || null,
          streamSource: camera.streamSource,
          isStreaming: camera.isStreaming ?? null,
        },
        updatedAt,
      });
    }
    for (const sensor of snapshotArg.binarySensors) {
      features.push({ id: `binary_${this.slug(sensor.key)}`, capability: 'sensor', name: sensor.name, readable: true, writable: false });
      state.push({ featureId: `binary_${this.slug(sensor.key)}`, value: sensor.isOn, updatedAt });
    }
    for (const sensor of snapshotArg.sensors) {
      features.push({ id: `sensor_${this.slug(sensor.key)}`, capability: 'sensor', name: sensor.name, readable: true, writable: false, unit: sensor.unit });
      state.push({ featureId: `sensor_${this.slug(sensor.key)}`, value: this.deviceStateValue(sensor.value), updatedAt });
    }
    for (const switchArg of snapshotArg.switches) {
      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),
      integrationDomain: 'amcrest',
      name: this.deviceName(snapshotArg),
      protocol: 'http',
      manufacturer: snapshotArg.deviceInfo.manufacturer || 'Amcrest',
      model: snapshotArg.deviceInfo.model,
      online: snapshotArg.connected,
      features,
      state,
      metadata: {
        serialNumber: snapshotArg.deviceInfo.serialNumber,
        macAddress: snapshotArg.deviceInfo.macAddress,
        firmwareVersion: snapshotArg.deviceInfo.firmwareVersion,
        host: snapshotArg.deviceInfo.host,
        port: snapshotArg.deviceInfo.port,
        protocol: snapshotArg.deviceInfo.protocol,
        rtspPort: snapshotArg.deviceInfo.rtspPort,
        cameraStreams: snapshotArg.cameras.map((cameraArg) => ({
          id: cameraArg.id,
          channel: cameraArg.channel,
          resolution: cameraArg.resolution,
          subtype: cameraArg.subtype,
          streamSource: cameraArg.streamSource,
          snapshotUrl: cameraArg.snapshotUrl,
          mjpegUrl: cameraArg.mjpegUrl,
          rtspUrl: cameraArg.rtspUrl,
        })),
      },
    }];
  }

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

    for (const camera of snapshotArg.cameras) {
      entities.push(this.entity('camera' as TEntityPlatform, camera.name || `${this.deviceName(snapshotArg)} Camera`, deviceId, `amcrest_${this.uniqueBase(snapshotArg)}_camera_${this.slug(camera.id)}`, camera.available === false || !snapshotArg.connected ? 'unavailable' : camera.isStreaming === false ? 'off' : 'idle', usedIds, {
        cameraId: camera.id,
        channel: camera.channel,
        resolution: camera.resolution,
        subtype: camera.subtype,
        streamSource: camera.streamSource,
        streamSourceUrl: this.streamSourceUrl(camera),
        snapshotUrl: camera.snapshotUrl,
        stillImageUrl: camera.snapshotUrl,
        mjpegUrl: camera.mjpegUrl,
        rtspUrl: camera.rtspUrl,
        supportedFeatures: camera.supportsPtz ? ['stream', 'snapshot', 'ptz'] : ['stream', 'snapshot'],
        isStreaming: camera.isStreaming,
        isRecording: camera.isRecording,
        motionDetectionEnabled: camera.motionDetectionEnabled,
        audio: stateFromBoolean(camera.audioEnabled),
        motionRecording: stateFromBoolean(camera.motionRecordingEnabled),
        color_bw: camera.colorBw,
        serviceMappings: {
          snapshot: 'camera.snapshot',
          streamSource: 'camera.stream_source',
          ptzControl: 'amcrest.ptz_control',
          gotoPreset: 'amcrest.goto_preset',
        },
        ...camera.attributes,
      }, snapshotArg.connected && camera.available !== false));
    }

    for (const sensor of snapshotArg.binarySensors) {
      entities.push(this.entity('binary_sensor', sensor.name, deviceId, `amcrest_${this.uniqueBase(snapshotArg)}_${this.slug(sensor.key)}`, sensor.isOn ? 'on' : 'off', usedIds, {
        key: sensor.key,
        deviceClass: sensor.deviceClass,
        eventCodes: sensor.eventCodes,
        shouldPoll: sensor.shouldPoll,
        ...sensor.attributes,
      }, sensor.key === 'online' || (snapshotArg.connected && sensor.available !== false)));
    }
    for (const sensor of snapshotArg.sensors) {
      entities.push(this.entity('sensor', sensor.name, deviceId, `amcrest_${this.uniqueBase(snapshotArg)}_${this.slug(sensor.key)}`, sensor.value ?? 'unknown', usedIds, {
        key: sensor.key,
        unit: sensor.unit,
        deviceClass: sensor.deviceClass,
        entityCategory: sensor.entityCategory,
        ...sensor.attributes,
      }, snapshotArg.connected && sensor.available !== false));
    }
    for (const switchArg of snapshotArg.switches) {
      entities.push(this.entity('switch', switchArg.name, deviceId, `amcrest_${this.uniqueBase(snapshotArg)}_${this.slug(switchArg.key)}`, switchArg.isOn ? 'on' : 'off', usedIds, {
        key: switchArg.key,
        command: switchArg.command,
        entityCategory: switchArg.entityCategory,
        ...switchArg.attributes,
      }, snapshotArg.connected && switchArg.available !== false));
    }
    for (const event of snapshotArg.events) {
      entities.push(this.entity('event' as TEntityPlatform, event.name || event.id, deviceId, `amcrest_${this.uniqueBase(snapshotArg)}_event_${this.slug(event.id)}`, event.state || (event.isOn ? 'on' : 'off'), usedIds, {
        eventId: event.id,
        code: event.code,
        updatedAt: event.updatedAt,
        payload: event.payload,
      }, true));
    }

    return entities;
  }

  public static commandForService(snapshotArg: IAmcrestSnapshot, requestArg: IServiceCallRequest): IAmcrestClientCommand | 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,
        channel: camera?.channel,
        resolution: this.resolutionValue(requestArg.data?.resolution) || camera?.resolution,
        streamSource: this.streamSourceValue(requestArg.data?.stream_source ?? requestArg.data?.streamSource) || camera?.streamSource,
      };
    }
    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,
        channel: camera?.channel,
        filename: this.stringValue(requestArg.data?.filename),
        httpCommands: camera ? [{ label: 'snapshot', method: 'GET', path: `/cgi-bin/snapshot.cgi?channel=${camera.channel}`, expect: 'image' }] : undefined,
      };
    }
    if (requestArg.domain === 'camera') {
      return this.cameraCommand(snapshotArg, 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 {
        type: 'set_privacy_mode',
        service: requestArg.service,
        target: requestArg.target,
        data: requestArg.data,
        enabled,
        httpCommands: this.booleanHttpCommands('privacy_mode', snapshotArg, enabled),
      };
    }
    if (requestArg.domain === 'amcrest') {
      return this.amcrestCommand(snapshotArg, requestArg);
    }
    return undefined;
  }

  public static deviceId(snapshotArg: IAmcrestSnapshot): string {
    return `amcrest.device.${this.uniqueBase(snapshotArg)}`;
  }

  private static cameraCommand(snapshotArg: IAmcrestSnapshot, requestArg: IServiceCallRequest): IAmcrestClientCommand | undefined {
    const camera = this.findCamera(snapshotArg, requestArg);
    if (requestArg.service === 'turn_on' || requestArg.service === 'turn_off') {
      const enabled = requestArg.service === 'turn_on';
      return { type: 'set_video', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('video', snapshotArg, enabled, camera) };
    }
    if (requestArg.service === 'enable_motion_detection' || requestArg.service === 'disable_motion_detection') {
      const enabled = requestArg.service === 'enable_motion_detection';
      return { type: 'set_motion_detection', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('motion_detection', snapshotArg, enabled, camera) };
    }
    return undefined;
  }

  private static amcrestCommand(snapshotArg: IAmcrestSnapshot, requestArg: IServiceCallRequest): IAmcrestClientCommand | 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 === 'enable_recording' || requestArg.service === 'disable_recording') {
      const enabled = requestArg.service === 'enable_recording';
      return { type: 'set_recording', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled };
    }
    if (requestArg.service === 'enable_audio' || requestArg.service === 'disable_audio') {
      const enabled = requestArg.service === 'enable_audio';
      return { type: 'set_audio', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('audio', snapshotArg, enabled, camera) };
    }
    if (requestArg.service === 'enable_motion_recording' || requestArg.service === 'disable_motion_recording') {
      const enabled = requestArg.service === 'enable_motion_recording';
      return { type: 'set_motion_recording', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('motion_recording', snapshotArg, enabled, camera) };
    }
    if (requestArg.service === 'set_privacy_mode') {
      const enabled = this.booleanFromData(requestArg.data);
      return enabled === undefined ? undefined : { type: 'set_privacy_mode', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('privacy_mode', snapshotArg, enabled, camera) };
    }
    if (requestArg.service === 'set_video') {
      const enabled = this.booleanFromData(requestArg.data);
      return enabled === undefined ? undefined : { type: 'set_video', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('video', snapshotArg, enabled, camera) };
    }
    if (requestArg.service === 'set_audio') {
      const enabled = this.booleanFromData(requestArg.data);
      return enabled === undefined ? undefined : { type: 'set_audio', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('audio', snapshotArg, enabled, camera) };
    }
    if (requestArg.service === 'set_motion_detection') {
      const enabled = this.booleanFromData(requestArg.data);
      return enabled === undefined ? undefined : { type: 'set_motion_detection', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, enabled, httpCommands: this.booleanHttpCommands('motion_detection', snapshotArg, enabled, camera) };
    }
    if (requestArg.service === 'goto_preset') {
      const preset = this.numberValue(requestArg.data?.preset);
      return preset === undefined ? undefined : { type: 'goto_preset', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, preset, httpCommands: [{ label: 'goto_preset', method: 'GET', path: `/cgi-bin/ptz.cgi?action=start&channel=${camera?.channel ?? 0}&code=GotoPreset&arg1=0&arg2=${Math.round(preset)}&arg3=0`, expect: 'ok' }] };
    }
    if (requestArg.service === 'ptz_control') {
      const movement = this.ptzMovement(requestArg.data?.movement);
      if (!movement) {
        return undefined;
      }
      return { type: 'ptz_control', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, movement, travelTime: this.numberValue(requestArg.data?.travel_time ?? requestArg.data?.travelTime) ?? 0.2 };
    }
    if (requestArg.service === 'set_color_bw') {
      const colorBw = this.colorBw(requestArg.data?.color_bw ?? requestArg.data?.colorBw);
      return colorBw ? { type: 'set_color_bw', service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel, colorBw } : undefined;
    }
    if (requestArg.service === 'start_tour' || requestArg.service === 'stop_tour') {
      return { type: requestArg.service, service: requestArg.service, target: requestArg.target, data: requestArg.data, cameraId: camera?.id, channel: camera?.channel } as IAmcrestClientCommand;
    }
    return undefined;
  }

  private static booleanHttpCommands(kindArg: 'privacy_mode' | 'video' | 'audio' | 'motion_detection' | 'motion_recording', snapshotArg: IAmcrestSnapshot, enabledArg: boolean, cameraArg?: IAmcrestCamera) {
    const camera = cameraArg || snapshotArg.cameras[0];
    const channel = camera?.channel ?? 0;
    const subtype = camera?.subtype ?? amcrestResolutionSubtype.high;
    const format = `${amcrestSubtypeStream[subtype]}Format`;
    const value = String(enabledArg).toLowerCase();
    const field = kindArg === 'privacy_mode'
      ? `LeLensMask[${channel}].Enable`
      : kindArg === 'video'
        ? `Encode[${channel}].${format}[0].VideoEnable`
        : kindArg === 'audio'
          ? `Encode[${channel}].${format}[0].AudioEnable`
          : kindArg === 'motion_detection'
            ? `MotionDetect[${channel}].Enable`
            : `MotionDetect[${channel}].EventHandler.RecordEnable`;
    return [{ label: kindArg, method: 'GET' as const, path: `/cgi-bin/configManager.cgi?action=setConfig&${field}=${value}`, expect: 'ok' as const }];
  }

  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: 'amcrest',
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes(attributesArg),
      available: availableArg,
    };
  }

  private static findSwitch(snapshotArg: IAmcrestSnapshot, requestArg: IServiceCallRequest): IAmcrestSwitch | undefined {
    const target = requestArg.target.entityId || requestArg.target.deviceId || this.stringValue(requestArg.data?.entity_id ?? requestArg.data?.entityId ?? requestArg.data?.key);
    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) || target;
    return snapshotArg.switches.find((switchArg) => switchArg.key === key || switchArg.name === target || this.deviceId(snapshotArg) === target);
  }

  private static findCamera(snapshotArg: IAmcrestSnapshot, requestArg: IServiceCallRequest): IAmcrestCamera | undefined {
    const target = requestArg.target.entityId || this.stringValue(requestArg.data?.cameraId ?? requestArg.data?.camera_id ?? requestArg.data?.camera ?? requestArg.data?.channel);
    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?.channel === target);
    const cameraId = this.stringValue(entity?.attributes?.cameraId) || target;
    return snapshotArg.cameras.find((cameraArg) => cameraArg.id === cameraId || String(cameraArg.channel) === cameraId) || snapshotArg.cameras[0];
  }

  private static streamSourceUrl(cameraArg: IAmcrestCamera): string | undefined {
    if (cameraArg.streamSource === 'rtsp') {
      return cameraArg.rtspUrl;
    }
    if (cameraArg.streamSource === 'mjpeg') {
      return cameraArg.mjpegUrl;
    }
    return cameraArg.snapshotUrl;
  }

  private static deviceName(snapshotArg: IAmcrestSnapshot): string {
    return snapshotArg.deviceInfo.name || snapshotArg.deviceInfo.host || 'Amcrest Camera';
  }

  private static uniqueBase(snapshotArg: IAmcrestSnapshot): string {
    return this.slug(snapshotArg.deviceInfo.macAddress || snapshotArg.deviceInfo.serialNumber || snapshotArg.deviceInfo.id || snapshotArg.deviceInfo.host || 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 deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
    if (valueArg === undefined) {
      return null;
    }
    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 String(valueArg);
  }

  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 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 {
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : typeof valueArg === 'number' ? String(valueArg) : undefined;
  }

  private static resolutionValue(valueArg: unknown): TAmcrestResolution | undefined {
    const value = this.stringValue(valueArg);
    return value === 'high' || value === 'low' ? value : undefined;
  }

  private static streamSourceValue(valueArg: unknown): TAmcrestStreamSource | undefined {
    const value = this.stringValue(valueArg);
    return value === 'snapshot' || value === 'mjpeg' || value === 'rtsp' ? value : undefined;
  }

  private static colorBw(valueArg: unknown): TAmcrestColorBw | undefined {
    const value = this.stringValue(valueArg)?.toLowerCase();
    return value && amcrestColorModes.includes(value as TAmcrestColorBw) ? value as TAmcrestColorBw : undefined;
  }

  private static ptzMovement(valueArg: unknown): TAmcrestPtzMovement | undefined {
    const value = this.stringValue(valueArg)?.toLowerCase();
    return value && amcrestPtzMovements.includes(value as TAmcrestPtzMovement) ? value as TAmcrestPtzMovement : undefined;
  }

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

const stateFromBoolean = (valueArg: boolean | undefined): string | undefined => {
  return valueArg === undefined ? undefined : valueArg ? 'on' : 'off';
};
