import {
  mdiCellphoneKey,
  mdiDeleteOutline,
  mdiDevices,
  mdiDotsVertical,
  mdiInformationOutline,
} from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { stringCompare } from "../../../../../common/string/compare";
import { extractSearchParam } from "../../../../../common/url/search-params";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
import "../../../../../components/ha-dropdown-item";
import { getSignedPath } from "../../../../../data/auth";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../../../data/diagnostics";
import type { OTBRInfo, OTBRInfoDict } from "../../../../../data/otbr";
import {
  OTBRCreateNetwork,
  OTBRSetChannel,
  OTBRSetNetwork,
  getOTBRInfo,
} from "../../../../../data/otbr";
import type { ThreadDataSet, ThreadRouter } from "../../../../../data/thread";
import {
  addThreadDataSet,
  getThreadDataSetTLV,
  listThreadDataSets,
  removeThreadDataSet,
  setPreferredBorderAgent,
  setPreferredThreadDataSet,
  subscribeDiscoverThreadRouters,
} from "../../../../../data/thread";
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
import {
  showAlertDialog,
  showConfirmationDialog,
  showPromptDialog,
} from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
import { documentationUrl } from "../../../../../util/documentation-url";
import { fileDownload } from "../../../../../util/file_download";
import { showThreadDatasetDialog } from "./show-dialog-thread-dataset";

export interface ThreadNetwork {
  name: string;
  dataset?: ThreadDataSet;
  routers?: ThreadRouter[];
}

@customElement("thread-config-panel")
export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
  @property({ attribute: false }) public hass!: HomeAssistant;

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

  @state() private _configEntryId: string | null = null;

  @state() private _routers: ThreadRouter[] = [];

  @state() private _datasets: ThreadDataSet[] = [];

  @state() private _otbrInfo?: OTBRInfoDict;

  protected render(): TemplateResult {
    const networks = this._groupRoutersByNetwork(this._routers, this._datasets);

    return html`
      <hass-subpage
        .narrow=${this.narrow}
        .hass=${this.hass}
        header="Thread"
        back-path="/config"
      >
        <ha-dropdown slot="toolbar-icon">
          <ha-icon-button
            .path=${mdiDotsVertical}
            slot="trigger"
            .label=${this.hass.localize("ui.common.menu")}
          ></ha-icon-button>
          <a
            href=${getConfigEntryDiagnosticsDownloadUrl(
              this._configEntryId || ""
            )}
            target="_blank"
            rel="noreferrer"
            @click=${this._signUrl}
          >
            <ha-dropdown-item>
              ${this.hass.localize(
                "ui.panel.config.integrations.config_entry.download_diagnostics"
              )}
            </ha-dropdown-item>
          </a>
          <ha-dropdown-item @click=${this._addTLV}
            >${this.hass.localize(
              "ui.panel.config.thread.add_dataset_from_tlv"
            )}</ha-dropdown-item
          >
          <ha-dropdown-item @click=${this._addOTBR}
            >${this.hass.localize(
              "ui.panel.config.thread.add_open_thread_border_router"
            )}</ha-dropdown-item
          >
        </ha-dropdown>
        <div class="content">
          <h2>${this.hass.localize("ui.panel.config.thread.my_network")}</h2>
          ${networks.preferred
            ? this._renderNetwork(networks.preferred)
            : html`<ha-card>
                <div class="card-content no-routers">
                  <h3>
                    ${this.hass.localize(
                      "ui.panel.config.thread.no_preferred_network"
                    )}
                  </h3>
                  <ha-svg-icon .path=${mdiDevices}></ha-svg-icon>
                  <ha-button
                    appearance="plain"
                    size="s"
                    href=${documentationUrl(this.hass, `/integrations/thread`)}
                    target="_blank"
                  >
                    ${this.hass.localize(
                      "ui.panel.config.thread.more_info"
                    )}</ha-button
                  >
                </div>
              </ha-card>`}
          ${networks.networks.length
            ? html`<h3>
                  ${this.hass.localize("ui.panel.config.thread.other_networks")}
                </h3>
                ${networks.networks.map((network) =>
                  this._renderNetwork(network)
                )}`
            : ""}
          ${this.hass.auth.external?.config.canImportThreadCredentials
            ? html`<h3>
                  ${this.hass.localize(
                    "ui.panel.config.thread.thread_network_send_credentials_ha"
                  )}
                </h3>
                <ha-card>
                  <div class="card-content">
                    ${this.hass.localize(
                      "ui.panel.config.thread.thread_network_send_credentials_ha_description"
                    )}
                  </div>
                  <div class="card-actions">
                    <ha-button
                      size="s"
                      @click=${this._importExternalThreadCredentials}
                    >
                      ${this.hass.localize(
                        "ui.panel.config.thread.thread_network_send_credentials_ha"
                      )}
                    </ha-button>
                  </div>
                </ha-card>`
            : nothing}
        </div>
      </hass-subpage>
    `;
  }

  private _renderNetwork(network: ThreadNetwork) {
    const otbrForNetwork =
      this._otbrInfo &&
      network.dataset &&
      ((network.dataset.preferred_extended_address &&
        this._otbrInfo[network.dataset.preferred_extended_address]) ||
        Object.values(this._otbrInfo).find(
          (otbr) => otbr.extended_pan_id === network.dataset!.extended_pan_id
        ));
    const canImportKeychain =
      this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain;

    return html`<ha-card>
      <div class="card-header">
        ${network.name}${network.dataset
          ? html`<div>
              <ha-icon-button
                .label=${this.hass.localize(
                  "ui.panel.config.thread.thread_network_info"
                )}
                .otbr=${otbrForNetwork}
                .network=${network}
                .path=${mdiInformationOutline}
                @click=${this._showDatasetInfo}
              ></ha-icon-button
              >${!network.dataset.preferred && !network.routers?.length
                ? html`<ha-icon-button
                    .label=${this.hass.localize(
                      "ui.panel.config.thread.thread_network_delete_credentials"
                    )}
                    .networkDataset=${network.dataset}
                    .path=${mdiDeleteOutline}
                    @click=${this._removeDataset}
                  ></ha-icon-button>`
                : ""}
            </div>`
          : ""}
      </div>
      ${network.routers?.length
        ? html`<div class="card-content routers">
              <h4>
                ${this.hass.localize("ui.panel.config.thread.border_routers", {
                  count: network.routers.length,
                })}
              </h4>
            </div>
            ${network.routers.map((router) => {
              const otbr =
                this._otbrInfo && this._otbrInfo[router.extended_address];
              const showDefaultRouter = !!network.dataset;
              const isDefaultRouter =
                showDefaultRouter &&
                router.extended_address ===
                  network.dataset!.preferred_extended_address;
              const showOverflow = showDefaultRouter || otbr;
              return html`<ha-list-item
                class="router"
                twoline
                graphic="avatar"
                .hasMeta=${showOverflow}
              >
                <img
                  slot="graphic"
                  .src=${brandsUrl(
                    {
                      domain: router.brand,
                      type: "icon",
                      darkOptimized: this.hass.themes?.darkMode,
                    },
                    this.hass.auth.data.hassUrl
                  )}
                  alt=${router.brand}
                  crossorigin="anonymous"
                  referrerpolicy="no-referrer"
                  @error=${this._onImageError}
                  @load=${this._onImageLoad}
                />
                ${router.instance_name ||
                router.model_name ||
                router.server?.replace(".local.", "") ||
                ""}
                <span slot="secondary">${router.server}</span>
                ${showOverflow
                  ? html`${isDefaultRouter
                        ? html`<ha-svg-icon
                            .path=${mdiCellphoneKey}
                            .title=${this.hass.localize(
                              "ui.panel.config.thread.default_router"
                            )}
                          ></ha-svg-icon>`
                        : ""}
                      <ha-dropdown
                        slot="meta"
                        .network=${network}
                        .router=${router}
                        .otbr=${otbr}
                        @wa-select=${this._handleRouterAction}
                      >
                        <ha-icon-button
                          .label=${this.hass.localize(
                            "ui.common.overflow_menu"
                          )}
                          .path=${mdiDotsVertical}
                          slot="trigger"
                        ></ha-icon-button>
                        ${showDefaultRouter
                          ? html`<ha-dropdown-item
                              value="set-default"
                              .disabled=${isDefaultRouter}
                            >
                              ${isDefaultRouter
                                ? this.hass.localize(
                                    "ui.panel.config.thread.default_router"
                                  )
                                : this.hass.localize(
                                    "ui.panel.config.thread.set_default_router"
                                  )}
                            </ha-dropdown-item>`
                          : ""}
                        ${otbr
                          ? html`<ha-dropdown-item value="reset-router">
                                ${this.hass.localize(
                                  "ui.panel.config.thread.reset_border_router"
                                )}</ha-dropdown-item
                              >
                              <ha-dropdown-item value="change-channel">
                                ${this.hass.localize(
                                  "ui.panel.config.thread.change_channel"
                                )}</ha-dropdown-item
                              >
                              ${network.dataset?.preferred
                                ? ""
                                : html`<ha-dropdown-item value="add-to-network">
                                    ${this.hass.localize(
                                      "ui.panel.config.thread.add_to_my_network"
                                    )}
                                  </ha-dropdown-item>`}`
                          : ""}
                      </ha-dropdown>`
                  : ""}
              </ha-list-item>`;
            })}`
        : html`<div class="card-content no-routers">
            <ha-svg-icon .path=${mdiDevices}></ha-svg-icon>
            ${otbrForNetwork
              ? html`${this.hass.localize(
                    "ui.panel.config.thread.no_routers_otbr_network"
                  )}
                  <ha-button
                    appearance="plain"
                    size="s"
                    .otbr=${otbrForNetwork}
                    @click=${this._resetBorderRouterEvent}
                    >${this.hass.localize(
                      "ui.panel.config.thread.reset_border_router"
                    )}</ha-button
                  >`
              : this.hass.localize("ui.panel.config.thread.no_border_routers")}
          </div> `}
      ${network.dataset && !network.dataset.preferred
        ? html`<div class="card-actions">
            <ha-button
              size="s"
              .datasetId=${network.dataset.dataset_id}
              @click=${this._setPreferred}
              >${this.hass.localize(
                "ui.panel.config.thread.thread_network_make_preferred"
              )}</ha-button
            >
          </div>`
        : ""}
      ${canImportKeychain &&
      network.dataset?.preferred &&
      network.routers?.length
        ? html`<div class="card-actions">
            <p class="send-to-phone-description">
              ${this.hass.localize(
                "ui.panel.config.thread.thread_network_send_credentials_phone_description"
              )}
            </p>
            <ha-button
              size="s"
              .networkDataset=${network.dataset}
              @click=${this._sendCredentials}
              >${this.hass.localize(
                "ui.panel.config.thread.thread_network_send_credentials_phone"
              )}</ha-button
            >
          </div>`
        : ""}
    </ha-card>`;
  }

  private async _sendCredentials(ev) {
    const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
    if (!dataset) {
      return;
    }
    if (
      !dataset.preferred_extended_address &&
      !dataset.preferred_border_agent_id
    ) {
      showAlertDialog(this, {
        title: "Error",
        text: this.hass.localize("ui.panel.config.thread.no_preferred_router"),
      });
      return;
    }
    this.hass.auth.external!.fireMessage({
      type: "thread/store_in_platform_keychain",
      payload: {
        mac_extended_address: dataset.preferred_extended_address,
        border_agent_id: dataset.preferred_border_agent_id,
        active_operational_dataset: (
          await getThreadDataSetTLV(this.hass, dataset.dataset_id)
        ).tlv,
        extended_pan_id: dataset.extended_pan_id,
      },
    });
  }

  private async _showDatasetInfo(ev: Event) {
    const network = (ev.currentTarget as any).network as ThreadNetwork;
    const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
    showThreadDatasetDialog(this, { network, otbrInfo: otbr });
  }

  private _importExternalThreadCredentials() {
    this.hass.auth.external!.fireMessage({
      type: "thread/import_credentials",
    });
  }

  private _onImageError(ev) {
    ev.target.style.display = "none";
  }

  private _onImageLoad(ev) {
    ev.target.style.display = "";
  }

  hassSubscribe() {
    return [
      subscribeDiscoverThreadRouters(this.hass, (routers: ThreadRouter[]) => {
        this._routers = routers;
      }),
    ];
  }

  protected override firstUpdated(changedProps: PropertyValues<this>) {
    super.firstUpdated(changedProps);

    this._refresh();

    this._configEntryId = extractSearchParam("config_entry");
  }

  private _groupRoutersByNetwork = memoizeOne(
    (
      routers: ThreadRouter[],
      datasets: ThreadDataSet[]
    ): { preferred?: ThreadNetwork; networks: ThreadNetwork[] } => {
      let preferred: ThreadNetwork | undefined;
      const networks: Record<string, ThreadNetwork> = {};
      for (const router of routers) {
        const network = router.extended_pan_id;
        if (network in networks) {
          networks[network].routers!.push(router);
        } else {
          networks[network] = {
            name: router.network_name || "",
            routers: [router],
          };
        }
      }
      for (const dataset of datasets) {
        const network = dataset.extended_pan_id;
        if (!network) {
          continue;
        }
        if (dataset.preferred) {
          preferred = {
            name: dataset.network_name,
            dataset: dataset,
            routers: networks[network]?.routers,
          };
          delete networks[network];
          continue;
        }
        if (network in networks) {
          networks[network].dataset = dataset;
        } else {
          networks[network] = { name: dataset.network_name, dataset: dataset };
        }
      }
      return {
        preferred,
        networks: Object.values(networks).sort((a, b) =>
          stringCompare(a.name, b.name, this.hass.locale.language)
        ),
      };
    }
  );

  private async _refresh() {
    listThreadDataSets(this.hass).then((datasets) => {
      this._datasets = datasets.datasets;
    });
    if (!isComponentLoaded(this.hass.config, "otbr")) {
      return;
    }
    try {
      this._otbrInfo = await getOTBRInfo(this.hass);
    } catch (_err) {
      this._otbrInfo = undefined;
    }
  }

  private async _signUrl(ev) {
    const anchor = ev.target.closest("a");
    ev.preventDefault();
    const signedUrl = await getSignedPath(
      this.hass,
      anchor.getAttribute("href")
    );
    fileDownload(signedUrl.path);
  }

  private _addOTBR() {
    showConfigFlowDialog(this, {
      dialogClosedCallback: () => {
        this._refresh();
      },
      startFlowHandler: "otbr",
    });
  }

  private _handleRouterAction(ev: HaDropdownSelectEvent) {
    const network = (ev.currentTarget as any).network as ThreadNetwork;
    const router = (ev.currentTarget as any).router as ThreadRouter;
    const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
    const action = ev.detail.item.value;
    switch (action) {
      case "set-default":
        this._setPreferredBorderAgent(network.dataset!, router);
        break;
      case "reset-router":
        this._resetBorderRouter(otbr);
        break;
      case "change-channel":
        this._changeChannel(otbr);
        break;
      case "add-to-network":
        this._setDataset(otbr);
        break;
    }
  }

  private _resetBorderRouterEvent(ev) {
    const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
    this._resetBorderRouter(otbr);
  }

  private async _resetBorderRouter(otbr: OTBRInfo) {
    const confirm = await showConfirmationDialog(this, {
      title: this.hass.localize(
        "ui.panel.config.thread.confirm_reset_border_router"
      ),
      text: this.hass.localize(
        "ui.panel.config.thread.confirm_reset_border_router_text"
      ),
    });
    if (!confirm) {
      return;
    }
    try {
      await OTBRCreateNetwork(this.hass, otbr.extended_address);
    } catch (err: any) {
      showAlertDialog(this, {
        title: this.hass.localize("ui.panel.config.thread.otbr_config_failed"),
        text: err.message,
      });
    }
    this._refresh();
  }

  private async _setDataset(otbr: OTBRInfo) {
    const networks = this._groupRoutersByNetwork(this._routers, this._datasets);
    const preferedDatasetId = networks.preferred?.dataset?.dataset_id;
    if (!preferedDatasetId) {
      return;
    }
    const confirm = await showConfirmationDialog(this, {
      title: this.hass.localize(
        "ui.panel.config.thread.confirm_set_dataset_border_router"
      ),
      text: this.hass.localize(
        "ui.panel.config.thread.confirm_set_dataset_border_router_text"
      ),
    });
    if (!confirm) {
      return;
    }
    try {
      await OTBRSetNetwork(this.hass, otbr.extended_address, preferedDatasetId);
    } catch (err: any) {
      showAlertDialog(this, {
        title: this.hass.localize("ui.panel.config.thread.otbr_config_failed"),
        text: err.message,
      });
    }
    this._refresh();
  }

  private async _setPreferred(ev) {
    const datasetId = ev.target.datasetId;
    await setPreferredThreadDataSet(this.hass, datasetId);
    this._refresh();
  }

  private async _setPreferredBorderAgent(
    dataset: ThreadDataSet,
    router: ThreadRouter
  ) {
    await setPreferredBorderAgent(
      this.hass,
      dataset.dataset_id,
      router.border_agent_id,
      router.extended_address
    );
    this._refresh();
  }

  private async _addTLV() {
    const tlv = await showPromptDialog(this, {
      title: this.hass.localize("ui.panel.config.thread.add_dataset"),
      inputLabel: this.hass.localize(
        "ui.panel.config.thread.add_dataset_label"
      ),
      confirmText: this.hass.localize(
        "ui.panel.config.thread.add_dataset_button"
      ),
    });
    if (!tlv) {
      return;
    }
    try {
      await addThreadDataSet(this.hass, "manual", tlv);
    } catch (err: any) {
      showAlertDialog(this, {
        title: "Error",
        text: err.message || err,
      });
    }
    this._refresh();
  }

  private async _removeDataset(ev: Event) {
    const dataset = (ev.currentTarget as any).networkDataset as ThreadDataSet;
    const confirm = await showConfirmationDialog(this, {
      title: this.hass.localize(
        "ui.panel.config.thread.confirm_delete_dataset",
        { name: dataset.network_name }
      ),
      text: this.hass.localize(
        "ui.panel.config.thread.confirm_delete_dataset_text"
      ),
      destructive: true,
      confirmText: this.hass.localize("ui.common.delete"),
    });
    if (!confirm) {
      return;
    }
    try {
      await removeThreadDataSet(this.hass, dataset.dataset_id);
    } catch (err: any) {
      showAlertDialog(this, {
        title: "Error",
        text: err.message || err,
      });
    }
    this._refresh();
  }

  private async _changeChannel(otbr: OTBRInfo) {
    const currentChannel = otbr.channel;
    const channelStr = await showPromptDialog(this, {
      title: this.hass.localize("ui.panel.config.thread.change_channel"),
      text: this.hass.localize("ui.panel.config.thread.change_channel_text"),
      inputLabel: this.hass.localize(
        "ui.panel.config.thread.change_channel_label"
      ),
      confirmText: this.hass.localize("ui.panel.config.thread.change_channel"),
      inputType: "number",
      inputMin: "11",
      inputMax: "26",
      defaultValue: currentChannel ? currentChannel.toString() : undefined,
    });
    if (!channelStr) {
      return;
    }
    const channel = parseInt(channelStr);
    if (channel < 11 || channel > 26) {
      showAlertDialog(this, {
        title: this.hass.localize(
          "ui.panel.config.thread.change_channel_invalid"
        ),
        text: this.hass.localize("ui.panel.config.thread.change_channel_range"),
      });
      return;
    }
    try {
      const result = await OTBRSetChannel(
        this.hass,
        otbr.extended_address,
        channel
      );
      showAlertDialog(this, {
        title: this.hass.localize(
          "ui.panel.config.thread.change_channel_initiated_title"
        ),
        text: this.hass.localize(
          "ui.panel.config.thread.change_channel_initiated_text",
          { delay: Math.floor(result.delay / 60) }
        ),
      });
    } catch (err: any) {
      if (err.code === "multiprotocol_enabled") {
        showAlertDialog(this, {
          title: this.hass.localize(
            "ui.panel.config.thread.change_channel_multiprotocol_enabled_title"
          ),
          text: this.hass.localize(
            "ui.panel.config.thread.change_channel_multiprotocol_enabled_text"
          ),
        });
        return;
      }
      showAlertDialog(this, {
        title: "Error",
        text: err.message || err,
      });
    }
    this._refresh();
  }

  static styles = [
    haStyle,
    css`
      .content {
        padding: 24px 8px 32px;
        max-width: 600px;
        margin: 0 auto;
        direction: ltr;
      }
      ha-list-item.router {
        --mdc-list-item-meta-size: auto;
        --mdc-list-item-meta-display: flex;
        --mdc-list-side-padding: 16px;
        cursor: default;
        overflow: visible;
      }
      ha-list-item img {
        border-radius: var(--ha-border-radius-square);
      }
      ha-svg-icon[slot="meta"] {
        width: 24px;
      }
      ha-dropdown a {
        text-decoration: none;
      }
      .routers {
        padding-bottom: 0;
      }
      .no-routers {
        display: flex;
        flex-direction: column;
        align-items: center;
        text-align: center;
      }
      .no-routers ha-svg-icon {
        background-color: var(--light-primary-color);
        color: var(--secondary-text-color);
        padding: 16px;
        border-radius: var(--ha-border-radius-circle);
        margin-bottom: 8px;
      }
      ha-card {
        margin-bottom: 16px;
      }
      h3 {
        margin-top: var(--ha-space-8);
      }
      h4 {
        margin: 0;
      }
      .card-header {
        display: flex;
        justify-content: space-between;
      }

      .send-to-phone-description {
        color: var(--secondary-text-color);
        font-size: var(--ha-font-size-s);
      }
    `,
  ];
}

declare global {
  interface HTMLElementTagNameMap {
    "thread-config-panel": ThreadConfigPanel;
  }
}
