/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
/* eslint-disable max-len */
import * as herdsman from 'zigbee-herdsman-converters';
import { ExposesEntry, exposesHasFeatures, exposesHasProperty } from './src/z2mModels';

// Filter out devices that only expose a `linkquality`
// and add white label devices
const allDevices = herdsman.definitions.filter(
  (d) => typeof d.exposes === 'function' || d.exposes.find((e) => e.name !== 'linkquality') !== undefined,
);
for (const device of allDevices) {
  if (typeof device.exposes === 'function') {
    // Call function to generate array of exposes information.
    console.log(`Generating exposes array for ${device.vendor} ${device.model}`);
    device.exposes = device.exposes();
  }

  if (device.whiteLabel) {
    for (const whiteLabel of device.whiteLabel) {
      const whiteLabelDevice = {
        ...device,
        model: whiteLabel.model,
        vendor: whiteLabel.vendor,
        description: whiteLabel.description,
        whiteLabelOf: device,
      };

      delete whiteLabelDevice.whiteLabel;
      allDevices.push(whiteLabelDevice);
    }
  }
}

function generateZigbee2MqttLink(device: any) {
  const find = '[/| |:]';
  const re = new RegExp(find, 'g');
  return 'https://www.zigbee2mqtt.io/devices/' + encodeURIComponent(device.model.replace(re, '_')) + '.html';
}

function getEndpointMapping(exposes: ExposesEntry[], previousEndpoint?: string | undefined): Record<string, Set<string>> {
  const mapping: Record<string, Set<string>> = {};
  for (const e of exposes) {
    if (exposesHasFeatures(e)) {
      const propertyPrefix = e.property !== undefined ? `${e.property}.` : '';
      const endpoint = e.endpoint === undefined ? previousEndpoint : e.endpoint;
      const subMapping = getEndpointMapping(e.features, endpoint);
      for (const [endpoint, properties] of Object.entries(subMapping)) {
        if (mapping[endpoint] === undefined) {
          mapping[endpoint] = new Set();
        }
        for (const property of properties) {
          mapping[endpoint].add(propertyPrefix + property);
        }
      }

      // No need to check for properties again.
      continue;
    }

    if (exposesHasProperty(e)) {
      if (e.property === 'linkquality' || e.property === 'battery') {
        // Ignore this one
        continue;
      }
      let endpoint = e.endpoint === undefined ? previousEndpoint : e.endpoint;
      if (endpoint === undefined) {
        endpoint = 'UNDEFINED';
      }
      if (mapping[endpoint] === undefined) {
        mapping[endpoint] = new Set();
      }
      mapping[endpoint].add(e.property);
    }
  }
  return mapping;
}

function hasExposesWithNameWithoutEndpoint(exposes: ExposesEntry[]) {
  for (const e of exposes) {
    if (exposesHasProperty(e)) {
      if (e.property === 'linkquality') {
        // Ignore this one
        continue;
      }
      if (e.endpoint === undefined) {
        return true;
      }
    }
    // If this entry defines an endpoint, we assume the features inherit it.
    // TODO: update if statement
    if (exposesHasFeatures(e)) {
      if (e.endpoint !== undefined) {
        continue;
      }
      if (hasExposesWithNameWithoutEndpoint(e.features)) {
        return true;
      }
    }
  }
  return false;
}

function hasAtLeastOneDefinedEndpoint(exposes: ExposesEntry[]) {
  for (const e of exposes) {
    if (e.endpoint !== undefined) {
      return true;
    }
    if (exposesHasProperty(e)) {
      if (e.property === 'linkquality' || e.property === 'battery') {
        // Ignore this one
        continue;
      }
      if (e.endpoint !== undefined) {
        return true;
      }
    }
    if (exposesHasFeatures(e)) {
      if (hasAtLeastOneDefinedEndpoint(e.features)) {
        return true;
      }
    }
  }
  return false;
}

const devices: string[] = [];

allDevices.forEach((d) => {
  try {
    if (d.whiteLabelOf === undefined) {
      if (hasAtLeastOneDefinedEndpoint(d.exposes) && hasExposesWithNameWithoutEndpoint(d.exposes)) {
        let entry = `* [${d.vendor} ${d.model}](${generateZigbee2MqttLink(d)}):`;
        const mapping = getEndpointMapping(d.exposes);
        const keys = Object.keys(mapping).sort();
        for (const endpoint of keys) {
          entry += `\n  * Endpoint \`${endpoint}\`: \`${Array.from(mapping[endpoint]).join('`, `')}\``;
        }
        devices.push(entry);
      }
    }
  } catch (Error) {
    console.log(`ERROR: ${d.vendor} ${d.model}: ${Error}`);
  }
});

console.log('---------------');
console.log(`Devices with exposes without endpoint (${devices.length}):`);
// Sort list alphabetically
devices.sort();
devices.forEach((d) => console.log(d));
