import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type {
  IAgentDvrCommandRequest,
  IAgentDvrDeviceSnapshot,
  IAgentDvrHttpCommand,
  IAgentDvrSnapshot,
  TAgentDvrAlarmProfile,
  TAgentDvrAlarmState,
  TAgentDvrCameraCommandName,
} from './agent_dvr.types.js';
import { agentDvrAlarmProfiles, agentDvrCameraTypeId, agentDvrDomain } from './agent_dvr.types.js';

const cameraStreamServices = new Set(['stream', 'stream_source', 'get_stream']);
const cameraSnapshotServices = new Set(['snapshot', 'camera_image', 'camera_snapshot']);

type TAgentDvrSwitchKey = 'enabled' | 'alerts' | 'detector' | 'recording';

export class AgentDvrMapper {
  public static toDevices(snapshotArg: IAgentDvrSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [{
      id: this.serverDeviceId(snapshotArg),
      integrationDomain: agentDvrDomain,
      name: snapshotArg.server.name || 'Agent DVR',
      protocol: 'http',
      manufacturer: 'iSpyConnect',
      model: 'Agent DVR',
      online: snapshotArg.connected,
      features: [
        { id: 'alarm', capability: 'sensor', name: 'Alarm', readable: true, writable: true },
        { id: 'active_profile', capability: 'sensor', name: 'Active profile', readable: true, writable: true },
        { id: 'device_count', capability: 'sensor', name: 'Device count', readable: true, writable: false },
      ],
      state: [
        { featureId: 'alarm', value: this.alarmState(snapshotArg), updatedAt },
        { featureId: 'active_profile', value: snapshotArg.server.activeProfile || null, updatedAt },
        { featureId: 'device_count', value: snapshotArg.server.deviceCount ?? snapshotArg.devices.length, updatedAt },
      ],
      metadata: this.cleanAttributes({
        host: snapshotArg.server.host,
        port: snapshotArg.server.port,
        protocol: snapshotArg.server.protocol,
        serverUrl: snapshotArg.server.serverUrl,
        unique: snapshotArg.server.unique,
        version: snapshotArg.server.version,
        remoteAccess: snapshotArg.server.remoteAccess,
        source: snapshotArg.source,
        error: snapshotArg.error,
      }),
    }];

    for (const camera of this.cameras(snapshotArg)) {
      devices.push({
        id: this.cameraDeviceId(snapshotArg, camera),
        integrationDomain: agentDvrDomain,
        name: camera.name,
        protocol: 'http',
        manufacturer: 'Agent',
        model: 'Camera',
        online: snapshotArg.connected && camera.available,
        features: [
          { id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
          { id: 'camera', capability: 'camera', name: camera.name, readable: true, writable: true },
          { id: 'enabled', capability: 'switch', name: 'Enabled', readable: true, writable: true },
          { id: 'recording', capability: 'switch', name: 'Recording', readable: true, writable: true },
          { id: 'alerts', capability: 'switch', name: 'Alerts', readable: true, writable: true },
          { id: 'detector', capability: 'switch', name: 'Motion detector', readable: true, writable: true },
        ],
        state: [
          { featureId: 'connectivity', value: camera.available ? 'online' : 'offline', updatedAt },
          { featureId: 'camera', value: this.deviceStateValue({ stillImageUrl: camera.urls.stillImageUrl || null, mjpegImageUrl: camera.urls.mjpegImageUrl || null }), updatedAt },
          { featureId: 'enabled', value: Boolean(camera.data.online), updatedAt },
          { featureId: 'recording', value: Boolean(camera.data.recording), updatedAt },
          { featureId: 'alerts', value: Boolean(camera.data.alertsActive), updatedAt },
          { featureId: 'detector', value: Boolean(camera.data.detectorActive), updatedAt },
        ],
        metadata: this.cleanAttributes({
          objectId: camera.objectId,
          objectTypeId: camera.typeId,
          location: camera.location,
          width: camera.data.width,
          height: camera.data.height,
          hasPtz: this.hasPtz(camera),
          source: snapshotArg.source,
        }),
      });
    }

    return devices;
  }

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

    entities.push(this.entity('alarm_control_panel' as TEntityPlatform, `${snapshotArg.server.name || 'Agent DVR'} Alarm Panel`, serverDeviceId, `${agentDvrDomain}_${this.serverUniqueBase(snapshotArg)}_alarm`, this.alarmState(snapshotArg), usedIds, {
      supportedFeatures: ['arm_home', 'arm_away', 'arm_night'],
      codeArmRequired: false,
      activeProfile: snapshotArg.server.activeProfile,
      armed: snapshotArg.server.armed,
      serviceMappings: {
        disarm: 'alarm_control_panel.alarm_disarm',
        armAway: 'alarm_control_panel.alarm_arm_away',
        armHome: 'alarm_control_panel.alarm_arm_home',
        armNight: 'alarm_control_panel.alarm_arm_night',
      },
    }, snapshotArg.connected));

    entities.push(this.entity('sensor', `${snapshotArg.server.name || 'Agent DVR'} Active Profile`, serverDeviceId, `${agentDvrDomain}_${this.serverUniqueBase(snapshotArg)}_active_profile`, snapshotArg.server.activeProfile || 'unknown', usedIds, { deviceClass: 'enum' }, snapshotArg.connected));
    entities.push(this.entity('sensor', `${snapshotArg.server.name || 'Agent DVR'} Device Count`, serverDeviceId, `${agentDvrDomain}_${this.serverUniqueBase(snapshotArg)}_device_count`, snapshotArg.server.deviceCount ?? snapshotArg.devices.length, usedIds, {}, snapshotArg.connected));

    for (const camera of this.cameras(snapshotArg)) {
      const deviceId = this.cameraDeviceId(snapshotArg, camera);
      const cameraBase = this.cameraUniqueBase(snapshotArg, camera);
      entities.push(this.entity('camera' as TEntityPlatform, camera.name, deviceId, `${agentDvrDomain}_${cameraBase}_camera`, camera.available ? 'idle' : 'unavailable', usedIds, {
        objectId: camera.objectId,
        objectTypeId: camera.typeId,
        stillImageUrl: camera.urls.stillImageUrl,
        snapshotUrl: camera.urls.stillImageUrl,
        mjpegImageUrl: camera.urls.mjpegImageUrl,
        streamSourceUrl: camera.urls.streamSourceUrl || camera.urls.mjpegImageUrl,
        mp4Url: camera.urls.mp4Url,
        webmUrl: camera.urls.webmUrl,
        supportedFeatures: ['on_off', 'snapshot', 'stream', 'recording', 'alerts', 'motion_detection'],
        editable: false,
        enabled: Boolean(camera.data.online),
        connected: Boolean(camera.data.connected),
        detected: Boolean(camera.data.detected),
        alerted: Boolean(camera.data.alerted),
        hasPtz: this.hasPtz(camera),
        alertsEnabled: Boolean(camera.data.alertsActive),
        motionDetectionEnabled: Boolean(camera.data.detectorActive),
        recording: Boolean(camera.data.recording),
        location: camera.location,
        serviceMappings: {
          enableAlerts: `${agentDvrDomain}.enable_alerts`,
          disableAlerts: `${agentDvrDomain}.disable_alerts`,
          startRecording: `${agentDvrDomain}.start_recording`,
          stopRecording: `${agentDvrDomain}.stop_recording`,
          snapshot: `${agentDvrDomain}.snapshot`,
          snapshotImage: 'camera.snapshot',
        },
        ...camera.attributes,
      }, snapshotArg.connected && camera.available));

      for (const key of ['enabled', 'alerts', 'detector', 'recording'] as TAgentDvrSwitchKey[]) {
        entities.push(this.cameraSwitchEntity(snapshotArg, camera, key, usedIds));
      }
      entities.push(this.entity('binary_sensor', `${camera.name} Connected`, deviceId, `${agentDvrDomain}_${cameraBase}_connected`, camera.data.connected ? 'on' : 'off', usedIds, { objectId: camera.objectId, objectTypeId: camera.typeId, deviceClass: 'connectivity' }, snapshotArg.connected));
      entities.push(this.entity('binary_sensor', `${camera.name} Detected`, deviceId, `${agentDvrDomain}_${cameraBase}_detected`, camera.data.detected ? 'on' : 'off', usedIds, { objectId: camera.objectId, objectTypeId: camera.typeId }, snapshotArg.connected && camera.available));
      entities.push(this.entity('binary_sensor', `${camera.name} Alerted`, deviceId, `${agentDvrDomain}_${cameraBase}_alerted`, camera.data.alerted ? 'on' : 'off', usedIds, { objectId: camera.objectId, objectTypeId: camera.typeId }, snapshotArg.connected && camera.available));
    }

    return entities;
  }

  public static commandForService(snapshotArg: IAgentDvrSnapshot, requestArg: IServiceCallRequest): IAgentDvrCommandRequest | undefined {
    if (requestArg.domain === agentDvrDomain && (requestArg.service === 'refresh' || requestArg.service === 'status')) {
      return { type: requestArg.service === 'refresh' ? 'refresh' : 'status', service: requestArg.service, target: requestArg.target, data: requestArg.data };
    }

    const alarmCommand = this.alarmCommand(requestArg);
    if (alarmCommand) {
      return alarmCommand;
    }

    if (requestArg.domain === agentDvrDomain && requestArg.service === 'set_profile') {
      const profile = this.profileValue(requestArg.data?.profile ?? requestArg.data?.name);
      return profile ? this.profileCommand(profile, requestArg) : undefined;
    }

    if (requestArg.domain === 'switch' && ['turn_on', 'turn_off', 'toggle'].includes(requestArg.service)) {
      const switchCommand = this.switchCommand(snapshotArg, requestArg);
      if (switchCommand) {
        return switchCommand;
      }
    }

    const camera = this.findCamera(snapshotArg, requestArg);
    if (!camera) {
      return undefined;
    }

    if (requestArg.domain === 'camera' && cameraStreamServices.has(requestArg.service)) {
      return { type: 'stream_source', service: requestArg.service, target: requestArg.target, data: requestArg.data, objectId: camera.objectId, objectTypeId: camera.typeId };
    }
    if (requestArg.domain === 'camera' && cameraSnapshotServices.has(requestArg.service)) {
      return { type: 'snapshot_image', service: requestArg.service, target: requestArg.target, data: requestArg.data, objectId: camera.objectId, objectTypeId: camera.typeId, filename: this.stringValue(requestArg.data?.filename), size: this.stringValue(requestArg.data?.size) };
    }
    if (requestArg.domain === 'camera' && requestArg.service === 'turn_on') {
      return this.cameraCommand(camera, 'switchOn', requestArg);
    }
    if (requestArg.domain === 'camera' && requestArg.service === 'turn_off') {
      return this.cameraCommand(camera, 'switchOff', requestArg);
    }
    if (requestArg.domain === 'camera' && requestArg.service === 'enable_motion_detection') {
      return this.cameraCommand(camera, 'detectorOn', requestArg);
    }
    if (requestArg.domain === 'camera' && requestArg.service === 'disable_motion_detection') {
      return this.cameraCommand(camera, 'detectorOff', requestArg);
    }

    if (requestArg.domain === agentDvrDomain) {
      if (requestArg.service === 'enable_alerts') {
        return this.cameraCommand(camera, 'alertOn', requestArg);
      }
      if (requestArg.service === 'disable_alerts') {
        return this.cameraCommand(camera, 'alertOff', requestArg);
      }
      if (requestArg.service === 'start_recording') {
        return this.cameraCommand(camera, 'record', requestArg);
      }
      if (requestArg.service === 'stop_recording') {
        return this.cameraCommand(camera, 'recordStop', requestArg);
      }
      if (requestArg.service === 'snapshot') {
        return this.cameraCommand(camera, 'snapshot', requestArg);
      }
      if (requestArg.service === 'snapshot_image') {
        return { type: 'snapshot_image', service: requestArg.service, target: requestArg.target, data: requestArg.data, objectId: camera.objectId, objectTypeId: camera.typeId, filename: this.stringValue(requestArg.data?.filename), size: this.stringValue(requestArg.data?.size) };
      }
    }

    return undefined;
  }

  public static alarmState(snapshotArg: IAgentDvrSnapshot): TAgentDvrAlarmState {
    if (snapshotArg.server.armed === false) {
      return 'disarmed';
    }
    if (snapshotArg.server.armed === true) {
      const profile = String(snapshotArg.server.activeProfile || '').toLowerCase();
      if (profile === 'home') {
        return 'armed_home';
      }
      if (profile === 'night') {
        return 'armed_night';
      }
      if (profile === 'away' || !profile) {
        return 'armed_away';
      }
      return 'armed_custom_bypass';
    }
    return 'unknown';
  }

  public static serverDeviceId(snapshotArg: IAgentDvrSnapshot): string {
    return `${agentDvrDomain}.server.${this.serverUniqueBase(snapshotArg)}`;
  }

  public static cameraDeviceId(snapshotArg: IAgentDvrSnapshot, cameraArg: IAgentDvrDeviceSnapshot): string {
    return `${agentDvrDomain}.camera.${this.cameraUniqueBase(snapshotArg, cameraArg)}`;
  }

  private static cameraSwitchEntity(snapshotArg: IAgentDvrSnapshot, cameraArg: IAgentDvrDeviceSnapshot, keyArg: TAgentDvrSwitchKey, usedIdsArg: Map<string, number>): IIntegrationEntity {
    const state = this.switchState(cameraArg, keyArg);
    return this.entity('switch', `${cameraArg.name} ${this.switchName(keyArg)}`, this.cameraDeviceId(snapshotArg, cameraArg), `${agentDvrDomain}_${this.cameraUniqueBase(snapshotArg, cameraArg)}_${keyArg}`, state ? 'on' : 'off', usedIdsArg, {
      key: keyArg,
      objectId: cameraArg.objectId,
      objectTypeId: cameraArg.typeId,
      nativeType: `agent_dvr_${keyArg}`,
    }, snapshotArg.connected && (keyArg === 'enabled' || cameraArg.available));
  }

  private static alarmCommand(requestArg: IServiceCallRequest): IAgentDvrCommandRequest | undefined {
    const domainMatches = requestArg.domain === 'alarm_control_panel' || requestArg.domain === agentDvrDomain;
    if (!domainMatches) {
      return undefined;
    }
    if (requestArg.service === 'alarm_disarm' || requestArg.service === 'disarm') {
      return { type: 'alarm_command', service: requestArg.service, target: requestArg.target, data: requestArg.data, command: 'disarm', httpCommands: [this.httpCommand('disarm')] };
    }
    const profile = requestArg.service === 'alarm_arm_home' || requestArg.service === 'arm_home'
      ? 'home'
      : requestArg.service === 'alarm_arm_night' || requestArg.service === 'arm_night'
        ? 'night'
        : requestArg.service === 'alarm_arm_away' || requestArg.service === 'arm_away' || requestArg.service === 'arm'
          ? 'away'
          : undefined;
    if (!profile) {
      return undefined;
    }
    return {
      type: 'alarm_command',
      service: requestArg.service,
      target: requestArg.target,
      data: requestArg.data,
      command: 'arm',
      profile,
      httpCommands: [this.httpCommand('arm'), this.httpCommand('setProfileByName', { name: profile })],
    };
  }

  private static profileCommand(profileArg: TAgentDvrAlarmProfile | string, requestArg: IServiceCallRequest): IAgentDvrCommandRequest {
    return {
      type: 'set_profile',
      service: requestArg.service,
      target: requestArg.target,
      data: requestArg.data,
      command: 'setProfileByName',
      profile: profileArg,
      httpCommands: [this.httpCommand('setProfileByName', { name: profileArg })],
    };
  }

  private static switchCommand(snapshotArg: IAgentDvrSnapshot, requestArg: IServiceCallRequest): IAgentDvrCommandRequest | undefined {
    const switchInfo = this.findCameraSwitch(snapshotArg, requestArg);
    if (!switchInfo) {
      return undefined;
    }
    const enabled = requestArg.service === 'turn_on' ? true : requestArg.service === 'turn_off' ? false : !this.switchState(switchInfo.camera, switchInfo.key);
    const commandByKey: Record<TAgentDvrSwitchKey, [TAgentDvrCameraCommandName, TAgentDvrCameraCommandName]> = {
      enabled: ['switchOn', 'switchOff'],
      alerts: ['alertOn', 'alertOff'],
      detector: ['detectorOn', 'detectorOff'],
      recording: ['record', 'recordStop'],
    };
    return this.cameraCommand(switchInfo.camera, enabled ? commandByKey[switchInfo.key][0] : commandByKey[switchInfo.key][1], requestArg);
  }

  private static cameraCommand(cameraArg: IAgentDvrDeviceSnapshot, commandArg: TAgentDvrCameraCommandName, requestArg: IServiceCallRequest): IAgentDvrCommandRequest {
    return {
      type: 'camera_command',
      service: requestArg.service,
      target: requestArg.target,
      data: requestArg.data,
      objectId: cameraArg.objectId,
      objectTypeId: cameraArg.typeId,
      command: commandArg,
      httpCommands: [this.httpCommand(commandArg, { oid: cameraArg.objectId, ot: cameraArg.typeId })],
    };
  }

  private static httpCommand(commandArg: string, paramsArg: Record<string, string | number | boolean> = {}): IAgentDvrHttpCommand {
    return { label: commandArg, path: 'command.cgi', params: { cmd: commandArg, ...paramsArg }, expect: 'json' };
  }

  private static findCamera(snapshotArg: IAgentDvrSnapshot, requestArg: IServiceCallRequest): IAgentDvrDeviceSnapshot | undefined {
    const target = requestArg.target?.entityId || requestArg.target?.deviceId || this.stringValue(requestArg.data?.entity_id ?? requestArg.data?.entityId ?? requestArg.data?.objectId ?? requestArg.data?.oid);
    const cameras = this.cameras(snapshotArg);
    if (!target) {
      return cameras[0];
    }
    const entities = this.toEntities(snapshotArg);
    const entity = entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || entityArg.deviceId === target);
    const objectId = this.numberValue(entity?.attributes?.objectId ?? target);
    return cameras.find((cameraArg) => cameraArg.objectId === objectId || this.cameraDeviceId(snapshotArg, cameraArg) === target || String(cameraArg.id) === target || cameraArg.name === target);
  }

  private static findCameraSwitch(snapshotArg: IAgentDvrSnapshot, requestArg: IServiceCallRequest): { camera: IAgentDvrDeviceSnapshot; key: TAgentDvrSwitchKey } | undefined {
    const target = requestArg.target?.entityId || requestArg.target?.deviceId || this.stringValue(requestArg.data?.entity_id ?? requestArg.data?.entityId ?? requestArg.data?.key);
    const entities = this.toEntities(snapshotArg).filter((entityArg) => entityArg.platform === 'switch');
    const entity = target ? entities.find((entityArg) => entityArg.id === target || entityArg.uniqueId === target || entityArg.attributes?.key === target) : entities[0];
    if (target && !entity) {
      return undefined;
    }
    const key = this.switchKey(entity?.attributes?.key || target);
    const objectId = this.numberValue(entity?.attributes?.objectId);
    const camera = this.cameras(snapshotArg).find((cameraArg) => cameraArg.objectId === objectId) || this.findCamera(snapshotArg, requestArg);
    return camera && key ? { camera, key } : undefined;
  }

  private static cameras(snapshotArg: IAgentDvrSnapshot): IAgentDvrDeviceSnapshot[] {
    return snapshotArg.devices.filter((deviceArg) => deviceArg.typeId === agentDvrCameraTypeId);
  }

  private static hasPtz(cameraArg: IAgentDvrDeviceSnapshot): boolean {
    return cameraArg.typeId === agentDvrCameraTypeId && this.numberValue(cameraArg.data.ptzid) !== -1;
  }

  private static switchState(cameraArg: IAgentDvrDeviceSnapshot, keyArg: TAgentDvrSwitchKey): boolean {
    if (keyArg === 'enabled') {
      return Boolean(cameraArg.data.online);
    }
    if (keyArg === 'alerts') {
      return Boolean(cameraArg.data.alertsActive);
    }
    if (keyArg === 'detector') {
      return Boolean(cameraArg.data.detectorActive);
    }
    return Boolean(cameraArg.data.recording);
  }

  private static switchName(keyArg: TAgentDvrSwitchKey): string {
    return keyArg === 'enabled' ? 'Enabled' : keyArg === 'alerts' ? 'Alerts' : keyArg === 'detector' ? 'Motion Detector' : 'Recording';
  }

  private static profileValue(valueArg: unknown): TAgentDvrAlarmProfile | string | undefined {
    const value = this.stringValue(valueArg)?.toLowerCase();
    if (!value) {
      return undefined;
    }
    return agentDvrAlarmProfiles.includes(value as TAgentDvrAlarmProfile) ? value as TAgentDvrAlarmProfile : value;
  }

  private static switchKey(valueArg: unknown): TAgentDvrSwitchKey | undefined {
    const value = this.stringValue(valueArg) as TAgentDvrSwitchKey | undefined;
    return value === 'enabled' || value === 'alerts' || value === 'detector' || value === 'recording' ? value : undefined;
  }

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

  private static serverUniqueBase(snapshotArg: IAgentDvrSnapshot): string {
    return this.slug(snapshotArg.server.unique || snapshotArg.server.id || snapshotArg.server.host || snapshotArg.server.name || 'agent_dvr');
  }

  private static cameraUniqueBase(snapshotArg: IAgentDvrSnapshot, cameraArg: IAgentDvrDeviceSnapshot): string {
    return this.slug(`${this.serverUniqueBase(snapshotArg)}_${cameraArg.typeId}_${cameraArg.objectId || cameraArg.id || cameraArg.name}`);
  }

  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 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 {
    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 slug(valueArg: string): string {
    return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || agentDvrDomain;
  }
}
