import type {
  Completion,
  CompletionContext,
  CompletionInfo,
  CompletionResult,
  CompletionSource,
} from "@codemirror/autocomplete";
import { undo, undoDepth, redo, redoDepth } from "@codemirror/commands";
import type { Extension, TransactionSpec } from "@codemirror/state";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import {
  mdiArrowExpand,
  mdiArrowCollapse,
  mdiContentCopy,
  mdiUndo,
  mdiRedo,
} from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, ReactiveElement, html, render } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { getEntityContext } from "../common/entity/context/get_entity_context";
import { copyToClipboard } from "../common/util/copy-clipboard";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import "./ha-code-editor-completion-items";
import type { CompletionItem } from "./ha-code-editor-completion-items";
import "./ha-icon";
import "./ha-icon-button-toolbar";
import type { HaIconButtonToolbar } from "./ha-icon-button-toolbar";

declare global {
  interface HASSDomEvents {
    "editor-save": undefined;
  }
}

const saveKeyBinding: KeyBinding = {
  key: "Mod-s",
  run: (view: EditorView) => {
    fireEvent(view.dom, "editor-save");
    return true;
  },
};

const renderIcon = (completion: Completion) => {
  const icon = document.createElement("ha-icon");
  icon.icon = completion.label;
  return icon;
};

@customElement("ha-code-editor")
export class HaCodeEditor extends ReactiveElement {
  public codemirror?: EditorView;

  @property() public mode = "yaml";

  public hass?: HomeAssistant;

  // eslint-disable-next-line lit/no-native-attributes
  @property({ type: Boolean }) public autofocus = false;

  @property({ attribute: "read-only", type: Boolean }) public readOnly = false;

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

  @property({ type: Boolean, attribute: "autocomplete-entities" })
  public autocompleteEntities = false;

  @property({ type: Boolean, attribute: "autocomplete-icons" })
  public autocompleteIcons = false;

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

  @property({ type: Boolean, attribute: "disable-fullscreen" })
  public disableFullscreen = false;

  @property({ type: Boolean, attribute: "has-toolbar" })
  public hasToolbar = true;

  @state() private _value = "";

  @state() private _isFullscreen = false;

  @state() private _canUndo = false;

  @state() private _canRedo = false;

  @state() private _canCopy = false;

  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
  private _loadedCodeMirror?: typeof import("../resources/codemirror");

  private _editorToolbar?: HaIconButtonToolbar;

  private _iconList?: Completion[];

  public set value(value: string) {
    this._value = value;
  }

  public get value(): string {
    return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
  }

  public get hasComments(): boolean {
    if (!this.codemirror || !this._loadedCodeMirror) {
      return false;
    }
    const className = this._loadedCodeMirror.highlightingFor(
      this.codemirror.state,
      [this._loadedCodeMirror.tags.comment]
    );
    return !!this.renderRoot.querySelector(`span.${className}`);
  }

  public connectedCallback() {
    super.connectedCallback();
    // Force update on reconnection so editor is recreated
    if (this.hasUpdated) {
      this.requestUpdate();
    }
    this.addEventListener("keydown", stopPropagation);
    this.addEventListener("keydown", this._handleKeyDown);
    // This is unreachable as editor will not exist yet,
    // but focus should not behave like this for good a11y.
    // (@steverep to fix in autofocus PR)
    if (!this.codemirror) {
      return;
    }
    if (this.autofocus !== false) {
      this.codemirror.focus();
    }
  }

  public disconnectedCallback() {
    super.disconnectedCallback();
    this.removeEventListener("keydown", stopPropagation);
    this.removeEventListener("keydown", this._handleKeyDown);
    this._updateFullscreenState(false);
    this.updateComplete.then(() => {
      this.codemirror!.destroy();
      delete this.codemirror;
    });
  }

  // Ensure CodeMirror module is loaded before any update
  protected override async scheduleUpdate() {
    this._loadedCodeMirror ??= await import("../resources/codemirror");
    super.scheduleUpdate();
  }

  protected update(changedProps: PropertyValues): void {
    super.update(changedProps);
    if (!this.codemirror) {
      this._createCodeMirror();
      return;
    }
    const transactions: TransactionSpec[] = [];
    if (changedProps.has("mode")) {
      transactions.push({
        effects: [
          this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
          this._loadedCodeMirror!.foldingCompartment.reconfigure(
            this._getFoldingExtensions()
          ),
        ],
      });
    }
    if (changedProps.has("readOnly")) {
      transactions.push({
        effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
          this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
        ),
      });
      this._updateToolbarButtons();
    }
    if (changedProps.has("linewrap")) {
      transactions.push({
        effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure(
          this.linewrap ? this._loadedCodeMirror!.EditorView.lineWrapping : []
        ),
      });
    }
    if (changedProps.has("_value") && this._value !== this.value) {
      transactions.push({
        changes: {
          from: 0,
          to: this.codemirror.state.doc.length,
          insert: this._value,
        },
      });
    }
    if (transactions.length > 0) {
      this.codemirror.dispatch(...transactions);
    }
    if (changedProps.has("hasToolbar")) {
      this._updateToolbar();
    }
    if (changedProps.has("error")) {
      this.classList.toggle("error-state", this.error);
    }
    if (changedProps.has("_isFullscreen")) {
      this.classList.toggle("fullscreen", this._isFullscreen);
      this._updateToolbarButtons();
    }
    if (
      changedProps.has("_canCopy") ||
      changedProps.has("_canUndo") ||
      changedProps.has("_canRedo")
    ) {
      this._updateToolbarButtons();
    }
    if (changedProps.has("disableFullscreen")) {
      this._updateFullscreenState();
    }
  }

  private get _mode() {
    return this._loadedCodeMirror!.langs[this.mode];
  }

  private _createCodeMirror() {
    if (!this._loadedCodeMirror) {
      throw new Error("Cannot create editor before CodeMirror is loaded");
    }
    const extensions: Extension[] = [
      this._loadedCodeMirror.lineNumbers(),
      this._loadedCodeMirror.history(),
      this._loadedCodeMirror.drawSelection(),
      this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
      this._loadedCodeMirror.rectangularSelection(),
      this._loadedCodeMirror.crosshairCursor(),
      this._loadedCodeMirror.highlightSelectionMatches(),
      this._loadedCodeMirror.highlightActiveLine(),
      this._loadedCodeMirror.indentationMarkers({
        thickness: 0,
        activeThickness: 1,
        colors: {
          activeLight: "var(--secondary-text-color)",
          activeDark: "var(--secondary-text-color)",
        },
      }),
      this._loadedCodeMirror.keymap.of([
        ...this._loadedCodeMirror.defaultKeymap,
        ...this._loadedCodeMirror.searchKeymap,
        ...this._loadedCodeMirror.historyKeymap,
        ...this._loadedCodeMirror.tabKeyBindings,
        saveKeyBinding,
      ]),
      this._loadedCodeMirror.langCompartment.of(this._mode),
      this._loadedCodeMirror.haTheme,
      this._loadedCodeMirror.haSyntaxHighlighting,
      this._loadedCodeMirror.readonlyCompartment.of(
        this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
      ),
      this._loadedCodeMirror.linewrapCompartment.of(
        this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
      ),
      this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
      this._loadedCodeMirror.foldingCompartment.of(
        this._getFoldingExtensions()
      ),
    ];

    if (!this.readOnly) {
      const completionSources: CompletionSource[] = [];
      if (this.autocompleteEntities && this.hass) {
        completionSources.push(this._entityCompletions.bind(this));
      }
      if (this.autocompleteIcons) {
        completionSources.push(this._mdiCompletions.bind(this));
      }
      if (completionSources.length > 0) {
        extensions.push(
          this._loadedCodeMirror.autocompletion({
            override: completionSources,
            maxRenderedOptions: 10,
          })
        );
      }
    }

    // Create the code editor
    this.codemirror = new this._loadedCodeMirror.EditorView({
      state: this._loadedCodeMirror.EditorState.create({
        doc: this._value,
        extensions,
      }),
      parent: this.renderRoot,
    });
    this._canCopy = this._value?.length > 0;

    // Update the toolbar. Creating it if required
    this._updateToolbar();
  }

  private _fullscreenLabel(): string {
    if (this._isFullscreen)
      return (
        this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
        "Exit fullscreen"
      );
    return (
      this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
      "Enter fullscreen"
    );
  }

  private _fullscreenIcon(): string {
    return this._isFullscreen ? mdiArrowCollapse : mdiArrowExpand;
  }

  private _createEditorToolbar(): HaIconButtonToolbar {
    // Create the editor toolbar element
    const editorToolbar = document.createElement("ha-icon-button-toolbar");
    editorToolbar.classList.add("code-editor-toolbar");
    editorToolbar.items = [];
    return editorToolbar;
  }

  private _updateToolbar() {
    // Show/Hide the toolbar if we have one.
    this.classList.toggle("hasToolbar", this.hasToolbar);

    // Update fullscreen state. Handles toolbar and fullscreen mode being disabled.
    this._updateFullscreenState();

    // If we don't have a toolbar, nothing to update
    if (!this.hasToolbar) {
      return;
    }

    // If we don't yet have the toolbar, create it.
    if (!this._editorToolbar) {
      this._editorToolbar = this._createEditorToolbar();
    }

    // Ensure all toolbar buttons are correctly configured.
    this._updateToolbarButtons();

    // Render the toolbar. This must be placed as a child of the code
    // mirror element to ensure it doesn't affect the positioning and
    // size of codemirror.
    this.codemirror?.dom.appendChild(this._editorToolbar);
  }

  private _updateToolbarButtons() {
    // Re-render all toolbar items.
    if (!this._editorToolbar) {
      return;
    }

    this._editorToolbar.items = [
      {
        id: "undo",
        disabled: !this._canUndo,
        label: this.hass?.localize("ui.common.undo") || "Undo",
        path: mdiUndo,
        action: (e: Event) => this._handleUndoClick(e),
      },
      {
        id: "redo",
        disabled: !this._canRedo,
        label: this.hass?.localize("ui.common.redo") || "Redo",
        path: mdiRedo,
        action: (e: Event) => this._handleRedoClick(e),
      },
      {
        id: "copy",
        disabled: !this._canCopy,
        label:
          this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
          "Copy to Clipboard",
        path: mdiContentCopy,
        action: (e: Event) => this._handleClipboardClick(e),
      },
      {
        id: "fullscreen",
        disabled: this.disableFullscreen,
        label: this._fullscreenLabel(),
        path: this._fullscreenIcon(),
        action: (e: Event) => this._handleFullscreenClick(e),
      },
    ];
  }

  private _updateFullscreenState(
    fullscreen: boolean = this._isFullscreen
  ): boolean {
    // Update the current fullscreen state based on selected value. If fullscreen
    // is disabled, or we have no toolbar, ensure we are not in fullscreen mode.
    this._isFullscreen =
      fullscreen && !this.disableFullscreen && this.hasToolbar;
    // Return whether successfully in requested state
    return this._isFullscreen === fullscreen;
  }

  private _handleClipboardClick = async (e: Event) => {
    e.preventDefault();
    e.stopPropagation();
    if (this.value) {
      await copyToClipboard(this.value);
      showToast(this, {
        message:
          this.hass?.localize("ui.common.copied_clipboard") ||
          "Copied to clipboard",
      });
    }
  };

  private _handleUndoClick = (e: Event) => {
    e.preventDefault();
    e.stopPropagation();
    if (!this.codemirror) {
      return;
    }
    undo(this.codemirror);
  };

  private _handleRedoClick = (e: Event) => {
    e.preventDefault();
    e.stopPropagation();
    if (!this.codemirror) {
      return;
    }
    redo(this.codemirror);
  };

  private _handleFullscreenClick = (e: Event) => {
    e.preventDefault();
    e.stopPropagation();
    this._updateFullscreenState(!this._isFullscreen);
  };

  private _handleKeyDown = (e: KeyboardEvent) => {
    if (
      (e.key === "Escape" &&
        this._isFullscreen &&
        this._updateFullscreenState(false)) ||
      (e.key === "F11" && this._updateFullscreenState(true))
    ) {
      // If we successfully performed the action, stop it propagating further.
      e.preventDefault();
      e.stopPropagation();
    }
  };

  private _renderInfo = (completion: Completion): CompletionInfo => {
    const key = completion.label;
    const context = getEntityContext(
      this.hass!.states[key],
      this.hass!.entities,
      this.hass!.devices,
      this.hass!.areas,
      this.hass!.floors
    );

    const completionInfo = document.createElement("div");
    completionInfo.classList.add("completion-info");

    const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);

    const completionItems: CompletionItem[] = [
      {
        label: this.hass!.localize(
          "ui.components.entity.entity-state-picker.state"
        ),
        value: formattedState,
        subValue:
          // If the state exactly matches the formatted state, don't show the raw state
          this.hass!.states[key].state === formattedState
            ? undefined
            : this.hass!.states[key].state,
      },
    ];

    if (context.device && context.device.name) {
      completionItems.push({
        label: this.hass!.localize("ui.components.device-picker.device"),
        value: context.device.name,
      });
    }

    if (context.area && context.area.name) {
      completionItems.push({
        label: this.hass!.localize("ui.components.area-picker.area"),
        value: context.area.name,
      });
    }

    if (context.floor && context.floor.name) {
      completionItems.push({
        label: this.hass!.localize("ui.components.floor-picker.floor"),
        value: context.floor.name,
      });
    }

    render(
      html`
        <ha-code-editor-completion-items
          .items=${completionItems}
        ></ha-code-editor-completion-items>
      `,
      completionInfo
    );

    return completionInfo;
  };

  private _getStates = memoizeOne((states: HassEntities): Completion[] => {
    if (!states) {
      return [];
    }

    const options = Object.keys(states).map((key) => ({
      type: "variable",
      label: key,
      detail: states[key].attributes.friendly_name,
      info: this._renderInfo,
    }));

    return options;
  });

  private _entityCompletions(
    context: CompletionContext
  ): CompletionResult | null | Promise<CompletionResult | null> {
    // Check for YAML mode and entity-related fields
    if (this.mode === "yaml") {
      const currentLine = context.state.doc.lineAt(context.pos);
      const lineText = currentLine.text;

      // Properties that commonly contain entity IDs
      const entityProperties = [
        "entity_id",
        "entity",
        "entities",
        "badges",
        "devices",
        "lights",
        "light",
        "group_members",
        "scene",
        "zone",
        "zones",
      ];

      // Create regex pattern for all entity properties
      const propertyPattern = entityProperties.join("|");
      const entityFieldRegex = new RegExp(
        `^\\s*(-\\s+)?(${propertyPattern}):\\s*`
      );

      // Check if we're in an entity field (single entity or list item)
      const entityFieldMatch = lineText.match(entityFieldRegex);
      const listItemMatch = lineText.match(/^\s*-\s+/);

      if (entityFieldMatch) {
        // Calculate the position after the entity field
        const afterField = currentLine.from + entityFieldMatch[0].length;

        // If cursor is after the entity field, show all entities
        if (context.pos >= afterField) {
          const states = this._getStates(this.hass!.states);

          if (!states || !states.length) {
            return null;
          }

          // Find what's already typed after the field
          const typedText = context.state.sliceDoc(afterField, context.pos);

          // Filter states based on what's typed
          const filteredStates = typedText
            ? states.filter((entityState) =>
                entityState.label
                  .toLowerCase()
                  .startsWith(typedText.toLowerCase())
              )
            : states;

          return {
            from: afterField,
            options: filteredStates,
            validFor: /^[a-z_]*\.?\w*$/,
          };
        }
      } else if (listItemMatch) {
        // Check if this is a list item under an entity_id field
        const lineNumber = currentLine.number;

        // Look at previous lines to check if we're under an entity_id field
        for (let i = lineNumber - 1; i > 0 && i >= lineNumber - 10; i--) {
          const prevLine = context.state.doc.line(i);
          const prevText = prevLine.text;

          // Stop if we hit a non-indented line (new field)
          if (
            prevText.trim() &&
            !prevText.startsWith(" ") &&
            !prevText.startsWith("\t")
          ) {
            break;
          }

          // Check if we found an entity property field
          const entityListFieldRegex = new RegExp(
            `^\\s*(${propertyPattern}):\\s*$`
          );
          if (prevText.match(entityListFieldRegex)) {
            // We're in a list under an entity field
            const afterListMarker = currentLine.from + listItemMatch[0].length;

            if (context.pos >= afterListMarker) {
              const states = this._getStates(this.hass!.states);

              if (!states || !states.length) {
                return null;
              }

              // Find what's already typed after the list marker
              const typedText = context.state.sliceDoc(
                afterListMarker,
                context.pos
              );

              // Filter states based on what's typed
              const filteredStates = typedText
                ? states.filter((entityState) =>
                    entityState.label
                      .toLowerCase()
                      .startsWith(typedText.toLowerCase())
                  )
                : states;

              return {
                from: afterListMarker,
                options: filteredStates,
                validFor: /^[a-z_]*\.?\w*$/,
              };
            }
          }
        }
      }
    }

    // Original entity completion logic for non-YAML or when not in entity_id field
    const entityWord = context.matchBefore(/[a-z_]{3,}\.\w*/);

    if (
      !entityWord ||
      (entityWord.from === entityWord.to && !context.explicit)
    ) {
      return null;
    }

    const states = this._getStates(this.hass!.states);

    if (!states || !states.length) {
      return null;
    }

    return {
      from: Number(entityWord.from),
      options: states,
      validFor: /^[a-z_]{3,}\.\w*$/,
    };
  }

  private _getIconItems = async (): Promise<Completion[]> => {
    if (!this._iconList) {
      let iconList: {
        name: string;
        keywords: string[];
      }[];
      if (__SUPERVISOR__) {
        iconList = [];
      } else {
        iconList = (await import("../../build/mdi/iconList.json")).default;
      }

      this._iconList = iconList.map((icon) => ({
        type: "variable",
        label: `mdi:${icon.name}`,
        detail: icon.keywords.join(", "),
        info: renderIcon,
      }));
    }

    return this._iconList;
  };

  private async _mdiCompletions(
    context: CompletionContext
  ): Promise<CompletionResult | null> {
    const match = context.matchBefore(/mdi:\S*/);

    if (!match || (match.from === match.to && !context.explicit)) {
      return null;
    }

    const iconItems = await this._getIconItems();

    return {
      from: Number(match.from),
      options: iconItems,
      validFor: /^mdi:\S*$/,
    };
  }

  private _onUpdate = (update: ViewUpdate): void => {
    this._canUndo = !this.readOnly && undoDepth(update.state) > 0;
    this._canRedo = !this.readOnly && redoDepth(update.state) > 0;
    if (!update.docChanged) {
      return;
    }
    this._value = update.state.doc.toString();
    this._canCopy = this._value?.length > 0;
    fireEvent(this, "value-changed", { value: this._value });
  };

  private _getFoldingExtensions = (): Extension => {
    if (this.mode === "yaml") {
      return [
        this._loadedCodeMirror!.foldGutter(),
        this._loadedCodeMirror!.foldingOnIndent,
      ];
    }

    return [];
  };

  static styles = css`
    :host {
      position: relative;
      display: block;
      --code-editor-toolbar-height: 28px;
    }

    :host(.error-state) .cm-gutters {
      border-color: var(--error-state-color, var(--error-color)) !important;
    }

    :host(.hasToolbar) .cm-gutters {
      padding-top: 0;
    }

    :host(.hasToolbar) .cm-focused .cm-gutters {
      padding-top: 1px;
    }

    :host(.error-state) .cm-content {
      border-color: var(--error-state-color, var(--error-color)) !important;
    }

    :host(.hasToolbar) .cm-content {
      border: none;
      border-top: 1px solid var(--secondary-text-color);
    }

    :host(.hasToolbar) .cm-focused .cm-content {
      border-top: 2px solid var(--primary-color);
      padding-top: 15px;
    }

    :host(.fullscreen) {
      position: fixed !important;
      top: calc(var(--header-height, 56px) + 8px) !important;
      left: 8px !important;
      right: 8px !important;
      bottom: 8px !important;
      z-index: 6;
      border-radius: 12px !important;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
      overflow: hidden !important;
      background-color: var(
        --code-editor-background-color,
        var(--card-background-color)
      ) !important;
      margin: 0 !important;
      padding-top: var(--safe-area-inset-top) !important;
      padding-left: var(--safe-area-inset-left) !important;
      padding-right: var(--safe-area-inset-right) !important;
      padding-bottom: var(--safe-area-inset-bottom) !important;
      box-sizing: border-box !important;
      display: block !important;
    }

    :host(.hasToolbar) .cm-editor {
      padding-top: var(--code-editor-toolbar-height);
    }

    :host(.fullscreen) .cm-editor {
      height: 100% !important;
      max-height: 100% !important;
      border-radius: 0 !important;
    }

    :host(:not(.hasToolbar)) .code-editor-toolbar {
      display: none !important;
    }

    .code-editor-toolbar {
      --icon-button-toolbar-height: var(--code-editor-toolbar-height);
      --icon-button-toolbar-color: var(
        --code-editor-gutter-color,
        var(--secondary-background-color, whitesmoke)
      );
      border-top-left-radius: var(--ha-border-radius-sm);
      border-top-right-radius: var(--ha-border-radius-sm);
    }

    .completion-info {
      display: grid;
      gap: 3px;
      padding: 8px;
    }

    /* Hide completion info on narrow screens */
    @media (max-width: 600px) {
      .cm-completionInfo,
      .completion-info {
        display: none;
      }
    }
  `;
}

declare global {
  interface HTMLElementTagNameMap {
    "ha-code-editor": HaCodeEditor;
  }
}
