import { ItemView, WorkspaceLeaf, type EventRef, setIcon } from "obsidian";
import { ICON_NAME, VIEW_TYPE_SIDEPANEL } from "src/constants/constants";
import ExcalidrawPlugin from "src/core/main";
import { t } from "src/lang/helpers";
import type { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { getLastActiveExcalidrawView } from "src/utils/excalidrawViewLookup";
import type ExcalidrawView from "src/view/ExcalidrawView";
import { ExcalidrawSidepanelTab } from "./SidepanelTab";
import { hideElement, showElement } from "src/utils/styleUtils";

type TabCreationConfig = {
  title: string;
  scriptName?: string;
  reuseExisting?: boolean;
  hostEA?: ExcalidrawAutomate;
};

export class ExcalidrawSidepanelView extends ItemView {
  private static singleton: ExcalidrawSidepanelView | null = null;
  private static restoreSilent = false;
  private static restoreSkipScripts = new Set<string>();

  /**
   * Returns the singleton sidepanel view if it exists, optionally revealing the leaf unless restoration is running silently.
   */
  public static getExisting(
    reveal: boolean = true,
  ): ExcalidrawSidepanelView | null {
    const spView = ExcalidrawSidepanelView.singleton;
    if (spView && reveal && !ExcalidrawSidepanelView.restoreSilent) {
      void spView.plugin.app.workspace.revealLeaf(spView.leaf);
    }
    return spView;
  }

  /**
   * Ensures a sidepanel view exists (creating it if needed), optionally revealing it, and waits until DOM is ready.
   */
  public static async getOrCreate(
    plugin: ExcalidrawPlugin,
    reveal: boolean = true,
  ): Promise<ExcalidrawSidepanelView | null> {
    const effectiveReveal = reveal && !ExcalidrawSidepanelView.restoreSilent;
    let leaf = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_SIDEPANEL)[0];
    if (!leaf) {
      leaf = plugin.app.workspace.getRightLeaf(false);
      if (!leaf) {
        return null;
      }
      await leaf.setViewState({
        type: VIEW_TYPE_SIDEPANEL,
        active: effectiveReveal,
      });
    }
    let spView = leaf.view;
    if (!(spView instanceof ExcalidrawSidepanelView)) {
      // Obsidian startup sequence. Leaves constructors are not called until they are revealed.
      // So when getOrCreate is called from a script, we may have a leaf of the right type but with a generic ItemView.
      await leaf.setViewState({ type: VIEW_TYPE_SIDEPANEL, active: true });
      spView = leaf.view;
      if (!(spView instanceof ExcalidrawSidepanelView)) {
        return null;
      }
    }
    if (effectiveReveal) {
      await plugin.app.workspace.revealLeaf(leaf);
    }
    await spView.waitUntilReady();
    return spView;
  }

  /**
   * Cleans up singleton references and detaches sidepanel leaves when the plugin unloads.
   */
  public static onPluginUnload(plugin: ExcalidrawPlugin) {
    ExcalidrawSidepanelView.singleton = null;
    ExcalidrawSidepanelView.restoreSkipScripts.clear();
    plugin.app.workspace.detachLeavesOfType(VIEW_TYPE_SIDEPANEL);
  }

  /**
   * Queues a script to be skipped once during persisted sidepanel restoration.
   */
  public static skipScriptRestore(scriptName?: string): void {
    if (!scriptName) {
      return;
    }
    ExcalidrawSidepanelView.restoreSkipScripts.add(scriptName);
  }

  /**
   * Returns whether a script restore should be skipped and consumes the skip marker.
   */
  private static consumeScriptRestoreSkip(scriptName: string): boolean {
    if (!scriptName) {
      return false;
    }
    if (!ExcalidrawSidepanelView.restoreSkipScripts.has(scriptName)) {
      return false;
    }
    ExcalidrawSidepanelView.restoreSkipScripts.delete(scriptName);
    return true;
  }

  private tabs = new Map<string, ExcalidrawSidepanelTab>();
  private scriptTabs = new Map<string, ExcalidrawSidepanelTab>();
  private tabHosts = new Map<string, ExcalidrawAutomate>();
  private tabOptions = new Map<string, HTMLOptionElement>();
  private persistedScripts = new Map<string, { title: string }>();
  private activeTabId: string | null = null;
  private selectEl: HTMLSelectElement | null = null;
  private closeButtonEl: HTMLButtonElement | null = null;
  private bodyEl: HTMLDivElement | null = null;
  private emptyStateEl: HTMLDivElement | null = null;
  private readyPromise: Promise<void>;
  private resolveReady: (() => void) | null = null;
  private restorePromise: Promise<void> | null = null;
  private leafChangeRef: EventRef | null = null;
  private windowMigrationCleanup: (() => void) | null = null;
  private restoreRetryTimer: number | null = null;

  constructor(
    leaf: WorkspaceLeaf,
    private plugin: ExcalidrawPlugin,
  ) {
    super(leaf);
    this.loadPersistedScriptsFromSettings();
    this.readyPromise = new Promise((resolve) => {
      this.resolveReady = resolve;
    });
  }

  getViewType(): string {
    return VIEW_TYPE_SIDEPANEL;
  }

  getDisplayText(): string {
    return t("EXCALIDRAW_SIDEPANEL") ?? "Excalidraw Sidepanel";
  }

  getIcon(): string {
    return ICON_NAME;
  }

  protected async onOpen() {
    await super.onOpen();
    const hasWindowMigrationAPI =
      typeof this.containerEl.onWindowMigrated === "function";
    if (hasWindowMigrationAPI) {
      this.windowMigrationCleanup = this.containerEl.onWindowMigrated(
        (win: Window) => {
          this.notifyTabsWindowMigrated(win);
        },
      );
    }
    ExcalidrawSidepanelView.singleton = this;
    this.containerEl.empty();
    this.containerEl.addClass("excalidraw-sidepanel-container");
    const wrapper = this.containerEl.createDiv({ cls: "excalidraw-sidepanel" });
    const controls = wrapper.createDiv({
      cls: "excalidraw-sidepanel__controls",
    });
    this.selectEl = controls.createEl("select", {
      cls: "excalidraw-sidepanel__select",
    });
    this.selectEl.addEventListener("change", () => {
      const targetId = this.selectEl?.value ?? null;
      const tab = targetId ? (this.tabs.get(targetId) ?? null) : null;
      this.setActiveTab(tab ?? null);
    });
    this.closeButtonEl = controls.createEl("button", {
      cls: "clickable-icon",
      attr: { type: "button", "aria-label": "Close sidepanel tab" },
    });
    setIcon(this.closeButtonEl, "x");
    this.closeButtonEl.addEventListener("click", () => {
      if (!this.activeTabId) {
        return;
      }
      const active = this.tabs.get(this.activeTabId);
      if (active) {
        this.removeTab(active);
      }
    });
    this.bodyEl = wrapper.createDiv({ cls: "excalidraw-sidepanel__body" });
    this.emptyStateEl = this.bodyEl.createDiv({
      cls: "excalidraw-sidepanel__empty",
    });
    this.emptyStateEl.setText(
      "Excalidraw sidepanel is empty. Run a panel-enabled Excalidraw script to add a panel.",
    );
    this.selectEl.disabled = true;
    this.closeButtonEl.disabled = true;
    this.leafChangeRef = this.app.workspace.on("active-leaf-change", (leaf) => {
      if (leaf === this.leaf) {
        this.triggerActiveTabFocus();
      }
    });
    this.resolveReady?.();
    this.resolveReady = null;
    if (this.app.workspace.layoutReady) {
      await this.restorePersistedTabs();
    } else {
      await this.restorePersistedTabs();
    }
    this.updateEmptyStateVisibility();
  }

  /**
   * Tears down listeners, destroys tabs, and clears UI references when the sidepanel closes.
   */
  protected async onClose() {
    await super.onClose();
    if (this.restoreRetryTimer !== null) {
      window.clearTimeout(this.restoreRetryTimer);
      this.restoreRetryTimer = null;
    }
    if (this.windowMigrationCleanup) {
      this.windowMigrationCleanup();
      this.windowMigrationCleanup = null;
    }
    if (this.leafChangeRef) {
      this.app.workspace.offref(this.leafChangeRef);
      this.leafChangeRef = null;
    }
    this.tabs.forEach((tab) => {
      tab.notifyWillClose();
      const hostEA = this.tabHosts.get(tab.id);
      if (hostEA && hostEA.sidepanelTab === tab) {
        hostEA.sidepanelTab = null;
      }
      this.tabHosts.delete(tab.id);
      tab.destroy();
    });
    this.tabs.clear();
    this.scriptTabs.clear();
    this.tabOptions.clear();
    this.tabHosts.clear();
    this.activeTabId = null;
    this.selectEl = null;
    this.closeButtonEl = null;
    this.bodyEl = null;
    this.emptyStateEl = null;
    this.containerEl.onWindowMigrated = null;
    if (ExcalidrawSidepanelView.singleton === this) {
      ExcalidrawSidepanelView.singleton = null;
    }
  }

  public async waitUntilReady(): Promise<void> {
    await this.readyPromise;
  }

  /**
   * Creates or reuses a tab based on script identity and returns the active tab instance.
   */
  public async createTab(
    config: TabCreationConfig = { title: "unknown" },
  ): Promise<ExcalidrawSidepanelTab> {
    await this.waitUntilReady();
    const scriptName = config.scriptName;
    const reuse = config.reuseExisting !== false;
    if (scriptName && reuse) {
      const existing = this.scriptTabs.get(scriptName);
      if (existing) {
        existing.reset(config.title);
        this.setActiveTab(existing);
        if (config.hostEA) {
          this.tabHosts.set(existing.id, config.hostEA);
        }
        return existing;
      }
    }
    const tab = this.createTabInternal(config.title, config.hostEA, scriptName);
    if (config.hostEA) {
      this.tabHosts.set(tab.id, config.hostEA);
    }
    return tab;
  }

  /**
   * Marks a tab as persistent so its script is restored across sessions.
   */
  public markTabPersistent(tab: ExcalidrawSidepanelTab) {
    const scriptName = tab.scriptName;
    if (!scriptName) {
      return;
    }
    this.scriptTabs.set(scriptName, tab);
    const title = tab.title;
    const existing = this.persistedScripts.get(scriptName);
    if (!existing || existing.title !== title) {
      this.persistedScripts.set(scriptName, { title });
      this.savePersistentScripts();
    }
  }

  /**
   * Removes persistence metadata for a script-backed tab.
   */
  public unmarkTabPersistent(scriptName?: string) {
    if (!scriptName) {
      return;
    }
    if (this.persistedScripts.delete(scriptName)) {
      this.savePersistentScripts();
    }
  }

  /**
   * Removes a tab from the UI and state, running close hooks and updating active selection.
   */
  public removeTab(tab: ExcalidrawSidepanelTab) {
    tab.notifyWillClose();
    this.tabs.delete(tab.id);
    this.removeTabOption(tab);
    const scriptName = tab.scriptName;
    if (scriptName && this.scriptTabs.get(scriptName) === tab) {
      this.scriptTabs.delete(scriptName);
      this.unmarkTabPersistent(scriptName);
    }
    const hostEA = this.tabHosts.get(tab.id);
    if (hostEA && hostEA.sidepanelTab === tab) {
      hostEA.sidepanelTab = null;
    }
    this.tabHosts.delete(tab.id);
    tab.destroy();
    if (this.activeTabId === tab.id) {
      this.activeTabId = null;
      let nextTab: ExcalidrawSidepanelTab | null = null;
      for (const t of this.tabs.values()) {
        nextTab = t;
        break; // Grab just the first one
      }
      this.setActiveTab(nextTab);
    }
    this.updateEmptyStateVisibility();
  }

  /**
   * Activates the given tab (or clears active selection) and syncs UI controls.
   */
  public setActiveTab(tab: ExcalidrawSidepanelTab | null) {
    this.activeTabId = tab?.id ?? null;
    this.tabs.forEach((candidate) => candidate.setActive(candidate === tab));
    if (this.selectEl) {
      this.selectEl.value = tab?.id ?? "";
    }
    this.updateEmptyStateVisibility();
  }

  /**
   * Checks if there are any ExcalidrawAutomate instances hosted by tabs targeting the specified view.
   */
  public hasEATargetingView(view: ExcalidrawView): boolean {
    for (const ea of this.tabHosts.values()) {
      if (ea && ea.targetView === view) return true;
    }
    return false;
  }

  /**
   * Executes a callback for each ExcalidrawAutomate instance targeting the specified view.
   * Avoids allocating intermediate arrays for performance.
   */
  public forEachEATargetingView(
    view: ExcalidrawView,
    cb: (ea: ExcalidrawAutomate) => void,
  ): void {
    for (const ea of this.tabHosts.values()) {
      if (ea && ea.targetView === view) cb(ea);
    }
  }

  /**
   * Returns a tab by its backing script name if present.
   */
  public getTabByScript(scriptName: string): ExcalidrawSidepanelTab | null {
    return this.scriptTabs.get(scriptName) ?? null;
  }

  /**
   * Checks whether a script has been marked for persistent restoration.
   */
  public hasPersistentScript(scriptName: string): boolean {
    return this.persistedScripts.has(scriptName);
  }

  private createTabInternal(
    title: string,
    ea: ExcalidrawAutomate,
    scriptName?: string,
  ): ExcalidrawSidepanelTab {
    if (!this.bodyEl) {
      throw new Error("Sidepanel DOM is not ready");
    }
    const tab = new ExcalidrawSidepanelTab(
      title,
      {
        activate: (target, reveal) => {
          this.setActiveTab(target);
          if (reveal) {
            this.reveal();
          }
        },
        close: (target) => this.removeTab(target),
        updateTitle: (target) => this.updateTabOptionTitle(target),
        plugin: this.plugin,
        ea,
        isVisible: () => this.leaf.isVisible?.(),
        reveal: () => this.reveal(),
      },
      { bodyEl: this.bodyEl },
      scriptName,
    );
    if (scriptName) {
      this.scriptTabs.set(scriptName, tab);
    }
    this.tabs.set(tab.id, tab);
    this.addTabOption(tab);
    this.setActiveTab(tab);
    return tab;
  }

  /**
   * Restores persisted tabs on startup without revealing the panel, executing scripts to rebuild content.
   */
  private async restorePersistedTabs(): Promise<void> {
    if (this.restorePromise !== null) {
      return this.restorePromise;
    }
    this.restorePromise = (async () => {
      const restoreReady = await this.awaitRestoreReadiness();
      if (!restoreReady) {
        this.queueRestoreRetry();
        return;
      }
      this.loadPersistedScriptsFromSettings();
      if (!this.persistedScripts.size) {
        return;
      }
      ExcalidrawSidepanelView.restoreSilent = true;
      try {
        for (const [scriptName, meta] of Array.from(
          this.persistedScripts.entries(),
        )) {
          if (!scriptName) {
            continue;
          }
          if (ExcalidrawSidepanelView.consumeScriptRestoreSkip(scriptName)) {
            continue;
          }
          if (this.getTabByScript(scriptName)) {
            continue;
          }
          await this.runScriptByName(scriptName, meta.title);
        }
      } finally {
        ExcalidrawSidepanelView.restoreSilent = false;
      }
    })();
    return this.restorePromise;
  }

  /**
   * Waits until the minimum prerequisites for sidepanel script restoration are available.
   * Sidepanel restoration is independent from Excalidraw view loading.
   */
  private async awaitRestoreReadiness(): Promise<boolean> {
    let counter = 0;
    while (
      (!this.plugin.settings || !this.plugin.scriptEngine) &&
      counter++ < 200
    ) {
      await sleep(50);
    }
    return Boolean(this.plugin.settings && this.plugin.scriptEngine);
  }

  /**
   * Re-attempts restore when sidepanel opens before plugin startup prerequisites are ready.
   */
  private queueRestoreRetry(): void {
    if (this.restoreRetryTimer !== null) {
      return;
    }
    this.restoreRetryTimer = window.setTimeout(() => {
      this.restoreRetryTimer = null;
      this.restorePromise = null;
      void this.restorePersistedTabs();
    }, 500);
  }

  /**
   * Re-runs the script backing an existing tab, typically after the script file was updated.
   */
  public async restartTabForScript(scriptName: string): Promise<void> {
    await this.waitUntilReady();
    if (!scriptName) {
      return;
    }
    const tab = this.getTabByScript(scriptName);
    if (!tab) {
      return;
    }

    const title = tab.title;
    const wasPersisted = this.persistedScripts.has(scriptName);

    // Fully terminate the existing tab and its EA host before rerunning the script
    // to avoid scripts early-exiting when they detect an already active tab.
    this.removeTab(tab);

    const previousSilent = ExcalidrawSidepanelView.restoreSilent;
    ExcalidrawSidepanelView.restoreSilent = true;
    try {
      await this.runScriptByName(scriptName, title);
      const restarted = this.getTabByScript(scriptName);
      if (wasPersisted && restarted) {
        this.markTabPersistent(restarted);
      }
    } finally {
      ExcalidrawSidepanelView.restoreSilent = previousSilent;
    }
  }

  /**
   * Executes a script by name to reconstruct its sidepanel tab, updating title if needed.
   */
  private async runScriptByName(scriptName: string, title: string) {
    const scriptEngine = this.plugin.scriptEngine;
    if (!scriptEngine) {
      return;
    }
    const file = scriptEngine.getScriptFileByName(scriptName);
    if (!file) {
      console.warn(
        `Excalidraw: could not restore sidepanel script '${scriptName}' because the file was not found.`,
      );
      this.persistedScripts.delete(scriptName);
      this.savePersistentScripts();
      return;
    }
    try {
      const script = await this.plugin.app.vault.read(file);
      await scriptEngine.executeScript(undefined, script, scriptName, file);
      const restoredTab = this.scriptTabs.get(scriptName);
      if (restoredTab) {
        restoredTab.setTitle(title);
      }
    } catch (error) {
      console.error(
        `Excalidraw: error while restoring sidepanel script '${scriptName}'`,
        error,
      );
    }
  }

  /**
   * Loads persisted sidepanel script descriptors from settings.
   * This may run before settings are available, in which case it leaves the map unchanged.
   */
  private loadPersistedScriptsFromSettings() {
    const sidepanelTabs = this.plugin.settings?.sidepanelTabs;
    if (!sidepanelTabs) {
      return;
    }

    this.persistedScripts.clear();
    sidepanelTabs
      .filter((entry) => !!entry)
      .forEach((entry) => {
        let scriptName = "";
        let title = "";
        try {
          const parsed: unknown = JSON.parse(entry);
          if (parsed && typeof parsed === "object") {
            const p = parsed as Record<string, unknown>;
            const pScript = typeof p.script === "string" ? p.script : "";
            const pName = typeof p.name === "string" ? p.name : "";
            const pTitle = typeof p.title === "string" ? p.title : "";

            scriptName = pScript || pName || "";
            title = pTitle || scriptName;
          }
        } catch (error) {
          console.error(
            "unexpected error in parsing sidepanel tab entry",
            entry,
            error,
          );
          if (entry.includes("::")) {
            const [namePart, ...rest] = entry.split("::");
            scriptName = namePart;
            title = rest.join("::") || namePart;
          } else {
            scriptName = entry;
            title = entry;
          }
        }
        if (scriptName) {
          this.persistedScripts.set(scriptName, { title: title || scriptName });
        }
      });
  }

  /**
   * Persists current set of script tabs to plugin settings.
   */
  private savePersistentScripts() {
    this.plugin.settings.sidepanelTabs = Array.from(
      this.persistedScripts.entries(),
    ).map(([script, meta]) =>
      JSON.stringify({ script, title: meta.title ?? script }),
    );
    void this.plugin.saveSettings();
  }

  /**
   * Triggers focus handlers for the active tab using the last active Excalidraw view.
   */
  private triggerActiveTabFocus(
    view = getLastActiveExcalidrawView(this.plugin),
  ) {
    if (!this.activeTabId) {
      return;
    }
    const active = this.tabs.get(this.activeTabId);
    if (active) {
      active.handleFocus(view ?? null);
    }
  }

  private notifyTabsWindowMigrated(win: Window) {
    this.tabs.forEach((tab) => tab.handleWindowMigration(win));
  }

  /**
   * Clears references to a view from hosted ExcalidrawAutomate instances and notifies their tabs.
   */
  public removeViewEAs(view: ExcalidrawView) {
    this.tabHosts.forEach((hostEA, tabId) => {
      if (!hostEA || hostEA.targetView !== view) {
        return;
      }
      hostEA.targetView = null;
      const tab = this.tabs.get(tabId);
      tab?.onExcalidrawViewClosed();
    });
  }

  private addTabOption(tab: ExcalidrawSidepanelTab) {
    if (!this.selectEl) {
      return;
    }
    const option = this.selectEl.createEl("option", {
      value: tab.id,
      text: tab.title,
    });
    this.tabOptions.set(tab.id, option);
    this.selectEl.disabled = false;
    if (this.closeButtonEl) {
      this.closeButtonEl.disabled = false;
    }
  }

  /**
   * Removes the select option corresponding to a tab and disables controls when empty.
   */
  private removeTabOption(tab: ExcalidrawSidepanelTab) {
    const option = this.tabOptions.get(tab.id);
    if (option) {
      option.remove();
      this.tabOptions.delete(tab.id);
    }
    if (!this.tabOptions.size && this.selectEl) {
      this.selectEl.disabled = true;
      this.selectEl.value = "";
    }
    if (!this.tabOptions.size && this.closeButtonEl) {
      this.closeButtonEl.disabled = true;
    }
  }

  /**
   * Syncs the select option label with the tab title.
   */
  private updateTabOptionTitle(tab: ExcalidrawSidepanelTab) {
    const option = this.tabOptions.get(tab.id);
    if (option) {
      option.text = tab.title;
    }
  }

  /**
   * Shows empty-state messaging and disables controls when no tabs exist.
   */
  private updateEmptyStateVisibility() {
    const hasTabs = this.tabs.size > 0;
    if (this.emptyStateEl) {
      if (hasTabs) {
        hideElement(this.emptyStateEl);
      } else {
        showElement(this.emptyStateEl);
      }
    }
    if (this.closeButtonEl) {
      this.closeButtonEl.disabled = !hasTabs;
    }
    if (this.selectEl) {
      this.selectEl.disabled = !hasTabs;
      if (!hasTabs) {
        this.selectEl.value = "";
      }
    }
  }

  public reveal(): void {
    void this.app.workspace.revealLeaf(this.leaf);
  }
}
