import * as plugins from '../../plugins.js';
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
import type { IMqttConfig, IMqttDeviceInfo, IMqttEntityConfig, IMqttMessage, IMqttPayload, IMqttPublishOptions, IMqttSnapshot } from './mqtt.types.js';

const defaultDiscoveryPrefix = 'homeassistant';
const defaultPayloadOn = 'ON';
const defaultPayloadOff = 'OFF';
const defaultPayloadAvailable = 'online';
const defaultPayloadNotAvailable = 'offline';

const abbreviations: Record<string, string> = {
  avty: 'availability',
  avty_t: 'availability_topic',
  cmd_t: 'command_topic',
  cmps: 'components',
  def_ent_id: 'default_entity_id',
  dev: 'device',
  dev_cla: 'device_class',
  json_attr_t: 'json_attributes_topic',
  name: 'name',
  o: 'origin',
  p: 'platform',
  pl_avail: 'payload_available',
  pl_not_avail: 'payload_not_available',
  pl_off: 'payload_off',
  pl_on: 'payload_on',
  pl_prs: 'payload_press',
  pl_cls: 'payload_close',
  pl_open: 'payload_open',
  pl_stop: 'payload_stop',
  qos: 'qos',
  ret: 'retain',
  stat_t: 'state_topic',
  stat_val_tpl: 'state_value_template',
  t: 'topic',
  uniq_id: 'unique_id',
  unit_of_meas: 'unit_of_measurement',
  val_tpl: 'value_template',
};

const deviceAbbreviations: Record<string, string> = {
  cns: 'connections',
  cu: 'configuration_url',
  hw: 'hw_version',
  ids: 'identifiers',
  mf: 'manufacturer',
  mdl: 'model',
  mdl_id: 'model_id',
  name: 'name',
  sa: 'suggested_area',
  sn: 'serial_number',
  sw: 'sw_version',
};

const originAbbreviations: Record<string, string> = {
  name: 'name',
  sw: 'sw_version',
  url: 'support_url',
};

export class MqttMapper {
  public static toSnapshot(configArg: IMqttConfig, connectedArg = false, receivedMessagesArg: IMqttMessage[] = []): IMqttSnapshot {
    const messages = [...(configArg.retainedMessages || []), ...(configArg.discoveryMessages || []), ...receivedMessagesArg];
    const entities = [
      ...(configArg.entities || []),
      ...this.entitiesFromDiscoveryMessages(configArg.discoveryMessages || [], configArg.discoveryPrefix || defaultDiscoveryPrefix),
    ];
    return {
      brokerInfo: {
        host: configArg.host,
        port: configArg.port || (configArg.protocol === 'mqtts' ? 8883 : 1883),
        protocol: configArg.protocol || 'mqtt',
        clientId: configArg.clientId,
        connected: connectedArg,
        discoveryPrefix: configArg.discoveryPrefix || defaultDiscoveryPrefix,
      },
      entities: this.applyStates(entities, messages),
      messages,
    };
  }

  public static toDevices(snapshotArg: IMqttSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = new Date().toISOString();
    const devices = new Map<string, plugins.shxInterfaces.data.IDeviceDefinition>();
    const brokerId = this.brokerDeviceId(snapshotArg);
    devices.set(brokerId, {
      id: brokerId,
      integrationDomain: 'mqtt',
      name: snapshotArg.brokerInfo.host ? `MQTT broker ${snapshotArg.brokerInfo.host}` : 'MQTT broker',
      protocol: 'mqtt',
      manufacturer: 'MQTT',
      model: 'Broker',
      online: snapshotArg.brokerInfo.connected || Boolean(snapshotArg.messages.length || snapshotArg.entities.length),
      features: [
        { id: 'connection', capability: 'sensor', name: 'Connection', readable: true, writable: false },
        { id: 'discovery_prefix', capability: 'sensor', name: 'Discovery prefix', readable: true, writable: false },
      ],
      state: [
        { featureId: 'connection', value: snapshotArg.brokerInfo.connected ? 'connected' : 'configured', updatedAt },
        { featureId: 'discovery_prefix', value: snapshotArg.brokerInfo.discoveryPrefix, updatedAt },
      ],
      metadata: {
        host: snapshotArg.brokerInfo.host,
        port: snapshotArg.brokerInfo.port,
        protocol: snapshotArg.brokerInfo.protocol,
      },
    });

    for (const entity of snapshotArg.entities) {
      const deviceId = this.deviceIdForEntity(entity, snapshotArg);
      const existing = devices.get(deviceId);
      const feature = this.featureForEntity(entity);
      const state = { featureId: feature.id, value: this.deviceStateValue(entity.state ?? null), updatedAt };
      if (existing) {
        existing.features.push(feature);
        existing.state.push(state);
        continue;
      }
      devices.set(deviceId, {
        id: deviceId,
        integrationDomain: 'mqtt',
        name: this.deviceName(entity.device, entity.name || entity.objectId || 'MQTT device'),
        protocol: 'mqtt',
        manufacturer: entity.device?.manufacturer,
        model: entity.device?.model,
        online: this.available(entity, snapshotArg.messages),
        features: [feature],
        state: [state],
        metadata: {
          identifiers: entity.device?.identifiers,
          connections: entity.device?.connections,
          serialNumber: entity.device?.serialNumber,
          swVersion: entity.device?.swVersion,
          hwVersion: entity.device?.hwVersion,
          origin: entity.origin,
        },
      });
    }

    return [...devices.values()];
  }

  public static toEntities(snapshotArg: IMqttSnapshot): IIntegrationEntity[] {
    return snapshotArg.entities.map((entityArg) => ({
      id: entityArg.defaultEntityId || `${this.toEntityPlatform(entityArg.platform)}.${this.slug(entityArg.name || entityArg.objectId || entityArg.uniqueId || 'mqtt')}`,
      uniqueId: entityArg.uniqueId || `mqtt_${this.slug(entityArg.objectId || entityArg.name || entityArg.stateTopic || entityArg.commandTopic || 'entity')}`,
      integrationDomain: 'mqtt',
      deviceId: this.deviceIdForEntity(entityArg, snapshotArg),
      platform: this.toEntityPlatform(entityArg.platform),
      name: entityArg.name || entityArg.objectId || 'MQTT entity',
      state: this.entityState(entityArg),
      attributes: {
        ...entityArg.attributes,
        mqttPlatform: entityArg.platform,
        stateTopic: entityArg.stateTopic,
        commandTopic: entityArg.commandTopic,
        availabilityTopic: entityArg.availabilityTopic,
        deviceClass: entityArg.deviceClass,
        unit: entityArg.unitOfMeasurement,
        discoveryTopic: entityArg.discoveryTopic,
      },
      available: this.available(entityArg, snapshotArg.messages),
    }));
  }

  public static commandForService(snapshotArg: IMqttSnapshot, requestArg: IServiceCallRequest): IMqttPublishOptions | undefined {
    const entity = this.findTargetEntity(snapshotArg, requestArg);
    if (!entity?.commandTopic) {
      return undefined;
    }
    const raw = entity.rawConfig || {};
    let payload: IMqttPayload | undefined;
    if (requestArg.service === 'turn_on') {
      payload = entity.payloadOn || defaultPayloadOn;
    } else if (requestArg.service === 'turn_off') {
      payload = entity.payloadOff || defaultPayloadOff;
    } else if (requestArg.service === 'press') {
      payload = typeof raw.payload_press === 'string' ? raw.payload_press : 'PRESS';
    } else if (requestArg.service === 'open_cover') {
      payload = typeof raw.payload_open === 'string' ? raw.payload_open : 'OPEN';
    } else if (requestArg.service === 'close_cover') {
      payload = typeof raw.payload_close === 'string' ? raw.payload_close : 'CLOSE';
    } else if (requestArg.service === 'stop_cover') {
      payload = typeof raw.payload_stop === 'string' ? raw.payload_stop : 'STOP';
    } else if (requestArg.service === 'set_value') {
      payload = requestArg.data?.value as IMqttPayload;
    } else if (requestArg.service === 'select_option') {
      payload = requestArg.data?.option as IMqttPayload;
    } else if (requestArg.service === 'set_temperature') {
      payload = requestArg.data?.temperature as IMqttPayload;
    }
    if (payload === undefined) {
      return undefined;
    }
    return {
      topic: entity.commandTopic,
      payload,
      qos: entity.qos || 0,
      retain: entity.retain || false,
    };
  }

  public static entitiesFromDiscoveryMessages(messagesArg: IMqttMessage[], prefixArg = defaultDiscoveryPrefix): IMqttEntityConfig[] {
    const entities: IMqttEntityConfig[] = [];
    for (const message of messagesArg) {
      const parsedTopic = this.parseDiscoveryTopic(message.topic, prefixArg);
      if (!parsedTopic || message.payload === '' || message.payload === null) {
        continue;
      }
      const payload = this.discoveryPayload(message.payload);
      if (!payload) {
        continue;
      }
      this.replaceAllAbbreviations(payload);
      this.replaceTopicBase(payload);
      if (parsedTopic.component === 'device') {
        const components = this.asRecord(payload.components);
        for (const [componentId, componentConfig] of Object.entries(components)) {
          const componentPayload = { ...payload, ...this.asRecord(componentConfig) };
          delete componentPayload.components;
          entities.push(this.entityFromPayload(componentPayload, String(componentPayload.platform || 'sensor'), parsedTopic.objectId, parsedTopic.nodeId, message.topic, componentId));
        }
        continue;
      }
      entities.push(this.entityFromPayload(payload, parsedTopic.component, parsedTopic.objectId, parsedTopic.nodeId, message.topic));
    }
    return entities;
  }

  private static entityFromPayload(payloadArg: Record<string, unknown>, platformArg: string, objectIdArg: string, nodeIdArg: string | undefined, topicArg: string, componentIdArg?: string): IMqttEntityConfig {
    const device = this.deviceFromPayload(this.asRecord(payloadArg.device));
    const origin = this.originFromPayload(this.asRecord(payloadArg.origin));
    return {
      platform: platformArg,
      objectId: componentIdArg || objectIdArg,
      nodeId: nodeIdArg,
      uniqueId: this.stringValue(payloadArg.unique_id),
      defaultEntityId: this.stringValue(payloadArg.default_entity_id),
      name: payloadArg.name === null ? null : this.stringValue(payloadArg.name),
      device,
      origin,
      stateTopic: this.stringValue(payloadArg.state_topic),
      commandTopic: this.stringValue(payloadArg.command_topic),
      availabilityTopic: this.stringValue(payloadArg.availability_topic),
      jsonAttributesTopic: this.stringValue(payloadArg.json_attributes_topic),
      valueTemplate: this.stringValue(payloadArg.value_template),
      stateValueTemplate: this.stringValue(payloadArg.state_value_template),
      deviceClass: this.stringValue(payloadArg.device_class),
      unitOfMeasurement: this.stringValue(payloadArg.unit_of_measurement),
      payloadOn: this.stringValue(payloadArg.payload_on),
      payloadOff: this.stringValue(payloadArg.payload_off),
      payloadAvailable: this.stringValue(payloadArg.payload_available),
      payloadNotAvailable: this.stringValue(payloadArg.payload_not_available),
      qos: this.qosValue(payloadArg.qos),
      retain: typeof payloadArg.retain === 'boolean' ? payloadArg.retain : undefined,
      discoveryTopic: topicArg,
      rawConfig: payloadArg,
    };
  }

  private static applyStates(entitiesArg: IMqttEntityConfig[], messagesArg: IMqttMessage[]): IMqttEntityConfig[] {
    return entitiesArg.map((entityArg) => {
      const stateMessage = entityArg.stateTopic ? this.latestMessage(messagesArg, entityArg.stateTopic) : undefined;
      const attributesMessage = entityArg.jsonAttributesTopic ? this.latestMessage(messagesArg, entityArg.jsonAttributesTopic) : undefined;
      return {
        ...entityArg,
        state: stateMessage ? this.renderValue(stateMessage.payload, entityArg.valueTemplate || entityArg.stateValueTemplate) : entityArg.state,
        attributes: attributesMessage ? this.attributesPayload(attributesMessage.payload) : entityArg.attributes,
      };
    });
  }

  private static parseDiscoveryTopic(topicArg: string, prefixArg: string): { component: string; nodeId?: string; objectId: string } | undefined {
    const parts = topicArg.split('/').filter(Boolean);
    if (parts[0] !== prefixArg || parts[parts.length - 1] !== 'config') {
      return undefined;
    }
    if (parts.length === 4) {
      return { component: parts[1], objectId: parts[2] };
    }
    if (parts.length === 5) {
      return { component: parts[1], nodeId: parts[2], objectId: parts[3] };
    }
    return undefined;
  }

  private static discoveryPayload(payloadArg: IMqttPayload): Record<string, unknown> | undefined {
    if (typeof payloadArg === 'string') {
      try {
        const parsed = JSON.parse(payloadArg) as unknown;
        return this.asRecord(parsed);
      } catch {
        return undefined;
      }
    }
    return this.asRecord(payloadArg);
  }

  private static replaceAllAbbreviations(payloadArg: Record<string, unknown>): void {
    this.replaceAbbreviations(payloadArg, abbreviations);
    if (this.isRecord(payloadArg.device)) {
      this.replaceAbbreviations(payloadArg.device, deviceAbbreviations);
    }
    if (this.isRecord(payloadArg.origin)) {
      this.replaceAbbreviations(payloadArg.origin, originAbbreviations);
    }
    if (this.isRecord(payloadArg.components)) {
      for (const component of Object.values(payloadArg.components)) {
        if (this.isRecord(component)) {
          this.replaceAbbreviations(component, abbreviations);
        }
      }
    }
  }

  private static replaceAbbreviations(payloadArg: Record<string, unknown>, mapArg: Record<string, string>): void {
    for (const [shortKey, longKey] of Object.entries(mapArg)) {
      if (shortKey in payloadArg && !(longKey in payloadArg)) {
        payloadArg[longKey] = payloadArg[shortKey];
      }
    }
  }

  private static replaceTopicBase(payloadArg: Record<string, unknown>): void {
    const topicBase = this.stringValue(payloadArg['~']);
    if (!topicBase) {
      return;
    }
    for (const [key, value] of Object.entries(payloadArg)) {
      if (!key.endsWith('topic') || typeof value !== 'string') {
        continue;
      }
      if (value.startsWith('~')) {
        payloadArg[key] = `${topicBase}${value.slice(1)}`;
      } else if (value.endsWith('~')) {
        payloadArg[key] = `${value.slice(0, -1)}${topicBase}`;
      }
    }
  }

  private static deviceFromPayload(payloadArg: Record<string, unknown>): IMqttDeviceInfo | undefined {
    if (!Object.keys(payloadArg).length) {
      return undefined;
    }
    return {
      identifiers: this.stringOrStringArray(payloadArg.identifiers),
      connections: Array.isArray(payloadArg.connections) ? payloadArg.connections as string[][] : undefined,
      name: this.stringValue(payloadArg.name),
      manufacturer: this.stringValue(payloadArg.manufacturer),
      model: this.stringValue(payloadArg.model),
      modelId: this.stringValue(payloadArg.model_id),
      swVersion: this.stringValue(payloadArg.sw_version),
      hwVersion: this.stringValue(payloadArg.hw_version),
      serialNumber: this.stringValue(payloadArg.serial_number),
      configurationUrl: this.stringValue(payloadArg.configuration_url),
      suggestedArea: this.stringValue(payloadArg.suggested_area),
    };
  }

  private static originFromPayload(payloadArg: Record<string, unknown>) {
    if (!Object.keys(payloadArg).length) {
      return undefined;
    }
    return {
      name: this.stringValue(payloadArg.name),
      swVersion: this.stringValue(payloadArg.sw_version),
      supportUrl: this.stringValue(payloadArg.support_url),
    };
  }

  private static latestMessage(messagesArg: IMqttMessage[], topicArg: string): IMqttMessage | undefined {
    const matches = messagesArg.filter((messageArg) => messageArg.topic === topicArg);
    return matches.sort((leftArg, rightArg) => (rightArg.timestamp || 0) - (leftArg.timestamp || 0))[0] || matches[matches.length - 1];
  }

  private static renderValue(payloadArg: IMqttPayload, templateArg?: string): IMqttPayload {
    if (!templateArg) {
      return payloadArg;
    }
    if (templateArg.includes('value_json')) {
      const parsed = typeof payloadArg === 'string' ? this.safeJson(payloadArg) : payloadArg;
      const pathMatch = /value_json(?:\.([a-zA-Z0-9_]+)|\[['"]([^'"]+)['"]\])/.exec(templateArg);
      const key = pathMatch?.[1] || pathMatch?.[2];
      if (key && this.isRecord(parsed)) {
        return parsed[key] as IMqttPayload;
      }
    }
    if (templateArg.includes('value')) {
      return payloadArg;
    }
    return payloadArg;
  }

  private static attributesPayload(payloadArg: IMqttPayload): Record<string, unknown> {
    if (this.isRecord(payloadArg)) {
      return payloadArg;
    }
    if (typeof payloadArg === 'string') {
      return this.asRecord(this.safeJson(payloadArg));
    }
    return {};
  }

  private static entityState(entityArg: IMqttEntityConfig): unknown {
    const state = entityArg.state;
    if (entityArg.platform === 'switch' || entityArg.platform === 'light' || entityArg.platform === 'fan') {
      if (state === (entityArg.payloadOn || defaultPayloadOn)) {
        return 'on';
      }
      if (state === (entityArg.payloadOff || defaultPayloadOff)) {
        return 'off';
      }
    }
    return state ?? 'unknown';
  }

  private static available(entityArg: IMqttEntityConfig, messagesArg: IMqttMessage[]): boolean {
    if (!entityArg.availabilityTopic) {
      return true;
    }
    const availabilityMessage = this.latestMessage(messagesArg, entityArg.availabilityTopic);
    if (!availabilityMessage) {
      return false;
    }
    return availabilityMessage.payload !== (entityArg.payloadNotAvailable || defaultPayloadNotAvailable) && availabilityMessage.payload === (entityArg.payloadAvailable || defaultPayloadAvailable);
  }

  private static featureForEntity(entityArg: IMqttEntityConfig): plugins.shxInterfaces.data.IDeviceFeature {
    const platform = this.toEntityPlatform(entityArg.platform);
    return {
      id: this.slug(entityArg.uniqueId || entityArg.objectId || entityArg.name || platform),
      capability: this.capabilityForPlatform(platform),
      name: entityArg.name || entityArg.objectId || 'MQTT entity',
      readable: Boolean(entityArg.stateTopic),
      writable: Boolean(entityArg.commandTopic),
      unit: entityArg.unitOfMeasurement,
    };
  }

  private static capabilityForPlatform(platformArg: TEntityPlatform): plugins.shxInterfaces.data.TDeviceCapability {
    if (platformArg === 'light') {
      return 'light';
    }
    if (platformArg === 'switch' || platformArg === 'button' || platformArg === 'cover' || platformArg === 'select' || platformArg === 'number' || platformArg === 'text') {
      return 'switch';
    }
    if (platformArg === 'climate') {
      return 'climate';
    }
    if (platformArg === 'fan') {
      return 'fan';
    }
    if (platformArg === 'media_player') {
      return 'media';
    }
    return 'sensor';
  }

  private static toEntityPlatform(platformArg: string): TEntityPlatform {
    const allowed: TEntityPlatform[] = ['light', 'switch', 'sensor', 'binary_sensor', 'button', 'climate', 'cover', 'fan', 'media_player', 'number', 'select', 'text', 'update'];
    return allowed.includes(platformArg as TEntityPlatform) ? platformArg as TEntityPlatform : 'sensor';
  }

  private static findTargetEntity(snapshotArg: IMqttSnapshot, requestArg: IServiceCallRequest): IMqttEntityConfig | undefined {
    if (requestArg.target.entityId) {
      const entities = this.toEntities(snapshotArg);
      const entity = entities.find((entityArg) => entityArg.id === requestArg.target.entityId || entityArg.uniqueId === requestArg.target.entityId);
      return entity ? snapshotArg.entities.find((entityArg) => (entityArg.uniqueId || `mqtt_${this.slug(entityArg.objectId || entityArg.name || entityArg.stateTopic || entityArg.commandTopic || 'entity')}`) === entity.uniqueId) : undefined;
    }
    if (requestArg.target.deviceId) {
      return snapshotArg.entities.find((entityArg) => this.deviceIdForEntity(entityArg, snapshotArg) === requestArg.target.deviceId && entityArg.commandTopic);
    }
    return snapshotArg.entities.find((entityArg) => entityArg.commandTopic);
  }

  private static deviceIdForEntity(entityArg: IMqttEntityConfig, snapshotArg: IMqttSnapshot): string {
    const identifiers = entityArg.device?.identifiers;
    const identifier = Array.isArray(identifiers) ? identifiers[0] : identifiers;
    const connection = entityArg.device?.connections?.[0]?.join('_');
    return `mqtt.device.${this.slug(identifier || connection || entityArg.uniqueId || entityArg.objectId || snapshotArg.brokerInfo.host || 'broker')}`;
  }

  private static brokerDeviceId(snapshotArg: IMqttSnapshot): string {
    return `mqtt.broker.${this.slug(snapshotArg.brokerInfo.host || 'configured')}`;
  }

  private static deviceName(deviceArg: IMqttDeviceInfo | undefined, fallbackArg: string): string {
    return deviceArg?.name || fallbackArg;
  }

  private static deviceStateValue(valueArg: IMqttPayload): plugins.shxInterfaces.data.TDeviceStateValue {
    return Array.isArray(valueArg) ? JSON.stringify(valueArg) : valueArg as plugins.shxInterfaces.data.TDeviceStateValue;
  }

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

  private static stringOrStringArray(valueArg: unknown): string | string[] | undefined {
    if (typeof valueArg === 'string') {
      return valueArg;
    }
    if (Array.isArray(valueArg)) {
      return valueArg.filter((itemArg): itemArg is string => typeof itemArg === 'string');
    }
    return undefined;
  }

  private static qosValue(valueArg: unknown): 0 | 1 | 2 | undefined {
    return valueArg === 0 || valueArg === 1 || valueArg === 2 ? valueArg : undefined;
  }

  private static safeJson(valueArg: string): unknown {
    try {
      return JSON.parse(valueArg) as unknown;
    } catch {
      return valueArg;
    }
  }

  private static asRecord(valueArg: unknown): Record<string, unknown> {
    return this.isRecord(valueArg) ? valueArg : {};
  }

  private static isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
    return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
  }

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