import { mdiDotsVertical } from "@mdi/js";
import { defineScalarTag, YAML11_SCHEMA } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-form/ha-form";
import type {
  HaFormDataContainer,
  HaFormSchema,
} from "../../../../../components/ha-form/types";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
import type {
  HassioAddonDetails,
  HassioAddonSetOptionParams,
} from "../../../../../data/hassio/addon";
import {
  setHassioAddonOption,
  validateHassioAddonOption,
} from "../../../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
import type { ObjectSelector, Selector } from "../../../../../data/selector";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { DirtyStateProviderMixin } from "../../../../../mixins/dirty-state-provider-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";

const SUPPORTED_UI_TYPES = [
  "string",
  "select",
  "boolean",
  "integer",
  "float",
  "schema",
];

const secretTag = defineScalarTag("!secret", {
  resolve: (data) => `!secret ${data}`,
});

const ADDON_YAML_SCHEMA = YAML11_SCHEMA.withTags(secretTag);

const MASKED_FIELDS = ["password", "secret", "token"];

@customElement("supervisor-app-config")
class SupervisorAppConfig extends DirtyStateProviderMixin<
  Record<string, unknown>
>()(LitElement) {
  @property({ attribute: false }) public addon!: HassioAddonDetails;

  @property({ attribute: false }) public hass!: HomeAssistant;

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

  @state() private _valid = true;

  @state() private _canShowSchema = false;

  @state() private _showOptional = false;

  @state() private _error?: string;

  @state() private _options?: Record<string, unknown>;

  @state() private _yamlMode = false;

  @query("ha-yaml-editor") private _editor?: HaYamlEditor;

  private _getTranslationEntry(
    language: string,
    entry: HaFormSchema,
    options?: { path?: string[] }
  ) {
    let parent = this.addon.translations[language]?.configuration;
    if (!parent) return undefined;
    if (options?.path) {
      for (const key of options.path) {
        parent = parent[key]?.fields;
        if (!parent) return undefined;
      }
    }
    return parent[entry.name];
  }

  public computeLabel = (
    entry: HaFormSchema,
    _data: HaFormDataContainer,
    options?: { path?: string[] }
  ): string =>
    this._getTranslationEntry(this.hass.language, entry, options)?.name ||
    this._getTranslationEntry("en", entry, options)?.name ||
    entry.name;

  public computeHelper = (
    entry: HaFormSchema,
    options?: { path?: string[] }
  ): string =>
    this._getTranslationEntry(this.hass.language, entry, options)
      ?.description ||
    this._getTranslationEntry("en", entry, options)?.description ||
    "";

  private _convertSchema = memoizeOne(
    // Convert supervisor schema to selectors
    (
      schema: readonly HaFormSchema[],
      language: string,
      translations: HassioAddonDetails["translations"]
    ): HaFormSchema[] =>
      this._convertSchemaElements(schema, language, translations)
  );

  private _convertSchemaElements(
    schema: readonly HaFormSchema[],
    language: string,
    translations: HassioAddonDetails["translations"],
    path: string[] = []
  ): HaFormSchema[] {
    return schema.map((entry) =>
      this._convertSchemaElement(entry, language, translations, path)
    );
  }

  private _convertSchemaElement(
    entry: any,
    language: string,
    translations: HassioAddonDetails["translations"],
    path: string[]
  ): HaFormSchema {
    if (entry.type === "schema" && !entry.multiple) {
      return {
        name: entry.name,
        type: "expandable",
        required: entry.required,
        schema: this._convertSchemaElements(
          entry.schema,
          language,
          translations,
          [...path, entry.name]
        ),
      };
    }
    const selector = this._convertSchemaElementToSelector(
      entry,
      false,
      language,
      translations,
      path
    );
    if (selector) {
      return {
        name: entry.name,
        required: entry.required,
        selector,
      };
    }
    return entry;
  }

  private _convertSchemaElementToSelector(
    entry: any,
    force: boolean,
    language: string,
    translations: HassioAddonDetails["translations"],
    path: string[]
  ): Selector | null {
    if (entry.type === "select") {
      return { select: { options: entry.options, multiple: entry.multiple } };
    }
    if (entry.type === "string") {
      return entry.multiple
        ? { select: { options: [], multiple: true, custom_value: true } }
        : {
            text: {
              type: entry.format
                ? entry.format
                : MASKED_FIELDS.includes(entry.name)
                  ? "password"
                  : "text",
            },
          };
    }
    if (entry.type === "boolean") {
      return { boolean: {} };
    }
    if (entry.type === "schema") {
      const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
      const childPath = [...path, entry.name];
      for (const childEntry of entry.schema) {
        const translation =
          this._getTranslationEntry(language, childEntry, {
            path: childPath,
          }) ||
          this._getTranslationEntry("en", childEntry, { path: childPath });

        fields[childEntry.name] = {
          required: childEntry.required,
          label: translation?.name,
          description: translation?.description,
          selector: this._convertSchemaElementToSelector(
            childEntry,
            true,
            language,
            translations,
            childPath
          )!,
        };
      }
      return {
        object: {
          multiple: entry.multiple,
          fields,
        },
      };
    }
    if (entry.type === "float" || entry.type === "integer") {
      return {
        number: {
          mode: "box",
          step: entry.type === "float" ? "any" : undefined,
        },
      };
    }
    if (force) {
      return { object: {} };
    }
    return null;
  }

  private _filteredSchema = memoizeOne(
    (options: Record<string, unknown>, schema: HaFormSchema[]) =>
      schema.filter((entry) => entry.name in options || entry.required)
  );

  protected render(): TemplateResult {
    const showForm =
      !this._yamlMode && this._canShowSchema && this.addon.schema;
    const hasHiddenOptions =
      showForm &&
      JSON.stringify(this.addon.schema) !==
        JSON.stringify(
          this._filteredSchema(this.addon.options, this.addon.schema!)
        );
    return html`
      <h1>${this.addon.name}</h1>
      <ha-card outlined>
        <div class="header">
          <h2>
            ${this.hass.localize(
              "ui.panel.config.apps.configuration.options.header"
            )}
          </h2>
          <div class="card-menu">
            <ha-dropdown @wa-select=${this._handleAction}>
              <ha-icon-button
                .label=${this.hass.localize("ui.common.menu")}
                .path=${mdiDotsVertical}
                slot="trigger"
              ></ha-icon-button>
              <ha-dropdown-item
                value="toggle_yaml"
                .disabled=${!this._canShowSchema || this.disabled}
              >
                ${
                  this._yamlMode
                    ? this.hass.localize(
                        "ui.panel.config.apps.configuration.options.edit_in_ui"
                      )
                    : this.hass.localize(
                        "ui.panel.config.apps.configuration.options.edit_in_yaml"
                      )
                }
              </ha-dropdown-item>
              <ha-dropdown-item
                value="reset"
                .variant=${!this.disabled ? "danger" : "default"}
                .disabled=${this.disabled}
              >
                ${this.hass.localize(
                  "ui.panel.config.apps.configuration.reset_defaults"
                )}
              </ha-dropdown-item>
            </ha-dropdown>
          </div>
        </div>

        <div class="card-content">
          ${
            showForm
              ? html`<ha-form
                  .hass=${this.hass}
                  .disabled=${this.disabled}
                  .data=${this._options!}
                  @value-changed=${this._configChanged}
                  .computeLabel=${this.computeLabel}
                  .computeHelper=${this.computeHelper}
                  .schema=${this._convertSchema(
                    this._showOptional
                      ? this.addon.schema!
                      : this._filteredSchema(
                          this.addon.options,
                          this.addon.schema!
                        ),
                    this.hass.language,
                    this.addon.translations
                  )}
                ></ha-form>`
              : html`<ha-yaml-editor
                  @value-changed=${this._configChanged}
                  .yamlSchema=${ADDON_YAML_SCHEMA}
                ></ha-yaml-editor>`
          }
          ${
            this._error
              ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
              : ""
          }
          ${
            !this._yamlMode ||
            (this._canShowSchema && this.addon.schema) ||
            this._valid
              ? ""
              : html`
                  <ha-alert alert-type="error">
                    ${this.hass.localize(
                      "ui.panel.config.apps.configuration.options.invalid_yaml"
                    )}
                  </ha-alert>
                `
          }
        </div>
        ${
          hasHiddenOptions
            ? html`<ha-formfield
                class="show-additional"
                .label=${this.hass.localize(
                  "ui.panel.config.apps.configuration.options.show_unused_optional"
                )}
              >
                <ha-switch
                  @change=${this._toggleOptional}
                  .checked=${this._showOptional}
                >
                </ha-switch>
              </ha-formfield>`
            : ""
        }
        <div class="card-actions right">
          <ha-progress-button
            @click=${this._saveTapped}
            .disabled=${this.disabled || !this.isDirtyState || !this._valid}
          >
            ${this.hass.localize("ui.common.save")}
          </ha-progress-button>
        </div>
      </ha-card>
    `;
  }

  protected firstUpdated(changedProps: PropertyValues<this>) {
    super.firstUpdated(changedProps);
    this._canShowSchema =
      this.addon.schema !== null &&
      !this.addon.schema!.find(
        (entry) =>
          // @ts-ignore
          !SUPPORTED_UI_TYPES.includes(entry.type)
      );
    this._yamlMode = !this._canShowSchema;
  }

  protected updated(changedProperties: PropertyValues): void {
    if (changedProperties.has("addon")) {
      this._options = { ...this.addon.options };
      this._initDirtyTracking({ type: "deep" }, this.addon.options);
    }
    super.updated(changedProperties);
    if (
      changedProperties.has("_yamlMode") ||
      changedProperties.has("_options")
    ) {
      if (this._yamlMode) {
        const editor = this._editor;
        if (editor) {
          editor.setValue(this._options!);
        }
      }
    }
  }

  private _handleAction(ev: HaDropdownSelectEvent) {
    const action = ev.detail.item.value;

    if (!action) {
      return;
    }

    switch (action) {
      case "toggle_yaml":
        this._yamlMode = !this._yamlMode;
        return;
      case "reset":
        this._resetTapped(ev);
    }
  }

  private _toggleOptional() {
    this._showOptional = !this._showOptional;
  }

  private _configChanged(ev): void {
    if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
      this._valid = true;
      this._options = ev.detail.value;
      this._updateDirtyState(ev.detail.value);
    } else {
      this._valid = ev.detail.isValid;
      if (ev.detail.isValid) {
        this._updateDirtyState(ev.detail.value);
      }
    }
  }

  private async _resetTapped(ev: CustomEvent): Promise<void> {
    const button = ev.currentTarget as any;
    button.progress = true;

    const confirmed = await showConfirmationDialog(this, {
      title: this.hass.localize(
        "ui.panel.config.apps.configuration.confirm.reset_options.title"
      ),
      text: this.hass.localize(
        "ui.panel.config.apps.configuration.confirm.reset_options.text"
      ),
      confirmText: this.hass.localize("ui.common.reset_options"),
      dismissText: this.hass.localize("ui.common.cancel"),
      destructive: true,
    });

    if (!confirmed) {
      button.progress = false;
      return;
    }

    this._error = undefined;
    const data: HassioAddonSetOptionParams = {
      options: null,
    };
    try {
      await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
      this._markDirtyStateClean();
      const eventdata = {
        success: true,
        response: undefined,
        path: "options",
      };
      fireEvent(this, "hass-api-called", eventdata);
    } catch (err: any) {
      this._error = this.hass.localize(
        "ui.panel.config.apps.dashboard.failed_to_reset",
        {
          error: extractApiErrorMessage(err),
        }
      );
    }
    button.progress = false;
  }

  private async _saveTapped(ev: CustomEvent): Promise<void> {
    if (this.disabled || !this.isDirtyState || !this._valid) {
      return;
    }

    const button = ev.currentTarget as any;
    const options: Record<string, unknown> = this._yamlMode
      ? this._editor?.value
      : this._options;
    const eventdata = {
      success: true,
      response: undefined,
      path: "options",
    };
    button.progress = true;

    this._error = undefined;

    try {
      const validation = await validateHassioAddonOption(
        this.hass.callWS,
        this.addon.slug,
        options
      );
      if (!validation.valid) {
        throw Error(validation.message);
      }
      await setHassioAddonOption(this.hass.callWS, this.addon.slug, {
        options,
      });

      this._markDirtyStateClean();
      if (this.addon?.state === "started") {
        await suggestSupervisorAppRestart(this, this.hass, this.addon);
      }
    } catch (err: any) {
      this._error = this.hass.localize(
        "ui.panel.config.apps.dashboard.failed_to_save",
        {
          error: extractApiErrorMessage(err),
        }
      );
      eventdata.success = false;
    }
    button.progress = false;
    fireEvent(this, "hass-api-called", eventdata);
  }

  static get styles(): CSSResultGroup {
    return [
      haStyle,
      supervisorAppsStyle,
      css`
        :host {
          display: block;
        }
        ha-card {
          display: block;
        }
        .card-actions {
          display: flex;
          justify-content: space-between;
        }

        .card-menu {
          float: right;
          z-index: 3;
          --mdc-theme-text-primary-on-background: var(--primary-text-color);
        }
        ha-dropdown-item[disabled] {
          --mdc-theme-text-primary-on-background: var(--disabled-text-color);
        }
        .header {
          display: flex;
          justify-content: space-between;
        }
        .header h2 {
          color: var(--ha-card-header-color, var(--primary-text-color));
          font-family: var(--ha-card-header-font-family, inherit);
          font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
          letter-spacing: -0.012em;
          line-height: var(--ha-line-height-expanded);
          padding: 12px 16px 16px;
          display: block;
          margin-block: 0px;
          font-weight: var(--ha-font-weight-normal);
        }
        .card-actions.right {
          justify-content: flex-end;
        }

        .show-additional {
          padding: 16px;
        }
      `,
    ];
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "supervisor-app-config": SupervisorAppConfig;
  }
}
