import { consume } from "@lit/context";
import {
  mdiChevronLeft,
  mdiChevronRight,
  mdiClose,
  mdiDevices,
  mdiHome,
  mdiLabel,
  mdiMinusBox,
  mdiTextureBox,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import {
  css,
  html,
  LitElement,
  nothing,
  type PropertyValues,
  type TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import {
  computeDeviceName,
  computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area/area_registry";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import type {
  DeviceCompositeSplits,
  DeviceRegistryEntry,
} from "../../data/device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import {
  areaMeetsFilter,
  deviceMeetsFilter,
  entityRegMeetsFilter,
  extractFromTarget,
  type ExtractFromTargetResult,
  type ExtractFromTargetResultReferenced,
  type TargetType,
} from "../../data/target";
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
import { buttonLinkStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
import { floorDefaultIconPath } from "../ha-floor-icon";
import "../ha-button";
import "../ha-icon-button";
import "../ha-state-icon";
import "../ha-svg-icon";
import "../item/ha-list-item-base";
import "../item/ha-list-item-button";
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";

@customElement("ha-target-picker-item-row")
export class HaTargetPickerItemRow extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @property({ reflect: true }) public type!: TargetType;

  @property({ attribute: "item-id" }) public itemId!: string;

  @property({ type: Boolean }) public expand = false;

  @property({ type: Boolean, attribute: "sub-entry", reflect: true })
  public subEntry = false;

  @property({ attribute: false })
  public subLevel = 0;

  @property({ type: Boolean, attribute: "hide-context" })
  public hideContext = false;

  @property({ attribute: false })
  public parentEntries?: ExtractFromTargetResultReferenced;

  @property({ attribute: false })
  public deviceFilter?: HaDevicePickerDeviceFilterFunc;

  @property({ attribute: false })
  public entityFilter?: HaEntityPickerEntityFilterFunc;

  /**
   * Show only targets with entities from specific domains.
   * @type {Array}
   * @attr include-domains
   */
  @property({ type: Array, attribute: "include-domains" })
  public includeDomains?: string[];

  /**
   * Show only targets with entities of these device classes.
   * @type {Array}
   * @attr include-device-classes
   */
  @property({ type: Array, attribute: "include-device-classes" })
  public includeDeviceClasses?: string[];

  @property({ type: Boolean, attribute: "primary-entities-only" })
  public primaryEntitiesOnly?: boolean;

  @property({ attribute: false })
  public compositeSplits?: DeviceCompositeSplits;

  @state() private _iconImg?: string;

  @state() private _domainName?: string;

  @state() private _entries?: ExtractFromTargetResult;

  @state()
  @consume({ context: labelsContext, subscribe: true })
  _labelRegistry!: LabelRegistryEntry[];

  protected willUpdate(changedProps: PropertyValues<this>) {
    if (!this.subEntry && changedProps.has("itemId")) {
      this._updateItemData();
    }
  }

  protected render() {
    const { name, context, iconPath, fallbackIconPath, stateObject, notFound } =
      this._itemData(this.type, this.itemId);

    const replacement =
      this.type === "device" && notFound
        ? this._getReplacement(this.itemId)
        : undefined;
    // Only surface the "replaced" state when there is at least one available
    // replacement device to migrate to. If every replacement device was
    // deleted (or filtered out), fall back to the plain "not found" state.
    const canMigrate = !!replacement?.candidates.length;

    const showEntities = this.type !== "entity" && !notFound;

    const entries = this.parentEntries || this._entries;

    // Don't show sub entries that have no entities
    if (
      this.subEntry &&
      this.type !== "entity" &&
      (!entries || entries.referenced_entities.length === 0)
    ) {
      return nothing;
    }

    const replaceable = !this.subEntry && !this.expand;

    const content = html`
      <div class="icon" slot="start">
        ${
          iconPath
            ? html`<ha-icon .icon=${iconPath}></ha-icon>`
            : this._iconImg
              ? html`<img
                  alt=${this._domainName || ""}
                  crossorigin="anonymous"
                  referrerpolicy="no-referrer"
                  src=${this._iconImg}
                />`
              : fallbackIconPath
                ? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
                : this.type === "entity"
                  ? html`
                      <ha-state-icon
                        .stateObj=${
                          stateObject ||
                          ({
                            entity_id: this.itemId,
                            attributes: {},
                          } as HassEntity)
                        }
                      >
                      </ha-state-icon>
                    `
                  : nothing
        }
      </div>

      <div slot="headline">${(canMigrate && replacement?.name) || name}</div>
      ${
        notFound || (context && !this.hideContext)
          ? html`<span slot="supporting-text"
              >${
                notFound
                  ? canMigrate
                    ? this.hass.localize(
                        "ui.components.target-picker.device_replaced",
                        { count: replacement!.candidates.length }
                      )
                    : this.hass.localize(
                        `ui.components.target-picker.${this.type}_not_found`
                      )
                  : context
              }</span
            >`
          : nothing
      }
      ${
        stateObject && this.subEntry
          ? html`<span slot="supporting-text" class="state"
              >${this.hass.formatEntityState(stateObject)}</span
            >`
          : nothing
      }
      ${
        !this.subEntry && entries && showEntities
          ? html`
              <div slot="end" class="summary">
                ${
                  showEntities &&
                  !this.expand &&
                  entries?.referenced_entities.length
                    ? html`<button
                        class="main link"
                        @click=${this._openDetails}
                      >
                        ${this.hass.localize(
                          "ui.components.target-picker.entities_count",
                          {
                            count: entries?.referenced_entities.length,
                          }
                        )}
                      </button>`
                    : showEntities
                      ? html`<span class="main">
                          ${this.hass.localize(
                            "ui.components.target-picker.entities_count",
                            {
                              count: entries?.referenced_entities.length,
                            }
                          )}
                        </span>`
                      : nothing
                }
              </div>
            `
          : nothing
      }
      ${
        canMigrate
          ? html`
              <ha-button
                class="migrate"
                slot="end"
                appearance="plain"
                variant="warning"
                size="s"
                @click=${this._migrate}
              >
                ${this.hass.localize(
                  "ui.components.target-picker.replace_device"
                )}
              </ha-button>
            `
          : nothing
      }
      ${
        !this.expand && !this.subEntry
          ? html`
              <ha-icon-button
                .path=${mdiClose}
                slot="end"
                @click=${this._removeItem}
              ></ha-icon-button>
            `
          : this.subEntry && this.type === "entity"
            ? html`
                <ha-svg-icon
                  .path=${
                    computeRTL(
                      this.hass.language,
                      this.hass.translationMetadata.translations
                    )
                      ? mdiChevronLeft
                      : mdiChevronRight
                  }
                  slot="end"
                ></ha-svg-icon>
              `
            : nothing
      }
    `;

    let item: TemplateResult;

    if (replaceable || (this.subEntry && this.type === "entity")) {
      item = html`
        <ha-list-item-button
          class=${classMap({
            error: notFound,
            replaceable,
          })}
          @click=${
            replaceable
              ? this._replaceItem
              : this.subEntry && this.type === "entity"
                ? this._openMoreInfo
                : undefined
          }
        >
          ${content}
        </ha-list-item-button>
      `;
    } else {
      item = html`
        <ha-list-item-base
          class=${classMap({
            error: notFound,
          })}
        >
          ${content}
        </ha-list-item-base>
      `;
    }

    return html`
      ${item}
      ${
        this.expand && entries && entries.referenced_entities
          ? this._renderEntries()
          : nothing
      }
    `;
  }

  private _renderEntries() {
    const entries = this.parentEntries || this._entries;

    if (!entries || entries.referenced_entities.length === 0) {
      return this._renderEmptyEntries();
    }

    let nextType: TargetType =
      this.type === "floor"
        ? "area"
        : this.type === "area"
          ? "device"
          : "entity";

    if (this.type === "label") {
      if (entries?.referenced_areas.length) {
        nextType = "area";
      } else if (entries?.referenced_devices.length) {
        nextType = "device";
      }
    }

    const rows1 =
      (nextType === "area"
        ? entries?.referenced_areas
        : nextType === "device" && this.type !== "label"
          ? entries?.referenced_devices
          : this.type !== "label"
            ? entries?.referenced_entities
            : []) || [];

    const devicesInAreas = [] as string[];

    const rows1Entries =
      nextType === "entity"
        ? undefined
        : rows1.map((rowItem) => {
            const nextEntries = {
              referenced_areas: [] as string[],
              referenced_devices: [] as string[],
              referenced_entities: [] as string[],
            };

            if (nextType === "area") {
              nextEntries.referenced_devices =
                entries?.referenced_devices.filter(
                  (device_id) =>
                    this.hass.devices?.[device_id]?.area_id === rowItem &&
                    entries?.referenced_entities.some(
                      (entity_id) =>
                        this.hass.entities?.[entity_id]?.device_id === device_id
                    )
                ) || ([] as string[]);

              devicesInAreas.push(...nextEntries.referenced_devices);

              nextEntries.referenced_entities =
                entries?.referenced_entities.filter((entity_id) => {
                  const entity = this.hass.entities[entity_id];
                  return (
                    entity.area_id === rowItem ||
                    !entity.device_id ||
                    nextEntries.referenced_devices.includes(entity.device_id)
                  );
                }) || ([] as string[]);

              return nextEntries;
            }

            nextEntries.referenced_entities =
              entries?.referenced_entities.filter(
                (entity_id) =>
                  this.hass.entities?.[entity_id]?.device_id === rowItem
              ) || ([] as string[]);

            return nextEntries;
          });

    const entityRows =
      this.type === "label" && entries
        ? entries.referenced_entities.filter((entity_id) => {
            const entity = this.hass.entities[entity_id];
            return (
              entity.labels.includes(this.itemId) &&
              !entries.referenced_devices.includes(entity.device_id || "")
            );
          })
        : nextType === "device" && entries
          ? entries.referenced_entities.filter(
              (entity_id) =>
                this.hass.entities[entity_id].area_id === this.itemId
            )
          : [];

    const deviceRows =
      this.type === "label" && entries
        ? entries.referenced_devices.filter(
            (device_id) =>
              !devicesInAreas.includes(device_id) &&
              this.hass.devices[device_id].labels.includes(this.itemId)
          )
        : [];

    const deviceRowsEntries =
      deviceRows.length === 0
        ? undefined
        : deviceRows.map((device_id) => ({
            referenced_areas: [] as string[],
            referenced_devices: [] as string[],
            referenced_entities:
              entries?.referenced_entities.filter(
                (entity_id) =>
                  this.hass.entities?.[entity_id]?.device_id === device_id
              ) || ([] as string[]),
          }));

    const nextSubLevel = this.subLevel + 1;

    return html`
      ${rows1.map(
        (itemId, index) => html`
          <ha-target-picker-item-row
            sub-entry
            .subLevel=${nextSubLevel}
            style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
            .hass=${this.hass}
            .type=${nextType}
            .itemId=${itemId}
            .parentEntries=${rows1Entries?.[index]}
            .hideContext=${this.hideContext || this.type !== "label"}
            expand
          ></ha-target-picker-item-row>
        `
      )}
      ${deviceRows.map(
        (itemId, index) => html`
          <ha-target-picker-item-row
            sub-entry
            .subLevel=${nextSubLevel}
            style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
            .hass=${this.hass}
            type="device"
            .itemId=${itemId}
            .parentEntries=${deviceRowsEntries?.[index]}
            .hideContext=${this.hideContext || this.type !== "label"}
            expand
          ></ha-target-picker-item-row>
        `
      )}
      ${entityRows.map(
        (itemId) => html`
          <ha-target-picker-item-row
            sub-entry
            .subLevel=${nextSubLevel}
            style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
            .hass=${this.hass}
            type="entity"
            .itemId=${itemId}
            .hideContext=${this.hideContext || this.type !== "label"}
          ></ha-target-picker-item-row>
        `
      )}
    `;
  }

  private _renderEmptyEntries() {
    return html`<ha-list-item-base>
      <ha-svg-icon .path=${mdiMinusBox} slot="start" class="icon"></ha-svg-icon>
      <span slot="headline"
        >${this.hass.localize("ui.components.target-picker.no_targets")}</span
      >
    </ha-list-item-base>`;
  }

  private async _updateItemData() {
    if (this.type === "entity") {
      this._entries = undefined;
      return;
    }
    try {
      const entries = await extractFromTarget(
        this.hass.callWS,
        {
          [`${this.type}_id`]: [this.itemId],
        },
        false,
        this.primaryEntitiesOnly
      );

      const hiddenAreaIds: string[] = [];
      if (this.type === "floor" || this.type === "label") {
        entries.referenced_areas = entries.referenced_areas.filter(
          (area_id) => {
            const area = this.hass.areas[area_id];
            if (
              (this.type === "floor" || area.labels.includes(this.itemId)) &&
              areaMeetsFilter(
                area,
                this.hass.devices,
                this.hass.entities,
                this.deviceFilter,
                this.includeDomains,
                this.includeDeviceClasses,
                this.hass.states,
                this.entityFilter,
                !this.primaryEntitiesOnly
              )
            ) {
              return true;
            }

            hiddenAreaIds.push(area_id);
            return false;
          }
        );
      }

      const hiddenDeviceIds: string[] = [];
      if (
        this.type === "floor" ||
        this.type === "area" ||
        this.type === "label"
      ) {
        entries.referenced_devices = entries.referenced_devices.filter(
          (device_id) => {
            const device = this.hass.devices[device_id];
            if (
              !hiddenAreaIds.includes(device.area_id || "") &&
              deviceMeetsFilter(
                device,
                this.hass.entities,
                this.deviceFilter,
                this.includeDomains,
                this.includeDeviceClasses,
                this.hass.states,
                this.entityFilter,
                !this.primaryEntitiesOnly
              )
            ) {
              return true;
            }

            hiddenDeviceIds.push(device_id);
            return false;
          }
        );
      }

      entries.referenced_entities = entries.referenced_entities.filter(
        (entity_id) => {
          const entity = this.hass.entities[entity_id];
          if (hiddenDeviceIds.includes(entity.device_id || "")) {
            return false;
          }
          if (
            (this.type === "area" && entity.area_id === this.itemId) ||
            (this.type === "floor" &&
              entity.area_id &&
              entries.referenced_areas.includes(entity.area_id)) ||
            (this.type === "label" && entity.labels.includes(this.itemId)) ||
            entries.referenced_devices.includes(entity.device_id || "")
          ) {
            return entityRegMeetsFilter(
              entity,
              this.type === "label" || !this.primaryEntitiesOnly,
              this.includeDomains,
              this.includeDeviceClasses,
              this.hass.states,
              this.entityFilter
            );
          }
          return false;
        }
      );

      this._entries = entries;
    } catch (e) {
      // eslint-disable-next-line no-console
      console.error("Failed to extract target", e);
    }
  }

  private _itemData = memoizeOne((type: TargetType, item: string) => {
    if (type === "floor") {
      const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item];
      return {
        name: floor?.name || item,
        iconPath: floor?.icon,
        fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
        notFound: !floor,
      };
    }
    if (type === "area") {
      const area: AreaRegistryEntry | undefined = this.hass.areas?.[item];
      return {
        name: area?.name || item,
        context: area?.floor_id && this.hass.floors?.[area.floor_id]?.name,
        iconPath: area?.icon,
        fallbackIconPath: mdiTextureBox,
        notFound: !area,
      };
    }
    if (type === "device") {
      const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item];

      if (device?.primary_config_entry) {
        this._getDeviceDomain(device.primary_config_entry);
      }

      return {
        name: device
          ? computeDeviceNameDisplay(
              device,
              this.hass.localize,
              this.hass.states
            )
          : item,
        context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
        fallbackIconPath: mdiDevices,
        notFound: !device,
      };
    }
    if (type === "entity") {
      this._setDomainName(computeDomain(item));

      const stateObject: HassEntity | undefined = this.hass.states[item];
      const entityName = stateObject
        ? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
        : item;
      const { area, device } = stateObject
        ? getEntityContext(
            stateObject,
            this.hass.entities,
            this.hass.devices,
            this.hass.areas,
            this.hass.floors
          )
        : { area: undefined, device: undefined };
      const deviceName = device ? computeDeviceName(device) : undefined;
      const areaName = area ? computeAreaName(area) : undefined;
      const context = [areaName, entityName ? deviceName : undefined]
        .filter(Boolean)
        .join(
          computeRTL(
            this.hass.language,
            this.hass.translationMetadata.translations
          )
            ? " ◂ "
            : " ▸ "
        );
      return {
        name: entityName || deviceName || item,
        context,
        stateObject,
        notFound: !stateObject && item !== "all" && item !== "none",
      };
    }

    // type label
    const label: LabelRegistryEntry | undefined = this._labelRegistry.find(
      (lab) => lab.label_id === item
    );
    return {
      name: label?.name || item,
      iconPath: label?.icon,
      fallbackIconPath: mdiLabel,
      notFound: !label,
    };
  });

  private _setDomainName(domain: string) {
    this._domainName = domainToName(this.hass.localize, domain);
  }

  private _removeItem(ev: MouseEvent) {
    ev.stopPropagation();
    fireEvent(this, "remove-target-item", {
      type: this.type,
      id: this.itemId,
    });
  }

  private async _getDeviceDomain(configEntryId: string) {
    try {
      const data = await getConfigEntry(this.hass, configEntryId);
      const domain = data.config_entry.domain;
      this._iconImg = brandsUrl(
        {
          domain: domain,
          type: "icon",
          darkOptimized: this.hass.themes?.darkMode,
        },
        this.hass.auth.data.hassUrl
      );

      this._setDomainName(domain);
    } catch {
      // failed to load config entry -> ignore
    }
  }

  private _replaceItem(ev: MouseEvent) {
    ev.stopPropagation();
    fireEvent(this, "replace-target-item", {
      type: this.type,
      id: this.itemId,
    });
  }

  // Returns the split devices that replaced a removed composite device and
  // pass this row's filters, or undefined if the item is not a replaced device.
  private _getReplacement(item: string) {
    const split = this.compositeSplits?.[item];
    if (!split || this.hass.devices[item]) {
      return undefined;
    }
    const candidates = split.split_ids.filter((id) => {
      const device = this.hass.devices[id];
      return (
        device &&
        deviceMeetsFilter(
          device,
          this.hass.entities,
          this.deviceFilter,
          this.includeDomains,
          this.includeDeviceClasses,
          this.hass.states,
          this.entityFilter,
          !this.primaryEntitiesOnly
        )
      );
    });
    // Display the replaced reference using the primary replacement device's
    // name instead of the removed composite device id. Fall back to the first
    // available candidate if the primary device itself was deleted.
    const nameDevice =
      (split.primary_id && this.hass.devices[split.primary_id]) ||
      (candidates.length ? this.hass.devices[candidates[0]] : undefined);
    const name = nameDevice ? computeDeviceName(nameDevice) : undefined;
    return { candidates, name };
  }

  private _migrate = (ev: MouseEvent) => {
    ev.stopPropagation();
    const replacement = this._getReplacement(this.itemId);
    if (!replacement?.candidates.length) {
      return;
    }
    fireEvent(this, "migrate-target-item", {
      id: this.itemId,
      replacements: replacement.candidates,
    });
  };

  private _openDetails(ev: MouseEvent) {
    ev.stopPropagation();
    showTargetDetailsDialog(this, {
      title: this._itemData(this.type, this.itemId).name,
      type: this.type,
      itemId: this.itemId,
      deviceFilter: this.deviceFilter,
      entityFilter: this.entityFilter,
      includeDomains: this.includeDomains,
      includeDeviceClasses: this.includeDeviceClasses,
      primaryEntitiesOnly: this.primaryEntitiesOnly,
    });
  }

  private _openMoreInfo = () => {
    showMoreInfoDialog(this, {
      entityId: this.itemId,
    });
  };

  static styles = [
    buttonLinkStyle,
    css`
      :host {
        --md-list-item-top-space: 0;
        --md-list-item-bottom-space: 0;
        --md-list-item-leading-space: var(--ha-space-2);
        --md-list-item-trailing-space: var(--ha-space-2);
        --md-list-item-two-line-container-height: 56px;
      }

      .error {
        background: var(--ha-color-fill-warning-quiet-resting);
      }

      .error [slot="supporting-text"] {
        color: var(--ha-color-on-warning-normal);
      }

      .migrate {
        align-self: center;
        white-space: nowrap;
      }

      .replaceable {
        cursor: pointer;
      }

      .replaceable:hover {
        background-color: var(--ha-color-fill-neutral-quiet-hover);
      }

      state-badge {
        color: var(--ha-color-on-neutral-quiet);
      }

      .icon {
        width: 24px;
        display: flex;
        color: var(--ha-color-on-neutral-normal);
      }

      img {
        width: 24px;
        height: 24px;
        z-index: 1;
      }
      ha-icon-button {
        --ha-icon-button-size: 32px;
      }
      .summary {
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        line-height: var(--ha-line-height-condensed);
      }
      :host([sub-entry]) .summary {
        margin-inline-start: var(--ha-space-12);
      }
      .summary .main {
        font-weight: var(--ha-font-weight-medium);
      }
      :host([expand]) .summary .main {
        color: var(--ha-color-text-secondary);
        font-size: var(--ha-font-size-s);
        font-weight: var(--ha-font-weight-normal);
      }
      .summary .secondary {
        font-size: var(--ha-font-size-s);
        color: var(--secondary-text-color);
      }

      button.link {
        text-decoration: none;
        color: var(--primary-color);
      }

      button.link:hover,
      button.link:focus {
        text-decoration: underline;
      }

      .state {
        width: fit-content;
        font-size: var(--ha-font-size-s);
        color: var(--ha-color-text-secondary);
      }

      ha-list-item-button::part(end) {
        gap: var(--ha-space-2);
      }

      :host([sub-entry]) ha-list-item-button::part(base),
      :host([sub-entry]) ha-list-item-base::part(base) {
        padding-inline-start: var(--sub-entry-indent);
      }
    `,
  ];
}

declare global {
  interface HTMLElementTagNameMap {
    "ha-target-picker-item-row": HaTargetPickerItemRow;
  }
}
