import { mount, unmount } from "svelte";
import type { SettingsState } from "@/types/storage";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { isSeqtaEngageExperience, isSeqtaLoginPage } from "@/seqta/utils/isSeqtaEngage";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { waitForSeqtaMenu } from "@/seqta/utils/waitForSeqtaShell";
import Sidebar from "./Sidebar.svelte";
import { getNativeMenuList } from "./parseNativeMenu";
import {
  clearNativeDrillActive,
  sidebarState,
} from "./sidebarState.svelte";
import {
  applySidebarLook,
  applySidebarStyleClass,
  clearSidebarAppearance,
} from "./sidebarStyles";

const ROOT_ID = "bsplus-sidebar-root";
const MENU_CLASS = "bsplus-custom-sidebar";
const PENDING_CLASS = "bsplus-custom-sidebar-pending";

type ChangeListener = (newValue: unknown, oldValue: unknown) => void;

let app: ReturnType<typeof mount> | null = null;
let menuEl: HTMLElement | null = null;
let menuObserver: MutationObserver | null = null;
let syncTimer: ReturnType<typeof setTimeout> | null = null;
let hashListenerAttached = false;
let sidebarCaptureAttached = false;
let earlyPrepareStarted = false;
let catchupTimers: ReturnType<typeof setTimeout>[] = [];
let nativeMenuListenerAttached = false;
let lastMenuFingerprint = "";

const settingsListeners: Array<{
  key: keyof SettingsState;
  listener: ChangeListener;
}> = [];

/** Stable fingerprint of native menu keys so unchanged DOM does not re-sync. */
function menuFingerprint(menu: HTMLElement): string {
  const list = getNativeMenuList(menu);
  if (!list) return "";
  const keys: string[] = [];
  for (const node of list.children) {
    const el = node as HTMLElement;
    if (el.id === ROOT_ID) continue;
    const key = el.dataset.key ?? "";
    const path = el.dataset.path ?? "";
    const colour = el.dataset.colour ?? "";
    keys.push(`${key}|${path}|${colour}`);
  }
  return `${keys.length}:${keys.join(",")}`;
}

function syncFromMenu(force = false) {
  if (!menuEl) return;
  getNativeMenuList(menuEl)?.setAttribute("aria-hidden", "true");
  const next = menuFingerprint(menuEl);
  if (!force && next === lastMenuFingerprint) return;
  lastMenuFingerprint = next;
  sidebarState.syncFromNative(menuEl);
}

/**
 * Sparse catch-up after mount: plugins inject menu items shortly after first paint.
 * MutationObserver + scheduleSync cover ongoing changes; this only covers a short window.
 */
function startCatchupSync() {
  for (const t of catchupTimers) clearTimeout(t);
  catchupTimers = [];
  // Immediate, then a few delayed passes (~1s total) instead of 50ms×60.
  const delays = [0, 200, 500, 1000];
  for (const ms of delays) {
    catchupTimers.push(
      setTimeout(() => {
        syncFromMenu();
      }, ms),
    );
  }
  if (typeof requestIdleCallback === "function") {
    requestIdleCallback(() => syncFromMenu(), { timeout: 1500 });
  }
}

function onNativeMenuUpdated() {
  syncFromMenu(true);
}

function scheduleSync() {
  if (syncTimer) clearTimeout(syncTimer);
  syncTimer = setTimeout(() => {
    syncTimer = null;
    syncFromMenu();
  }, 50);
}

/**
 * Capture-phase: own all clicks inside the custom list so SEQTA's #menu handlers
 * never see them. Opening Goals/Folios via SEQTA + our drill UI freezes the tab.
 */
function onCustomSidebarCaptureClick(event: MouseEvent) {
  if (!menuEl || sidebarState.editMode || sidebarState.drillReturning) return;

  const root = document.getElementById(ROOT_ID);
  const target = event.target;
  if (!(target instanceof Element) || !root?.contains(target)) return;

  event.preventDefault();
  event.stopPropagation();
  event.stopImmediatePropagation();

  const back = target.closest(".back");
  if (back instanceof HTMLElement && root.contains(back)) {
    sidebarState.goBack();
    return;
  }

  const li = target.closest("li.item[data-key]");
  if (!(li instanceof HTMLElement) || !root.contains(li)) return;

  // Already-open folder chrome (renders its own `.sub`) — ignore; use Back.
  if (
    li.classList.contains("hasChildren") &&
    li.querySelector(":scope > .sub")
  ) {
    return;
  }

  const key = li.dataset.key;
  const path = li.dataset.path;
  const item = sidebarState.resolveItem(key, path);
  if (item) sidebarState.activateItem(item, menuEl);
}

function onHashChange() {
  sidebarState.syncActiveFromLocation();
  if (menuEl) clearNativeDrillActive(menuEl);

  if (!sidebarState.isDrilling) return;

  const path = sidebarState.activePath;
  if (!path) return;

  const openKey = sidebarState.drillStack[0]?.key;
  const top = sidebarState.visibleRootItems.find(
    (item) =>
      item.key === openKey ||
      item.path === path ||
      (item.path != null && path.startsWith(`${item.path}/`)),
  );

  if (openKey && top?.key === openKey) return;
  if (!path.includes("/")) sidebarState.resetDrill();
}

function ensureDefaultMenuOrder(menu: HTMLElement) {
  const list = getNativeMenuList(menu);
  if (!list) return;

  const keys = [...list.children]
    .map((node) => (node as HTMLElement).dataset.key)
    .filter((key): key is string => !!key);

  const current = settingsState.defaultmenuorder ?? [];
  if (!current.length) {
    settingsState.defaultmenuorder = keys;
    return;
  }

  const missing = keys.filter((key) => !current.includes(key));
  if (missing.length) {
    settingsState.defaultmenuorder = [...current, ...missing];
  }
}

function registerSetting(
  key: keyof SettingsState,
  listener: ChangeListener,
) {
  settingsState.register(key, listener);
  settingsListeners.push({ key, listener });
}

function clearSettingListeners() {
  for (const { key, listener } of settingsListeners) {
    settingsState.unregister(key, listener);
  }
  settingsListeners.length = 0;
}

function clearPendingClass() {
  document.documentElement.classList.remove(PENDING_CLASS);
}

/**
 * Hide the native SEQTA menu immediately (even under the loading overlay)
 * and begin mounting the Svelte sidebar as soon as `#menu` exists.
 */
export function prepareCustomSidebarEarly() {
  if (isSeqtaEngageExperience() || isSeqtaLoginPage()) return;
  if (!settingsState.onoff) return;
  if (earlyPrepareStarted) return;

  earlyPrepareStarted = true;
  document.documentElement.classList.add(PENDING_CLASS);
  // Width/blur vars before mount so layout doesn't jump.
  applySidebarLook(document.getElementById("menu"));
  void mountCustomSidebar();
}

export function openCustomSidebarEditor() {
  if (!app || !menuEl) return false;
  sidebarState.setEditMode(true);
  menuEl.classList.add("bsplus-sidebar-edit-mode");
  return true;
}

export async function mountCustomSidebar(): Promise<boolean> {
  if (isSeqtaEngageExperience()) return false;
  if (!settingsState.onoff) return false;

  // Already mounted — re-sync after Home/News/Analytics injections.
  if (app && menuEl) {
    ensureDefaultMenuOrder(menuEl);
    applySidebarStyleClass(menuEl, settingsState.sidebarStyle);
    applySidebarLook(menuEl);
    sidebarState.syncSettings();
    syncFromMenu();
    startCatchupSync();
    clearPendingClass();
    return true;
  }

  document.documentElement.classList.add(PENDING_CLASS);

  let menu: HTMLElement | null = null;
  try {
    menu = (await waitForSeqtaMenu(50, 200)) as HTMLElement;
  } catch {
    return false;
  }
  if (!menu) return false;

  // Prefer a populated native list, but don't block forever during loading.
  let list = getNativeMenuList(menu);
  if (!list) {
    await waitForElm("#menu > ul:not(#bsplus-sidebar-root)", true, 50, 120);
    list = getNativeMenuList(menu);
  }
  if (!list) return false;

  menuEl = menu;
  ensureDefaultMenuOrder(menu);
  sidebarState.syncSettings();
  syncFromMenu();

  if (app) {
    clearPendingClass();
    return true;
  }

  menu.classList.add(MENU_CLASS);
  applySidebarStyleClass(menu, settingsState.sidebarStyle);
  applySidebarLook(menu);
  document.getElementById(ROOT_ID)?.remove();

  app = mount(Sidebar, {
    target: menu,
    props: { menuEl: menu },
  });

  menu
    .querySelectorAll(".bsplus-sidebar-offscreen")
    .forEach((node) => node.classList.remove("bsplus-sidebar-offscreen"));

  menuObserver?.disconnect();
  menuObserver = new MutationObserver((mutations) => {
    const ours = document.getElementById(ROOT_ID);
    // Ignore our list entirely. Ignore native class/style churn — SEQTA and
    // theme transitions rewrite those constantly; syncing on them freezes the tab.
    const relevant = mutations.some((m) => {
      if (ours?.contains(m.target as Node)) return false;
      if (m.type === "attributes") {
        const attr = m.attributeName;
        if (attr === "class" || attr === "style") return false;
      }
      return true;
    });
    if (!relevant) return;
    scheduleSync();
  });
  menuObserver.observe(menu, {
    subtree: true,
    childList: true,
    attributes: true,
    attributeFilter: ["data-key", "data-path", "data-colour"],
  });

  if (!sidebarCaptureAttached) {
    document.addEventListener("click", onCustomSidebarCaptureClick, true);
    sidebarCaptureAttached = true;
  }

  if (!hashListenerAttached) {
    window.addEventListener("hashchange", onHashChange);
    hashListenerAttached = true;
  }

  if (!nativeMenuListenerAttached) {
    window.addEventListener("bsplus-native-menu-updated", onNativeMenuUpdated);
    nativeMenuListenerAttached = true;
  }

  clearSettingListeners();
  registerSetting("iconOnlySidebar", () => sidebarState.syncSettings());
  registerSetting("sidebarStyle", () =>
    applySidebarStyleClass(menuEl, settingsState.sidebarStyle),
  );
  for (const key of [
    "sidebarDensity",
    "sidebarCornerRadius",
    "sidebarActiveIndicator",
    "sidebarWidth",
    "sidebarBlur",
  ] as const) {
    registerSetting(key, () => applySidebarLook(menuEl));
  }
  const resync = () => syncFromMenu(true);
  registerSetting("menuorder", resync);
  registerSetting("menuitems", resync);

  startCatchupSync();
  clearPendingClass();
  return true;
}

export function unmountCustomSidebar() {
  menuObserver?.disconnect();
  menuObserver = null;
  if (syncTimer) clearTimeout(syncTimer);
  syncTimer = null;
  for (const t of catchupTimers) clearTimeout(t);
  catchupTimers = [];
  lastMenuFingerprint = "";

  clearSettingListeners();

  if (hashListenerAttached) {
    window.removeEventListener("hashchange", onHashChange);
    hashListenerAttached = false;
  }

  if (sidebarCaptureAttached) {
    document.removeEventListener("click", onCustomSidebarCaptureClick, true);
    sidebarCaptureAttached = false;
  }

  if (nativeMenuListenerAttached) {
    window.removeEventListener("bsplus-native-menu-updated", onNativeMenuUpdated);
    nativeMenuListenerAttached = false;
  }

  if (app) {
    unmount(app);
    app = null;
  }

  document.getElementById(ROOT_ID)?.remove();
  if (menuEl) {
    getNativeMenuList(menuEl)?.removeAttribute("aria-hidden");
    menuEl.classList.remove(MENU_CLASS, "bsplus-sidebar-edit-mode");
    clearSidebarAppearance(menuEl);
  }
  menuEl = null;
  sidebarState.resetDrill();
  sidebarState.setEditMode(false);
  earlyPrepareStarted = false;
  clearPendingClass();
}
