import { mdiContentCopy, mdiRestore } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { formatNumber } from "../../../common/number/format_number";
import { stringCompare } from "../../../common/string/compare";
import { autoCaseNoun } from "../../../common/translations/auto_case_noun";
import type {
  LocalizeFunc,
  LocalizeKeys,
} from "../../../common/translations/localize";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-picker";
import "../../../components/ha-labels-picker";
import "../../../components/ha-list-item";
import "../../../components/ha-radio";
import "../../../components/ha-select";
import "../../../components/ha-settings-row";
import "../../../components/ha-state-icon";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import {
  CAMERA_ORIENTATIONS,
  CAMERA_SUPPORT_STREAM,
  type CameraPreferences,
  fetchCameraCapabilities,
  fetchCameraPrefs,
  STREAM_TYPE_HLS,
  updateCameraPrefs,
} from "../../../data/camera";
import type { ConfigEntry } from "../../../data/config_entries";
import { deleteConfigEntry } from "../../../data/config_entries";
import {
  createConfigFlow,
  handleConfigFlowStep,
} from "../../../data/config_flow";
import type { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow";
import type { DeviceRegistryEntry } from "../../../data/device_registry";
import { updateDeviceRegistryEntry } from "../../../data/device_registry";
import type {
  AlarmControlPanelEntityOptions,
  EntityRegistryEntry,
  EntityRegistryEntryUpdateParams,
  ExtEntityRegistryEntry,
  LockEntityOptions,
  SensorEntityOptions,
} from "../../../data/entity_registry";
import {
  getAutomaticEntityIds,
  subscribeEntityRegistry,
  updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { entityIcon, entryIcon } from "../../../data/icons";
import {
  domainToName,
  fetchIntegrationManifest,
} from "../../../data/integration";
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
import {
  createOptionsFlow,
  handleOptionsFlowStep,
} from "../../../data/options_flow";
import {
  getSensorDeviceClassConvertibleUnits,
  getSensorNumericDeviceClasses,
} from "../../../data/sensor";
import type { WeatherUnits } from "../../../data/weather";
import { getWeatherConvertibleUnits } from "../../../data/weather";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import {
  showAlertDialog,
  showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showVoiceAssistantsView } from "../../../dialogs/more-info/components/voice/show-view-voice-assistants";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";

const OVERRIDE_DEVICE_CLASSES = {
  cover: [
    [
      "awning",
      "blind",
      "curtain",
      "damper",
      "door",
      "garage",
      "gate",
      "shade",
      "shutter",
      "window",
    ],
  ],
  binary_sensor: [
    ["lock"], // Lock
    ["window", "door", "garage_door", "opening"], // Door
    ["battery", "battery_charging"], // Battery
    ["cold", "gas", "heat"], // Climate
    ["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence
    ["power", "plug", "light"], // Power
    [
      "smoke",
      "safety",
      "sound",
      "problem",
      "tamper",
      "carbon_monoxide",
      "moisture",
    ], // Alarm
    ["connectivity"], // Connectivity
    ["update"], // Update
  ],
};

const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren", "valve"];
const SWITCH_AS_DOMAINS_INVERT = ["cover", "lock", "valve"];

const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];

@customElement("entity-registry-settings-editor")
export class EntityRegistrySettingsEditor extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @property({ type: Object }) public entry!: ExtEntityRegistryEntry;

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

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

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

  @property({ attribute: false }) public helperConfigEntry?: ConfigEntry;

  @state() private _name!: string;

  @state() private _icon!: string;

  @state() private _entityId!: string;

  @state() private _deviceClass?: string;

  @state() private _switchAsDomain = "switch";

  @state() private _switchAsInvert = false;

  @state() private _areaId?: string | null;

  @state() private _labels?: string[] | null;

  @state() private _disabledBy!: EntityRegistryEntry["disabled_by"];

  @state() private _hiddenBy!: EntityRegistryEntry["hidden_by"];

  @state() private _device?: DeviceRegistryEntry;

  @state() private _unit_of_measurement?: string | null;

  @state() private _precision?: number | null;

  @state() private _precipitation_unit?: string | null;

  @state() private _pressure_unit?: string | null;

  @state() private _temperature_unit?: string | null;

  @state() private _visibility_unit?: string | null;

  @state() private _wind_speed_unit?: string | null;

  @state() private _cameraPrefs?: CameraPreferences;

  @state() private _numberDeviceClassConvertibleUnits?: string[];

  @state() private _sensorDeviceClassConvertibleUnits?: string[];

  @state() private _sensorNumericalDeviceClasses?: string[];

  @state() private _weatherConvertibleUnits?: WeatherUnits;

  @state() private _defaultCode?: string | null;

  @state() private _noDeviceArea?: boolean;

  private _origEntityId!: string;

  private _deviceClassOptions?: string[][];

  protected willUpdate(changedProperties: PropertyValues) {
    super.willUpdate(changedProperties);
    if (
      !changedProperties.has("entry") ||
      changedProperties.get("entry")?.id === this.entry.id
    ) {
      return;
    }

    this._name = this.entry.name || "";
    this._icon = this.entry.icon || "";
    this._deviceClass =
      this.entry.device_class || this.entry.original_device_class;
    this._origEntityId = this.entry.entity_id;
    this._areaId = this.entry.area_id;
    this._labels = this.entry.labels;
    this._entityId = this.entry.entity_id;
    this._disabledBy = this.entry.disabled_by;
    this._hiddenBy = this.entry.hidden_by;
    this._device = this.entry.device_id
      ? this.hass.devices[this.entry.device_id]
      : undefined;
    this._switchAsInvert = this.entry.options?.switch_as_x?.invert === true;

    const domain = computeDomain(this.entry.entity_id);

    if (domain === "camera" && isComponentLoaded(this.hass, "stream")) {
      const stateObj: HassEntity | undefined =
        this.hass.states[this.entry.entity_id];
      if (stateObj && supportsFeature(stateObj, CAMERA_SUPPORT_STREAM)) {
        this._fetchCameraPrefs();
      }
    }

    if (domain === "number" || domain === "sensor") {
      const stateObj: HassEntity | undefined =
        this.hass.states[this.entry.entity_id];
      this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
    }

    if (domain === "sensor") {
      this._precision = this.entry.options?.sensor?.display_precision;
    }

    if (domain === "lock") {
      this._defaultCode = this.entry.options?.lock?.default_code;
    }

    if (domain === "alarm_control_panel") {
      this._defaultCode = this.entry.options?.alarm_control_panel?.default_code;
    }

    if (domain === "weather") {
      const stateObj: HassEntity | undefined =
        this.hass.states[this.entry.entity_id];
      this._precipitation_unit = stateObj?.attributes?.precipitation_unit;
      this._pressure_unit = stateObj?.attributes?.pressure_unit;
      this._temperature_unit = stateObj?.attributes?.temperature_unit;
      this._visibility_unit = stateObj?.attributes?.visibility_unit;
      this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit;
    }

    const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];

    if (!deviceClasses) {
      return;
    }

    this._deviceClassOptions = [[], []];
    for (const deviceClass of deviceClasses) {
      if (deviceClass.includes(this.entry.original_device_class!)) {
        this._deviceClassOptions[0] = deviceClass;
      } else {
        this._deviceClassOptions[1].push(...deviceClass);
      }
    }
  }

  private _precisionLabel(precision?: number, stateValue?: string) {
    const stateValueNumber = Number(stateValue);
    const value = !isNaN(stateValueNumber) ? stateValue! : 0;
    return formatNumber(value, this.hass.locale, {
      minimumFractionDigits: precision,
      maximumFractionDigits: precision,
    });
  }

  private _isInvalidDefaultCode(
    codeFormat?: string,
    value?: string | null
  ): boolean {
    if (codeFormat && value) {
      return !RegExp(codeFormat).test(value);
    }
    return false;
  }

  protected async updated(changedProps: PropertyValues): Promise<void> {
    if (changedProps.has("_deviceClass")) {
      const domain = computeDomain(this.entry.entity_id);

      if (domain === "number" && this._deviceClass) {
        const { units } = await getNumberDeviceClassConvertibleUnits(
          this.hass,
          this._deviceClass
        );
        this._numberDeviceClassConvertibleUnits = units;
      } else {
        this._numberDeviceClassConvertibleUnits = [];
      }
      if (domain === "sensor") {
        const { numeric_device_classes } = await getSensorNumericDeviceClasses(
          this.hass
        );
        this._sensorNumericalDeviceClasses = numeric_device_classes;
      } else {
        this._sensorNumericalDeviceClasses = [];
      }
      if (domain === "sensor" && this._deviceClass) {
        const { units } = await getSensorDeviceClassConvertibleUnits(
          this.hass,
          this._deviceClass
        );
        this._sensorDeviceClassConvertibleUnits = units;
      } else {
        this._sensorDeviceClassConvertibleUnits = [];
      }
    }
    if (changedProps.has("_entityId")) {
      const domain = computeDomain(this.entry.entity_id);
      if (domain === "switch") {
        this.hass.loadBackendTranslation("title", SWITCH_AS_DOMAINS, false);
      }

      if (domain === "weather") {
        const { units } = await getWeatherConvertibleUnits(this.hass);
        this._weatherConvertibleUnits = units;
      } else {
        this._weatherConvertibleUnits = undefined;
      }
    }
    if (changedProps.has("helperConfigEntry")) {
      if (this.helperConfigEntry?.domain === "switch_as_x") {
        this._switchAsDomain = computeDomain(this.entry.entity_id);
        this.hass.loadBackendTranslation("title", SWITCH_AS_DOMAINS, false);
      } else {
        this._switchAsDomain = "switch";
        this._switchAsInvert = false;
      }
    }
  }

  protected render() {
    if (this.entry.entity_id !== this._origEntityId) {
      return nothing;
    }
    const stateObj: HassEntity | undefined =
      this.hass.states[this.entry.entity_id];

    const domain = computeDomain(this.entry.entity_id);

    const invalidDefaultCode =
      domain === "lock" &&
      this._isInvalidDefaultCode(
        stateObj?.attributes?.code_format,
        this._defaultCode
      );

    const defaultPrecision =
      this.entry.options?.sensor?.suggested_display_precision ?? undefined;

    return html`
      ${this.hideName
        ? nothing
        : html`<ha-textfield
            .value=${this._name}
            .label=${this.hass.localize(
              "ui.dialogs.entity_registry.editor.name"
            )}
            .disabled=${this.disabled}
            .placeholder=${this.entry.original_name}
            @input=${this._nameChanged}
          ></ha-textfield>`}
      ${this.hideIcon
        ? nothing
        : html`
            <ha-icon-picker
              .hass=${this.hass}
              .value=${this._icon}
              @value-changed=${this._iconChanged}
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.icon"
              )}
              .placeholder=${this.entry.original_icon ||
              stateObj?.attributes.icon ||
              (stateObj && until(entityIcon(this.hass, stateObj))) ||
              until(entryIcon(this.hass, this.entry))}
              .disabled=${this.disabled}
            >
              ${!this._icon && !stateObj?.attributes.icon && stateObj
                ? html`
                    <ha-state-icon
                      slot="fallback"
                      .hass=${this.hass}
                      .stateObj=${stateObj}
                    ></ha-state-icon>
                  `
                : nothing}
            </ha-icon-picker>
          `}
      ${domain === "switch"
        ? html`<ha-select
            .label=${this.hass.localize(
              "ui.dialogs.entity_registry.editor.device_class"
            )}
            naturalMenuWidth
            fixedMenuPosition
            @selected=${this._switchAsDomainChanged}
            @closed=${stopPropagation}
          >
            <ha-list-item
              value="switch"
              .selected=${!this._deviceClass || this._deviceClass === "switch"}
            >
              ${domainToName(this.hass.localize, "switch")}
            </ha-list-item>
            <ha-list-item
              value="outlet"
              .selected=${this._deviceClass === "outlet"}
            >
              ${this.hass.localize(
                "ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
              )}
            </ha-list-item>
            <li divider role="separator"></li>
            ${this._switchAsDomainsSorted(
              SWITCH_AS_DOMAINS,
              this.hass.localize
            ).map(
              (entry) => html`
                <ha-list-item .value=${entry.domain}>
                  ${entry.label}
                </ha-list-item>
              `
            )}
          </ha-select>`
        : this.helperConfigEntry?.domain === "switch_as_x"
          ? html`<ha-select
                .label=${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.switch_as_x"
                )}
                .value=${this._switchAsDomain}
                naturalMenuWidth
                fixedMenuPosition
                @selected=${this._switchAsDomainChanged}
                @closed=${stopPropagation}
              >
                <ha-list-item value="switch">
                  ${domainToName(this.hass.localize, "switch")}
                </ha-list-item>
                <ha-list-item .value=${domain}>
                  ${domainToName(this.hass.localize, domain)}
                </ha-list-item>
                <li divider role="separator"></li>
                ${this._switchAsDomainsSorted(
                  SWITCH_AS_DOMAINS,
                  this.hass.localize
                ).map((entry) =>
                  domain === entry.domain
                    ? nothing
                    : html`
                        <ha-list-item .value=${entry.domain}>
                          ${entry.label}
                        </ha-list-item>
                      `
                )}
              </ha-select>
              ${SWITCH_AS_DOMAINS_INVERT.includes(this._switchAsDomain)
                ? html`
                    <ha-settings-row>
                      <span slot="heading"
                        >${this.hass.localize(
                          "ui.dialogs.entity_registry.editor.invert.label"
                        )}</span
                      >
                      <span slot="description"
                        >${this.hass.localize(
                          `ui.dialogs.entity_registry.editor.invert.descriptions.${this._switchAsDomain}`
                        )}</span
                      >
                      <ha-switch
                        .checked=${this.entry.options?.switch_as_x?.invert}
                        @change=${this._switchAsInvertChanged}
                      ></ha-switch>
                    </ha-settings-row>
                  `
                : nothing} `
          : nothing}
      ${this._deviceClassOptions
        ? html`
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.device_class"
              )}
              .value=${this._deviceClass}
              naturalMenuWidth
              fixedMenuPosition
              clearable
              @selected=${this._deviceClassChanged}
              @closed=${stopPropagation}
            >
              ${this._deviceClassesSorted(
                domain,
                this._deviceClassOptions[0],
                this.hass.localize
              ).map(
                (entry) => html`
                  <ha-list-item .value=${entry.deviceClass}>
                    ${entry.label}
                  </ha-list-item>
                `
              )}
              ${this._deviceClassOptions[0].length &&
              this._deviceClassOptions[1].length
                ? html`<li divider role="separator"></li>`
                : ""}
              ${this._deviceClassesSorted(
                domain,
                this._deviceClassOptions[1],
                this.hass.localize
              ).map(
                (entry) => html`
                  <ha-list-item .value=${entry.deviceClass}>
                    ${entry.label}
                  </ha-list-item>
                `
              )}
            </ha-select>
          `
        : ""}
      ${domain === "number" &&
      this._deviceClass &&
      stateObj?.attributes.unit_of_measurement &&
      this._numberDeviceClassConvertibleUnits?.includes(
        stateObj?.attributes.unit_of_measurement
      )
        ? html`
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.unit_of_measurement"
              )}
              .value=${stateObj.attributes.unit_of_measurement}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._unitChanged}
              @closed=${stopPropagation}
            >
              ${this._numberDeviceClassConvertibleUnits.map(
                (unit: string) => html`
                  <ha-list-item .value=${unit}>${unit}</ha-list-item>
                `
              )}
            </ha-select>
          `
        : ""}
      ${domain === "lock"
        ? html`
            <ha-textfield
              .validationMessage=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.default_code_error"
              )}
              .value=${this._defaultCode == null ? "" : this._defaultCode}
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.default_code"
              )}
              type="password"
              .invalid=${invalidDefaultCode}
              .disabled=${this.disabled}
              @input=${this._defaultcodeChanged}
            ></ha-textfield>
          `
        : ""}
      ${domain === "alarm_control_panel"
        ? html`
            <ha-textfield
              .value=${this._defaultCode == null ? "" : this._defaultCode}
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.default_code"
              )}
              type="password"
              .disabled=${this.disabled}
              @input=${this._defaultcodeChanged}
            ></ha-textfield>
          `
        : ""}
      ${domain === "sensor" &&
      this._deviceClass &&
      stateObj?.attributes.unit_of_measurement &&
      this._sensorDeviceClassConvertibleUnits?.includes(
        stateObj?.attributes.unit_of_measurement
      )
        ? html`
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.unit_of_measurement"
              )}
              .value=${stateObj.attributes.unit_of_measurement}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._unitChanged}
              @closed=${stopPropagation}
            >
              ${this._sensorDeviceClassConvertibleUnits.map(
                (unit: string) => html`
                  <ha-list-item .value=${unit}>${unit}</ha-list-item>
                `
              )}
            </ha-select>
          `
        : ""}
      ${domain === "sensor" &&
      // Allow customizing the precision for a sensor with numerical device class,
      // a unit of measurement or state class
      ((this._deviceClass &&
        this._sensorNumericalDeviceClasses?.includes(this._deviceClass)) ||
        stateObj?.attributes.unit_of_measurement ||
        stateObj?.attributes.state_class)
        ? html`
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.precision"
              )}
              .value=${this._precision == null
                ? "default"
                : this._precision.toString()}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._precisionChanged}
              @closed=${stopPropagation}
            >
              <ha-list-item value="default"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.precision_default",
                  {
                    value: this._precisionLabel(
                      defaultPrecision,
                      stateObj?.state
                    ),
                  }
                )}</ha-list-item
              >
              ${PRECISIONS.map(
                (precision) => html`
                  <ha-list-item .value=${precision.toString()}>
                    ${this._precisionLabel(precision, stateObj?.state)}
                  </ha-list-item>
                `
              )}
            </ha-select>
          `
        : ""}
      ${domain === "weather"
        ? html`
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.precipitation_unit"
              )}
              .value=${this._precipitation_unit}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._precipitationUnitChanged}
              @closed=${stopPropagation}
            >
              ${this._weatherConvertibleUnits?.precipitation_unit.map(
                (unit: string) => html`
                  <ha-list-item .value=${unit}>${unit}</ha-list-item>
                `
              )}
            </ha-select>
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.pressure_unit"
              )}
              .value=${this._pressure_unit}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._pressureUnitChanged}
              @closed=${stopPropagation}
            >
              ${this._weatherConvertibleUnits?.pressure_unit.map(
                (unit: string) => html`
                  <ha-list-item .value=${unit}>${unit}</ha-list-item>
                `
              )}
            </ha-select>
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.temperature_unit"
              )}
              .value=${this._temperature_unit}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._temperatureUnitChanged}
              @closed=${stopPropagation}
            >
              ${this._weatherConvertibleUnits?.temperature_unit.map(
                (unit: string) => html`
                  <ha-list-item .value=${unit}>${unit}</ha-list-item>
                `
              )}
            </ha-select>
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.visibility_unit"
              )}
              .value=${this._visibility_unit}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._visibilityUnitChanged}
              @closed=${stopPropagation}
            >
              ${this._weatherConvertibleUnits?.visibility_unit.map(
                (unit: string) => html`
                  <ha-list-item .value=${unit}>${unit}</ha-list-item>
                `
              )}
            </ha-select>
            <ha-select
              .label=${this.hass.localize(
                "ui.dialogs.entity_registry.editor.wind_speed_unit"
              )}
              .value=${this._wind_speed_unit}
              naturalMenuWidth
              fixedMenuPosition
              @selected=${this._windSpeedUnitChanged}
              @closed=${stopPropagation}
            >
              ${this._weatherConvertibleUnits?.wind_speed_unit.map(
                (unit: string) => html`
                  <ha-list-item .value=${unit}>${unit}</ha-list-item>
                `
              )}
            </ha-select>
          `
        : ""}
      <ha-textfield
        class="entityId"
        .value=${computeObjectId(this._entityId)}
        .prefix=${domain + "."}
        .label=${this.hass.localize(
          "ui.dialogs.entity_registry.editor.entity_id"
        )}
        .disabled=${this.disabled}
        required
        @input=${this._entityIdChanged}
        iconTrailing
        autocapitalize="none"
        autocomplete="off"
        .autocorrect=${false}
        input-spellcheck="false"
      >
        <div class="layout horizontal" slot="trailingIcon">
          <ha-icon-button
            @click=${this._restoreEntityId}
            .path=${mdiRestore}
          ></ha-icon-button>
          <ha-icon-button
            @click=${this._copyEntityId}
            .path=${mdiContentCopy}
          ></ha-icon-button>
        </div>
      </ha-textfield>
      ${!this.entry.device_id
        ? html`<ha-area-picker
            .hass=${this.hass}
            .value=${this._areaId}
            .disabled=${this.disabled}
            @value-changed=${this._areaPicked}
          ></ha-area-picker>`
        : ""}
      <ha-labels-picker
        .hass=${this.hass}
        .value=${this._labels}
        .disabled=${this.disabled}
        @value-changed=${this._labelsChanged}
      ></ha-labels-picker>
      ${this._cameraPrefs
        ? html`
            <ha-settings-row>
              <span slot="heading"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.stream.preload_stream"
                )}</span
              >
              <span slot="description"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.stream.preload_stream_description"
                )}</span
              >
              <ha-switch
                .checked=${this._cameraPrefs.preload_stream}
                .disabled=${this.disabled}
                @change=${this._handleCameraPrefsChanged}
              >
              </ha-switch>
            </ha-settings-row>
            <ha-settings-row>
              <span slot="heading"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.stream.stream_orientation"
                )}</span
              >
              <span slot="description"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.stream.stream_orientation_description"
                )}</span
              >
              <ha-select
                .label=${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.stream.stream_orientation"
                )}
                naturalMenuWidth
                fixedMenuPosition
                .disabled=${this.disabled}
                @selected=${this._handleCameraOrientationChanged}
                @closed=${stopPropagation}
              >
                ${CAMERA_ORIENTATIONS.map((num) => {
                  const localizeStr =
                    "ui.dialogs.entity_registry.editor.stream.stream_orientation_" +
                    num.toString();
                  return html`
                    <ha-list-item value=${num}>
                      ${this.hass.localize(localizeStr as LocalizeKeys)}
                    </ha-list-item>
                  `;
                })}
              </ha-select>
            </ha-settings-row>
          `
        : ""}
      ${this.helperConfigEntry &&
      this.helperConfigEntry.supports_options &&
      this.helperConfigEntry.domain !== "switch_as_x"
        ? html`
            <ha-list-item
              class="menu-item"
              twoline
              hasMeta
              .disabled=${this.disabled}
              @click=${this._showOptionsFlow}
            >
              <span
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.configure_state",
                  {
                    integration: domainToName(
                      this.hass.localize,
                      this.helperConfigEntry.domain
                    ),
                  }
                )}</span
              >
              <span slot="secondary"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.configure_state_secondary",
                  {
                    integration: domainToName(
                      this.hass.localize,
                      this.helperConfigEntry.domain
                    ),
                  }
                )}</span
              >
              <ha-icon-next slot="meta"></ha-icon-next>
            </ha-list-item>
          `
        : ""}

      <ha-list-item
        class="menu-item"
        twoline
        hasMeta
        .disabled=${this.disabled}
        @click=${this._handleVoiceAssistantsClicked}
      >
        <span
          >${this.hass.localize(
            "ui.dialogs.entity_registry.editor.voice_assistants"
          )}</span
        >
        <span slot="secondary">
          ${this.entry.aliases.length
            ? [...this.entry.aliases]
                .sort((a, b) => stringCompare(a, b, this.hass.locale.language))
                .join(", ")
            : this.hass.localize(
                "ui.dialogs.entity_registry.editor.no_aliases"
              )}
        </span>
        <ha-icon-next slot="meta"></ha-icon-next>
      </ha-list-item>

      ${this._disabledBy &&
      this._disabledBy !== "user" &&
      this._disabledBy !== "integration"
        ? html`<ha-alert alert-type="warning"
            >${this.hass.localize(
              "ui.dialogs.entity_registry.editor.enabled_cause",
              {
                cause: this.hass.localize(
                  `config_entry.disabled_by.${this._disabledBy!}`
                ),
              }
            )}</ha-alert
          >`
        : ""}

      <ha-settings-row>
        <span slot="heading"
          >${this.hass.localize(
            "ui.dialogs.entity_registry.editor.enabled_label"
          )}</span
        >
        <span slot="description"
          >${this.hass.localize(
            "ui.dialogs.entity_registry.editor.enabled_description"
          )}</span
        >
        <ha-switch
          .checked=${!this._disabledBy}
          .disabled=${this.disabled ||
          this._device?.disabled_by ||
          (this._disabledBy &&
            this._disabledBy !== "user" &&
            this._disabledBy !== "integration")}
          @change=${this._enabledChanged}
        ></ha-switch>
      </ha-settings-row>

      ${this._hiddenBy && this._hiddenBy !== "user"
        ? html`<ha-alert alert-type="warning"
            >${this.hass.localize(
              "ui.dialogs.entity_registry.editor.hidden_cause",
              {
                cause: this.hass.localize(
                  `config_entry.hidden_by.${this._hiddenBy!}`
                ),
              }
            )}</ha-alert
          >`
        : ""}

      <ha-settings-row>
        <span slot="heading"
          >${this.hass.localize(
            "ui.dialogs.entity_registry.editor.visible_label"
          )}</span
        >
        <span slot="description"
          >${this.hass.localize(
            "ui.dialogs.entity_registry.editor.hidden_explanation"
          )}</span
        >
        <ha-switch
          .checked=${!this._disabledBy && !this._hiddenBy}
          .disabled=${this.disabled || this._disabledBy}
          @change=${this._hiddenChanged}
        ></ha-switch>
      </ha-settings-row>

      ${this.entry.device_id
        ? html`<ha-settings-row>
              <span slot="heading"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.use_device_area"
                )}
                ${this.hass.devices[this.entry.device_id].area_id
                  ? `(${
                      this.hass.areas[
                        this.hass.devices[this.entry.device_id].area_id!
                      ]?.name
                    })`
                  : ""}</span
              >
              <span slot="description"
                >${this.hass.localize(
                  "ui.dialogs.entity_registry.editor.change_device_settings",
                  {
                    link: html`<button
                      class="link"
                      @click=${this._openDeviceSettings}
                    >
                      ${this.hass.localize(
                        "ui.dialogs.entity_registry.editor.change_device_area_link"
                      )}
                    </button>`,
                  }
                )}</span
              >
              <ha-switch
                .checked=${!this._areaId || this._noDeviceArea}
                .disabled=${this.disabled}
                @change=${this._useDeviceAreaChanged}
              >
              </ha-switch
            ></ha-settings-row>
            ${this._areaId || this._noDeviceArea
              ? html`<ha-area-picker
                  .hass=${this.hass}
                  .value=${this._areaId}
                  .placeholder=${this._device?.area_id}
                  .disabled=${this.disabled}
                  @value-changed=${this._areaPicked}
                ></ha-area-picker>`
              : ""} `
        : ""}
    `;
  }

  public async updateEntry(): Promise<{
    close: boolean;
    entry: ExtEntityRegistryEntry;
  }> {
    let close = true;
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    let parent: HTMLElement = this;
    while (parent?.localName !== "home-assistant") {
      parent = (parent.getRootNode() as ShadowRoot).host as HTMLElement;
    }

    const params: Partial<EntityRegistryEntryUpdateParams> = {
      name: this._name.trim() || null,
      icon: this._icon.trim() || null,
      area_id: this._areaId || null,
      labels: this._labels || [],
      new_entity_id: this._entityId.trim(),
    };

    // Only update device class if changed by user
    if (
      this._deviceClass !==
      (this.entry.device_class || this.entry.original_device_class)
    ) {
      params.device_class = this._deviceClass;
    }

    const stateObj: HassEntity | undefined =
      this.hass.states[this.entry.entity_id];
    const domain = computeDomain(this.entry.entity_id);

    if (
      this.entry.disabled_by !== this._disabledBy &&
      (this._disabledBy === null || this._disabledBy === "user")
    ) {
      params.disabled_by = this._disabledBy;
    }
    if (
      this.entry.hidden_by !== this._hiddenBy &&
      (this._hiddenBy === null || this._hiddenBy === "user")
    ) {
      params.hidden_by = this._hiddenBy;
    }
    if (
      (domain === "number" || domain === "sensor") &&
      stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement
    ) {
      params.options_domain = domain;
      params.options = this.entry.options?.[domain] || {};
      params.options.unit_of_measurement = this._unit_of_measurement;
    }
    if (
      domain === "sensor" &&
      this.entry.options?.[domain]?.display_precision !== this._precision
    ) {
      params.options_domain = domain;
      params.options = params.options || this.entry.options?.[domain] || {};
      (params.options as SensorEntityOptions).display_precision =
        this._precision;
    }
    if (
      domain === "lock" &&
      this.entry.options?.[domain]?.default_code !== this._defaultCode
    ) {
      params.options_domain = domain;
      params.options = this.entry.options?.[domain] || {};
      (params.options as LockEntityOptions).default_code = this._defaultCode;
    }
    if (
      domain === "alarm_control_panel" &&
      this.entry.options?.[domain]?.default_code !== this._defaultCode
    ) {
      params.options_domain = domain;
      params.options = this.entry.options?.[domain] || {};
      (params.options as AlarmControlPanelEntityOptions).default_code =
        this._defaultCode;
    }
    if (
      domain === "weather" &&
      (stateObj?.attributes?.precipitation_unit !== this._precipitation_unit ||
        stateObj?.attributes?.pressure_unit !== this._pressure_unit ||
        stateObj?.attributes?.temperature_unit !== this._temperature_unit ||
        stateObj?.attributes?.visbility_unit !== this._visibility_unit ||
        stateObj?.attributes?.wind_speed_unit !== this._wind_speed_unit)
    ) {
      params.options_domain = "weather";
      params.options = {
        precipitation_unit: this._precipitation_unit,
        pressure_unit: this._pressure_unit,
        temperature_unit: this._temperature_unit,
        visibility_unit: this._visibility_unit,
        wind_speed_unit: this._wind_speed_unit,
      };
    }

    const result = await updateEntityRegistryEntry(
      this.hass!,
      this.entry.entity_id,
      params
    );
    if (result.require_restart) {
      showAlertDialog(this, {
        text: this.hass.localize(
          "ui.dialogs.entity_registry.editor.enabled_restart_confirm"
        ),
      });
    }
    if (result.reload_delay) {
      showAlertDialog(this, {
        text: this.hass.localize(
          "ui.dialogs.entity_registry.editor.enabled_delay_confirm",
          { delay: result.reload_delay }
        ),
      });
    }

    if (domain === "switch" && this._switchAsDomain !== "switch") {
      // generate config flow for switch_as_x
      if (
        await showConfirmationDialog(this, {
          text: this.hass!.localize(
            "ui.dialogs.entity_registry.editor.switch_as_x_confirm",
            {
              domain: autoCaseNoun(
                domainToName(this.hass.localize, this._switchAsDomain),
                this.hass.locale.language
              ),
            }
          ),
        })
      ) {
        const configFlow = await createConfigFlow(this.hass, "switch_as_x");
        const configFlowResult = (await handleConfigFlowStep(
          this.hass,
          configFlow.flow_id,
          {
            entity_id: this._entityId.trim(),
            invert: this._switchAsInvert,
            target_domain: this._switchAsDomain,
          }
        )) as DataEntryFlowStepCreateEntry;
        if (configFlowResult.result?.entry_id) {
          try {
            const entry = await this._waitForEntityRegistryUpdate(
              configFlowResult.result.entry_id
            );
            showMoreInfoDialog(parent, { entityId: entry.entity_id });
            close = false;
          } catch (_err) {
            // ignore
          }
        }
      }
    } else if (
      this.helperConfigEntry?.domain === "switch_as_x" &&
      this._switchAsDomain !== domain
    ) {
      // change a current switch as x to something else
      if (
        await showConfirmationDialog(this, {
          text:
            this._switchAsDomain === "switch"
              ? this.hass!.localize(
                  "ui.dialogs.entity_registry.editor.switch_as_x_remove_confirm",
                  {
                    domain: autoCaseNoun(
                      domainToName(this.hass.localize, domain),
                      this.hass.locale.language
                    ),
                  }
                )
              : this.hass!.localize(
                  "ui.dialogs.entity_registry.editor.switch_as_x_change_confirm",
                  {
                    domain_1: autoCaseNoun(
                      domainToName(this.hass.localize, domain),
                      this.hass.locale.language
                    ),
                    domain_2: autoCaseNoun(
                      domainToName(this.hass.localize, this._switchAsDomain),
                      this.hass.locale.language
                    ),
                  }
                ),
        })
      ) {
        const origEntityId = this.entry.options?.switch_as_x?.entity_id;
        // remove current helper
        await deleteConfigEntry(this.hass, this.helperConfigEntry.entry_id);

        if (!origEntityId) {
          // should not happen, guard for types
        } else if (this._switchAsDomain === "switch") {
          // done, original switch is back
          showMoreInfoDialog(parent, { entityId: origEntityId });
          close = false;
        } else {
          const configFlow = await createConfigFlow(this.hass, "switch_as_x");
          const configFlowResult = (await handleConfigFlowStep(
            this.hass,
            configFlow.flow_id,
            {
              entity_id: origEntityId,
              invert: this._switchAsInvert,
              target_domain: this._switchAsDomain,
            }
          )) as DataEntryFlowStepCreateEntry;
          if (configFlowResult.result?.entry_id) {
            try {
              const entry = await this._waitForEntityRegistryUpdate(
                configFlowResult.result.entry_id
              );
              showMoreInfoDialog(parent, { entityId: entry.entity_id });
              close = false;
            } catch (_err) {
              // ignore
            }
          }
        }
      }
    } else if (
      this.helperConfigEntry?.domain === "switch_as_x" &&
      this._switchAsDomain === domain &&
      this._switchAsInvert !== this.entry.options?.switch_as_x?.invert
    ) {
      // Change invert setting
      const origEntityId = this.entry.options?.switch_as_x?.entity_id;
      if (!origEntityId) {
        // should not happen, guard for types
      } else {
        const configFlow = await createOptionsFlow(
          this.hass,
          this.helperConfigEntry.entry_id
        );
        await handleOptionsFlowStep(this.hass, configFlow.flow_id, {
          invert: this._switchAsInvert,
        });
      }
    }

    return { close, entry: result.entity_entry };
  }

  private async _waitForEntityRegistryUpdate(config_entry_id: string) {
    return new Promise<EntityRegistryEntry>((resolve, reject) => {
      const timeout = setTimeout(reject, 5000);
      const unsub = subscribeEntityRegistry(
        this.hass.connection,
        (entityRegistry) => {
          const entity = entityRegistry.find(
            (reg) => reg.config_entry_id === config_entry_id
          );
          if (entity) {
            clearTimeout(timeout);
            unsub();
            resolve(entity);
          }
        }
      );
      // @ts-ignore Force refresh
      this.hass.connection._entityRegistry?.refresh();
    });
  }

  private _nameChanged(ev): void {
    fireEvent(this, "change");
    this._name = ev.target.value;
  }

  private _iconChanged(ev: CustomEvent): void {
    fireEvent(this, "change");
    this._icon = ev.detail.value;
  }

  private async _restoreEntityId(): Promise<void> {
    const entityIds = await getAutomaticEntityIds(this.hass, [
      this._origEntityId,
    ]);
    this._entityId = entityIds[this._origEntityId] || this._origEntityId;
  }

  private async _copyEntityId(): Promise<void> {
    await copyToClipboard(this._entityId);
    showToast(this, {
      message: this.hass.localize("ui.common.copied_clipboard"),
    });
  }

  private _entityIdChanged(ev): void {
    fireEvent(this, "change");
    this._entityId = `${computeDomain(this._origEntityId)}.${ev.target.value}`;
  }

  private _deviceClassChanged(ev): void {
    fireEvent(this, "change");
    this._deviceClass = ev.target.value;
  }

  private _unitChanged(ev): void {
    fireEvent(this, "change");
    this._unit_of_measurement = ev.target.value;
  }

  private _defaultcodeChanged(ev): void {
    fireEvent(this, "change");
    this._defaultCode = ev.target.value === "" ? null : ev.target.value;
  }

  private _precipitationUnitChanged(ev): void {
    fireEvent(this, "change");
    this._precipitation_unit = ev.target.value;
  }

  private _precisionChanged(ev): void {
    fireEvent(this, "change");
    this._precision =
      ev.target.value === "default" ? null : Number(ev.target.value);
  }

  private _pressureUnitChanged(ev): void {
    fireEvent(this, "change");
    this._pressure_unit = ev.target.value;
  }

  private _temperatureUnitChanged(ev): void {
    fireEvent(this, "change");
    this._temperature_unit = ev.target.value;
  }

  private _visibilityUnitChanged(ev): void {
    fireEvent(this, "change");
    this._visibility_unit = ev.target.value;
  }

  private _windSpeedUnitChanged(ev): void {
    fireEvent(this, "change");
    this._wind_speed_unit = ev.target.value;
  }

  private _switchAsDomainChanged(ev): void {
    if (ev.target.value === "") {
      return;
    }

    // If value is "outlet" that means the user kept the "switch" domain, but actually changed
    // the device_class of the switch to "outlet".
    const switchAs = ev.target.value === "outlet" ? "switch" : ev.target.value;
    this._switchAsDomain = switchAs;

    if (
      (computeDomain(this.entry.entity_id) === "switch" &&
        ev.target.value === "outlet") ||
      ev.target.value === "switch"
    ) {
      this._deviceClass = ev.target.value;
    }
  }

  private _switchAsInvertChanged(ev): void {
    this._switchAsInvert = ev.target.checked;
  }

  private _useDeviceAreaChanged(ev): void {
    this._noDeviceArea = !ev.target.checked;
    if (!this._noDeviceArea) {
      this._areaId = undefined;
    }
  }

  private _areaPicked(ev: CustomEvent) {
    fireEvent(this, "change");
    this._areaId = ev.detail.value;
  }

  private _labelsChanged(ev: CustomEvent) {
    this._labels = ev.detail.value;
  }

  private async _fetchCameraPrefs() {
    const capabilities = await fetchCameraCapabilities(
      this.hass,
      this.entry.entity_id
    );

    // The stream component for HLS streams supports a server-side pre-load
    // option that client initiated WebRTC streams do not

    if (!capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS)) {
      this._cameraPrefs = undefined;
      return;
    }

    this._cameraPrefs = await fetchCameraPrefs(this.hass, this.entry.entity_id);
  }

  private async _handleCameraPrefsChanged(ev) {
    const checkbox = ev.currentTarget as HaSwitch;
    try {
      this._cameraPrefs = await updateCameraPrefs(
        this.hass,
        this.entry.entity_id,
        {
          preload_stream: checkbox.checked!,
        }
      );
    } catch (err: any) {
      showAlertDialog(this, { text: err.message });
      checkbox.checked = !checkbox.checked;
    }
  }

  private async _handleCameraOrientationChanged(ev) {
    try {
      this._cameraPrefs = await updateCameraPrefs(
        this.hass,
        this.entry.entity_id,
        {
          orientation: ev.currentTarget.value,
        }
      );
    } catch (err: any) {
      showAlertDialog(this, { text: err.message });
    }
  }

  private _enabledChanged(ev: CustomEvent): void {
    if ((ev.target as any).checked) {
      this._disabledBy = null;
    } else {
      this._disabledBy = "user";
    }
  }

  private _hiddenChanged(ev: CustomEvent): void {
    if ((ev.target as any).checked) {
      this._hiddenBy = null;
    } else {
      this._hiddenBy = "user";
    }
  }

  private _openDeviceSettings() {
    showDeviceRegistryDetailDialog(this, {
      device: this._device!,
      updateEntry: async (updates) => {
        await updateDeviceRegistryEntry(this.hass, this._device!.id, updates);
      },
    });
  }

  private _handleVoiceAssistantsClicked() {
    showVoiceAssistantsView(
      this,
      this.hass.localize("ui.dialogs.entity_registry.editor.voice_assistants")
    );
  }

  private async _showOptionsFlow() {
    showOptionsFlowDialog(this, this.helperConfigEntry!, {
      manifest: await fetchIntegrationManifest(
        this.hass,
        this.helperConfigEntry!.domain
      ),
    });
  }

  private _switchAsDomainsSorted = memoizeOne(
    (domains: string[], localize: LocalizeFunc) =>
      domains
        .map((domain) => ({
          domain,
          label: domainToName(localize, domain),
        }))
        .sort((a, b) =>
          stringCompare(a.label, b.label, this.hass.locale.language)
        )
  );

  private _deviceClassesSorted = memoizeOne(
    (domain: string, deviceClasses: string[], localize: LocalizeFunc) =>
      deviceClasses
        .map((entry) => ({
          deviceClass: entry,
          label: localize(
            `ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}`
          ),
        }))
        .sort((a, b) =>
          stringCompare(a.label, b.label, this.hass.locale.language)
        )
  );

  static get styles(): CSSResultGroup {
    return [
      haStyle,
      css`
        :host {
          display: block;
        }
        ha-textfield.entityId {
          --text-field-prefix-padding-right: 0;
          --textfield-icon-trailing-padding: 0;
        }
        ha-textfield.entityId ha-icon-button {
          position: relative;
          right: -8px;
          --mdc-icon-button-size: 36px;
          --mdc-icon-size: 20px;
          color: var(--secondary-text-color);
          inset-inline-start: initial;
          inset-inline-end: -8px;
          direction: var(--direction);
        }
        ha-switch {
          margin-right: 16px;
          margin-inline-end: 16px;
          margin-inline-start: initial;
        }
        ha-settings-row ha-switch {
          margin-right: 0;
          margin-inline-end: 0;
          margin-inline-start: initial;
        }
        ha-textfield,
        ha-icon-picker,
        ha-select,
        ha-area-picker {
          display: block;
          margin: 8px 0;
          width: 100%;
        }
        li[divider] {
          border-bottom-color: var(--divider-color);
        }
        .menu-item {
          border-radius: 4px;
          margin-top: 3px;
          margin-bottom: 3px;
          overflow: hidden;
          --mdc-list-side-padding: 13px;
        }
      `,
    ];
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "entity-registry-settings-editor": EntityRegistrySettingsEditor;
  }
}
