import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-alert";
import "../ha-selector/ha-selector";
import { getHiddenFields } from "./conditions";
import type {
  HaFormData,
  HaFormDataContainer,
  HaFormElement,
  HaFormSchema,
} from "./types";

type HaFormDataChangedEvent = ValueChangedEvent<HaFormData>;
type HaFormDataContainerChangedEvent = ValueChangedEvent<HaFormDataContainer>;

const LOAD_ELEMENTS = {
  boolean: () => import("./ha-form-boolean"),
  constant: () => import("./ha-form-constant"),
  divider: () => import("./ha-form-divider"),
  float: () => import("./ha-form-float"),
  grid: () => import("./ha-form-grid"),
  expandable: () => import("./ha-form-expandable"),
  integer: () => import("./ha-form-integer"),
  multi_select: () => import("./ha-form-multi_select"),
  positive_time_period_dict: () =>
    import("./ha-form-positive_time_period_dict"),
  select: () => import("./ha-form-select"),
  string: () => import("./ha-form-string"),
  optional_actions: () => import("./ha-form-optional_actions"),
};

const getValue = (obj, item) =>
  obj ? (!item.name || item.flatten ? obj : obj[item.name]) : undefined;

const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);

const getWarning = (obj, item) => (obj && item.name ? obj[item.name] : null);

@customElement("ha-form")
export class HaForm extends LitElement implements HaFormElement {
  @property({ attribute: false }) public hass?: HomeAssistant;

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

  @property({ attribute: false }) public data!: HaFormDataContainer;

  @property({ attribute: false }) public schema!: readonly HaFormSchema[];

  @property({ attribute: false }) public error?: Record<
    string,
    string | string[]
  >;

  @property({ attribute: false }) public warning?: Record<string, string>;

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

  @property({ attribute: false }) public computeError?: (
    schema: any,
    error
  ) => string;

  @property({ attribute: false }) public computeWarning?: (
    schema: any,
    warning
  ) => string;

  @property({ attribute: false }) public computeLabel?: (
    schema: any,
    data: HaFormDataContainer
  ) => string;

  @property({ attribute: false }) public computeHelper?: (
    schema: any
  ) => string | undefined;

  @property({ attribute: false }) public localizeValue?: (
    key: string
  ) => string;

  @property({ attribute: false }) public context?: Record<string, any>;

  protected getFormProperties(): Record<string, any> {
    return {};
  }

  static shadowRootOptions: ShadowRootInit = {
    mode: "open",
    delegatesFocus: true,
  };

  @query(".root") private _root?: HTMLElement;

  public reportValidity(): boolean {
    const root = this._root;
    if (!root) {
      return true;
    }

    const elements = [...root.children].filter(
      (child) => child.localName !== "ha-alert"
    ) as (HTMLElement & { reportValidity?: () => boolean })[];

    let isValid = true;
    let firstInvalidElement: HTMLElement | undefined;

    const hiddenFields = getHiddenFields(this.schema, this.data);
    const visibleSchema = this.schema.filter(
      (item) => !hiddenFields.has(item.name)
    );

    visibleSchema.forEach((item, index) => {
      const element = elements[index];
      if (!element) {
        return;
      }

      let elementValid = true;

      if (
        "reportValidity" in element &&
        typeof element.reportValidity === "function"
      ) {
        elementValid = element.reportValidity();
      } else if (
        item.required &&
        !(
          "type" in item && ["boolean", "constant"].includes(item.type ?? "")
        ) &&
        !(
          "selector" in item &&
          ("boolean" in item.selector || "constant" in item.selector)
        )
      ) {
        const value = getValue(this.data, item);
        elementValid = value !== undefined && value !== null && value !== "";
      }

      if (!elementValid) {
        isValid = false;
        if (!firstInvalidElement) {
          firstInvalidElement = element;
        }
      }
    });

    if (firstInvalidElement) {
      firstInvalidElement.focus?.();
    }

    return isValid;
  }

  protected willUpdate(changedProps: PropertyValues<this>) {
    if (changedProps.has("schema") && this.schema) {
      this.schema.forEach((item) => {
        if ("selector" in item) {
          return;
        }
        LOAD_ELEMENTS[item.type]?.();
      });
    }
  }

  protected render(): TemplateResult {
    const renderHiddenFields = getHiddenFields(this.schema, this.data);

    return html`
      <div class="root" part="root">
        ${
          this.error && this.error.base
            ? html`
                <ha-alert alert-type="error">
                  ${this._computeError(this.error.base, this.schema)}
                </ha-alert>
              `
            : ""
        }
        ${this.schema.map((item) => {
          if (renderHiddenFields.has(item.name)) {
            return nothing;
          }

          const error = getError(this.error, item);
          const warning = getWarning(this.warning, item);

          return html`
            ${
              error
                ? html`
                    <ha-alert own-margin alert-type="error">
                      ${this._computeError(error, item)}
                    </ha-alert>
                  `
                : warning
                  ? html`
                      <ha-alert own-margin alert-type="warning">
                        ${this._computeWarning(warning, item)}
                      </ha-alert>
                    `
                  : ""
            }
            ${
              "selector" in item
                ? html`<ha-selector
                    .schema=${item}
                    .hass=${this.hass}
                    .narrow=${this.narrow}
                    .name=${item.name}
                    .selector=${item.selector}
                    .value=${getValue(this.data, item)}
                    .label=${this._computeLabel(item, this.data)}
                    .disabled=${item.disabled || this.disabled || false}
                    .placeholder=${item.required ? undefined : item.default}
                    .helper=${this._computeHelper(item)}
                    .localizeValue=${this.localizeValue}
                    .required=${item.required || false}
                    .context=${this._generateContext(item)}
                  ></ha-selector>`
                : dynamicElement(this.fieldElementName(item.type), {
                    schema: item,
                    data: getValue(this.data, item),
                    label: this._computeLabel(item, this.data),
                    helper: this._computeHelper(item),
                    disabled: this.disabled || item.disabled || false,
                    hass: this.hass,
                    localize: this.hass?.localize,
                    computeLabel: this.computeLabel,
                    computeHelper: this.computeHelper,
                    localizeValue: this.localizeValue,
                    context: this._generateContext(item),
                    ...this.getFormProperties(),
                  })
            }
          `;
        })}
      </div>
    `;
  }

  protected fieldElementName(type: string): string {
    return `ha-form-${type}`;
  }

  private _generateContext(
    schema: HaFormSchema
  ): Record<string, any> | undefined {
    if (!schema.context && !this.context) {
      return undefined;
    }

    const context = { ...this.context };
    if (schema.context) {
      for (const [context_key, data_key] of Object.entries(schema.context)) {
        context[context_key] = this.data[data_key];
      }
    }
    return context;
  }

  protected createRenderRoot(): HTMLElement | DocumentFragment {
    const root = super.createRenderRoot() as ShadowRoot;
    // attach it as soon as possible to make sure we fetch all events.
    this.addValueChangedListener(root);
    return root;
  }

  protected addValueChangedListener(element: Element | ShadowRoot) {
    element.addEventListener("value-changed", (ev: Event) => {
      ev.stopPropagation();
      const schema = (ev.target as HaFormElement).schema as HaFormSchema;

      if (ev.target === this) return;

      const changeEv = ev as
        HaFormDataChangedEvent | HaFormDataContainerChangedEvent;
      const newValue =
        !schema.name || ("flatten" in schema && schema.flatten)
          ? (changeEv.detail.value as HaFormDataContainer)
          : { [schema.name]: changeEv.detail.value as HaFormData };

      this.data = {
        ...this.data,
        ...newValue,
      };

      fireEvent(this, "value-changed", {
        value: this.data,
      });
    });
  }

  private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) {
    return this.computeLabel
      ? this.computeLabel(schema, data)
      : schema
        ? schema.name
        : "";
  }

  private _computeHelper(schema: HaFormSchema) {
    return this.computeHelper ? this.computeHelper(schema) : "";
  }

  private _computeError(
    error: string | string[],
    schema: HaFormSchema | readonly HaFormSchema[]
  ): string | TemplateResult {
    if (Array.isArray(error)) {
      return html`<ul>
        ${error.map(
          (err) =>
            html`<li>
              ${this.computeError ? this.computeError(err, schema) : err}
            </li>`
        )}
      </ul>`;
    }
    return this.computeError ? this.computeError(error, schema) : error;
  }

  private _computeWarning(
    warning,
    schema: HaFormSchema | readonly HaFormSchema[]
  ) {
    return this.computeWarning ? this.computeWarning(warning, schema) : warning;
  }

  static styles = css`
    .root > * {
      display: block;
    }
    .root > *:not([own-margin]):not(:last-child) {
      margin-bottom: 24px;
    }
    ha-alert[own-margin] {
      margin-bottom: 4px;
    }
  `;
}

declare global {
  interface HTMLElementTagNameMap {
    "ha-form": HaForm;
  }
}
