import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest } from '../../core/types.js';
import type {
  IDelugeCommandRequest,
  IDelugeConfig,
  IDelugeRawData,
  IDelugeSensorSnapshot,
  IDelugeSessionStats,
  IDelugeSnapshot,
  IDelugeSnapshotInput,
  IDelugeTorrentSnapshot,
  TDelugeProtocol,
} from './deluge.types.js';
import { delugeDefaultRpcPort, delugeDefaultWebPort, delugeDisplayName, delugeDomain, delugeManufacturer, delugeSensorDescriptions } from './deluge.types.js';

const pauseServices = new Set(['pause', 'pause_torrent', 'pause_torrents', 'turn_off']);
const resumeServices = new Set(['resume', 'resume_torrent', 'resume_torrents', 'turn_on']);
const addServices = new Set(['add_torrent', 'add_torrents', 'add_magnet', 'add_url']);
const removeServices = new Set(['remove_torrent', 'remove_torrents']);

export class DelugeMapper {
  public static toSnapshot(inputArg: IDelugeSnapshotInput): IDelugeSnapshot {
    const config = inputArg.config || {};
    const rawData = inputArg.rawData || {};
    const webUi = rawData.webUi || rawData.updateUi;
    const connected = booleanValue(webUi?.connected) ?? booleanValue(rawData.connected) ?? Boolean(inputArg.online);
    const online = connected && inputArg.error === undefined;
    const stats = this.stats(rawData);
    const torrents = this.torrents(rawData, connected);
    const sensors = this.sensors(stats, torrents, online);
    const switches = [{
      key: 'enabled',
      name: 'Enabled',
      isOn: torrents.some((torrentArg) => !torrentArg.paused),
      available: online,
      attributes: {
        torrentCount: torrents.length,
        pausedCount: torrents.filter((torrentArg) => torrentArg.paused).length,
      },
    }];
    const endpoint = endpointFromConfig(config);
    const updatedAt = rawData.fetchedAt || new Date().toISOString();
    return {
      device: {
        id: config.uniqueId || endpoint.host || endpoint.url || 'manual-deluge',
        name: config.name || endpoint.host || delugeDisplayName,
        manufacturer: config.manufacturer || delugeManufacturer,
        model: config.model,
        host: endpoint.host,
        webPort: endpoint.webPort,
        rpcPort: endpoint.rpcPort,
        protocol: endpoint.protocol,
        url: endpoint.url,
        online,
      },
      stats,
      torrents,
      sensors,
      switches,
      connected,
      online,
      source: inputArg.source || 'manual',
      capabilities: {
        localRead: Boolean(config.client || config.snapshot || config.rawData || (endpoint.host && (config.password || config.webPassword))),
        localControl: Boolean(config.commandExecutor || config.client || (endpoint.host && (config.password || config.webPassword))),
        addTorrent: Boolean(config.commandExecutor || config.client || (endpoint.host && (config.password || config.webPassword))),
        removeTorrent: Boolean(config.commandExecutor || config.client || (endpoint.host && (config.password || config.webPassword))),
      },
      updatedAt,
      rawData,
      error: inputArg.error,
    };
  }

  public static toDevices(snapshotArg: IDelugeSnapshot): 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 },
      { id: 'enabled', capability: 'switch', name: 'Enabled', readable: true, writable: snapshotArg.capabilities.localControl },
    ];
    const state: plugins.shxInterfaces.data.IDeviceState[] = [
      { featureId: 'connectivity', value: snapshotArg.online ? 'online' : 'offline', updatedAt },
      { featureId: 'enabled', value: snapshotArg.switches[0]?.isOn ?? false, updatedAt },
    ];

    for (const sensor of snapshotArg.sensors) {
      features.push({ id: sensor.key, capability: 'sensor', name: sensor.name, readable: true, writable: false, unit: sensor.unit });
      state.push({ featureId: sensor.key, value: this.deviceStateValue(sensor.value), updatedAt });
    }
    features.push({ id: 'torrents', capability: 'sensor', name: 'Torrents', readable: true, writable: false });
    state.push({ featureId: 'torrents', value: snapshotArg.torrents.length, updatedAt });

    return [{
      id: this.deviceId(snapshotArg),
      integrationDomain: delugeDomain,
      name: this.deviceName(snapshotArg),
      protocol: 'http',
      manufacturer: snapshotArg.device.manufacturer || delugeManufacturer,
      model: snapshotArg.device.model,
      online: snapshotArg.online,
      features,
      state,
      metadata: this.cleanAttributes({
        host: snapshotArg.device.host,
        webPort: snapshotArg.device.webPort,
        rpcPort: snapshotArg.device.rpcPort,
        protocol: snapshotArg.device.protocol,
        url: snapshotArg.device.url,
        connected: snapshotArg.connected,
        source: snapshotArg.source,
        capabilities: snapshotArg.capabilities,
      }),
    }];
  }

  public static toEntities(snapshotArg: IDelugeSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const usedIds = new Map<string, number>();
    const deviceId = this.deviceId(snapshotArg);
    const baseName = this.deviceName(snapshotArg);
    for (const sensor of snapshotArg.sensors) {
      entities.push(this.entity('sensor', `${baseName} ${sensor.name}`, deviceId, `deluge_${this.uniqueBase(snapshotArg)}_${this.slug(sensor.key)}`, sensor.value, usedIds, {
        key: sensor.key,
        nativeUnitOfMeasurement: sensor.unit,
        deviceClass: sensor.deviceClass,
        stateClass: sensor.stateClass,
        entityCategory: sensor.entityCategory,
        ...sensor.attributes,
      }, snapshotArg.online && sensor.available !== false));
    }
    const enabledSwitch = snapshotArg.switches[0];
    entities.push(this.entity('switch', `${baseName} Enabled`, deviceId, `deluge_${this.uniqueBase(snapshotArg)}_enabled`, enabledSwitch?.isOn ? 'on' : 'off', usedIds, {
      key: 'enabled',
      torrentIds: snapshotArg.torrents.map((torrentArg) => torrentArg.id),
      serviceMappings: {
        turnOn: 'deluge.resume',
        turnOff: 'deluge.pause',
      },
      ...enabledSwitch?.attributes,
    }, snapshotArg.online && enabledSwitch?.available !== false));
    return entities;
  }

  public static commandForService(snapshotArg: IDelugeSnapshot, requestArg: IServiceCallRequest): IDelugeCommandRequest | undefined {
    if (requestArg.domain === delugeDomain) {
      if (requestArg.service === 'snapshot' || requestArg.service === 'status') {
        return { action: 'snapshot', service: requestArg.service, target: requestArg.target, data: requestArg.data };
      }
      if (requestArg.service === 'refresh') {
        return { action: 'refresh', service: requestArg.service, target: requestArg.target, data: requestArg.data };
      }
      if (pauseServices.has(requestArg.service)) {
        return { action: 'pause', service: requestArg.service, target: requestArg.target, data: requestArg.data, torrentIds: this.torrentIdsFromRequest(snapshotArg, requestArg) };
      }
      if (resumeServices.has(requestArg.service)) {
        return { action: 'resume', service: requestArg.service, target: requestArg.target, data: requestArg.data, torrentIds: this.torrentIdsFromRequest(snapshotArg, requestArg) };
      }
      if (addServices.has(requestArg.service)) {
        return {
          action: 'add_torrent',
          service: requestArg.service,
          target: requestArg.target,
          data: requestArg.data,
          magnet: this.stringValue(requestArg.data?.magnet ?? requestArg.data?.magnet_uri ?? requestArg.data?.uri),
          url: this.stringValue(requestArg.data?.url),
          path: this.stringValue(requestArg.data?.path ?? requestArg.data?.filename ?? requestArg.data?.torrentFilePath),
          cookie: this.stringValue(requestArg.data?.cookie),
          options: record(requestArg.data?.options) || {},
        };
      }
      if (removeServices.has(requestArg.service)) {
        const torrentIds = this.torrentIdsFromRequest(snapshotArg, requestArg);
        if (!torrentIds.length) {
          return undefined;
        }
        return { action: 'remove_torrent', service: requestArg.service, target: requestArg.target, data: requestArg.data, torrentIds, removeData: this.booleanValue(requestArg.data?.remove_data ?? requestArg.data?.removeData) ?? false };
      }
      return undefined;
    }
    if (requestArg.domain === 'switch' && pauseServices.has(requestArg.service)) {
      return { action: 'pause', service: requestArg.service, target: requestArg.target, data: requestArg.data, torrentIds: this.torrentIdsFromRequest(snapshotArg, requestArg) };
    }
    if (requestArg.domain === 'switch' && resumeServices.has(requestArg.service)) {
      return { action: 'resume', service: requestArg.service, target: requestArg.target, data: requestArg.data, torrentIds: this.torrentIdsFromRequest(snapshotArg, requestArg) };
    }
    if (requestArg.domain === 'switch' && requestArg.service === 'toggle') {
      return { action: snapshotArg.switches[0]?.isOn ? 'pause' : 'resume', service: requestArg.service, target: requestArg.target, data: requestArg.data, torrentIds: this.torrentIdsFromRequest(snapshotArg, requestArg) };
    }
    return undefined;
  }

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

  public static entityTorrentIds(snapshotArg: IDelugeSnapshot, entityIdArg: string): string[] | undefined {
    const entity = this.toEntities(snapshotArg).find((entityArg) => entityArg.id === entityIdArg || entityArg.uniqueId === entityIdArg);
    const torrentIds = entity?.attributes?.torrentIds;
    return Array.isArray(torrentIds) && torrentIds.every((idArg) => typeof idArg === 'string') ? torrentIds : undefined;
  }

  private static stats(rawDataArg: Partial<IDelugeRawData>): IDelugeSessionStats {
    const webUi = rawDataArg.webUi || rawDataArg.updateUi;
    const stats = {
      ...(record(webUi?.stats) || {}),
      ...(record(rawDataArg.stats) || {}),
      ...(record(rawDataArg.sessionStatus) || {}),
    };
    const uploadRate = numberValue(stats.upload_rate) ?? numberValue(stats.payload_upload_rate) ?? 0;
    const downloadRate = numberValue(stats.download_rate) ?? numberValue(stats.payload_download_rate) ?? 0;
    const protocolDownloadRate = numberValue(stats.dht_download_rate) ?? numberValue(stats.download_protocol_rate) ?? Math.max(0, downloadRate - (numberValue(stats.payload_download_rate) ?? downloadRate));
    const protocolUploadRate = numberValue(stats.dht_upload_rate) ?? numberValue(stats.upload_protocol_rate) ?? Math.max(0, uploadRate - (numberValue(stats.payload_upload_rate) ?? uploadRate));
    return {
      downloadRate,
      uploadRate,
      dhtDownloadRate: numberValue(stats.dht_download_rate) ?? protocolDownloadRate,
      dhtUploadRate: numberValue(stats.dht_upload_rate) ?? protocolUploadRate,
      protocolDownloadRate,
      protocolUploadRate,
      numConnections: numberValue(stats.num_connections) ?? numberValue(stats['peer.num_peers_connected']),
      dhtNodes: numberValue(stats.dht_nodes) ?? numberValue(stats['dht.dht_nodes']),
      freeSpace: numberValue(stats.free_space),
      externalIp: stringValue(stats.external_ip),
      maxDownload: numberValue(stats.max_download),
      maxUpload: numberValue(stats.max_upload),
      maxConnections: numberValue(stats.max_num_connections),
      hasIncomingConnections: booleanValue(stats.has_incoming_connections) ?? booleanValue(stats['net.has_incoming_connections']),
      raw: stats,
    };
  }

  private static torrents(rawDataArg: Partial<IDelugeRawData>, connectedArg: boolean): IDelugeTorrentSnapshot[] {
    const webUi = rawDataArg.webUi || rawDataArg.updateUi;
    const rawTorrents = record(rawDataArg.torrentsStatus) || record(webUi?.torrents) || {};
    const pausedData = record(rawDataArg.torrentsPaused) || {};
    return Object.entries(rawTorrents).map(([idArg, valueArg]) => {
      const status = record(valueArg) || {};
      const pausedStatus = record(pausedData[idArg]) || {};
      const state = stringValue(status.state) || (booleanValue(status.paused ?? pausedStatus.paused) ? 'Paused' : 'Unknown');
      const paused = booleanValue(status.paused ?? pausedStatus.paused) ?? state.toLowerCase() === 'paused';
      return {
        id: idArg,
        name: stringValue(status.name) || idArg,
        state,
        paused,
        progress: numberValue(status.progress),
        downloadRate: numberValue(status.download_payload_rate) ?? numberValue(status.download_rate),
        uploadRate: numberValue(status.upload_payload_rate) ?? numberValue(status.upload_rate),
        totalSize: numberValue(status.total_size),
        completedSize: numberValue(status.total_done) ?? numberValue(status.completed_size),
        ratio: numberValue(status.ratio),
        eta: numberValue(status.eta),
        seeds: numberValue(status.num_seeds),
        peers: numberValue(status.num_peers),
        trackerHost: stringValue(status.tracker_host),
        savePath: stringValue(status.save_path),
        available: connectedArg,
        attributes: status,
      };
    });
  }

  private static sensors(statsArg: IDelugeSessionStats, torrentsArg: IDelugeTorrentSnapshot[], onlineArg: boolean): IDelugeSensorSnapshot[] {
    const counts = torrentsArg.reduce((accumulatorArg, torrentArg) => {
      if (torrentArg.state === 'Downloading') {
        accumulatorArg.downloading += 1;
      }
      if (torrentArg.state === 'Seeding') {
        accumulatorArg.seeding += 1;
      }
      return accumulatorArg;
    }, { downloading: 0, seeding: 0 });
    const values: Record<string, unknown> = {
      current_status: currentStatus(statsArg.uploadRate, statsArg.downloadRate),
      download_speed: roundKiB(statsArg.downloadRate),
      upload_speed: roundKiB(statsArg.uploadRate),
      protocol_traffic_upload_speed: roundKiB(statsArg.protocolUploadRate),
      protocol_traffic_download_speed: roundKiB(statsArg.protocolDownloadRate),
      downloading_count: counts.downloading,
      seeding_count: counts.seeding,
    };
    return delugeSensorDescriptions.map((descriptionArg) => ({
      key: descriptionArg.key,
      name: descriptionArg.name,
      value: values[descriptionArg.key],
      unit: descriptionArg.unit,
      deviceClass: descriptionArg.deviceClass,
      stateClass: descriptionArg.stateClass,
      entityCategory: descriptionArg.entityCategory,
      available: onlineArg,
    }));
  }

  private static torrentIdsFromRequest(snapshotArg: IDelugeSnapshot, requestArg: IServiceCallRequest): string[] {
    const direct = this.stringArrayValue(requestArg.data?.torrent_ids ?? requestArg.data?.torrentIds ?? requestArg.data?.ids ?? requestArg.data?.hashes)
      || this.stringArrayValue(requestArg.data?.torrent_id ?? requestArg.data?.torrentId ?? requestArg.data?.id ?? requestArg.data?.hash);
    if (direct?.length) {
      return direct;
    }
    if (requestArg.target.entityId) {
      const entityIds = this.entityTorrentIds(snapshotArg, requestArg.target.entityId);
      if (entityIds?.length && requestArg.target.entityId !== this.toEntities(snapshotArg).find((entityArg) => entityArg.platform === 'switch')?.id) {
        return entityIds;
      }
    }
    return [];
  }

  private static entity(platformArg: IIntegrationEntity['platform'], nameArg: string, deviceIdArg: string, uniqueIdArg: string, stateArg: unknown, usedIdsArg: Map<string, number>, attributesArg: Record<string, unknown>, availableArg: boolean): IIntegrationEntity {
    const baseId = `${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: delugeDomain,
      deviceId: deviceIdArg,
      platform: platformArg,
      name: nameArg,
      state: stateArg,
      attributes: this.cleanAttributes(attributesArg),
      available: availableArg,
    };
  }

  private static deviceName(snapshotArg: IDelugeSnapshot): string {
    return snapshotArg.device.name || snapshotArg.device.host || delugeDisplayName;
  }

  private static uniqueBase(snapshotArg: IDelugeSnapshot): string {
    return this.slug(snapshotArg.device.id || snapshotArg.device.host || snapshotArg.device.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 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 stringArrayValue(valueArg: unknown): string[] | undefined {
    if (typeof valueArg === 'string' && valueArg.trim()) {
      return [valueArg.trim()];
    }
    return Array.isArray(valueArg) && valueArg.every((itemArg) => typeof itemArg === 'string' && Boolean(itemArg.trim())) ? valueArg.map((itemArg) => itemArg.trim()) : undefined;
  }

  private static stringValue(valueArg: unknown): string | undefined {
    return stringValue(valueArg);
  }

  private static booleanValue(valueArg: unknown): boolean | undefined {
    return booleanValue(valueArg);
  }

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

export const endpointFromConfig = (configArg: IDelugeConfig): { protocol: TDelugeProtocol; host?: string; webPort: number; rpcPort: number; url?: string } => {
  const url = safeUrl(configArg.url || configArg.host);
  if (url) {
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const webPort = url.port ? Number(url.port) : protocol === 'https' ? 443 : delugeDefaultWebPort;
    return {
      protocol,
      host: url.hostname,
      webPort,
      rpcPort: configArg.rpcPort || configArg.daemonPort || configArg.port || delugeDefaultRpcPort,
      url: url.toString().replace(/\/$/, ''),
    };
  }
  const protocol = configArg.protocol || 'http';
  const webPort = configArg.webPort || configArg.web_port || ((configArg.rpcPort || configArg.daemonPort) ? undefined : configArg.port) || delugeDefaultWebPort;
  return {
    protocol,
    host: configArg.host,
    webPort,
    rpcPort: configArg.rpcPort || configArg.daemonPort || ((configArg.webPort || configArg.web_port) ? configArg.port : undefined) || delugeDefaultRpcPort,
    url: configArg.host ? `${protocol}://${configArg.host}:${webPort}` : undefined,
  };
};

const currentStatus = (uploadRateArg: number, downloadRateArg: number): string => {
  if (uploadRateArg > 0 && downloadRateArg > 0) {
    return 'seeding_and_downloading';
  }
  if (uploadRateArg > 0 && downloadRateArg === 0) {
    return 'seeding';
  }
  if (uploadRateArg === 0 && downloadRateArg > 0) {
    return 'downloading';
  }
  return 'idle';
};

const roundKiB = (bytesPerSecondArg: number): number => {
  const value = bytesPerSecondArg / 1024;
  const precision = value < 0.1 ? 100 : 10;
  return Math.round(value * precision) / precision;
};

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

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

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

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

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