import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IIntegrationEvent, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type { IAxisEvent, IAxisPtzCommand, IAxisRelayCommand, IAxisSnapshot } from './axis.types.js';

export class AxisMapper {
  public static toDevices(snapshotArg: IAxisSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const deviceInfo = snapshotArg.deviceInfo;
    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.id}`, readable: true, writable: Boolean(camera.supportsPtz) });
      state.push({ featureId: `camera_${this.slug(camera.id)}`, value: camera.enabled !== false ? 'available' : 'disabled', updatedAt });
    }
    for (const relay of snapshotArg.relays) {
      features.push({ id: `relay_${this.slug(relay.id)}`, capability: 'switch', name: relay.name || `Relay ${relay.id}`, readable: true, writable: true });
      state.push({ featureId: `relay_${this.slug(relay.id)}`, value: relay.state === 'closed', updatedAt });
    }
    for (const sensor of snapshotArg.binarySensors) {
      features.push({ id: `binary_${this.slug(sensor.id)}`, capability: 'sensor', name: sensor.name, readable: true, writable: false });
      state.push({ featureId: `binary_${this.slug(sensor.id)}`, value: sensor.isOn, updatedAt });
    }
    for (const sensor of snapshotArg.sensors) {
      features.push({ id: `sensor_${this.slug(sensor.id)}`, capability: 'sensor', name: sensor.name, readable: true, writable: false, unit: sensor.unit });
      state.push({ featureId: `sensor_${this.slug(sensor.id)}`, value: this.deviceStateValue(sensor.value), updatedAt });
    }
    for (const light of snapshotArg.lights) {
      features.push({ id: `light_${this.slug(light.id)}`, capability: 'light', name: light.name || `Light ${light.id}`, readable: true, writable: true });
      state.push({ featureId: `light_${this.slug(light.id)}`, value: light.isOn ?? false, updatedAt });
    }

    return [{
      id: this.deviceId(snapshotArg),
      integrationDomain: 'axis',
      name: this.deviceName(snapshotArg),
      protocol: 'http',
      manufacturer: deviceInfo.manufacturer || 'Axis Communications AB',
      model: deviceInfo.model || deviceInfo.productNumber || deviceInfo.productType,
      online: snapshotArg.connected,
      features,
      state,
      metadata: {
        serialNumber: deviceInfo.serialNumber,
        macAddress: deviceInfo.macAddress,
        firmwareVersion: deviceInfo.firmwareVersion,
        productType: deviceInfo.productType,
        host: deviceInfo.host,
        port: deviceInfo.port,
        protocol: deviceInfo.protocol,
        apiIds: snapshotArg.apiDiscovery.map((apiArg) => apiArg.id),
        cameraStreams: snapshotArg.cameras.map((cameraArg) => ({
          id: cameraArg.id,
          snapshotUrl: cameraArg.snapshotUrl,
          mjpegUrl: cameraArg.mjpegUrl,
          rtspUrl: cameraArg.rtspUrl,
        })),
      },
    }];
  }

  public static toEntities(snapshotArg: IAxisSnapshot): 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 || `Camera ${camera.id}`, deviceId, `axis_${this.uniqueBase(snapshotArg)}_camera_${this.slug(camera.id)}`, camera.enabled !== false ? 'idle' : 'unavailable', usedIds, {
        cameraId: camera.id,
        videoSource: camera.videoSource,
        streamProfile: camera.streamProfile,
        resolution: camera.resolution,
        imageFormats: camera.imageFormats,
        snapshotUrl: camera.snapshotUrl,
        mjpegUrl: camera.mjpegUrl,
        rtspUrl: camera.rtspUrl,
        supportedFeatures: camera.supportsPtz ? ['stream', 'ptz'] : ['stream'],
        ...camera.attributes,
      }, camera.enabled !== false));
    }

    for (const relay of snapshotArg.relays) {
      entities.push(this.entity('switch', relay.name || `Relay ${relay.id}`, deviceId, `axis_${this.uniqueBase(snapshotArg)}_relay_${this.slug(relay.id)}`, relay.state === 'closed' ? 'on' : 'off', usedIds, {
        portId: relay.id,
        usage: relay.usage,
        normalState: relay.normalState,
        readonly: relay.readonly,
        ...relay.attributes,
      }, relay.available !== false));
    }

    for (const sensor of snapshotArg.binarySensors) {
      entities.push(this.entity('binary_sensor', sensor.name, deviceId, `axis_${this.uniqueBase(snapshotArg)}_binary_${this.slug(sensor.id)}`, sensor.isOn ? 'on' : 'off', usedIds, {
        deviceClass: sensor.deviceClass,
        eventTopic: sensor.eventTopic,
        source: sensor.source,
        ...sensor.attributes,
      }, sensor.available !== false));
    }

    for (const sensor of snapshotArg.sensors) {
      entities.push(this.entity('sensor', sensor.name, deviceId, `axis_${this.uniqueBase(snapshotArg)}_sensor_${this.slug(sensor.id)}`, sensor.value, usedIds, {
        unit: sensor.unit,
        deviceClass: sensor.deviceClass,
        category: sensor.category,
        ...sensor.attributes,
      }, sensor.available !== false));
    }

    for (const light of snapshotArg.lights) {
      entities.push(this.entity('light', light.name || `Light ${light.id}`, deviceId, `axis_${this.uniqueBase(snapshotArg)}_light_${this.slug(light.id)}`, light.isOn ? 'on' : 'off', usedIds, {
        lightId: light.id,
        lightType: light.lightType,
        numberOfLeds: light.numberOfLeds,
        brightness: light.brightness,
        maxIntensity: light.maxIntensity,
        ...light.attributes,
      }, light.available !== false && light.enabled !== false));
    }

    for (const event of snapshotArg.events) {
      entities.push(this.entity('event' as TEntityPlatform, event.name || this.eventName(event), deviceId, `axis_${this.uniqueBase(snapshotArg)}_event_${this.slug(event.id)}`, event.state || (event.isTripped ? 'on' : 'off'), usedIds, {
        eventId: event.id,
        topic: event.topic,
        topicBase: event.topicBase,
        group: event.group,
        source: event.source,
        sourceIndex: event.sourceIndex,
        operation: event.operation,
        deviceClass: event.deviceClass,
        updatedAt: event.updatedAt,
        data: event.data,
      }, true));
    }

    return entities;
  }

  public static relayCommandForService(snapshotArg: IAxisSnapshot, requestArg: IServiceCallRequest): IAxisRelayCommand | undefined {
    if (requestArg.domain === 'switch' && ['turn_on', 'turn_off', 'toggle'].includes(requestArg.service)) {
      const relay = this.findRelay(snapshotArg, requestArg);
      if (!relay) {
        return undefined;
      }
      const state = requestArg.service === 'turn_on' ? 'closed' : requestArg.service === 'turn_off' ? 'open' : relay.state === 'closed' ? 'open' : 'closed';
      return { portId: relay.id, state };
    }
    if (requestArg.domain === 'axis' && (requestArg.service === 'set_relay' || requestArg.service === 'set_port_state')) {
      const portId = this.stringData(requestArg, 'portId') || this.stringData(requestArg, 'port_id') || this.findRelay(snapshotArg, requestArg)?.id;
      if (!portId) {
        return undefined;
      }
      const stateValue = requestArg.data?.state;
      const onValue = requestArg.data?.on;
      const state = stateValue === 'closed' || stateValue === 'open' ? stateValue : onValue === true ? 'closed' : onValue === false ? 'open' : undefined;
      return state ? { portId, state } : undefined;
    }
    return undefined;
  }

  public static ptzCommandForService(snapshotArg: IAxisSnapshot, requestArg: IServiceCallRequest): IAxisPtzCommand | undefined {
    const supported = (requestArg.domain === 'axis' && ['ptz', 'ptz_control'].includes(requestArg.service))
      || (requestArg.domain === 'camera' && ['ptz', 'ptz_control'].includes(requestArg.service));
    if (!supported) {
      return undefined;
    }
    const data = requestArg.data || {};
    const camera = this.findCamera(snapshotArg, requestArg);
    const command: IAxisPtzCommand = {
      camera: this.stringData(requestArg, 'camera') || this.stringData(requestArg, 'cameraId') || this.stringData(requestArg, 'camera_id') || camera?.videoSource || camera?.id,
      move: this.moveValue(data.move),
      pan: this.numberValue(data.pan),
      tilt: this.numberValue(data.tilt),
      zoom: this.numberValue(data.zoom),
      focus: this.numberValue(data.focus),
      iris: this.numberValue(data.iris),
      brightness: this.numberValue(data.brightness),
      rpan: this.numberValue(data.rpan ?? data.relative_pan),
      rtilt: this.numberValue(data.rtilt ?? data.relative_tilt),
      rzoom: this.numberValue(data.rzoom ?? data.relative_zoom),
      rfocus: this.numberValue(data.rfocus ?? data.relative_focus),
      riris: this.numberValue(data.riris ?? data.relative_iris),
      rbrightness: this.numberValue(data.rbrightness ?? data.relative_brightness),
      autofocus: this.booleanValue(data.autofocus ?? data.auto_focus),
      autoiris: this.booleanValue(data.autoiris ?? data.auto_iris),
      continuouspantiltmove: this.numberPair(data.continuouspantiltmove ?? data.continuous_pan_tilt_move),
      continuouszoommove: this.numberValue(data.continuouszoommove ?? data.continuous_zoom_move),
      continuousfocusmove: this.numberValue(data.continuousfocusmove ?? data.continuous_focus_move),
      continuousirismove: this.numberValue(data.continuousirismove ?? data.continuous_iris_move),
      continuousbrightnessmove: this.numberValue(data.continuousbrightnessmove ?? data.continuous_brightness_move),
      auxiliary: this.stringValue(data.auxiliary),
      gotoserverpresetname: this.stringValue(data.gotoserverpresetname ?? data.preset ?? data.preset_name),
      gotoserverpresetno: this.numberValue(data.gotoserverpresetno ?? data.preset_number),
      gotodevicepreset: this.numberValue(data.gotodevicepreset ?? data.device_preset),
      speed: this.numberValue(data.speed),
      imagerotation: this.rotationValue(data.imagerotation ?? data.image_rotation),
      ircutfilter: this.stateValue(data.ircutfilter ?? data.ir_cut_filter),
      backlight: this.booleanValue(data.backlight),
      center: this.numberPair(data.center),
      areazoom: this.numberTriple(data.areazoom ?? data.area_zoom),
      imagewidth: this.numberValue(data.imagewidth ?? data.image_width),
      imageheight: this.numberValue(data.imageheight ?? data.image_height),
    };
    return Object.entries(command).some(([keyArg, valueArg]) => keyArg !== 'camera' && valueArg !== undefined) ? command : undefined;
  }

  public static toIntegrationEvent(eventArg: IAxisEvent): IIntegrationEvent {
    return {
      type: 'state_changed',
      integrationDomain: 'axis',
      entityId: eventArg.id,
      data: eventArg,
      timestamp: eventArg.updatedAt ? Date.parse(eventArg.updatedAt) : Date.now(),
    };
  }

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

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

  private static findRelay(snapshotArg: IAxisSnapshot, requestArg: IServiceCallRequest) {
    const target = requestArg.target.entityId || requestArg.target.deviceId || this.stringData(requestArg, 'entityId') || this.stringData(requestArg, 'entity_id') || this.stringData(requestArg, 'portId') || this.stringData(requestArg, 'port_id');
    if (!target) {
      return snapshotArg.relays[0];
    }
    const entities = this.toEntities(snapshotArg);
    const entity = entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || entityArg.attributes?.portId === target);
    const portId = entity?.attributes?.portId;
    return snapshotArg.relays.find((relayArg) => relayArg.id === portId || relayArg.id === target || `axis.device.${this.uniqueBase(snapshotArg)}` === target);
  }

  private static findCamera(snapshotArg: IAxisSnapshot, requestArg: IServiceCallRequest) {
    const target = requestArg.target.entityId || this.stringData(requestArg, 'cameraId') || this.stringData(requestArg, 'camera_id') || this.stringData(requestArg, 'camera');
    if (!target) {
      return snapshotArg.cameras[0];
    }
    const entities = this.toEntities(snapshotArg);
    const entity = entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || entityArg.attributes?.cameraId === target);
    const cameraId = entity?.attributes?.cameraId;
    return snapshotArg.cameras.find((cameraArg) => cameraArg.id === cameraId || String(cameraArg.videoSource) === target || cameraArg.id === target) || snapshotArg.cameras[0];
  }

  private static eventName(eventArg: IAxisEvent): string {
    return eventArg.name || eventArg.topicBase?.split('/').pop() || eventArg.topic?.split('/').pop() || `Event ${eventArg.id}`;
  }

  private static deviceName(snapshotArg: IAxisSnapshot): string {
    return snapshotArg.deviceInfo.name || snapshotArg.deviceInfo.model || snapshotArg.deviceInfo.host || 'Axis device';
  }

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

  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 cleanAttributes(attributesArg: Record<string, unknown>): Record<string, unknown> {
    return Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  }

  private static stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
    return this.stringValue(requestArg.data?.[keyArg]);
  }

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

  private static numberValue(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg)) ? Number(valueArg) : 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 moveValue(valueArg: unknown): IAxisPtzCommand['move'] | undefined {
    const value = this.stringValue(valueArg);
    const allowed = ['home', 'up', 'down', 'left', 'right', 'upleft', 'upright', 'downleft', 'downright', 'stop'];
    return allowed.includes(value || '') ? value as IAxisPtzCommand['move'] : undefined;
  }

  private static rotationValue(valueArg: unknown): IAxisPtzCommand['imagerotation'] | undefined {
    const value = this.stringValue(valueArg);
    return value === '0' || value === '90' || value === '180' || value === '270' ? value : undefined;
  }

  private static stateValue(valueArg: unknown): IAxisPtzCommand['ircutfilter'] | undefined {
    const value = this.stringValue(valueArg);
    return value === 'on' || value === 'off' || value === 'auto' ? value : undefined;
  }

  private static numberPair(valueArg: unknown): [number, number] | undefined {
    if (Array.isArray(valueArg) && valueArg.length >= 2) {
      const first = this.numberValue(valueArg[0]);
      const second = this.numberValue(valueArg[1]);
      return first !== undefined && second !== undefined ? [first, second] : undefined;
    }
    if (typeof valueArg === 'string') {
      const [first, second] = valueArg.split(',').map((value) => this.numberValue(value));
      return first !== undefined && second !== undefined ? [first, second] : undefined;
    }
    return undefined;
  }

  private static numberTriple(valueArg: unknown): [number, number, number] | undefined {
    if (Array.isArray(valueArg) && valueArg.length >= 3) {
      const first = this.numberValue(valueArg[0]);
      const second = this.numberValue(valueArg[1]);
      const third = this.numberValue(valueArg[2]);
      return first !== undefined && second !== undefined && third !== undefined ? [first, second, third] : undefined;
    }
    if (typeof valueArg === 'string') {
      const [first, second, third] = valueArg.split(',').map((value) => this.numberValue(value));
      return first !== undefined && second !== undefined && third !== undefined ? [first, second, third] : undefined;
    }
    return undefined;
  }

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