import type {
  CallbackDataParams,
  TopLevelFormatterParams,
} from "echarts/types/dist/shared";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { relativeTime } from "../../../../../common/datetime/relative_time";
import { getDeviceArea } from "../../../../../common/entity/context/get_device_context";
import { navigate } from "../../../../../common/navigate";
import { throttle } from "../../../../../common/util/throttle";
import "../../../../../components/chart/ha-network-graph";
import type {
  NetworkData,
  NetworkLink,
  NetworkNode,
} from "../../../../../components/chart/ha-network-graph";
import "../../../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../../../components/input/ha-input-search";
import type {
  BluetoothDeviceData,
  BluetoothScannersDetails,
} from "../../../../../data/bluetooth";
import {
  subscribeBluetoothAdvertisements,
  subscribeBluetoothScannersDetails,
} from "../../../../../data/bluetooth";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import "../../../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../../../types";

const UPDATE_THROTTLE_TIME = 10000;

const CORE_SOURCE_ID = "ha";
const CORE_SOURCE_LABEL = "Home Assistant";

const RSSI_COLOR_THRESHOLDS: [number, string][] = [
  [-70, "--green-color"], // Excellent: > -70 dBm
  [-75, "--lime-color"], // Good: -70 to -75 dBm
  [-80, "--yellow-color"], // Okay: -75 to -80 dBm
  [-85, "--amber-color"], // Marginal: -80 to -85 dBm
  [-90, "--orange-color"], // Weak: -85 to -90 dBm
  [-95, "--deep-orange-color"], // Poor: -90 to -95 dBm
  [-Infinity, "--red-color"], // Very poor: < -95 dBm
];

@customElement("bluetooth-network-visualization")
export class BluetoothNetworkVisualization extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @property({ type: Boolean, reflect: true }) public narrow = false;

  @property({ attribute: "is-wide", type: Boolean }) public isWide = false;

  @property({ attribute: false }) public route!: Route;

  @state() private _data: BluetoothDeviceData[] = [];

  @state() private _scanners: BluetoothScannersDetails = {};

  @state() private _sourceDevices: Record<string, DeviceRegistryEntry> = {};

  @state() private _searchFilter = "";

  private _unsub_advertisements?: UnsubscribeFunc;

  private _unsub_scanners?: UnsubscribeFunc;

  private _throttledUpdateData = throttle((data: BluetoothDeviceData[]) => {
    this._data = data;
  }, UPDATE_THROTTLE_TIME);

  public connectedCallback(): void {
    super.connectedCallback();
    if (this.hass) {
      this._unsub_advertisements = subscribeBluetoothAdvertisements(
        this.hass.connection,
        (data) => {
          if (!this._data.length) {
            this._data = data;
          } else {
            this._throttledUpdateData(data);
          }
        }
      );
      this._unsub_scanners = subscribeBluetoothScannersDetails(
        this.hass.connection,
        (scanners) => {
          this._scanners = scanners;
        }
      );

      const devices = Object.values(this.hass.devices);
      const bluetoothDevices = devices.filter((device) =>
        device.connections.find((connection) => connection[0] === "bluetooth")
      );
      this._sourceDevices = Object.fromEntries(
        bluetoothDevices.map((device) => {
          const connection = device.connections.find(
            (c) => c[0] === "bluetooth"
          )!;
          return [connection[1], device];
        })
      );
    }
  }

  public disconnectedCallback() {
    super.disconnectedCallback();
    if (this._unsub_advertisements) {
      this._unsub_advertisements();
      this._unsub_advertisements = undefined;
    }
    this._throttledUpdateData.cancel();
    if (this._unsub_scanners) {
      this._unsub_scanners();
      this._unsub_scanners = undefined;
    }
  }

  protected render() {
    return html`
      <hass-subpage
        .hass=${this.hass}
        .narrow=${this.narrow}
        .header=${this.hass.localize(
          "ui.panel.config.bluetooth.navigation.visualization"
        )}
        back-path="/config/bluetooth/dashboard"
      >
        ${this.narrow
          ? html`<div slot="header">${this._renderInputSearch()}</div>`
          : nothing}
        <ha-network-graph
          .hass=${this.hass}
          .searchFilter=${this._searchFilter}
          .data=${this._formatNetworkData(this._data, this._scanners)}
          .searchableAttributes=${this._getSearchableAttributes}
          .tooltipFormatter=${this._tooltipFormatter}
          @chart-click=${this._handleChartClick}
        >
          ${!this.narrow ? this._renderInputSearch("search") : nothing}
        </ha-network-graph>
      </hass-subpage>
    `;
  }

  private _renderInputSearch(slot = "") {
    return html`<ha-input-search
      appearance="outlined"
      slot=${slot}
      .value=${this._searchFilter}
      @input=${this._handleSearchChange}
    ></ha-input-search>`;
  }

  private _getSearchableAttributes = (nodeId: string): string[] => {
    const attributes: string[] = [];
    const device = this._sourceDevices[nodeId];
    if (device?.manufacturer) {
      attributes.push(device.manufacturer);
    }
    if (device?.model) {
      attributes.push(device.model);
    }
    const scanner = this._scanners[nodeId];
    if (scanner?.name) {
      attributes.push(scanner.name);
    }
    return attributes;
  };

  private _handleSearchChange(ev: InputEvent): void {
    this._searchFilter = (ev.target as HaInputSearch).value ?? "";
  }

  private _getRssiColorVar = memoizeOne((rssi: number): string => {
    for (const [threshold, colorVar] of RSSI_COLOR_THRESHOLDS) {
      if (rssi > threshold) {
        return colorVar;
      }
    }
    // Fallback (should never reach here)
    return "--red-color";
  });

  private _formatNetworkData = memoizeOne(
    (
      data: BluetoothDeviceData[],
      scanners: BluetoothScannersDetails
    ): NetworkData => {
      const style = getComputedStyle(this);
      const categories = [
        {
          name: CORE_SOURCE_LABEL,
          symbol: "roundRect",
          itemStyle: {
            color: style.getPropertyValue("--primary-color"),
          },
        },
        {
          name: this.hass.localize("ui.panel.config.bluetooth.scanners"),
          symbol: "circle",
          itemStyle: {
            color: style.getPropertyValue("--cyan-color"),
          },
        },
        {
          name: this.hass.localize("ui.panel.config.bluetooth.known_devices"),
          symbol: "circle",
          itemStyle: {
            color: style.getPropertyValue("--teal-color"),
          },
        },
        {
          name: this.hass.localize("ui.panel.config.bluetooth.unknown_devices"),
          symbol: "circle",
          itemStyle: {
            color: style.getPropertyValue("--disabled-color"),
          },
        },
      ];
      const nodes: NetworkNode[] = [
        {
          id: CORE_SOURCE_ID,
          name: CORE_SOURCE_LABEL,
          category: 0,
          value: 4,
          symbol: "roundRect",
          symbolSize: 40,
          polarDistance: 0,
        },
      ];
      const links: NetworkLink[] = [];
      Object.values(scanners).forEach((scanner) => {
        const scannerDevice = this._sourceDevices[scanner.source] as
          | DeviceRegistryEntry
          | undefined;
        const area = scannerDevice
          ? getDeviceArea(scannerDevice, this.hass.areas)
          : undefined;
        nodes.push({
          id: scanner.source,
          name:
            scannerDevice?.name_by_user || scannerDevice?.name || scanner.name,
          context: area?.name,
          category: 1,
          value: 5,
          symbol: "circle",
          symbolSize: 30,
          polarDistance: 0.25,
        });
        links.push({
          source: CORE_SOURCE_ID,
          target: scanner.source,
          value: 0,
          symbol: "none",
          lineStyle: {
            width: 3,
            color: style.getPropertyValue("--primary-color"),
          },
        });
      });
      data.forEach((node) => {
        if (scanners[node.address]) {
          // proxies sometimes appear as end devices too
          links.push({
            source: node.source,
            target: node.address,
            value: node.rssi,
            symbol: "none",
            lineStyle: {
              width: this._getLineWidth(node.rssi),
              color: style.getPropertyValue(this._getRssiColorVar(node.rssi)),
            },
          });
          return;
        }
        const device = this._sourceDevices[node.address] as
          | DeviceRegistryEntry
          | undefined;
        const area = device
          ? getDeviceArea(device, this.hass.areas)
          : undefined;
        nodes.push({
          id: node.address,
          name: this._getBluetoothDeviceName(node.address),
          context: area?.name,
          value: device ? 1 : 0,
          category: device ? 2 : 3,
          symbolSize: 20,
        });
        links.push({
          source: node.source,
          target: node.address,
          value: node.rssi,
          symbol: "none",
          lineStyle: {
            width: this._getLineWidth(node.rssi),
            color: device
              ? style.getPropertyValue(this._getRssiColorVar(node.rssi))
              : style.getPropertyValue("--disabled-color"),
          },
        });
      });
      return { nodes, links, categories };
    }
  );

  private _getBluetoothDeviceName(id: string): string {
    if (id === CORE_SOURCE_ID) {
      return CORE_SOURCE_LABEL;
    }
    if (this._sourceDevices[id]) {
      return (
        this._sourceDevices[id]?.name_by_user ||
        this._sourceDevices[id]?.name ||
        id
      );
    }
    if (this._scanners[id]) {
      return this._scanners[id]?.name || id;
    }
    return this._data.find((d) => d.address === id)?.name || id;
  }

  private _getLineWidth(rssi: number): number {
    return rssi > -33 ? 3 : rssi > -66 ? 2 : 1;
  }

  private _tooltipFormatter = (params: TopLevelFormatterParams) => {
    const { dataType, data } = params as CallbackDataParams;
    if (dataType === "edge") {
      const { source, target, value } = data as any;
      const sourceName = this._getBluetoothDeviceName(source);
      const targetName = this._getBluetoothDeviceName(target);
      return html`${sourceName} →
      ${targetName}${source !== CORE_SOURCE_ID
        ? html` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b>
            ${value}`
        : nothing}`;
    }
    const { id: address } = data as any;
    const name = this._getBluetoothDeviceName(address);
    const btDevice = this._data.find((d) => d.address === address);
    const device = this._sourceDevices[address];
    const area = device ? getDeviceArea(device, this.hass.areas) : undefined;
    const areaLine = area
      ? html`<br /><b
            >${this.hass.localize("ui.panel.config.bluetooth.area")}: </b
          >${area.name}`
      : nothing;
    if (btDevice) {
      return html`<b>${name}</b><br />
        <b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b>
        ${address}<br />
        <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b>
        ${btDevice.rssi}<br />
        <b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b>
        ${btDevice.source}<br />
        <b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b>
        ${relativeTime(
          new Date(btDevice.time * 1000),
          this.hass.locale
        )}${areaLine}`;
    }
    if (device) {
      return html`<b>${name}</b><br />
        <b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b>
        ${address}${areaLine}`;
    }
    return nothing;
  };

  private _handleChartClick(e: CustomEvent): void {
    if (
      e.detail.dataType === "node" &&
      e.detail.event.target.cursor === "pointer"
    ) {
      const { id } = e.detail.data;
      const device = this._sourceDevices[id];
      if (device) {
        navigate(`/config/devices/device/${device.id}`);
      }
    }
  }

  static get styles(): CSSResultGroup {
    return [
      css`
        ha-network-graph {
          height: 100%;
        }
        [slot="header"] {
          display: flex;
          align-items: center;
        }
        ha-input-search {
          flex: 1;
        }
      `,
    ];
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "bluetooth-network-visualization": BluetoothNetworkVisualization;
  }
}
