import { defineStore } from "pinia"
import { computed, ref } from "vue"
import type { CardElement } from "@/definitions/elements"
import type { DisplayCard } from "@/definitions/cards"
import type { ElementCategory, ElementType } from "@/definitions/shared/base"
import { elementDefaultStyle } from "@/engine/registry"
import {
  isSnippetToken,
  parseSnippetToken,
  snippetToken,
} from "@/adapters/snippets"

/**
 * State for a single FormBuilder instance. Multiple FormBuilders can
 * coexist on a page; each is keyed by `builderId`.
 *
 * The shape mirrors the v2 page config (a flat elements map, a flat
 * cards map, a root card id) so the builder's output can be passed
 * straight to ShowPage / CardRenderer.
 */
export interface BuilderSchema {
  /** Root card id — the top of the tree. */
  rootCardId: string
  cards: Record<string, DisplayCard>
  elements: Record<string, CardElement>
  /**
   * Reusable, named config fragments keyed by snippet key. Each is a
   * self-contained sub-schema editable in its own builder scope. Placements
   * elsewhere reference a snippet by the `snippet#<key>` child token.
   */
  snippets?: Record<string, SnippetDefinition>
  /**
   * Per-placement snippet override values, keyed by instance id. A placement
   * token `snippet#<key>:<instanceId>` looks its overrides up here.
   */
  snippetOverrides?: Record<string, Record<string, unknown>>
}

/** A self-contained snippet definition — same shape as a builder schema. */
export interface SnippetDefinition {
  rootCardId: string
  cards: Record<string, DisplayCard>
  elements: Record<string, CardElement>
  /** Overrides for any snippet placements nested inside this definition. */
  snippetOverrides?: Record<string, Record<string, unknown>>
  /** Human label shown in the snippets panel (defaults to the key). */
  label?: string
}

/** Identifies what's currently selected in the inspector. */
export interface BuilderSelection {
  kind: "element" | "card" | null
  id: string | null
}

/**
 * Separator used to address a single tab inside a `card.tab` element as a
 * drag/drop container: `"<elementId>::tab::<tabId>"`. Each tab owns its own
 * `elementList`, so this lets the generic container machinery (add / move /
 * reorder / remove) operate on a tab's contents with no special-casing.
 */
export const TAB_SEP = "::tab::"

/** Build a composite container id addressing a tab within a card.tab element. */
export function tabContainerId(elementId: string, tabId: string): string {
  return `${elementId}${TAB_SEP}${tabId}`
}

let idCounter = 0
function nextId(prefix: string): string {
  idCounter += 1
  return `${prefix}-${Date.now()}-${idCounter}`
}

/** Test helper to make ids deterministic. */
export function __resetFormBuilderIdCounter(): void {
  idCounter = 0
}

/** Deep clone of plain-JSON snippet definitions (no functions/refs inside). */
function structuredCloneSafe<T>(value: T): T {
  return JSON.parse(JSON.stringify(value)) as T
}

function emptyRoot(rootId: string): BuilderSchema {
  return {
    rootCardId: rootId,
    cards: {
      [rootId]: {
        id: rootId,
        cardType: "element-list",
        title: "",
        elementList: [],
      } as unknown as DisplayCard,
    },
    elements: {},
  }
}

export const useFormBuilderStore = defineStore("gx-modui/formBuilder", () => {
  /**
   * Per-builder state, keyed by `builderId`. Lets multiple builders
   * coexist on the same page without colliding.
   */
  const builders = ref<Record<string, BuilderSchema>>({})
  const selections = ref<Record<string, BuilderSelection>>({})
  /**
   * Per-builder active snippet key. When set, content mutations operate on
   * that snippet's self-contained sub-schema instead of the main schema
   * (so the user can edit a snippet definition in place). `null`/absent ⇒
   * the main schema is the working scope.
   */
  const snippetScopes = ref<Record<string, string | null>>({})

  function ensureSnippets(
    schema: BuilderSchema,
  ): Record<string, SnippetDefinition> {
    if (!schema.snippets) {
      schema.snippets = {}
    }
    return schema.snippets
  }

  /**
   * The schema that content operations currently target: the active snippet's
   * sub-schema when one is being edited, otherwise the main schema.
   */
  function workingSchema(builderId: string): BuilderSchema {
    const main = ensureBuilder(builderId)
    const key = snippetScopes.value[builderId]
    if (key && main.snippets?.[key]) {
      return main.snippets[key] as BuilderSchema
    }
    return main
  }

  /**
   * Return the raw mutable bag for a container ID. A container is either a
   * DisplayCard (in schema.cards), a card-type element (schema.elements
   * with a type starting with "card."), or a single tab inside a
   * `card.tab` element addressed as `"<elementId>::tab::<tabId>"`. Each
   * resolved bag carries an `elementList` array.
   */
  function getContainerBag(
    schema: BuilderSchema,
    containerId: string,
  ): Record<string, unknown> | null {
    const tabSep = containerId.indexOf(TAB_SEP)
    if (tabSep !== -1) {
      const elId = containerId.slice(0, tabSep)
      const tabId = containerId.slice(tabSep + TAB_SEP.length)
      const el = schema.elements[elId] as unknown as
        | Record<string, unknown>
        | undefined
      if (!el) return null
      const tabs = (el.tabs as Array<Record<string, unknown>> | undefined) ?? []
      const tab = tabs.find((t) => t.id === tabId)
      if (!tab) return null
      if (!Array.isArray(tab.elementList)) {
        tab.elementList = []
      }
      return tab
    }
    const card = schema.cards[containerId]
    if (card) return card as unknown as Record<string, unknown>
    const el = schema.elements[containerId]
    if (el) {
      const bag = el as unknown as Record<string, unknown>
      if (typeof bag.type === "string" && (bag.type as string).startsWith("card.")) {
        return bag
      }
    }
    return null
  }

  function ensureBuilder(builderId: string): BuilderSchema {
    if (!builders.value[builderId]) {
      const rootId = nextId("root")
      builders.value[builderId] = emptyRoot(rootId)
      selections.value[builderId] = { kind: null, id: null }
    }
    return builders.value[builderId]!
  }

  function loadSchema(builderId: string, schema: BuilderSchema): void {
    builders.value[builderId] = {
      rootCardId: schema.rootCardId,
      cards: { ...schema.cards },
      elements: { ...schema.elements },
      ...(schema.snippets
        ? { snippets: structuredCloneSafe(schema.snippets) }
        : {}),
      ...(schema.snippetOverrides
        ? { snippetOverrides: structuredCloneSafe(schema.snippetOverrides) }
        : {}),
    }
    selections.value[builderId] = { kind: null, id: null }
    snippetScopes.value[builderId] = null
  }

  function resetBuilder(builderId: string): void {
    delete builders.value[builderId]
    delete selections.value[builderId]
    delete snippetScopes.value[builderId]
  }

  /**
   * Append a fresh element of `type` to the given parent card's
   * elementList. `type` is the dot-notation discriminator
   * (e.g. `"input.text"`). When only a category is supplied
   * (`"input"`), the parent's default style is appended.
   *
   * Returns the new element's id so callers can immediately select it
   * for editing.
   */
  function addElement(
    builderId: string,
    containerId: string,
    type: ElementType | ElementCategory,
  ): string {
    const schema = workingSchema(builderId)
    const bag = getContainerBag(schema, containerId)
    if (!bag) {
      throw new Error(
        `addElement: container ${containerId} not found in builder ${builderId}`,
      )
    }
    const dotType: ElementType = type.includes(".")
      ? (type as ElementType)
      : (`${type}.${elementDefaultStyle[type as ElementCategory] ?? "text"}` as ElementType)
    const id = nextId(`el-${dotType.replace(".", "-")}`)
    const element = {
      id,
      type: dotType,
      label: `New ${dotType}`,
    } as unknown as Record<string, unknown>
    // A tab container needs at least one tab to be usable out of the box.
    if (dotType === "card.tab") {
      element.tabs = [
        { id: nextId("tab"), label: "Tab 1", elementList: [] },
      ]
    }
    schema.elements[id] = element as unknown as CardElement
    const list = (bag.elementList as string[] | undefined) ?? []
    bag.elementList = [...list, id]
    return id
  }

  /**
   * Append a fresh card of `cardType` to the given parent's cardList /
   * tabList. Returns the new card's id.
   */
  function addCard(
    builderId: string,
    parentCardId: string,
    cardType: DisplayCard["cardType"],
  ): string {
    const schema = workingSchema(builderId)
    const parent = schema.cards[parentCardId]
    if (!parent) {
      throw new Error(`addCard: parent card ${parentCardId} not found`)
    }
    const id = nextId(`card-${cardType}`)
    const card = {
      id,
      cardType,
      title: `New ${cardType}`,
    } as unknown as DisplayCard
    if (cardType === "element-list") {
      ;(card as unknown as Record<string, unknown>).elementList = []
    } else if (cardType === "card-list") {
      ;(card as unknown as Record<string, unknown>).cardList = []
    } else if (cardType === "tab-list") {
      ;(card as unknown as Record<string, unknown>).tabList = []
    }
    schema.cards[id] = card

    const parentBag = parent as unknown as Record<string, unknown>
    if (parent.cardType === "card-list") {
      const list = (parentBag.cardList as string[]) ?? []
      parentBag.cardList = [...list, id]
    } else if (parent.cardType === "tab-list") {
      const list = (parentBag.tabList as string[]) ?? []
      parentBag.tabList = [...list, id]
    } else {
      throw new Error(
        `addCard: parent ${parentCardId} (cardType=${parent.cardType}) does not accept nested cards`,
      )
    }
    return id
  }

  /**
   * Ensure a top-level "root" card exists under a specific id (used for the
   * main root and for each advanced card list, which are keyed by name —
   * e.g. `"settings"`, `"formTemplate"`). No-op if the card already exists
   * (so loading a saved schema that already contains the root is preserved).
   * A `tab-list` root is seeded with one empty element-list tab so it is
   * immediately editable.
   */
  function createRootCard(
    builderId: string,
    cardId: string,
    cardType: DisplayCard["cardType"],
    opts?: { title?: string },
  ): void {
    const schema = ensureBuilder(builderId)
    if (schema.cards[cardId]) {
      return
    }
    const card = {
      id: cardId,
      cardType,
      title: opts?.title ?? "",
    } as unknown as Record<string, unknown>
    if (cardType === "tab-list") {
      card.tabList = []
      schema.cards[cardId] = card as unknown as DisplayCard
      const tabId = addCard(builderId, cardId, "element-list")
      updateCard(builderId, tabId, { title: "Tab 1" } as Partial<DisplayCard>)
      return
    }
    if (cardType === "element-list") {
      card.elementList = []
    } else if (cardType === "card-list") {
      card.cardList = []
    }
    schema.cards[cardId] = card as unknown as DisplayCard
  }

  /* ---- root tab-list mode -------------------------------------------- */

  /**
   * Convert the root into a `tab-list` whose children are `element-list`
   * "tab" cards. Idempotent — if the root is already a tab-list it is left
   * untouched. Any elements directly on the old element-list root are
   * migrated into the first tab so toggling the mode never drops content.
   */
  function initRootTabs(builderId: string): void {
    const schema = ensureBuilder(builderId)
    const rootId = schema.rootCardId
    const root = schema.cards[rootId] as unknown as Record<string, unknown>
    if (!root || root.cardType === "tab-list") {
      return
    }
    const existing = (root.elementList as string[] | undefined) ?? []
    const tabId = nextId("tab-card")
    schema.cards[tabId] = {
      id: tabId,
      cardType: "element-list",
      title: "Tab 1",
      elementList: existing,
    } as unknown as DisplayCard
    schema.cards[rootId] = {
      id: rootId,
      cardType: "tab-list",
      title: "",
      tabList: [tabId],
    } as unknown as DisplayCard
  }

  /**
   * Append a new element-list "tab" card to the root tab-list. Returns the
   * new tab card's id (empty string if the root isn't a tab-list).
   */
  function addRootTab(
    builderId: string,
    title?: string,
    icon?: string,
    rootCardId?: string,
  ): string {
    const schema = workingSchema(builderId)
    const rootId = rootCardId ?? schema.rootCardId
    const root = schema.cards[rootId] as unknown as Record<string, unknown>
    if (!root || root.cardType !== "tab-list") {
      return ""
    }
    const tabCount = ((root.tabList as string[] | undefined) ?? []).length
    const id = addCard(builderId, rootId, "element-list")
    updateCard(builderId, id, {
      title: title ?? `Tab ${tabCount + 1}`,
      ...(icon ? { icon } : {}),
    } as Partial<DisplayCard>)
    return id
  }

  /** Reorder the root tab-list's tab cards (drag-reorder in the chrome bar). */
  function reorderRootTab(
    builderId: string,
    fromIndex: number,
    toIndex: number,
    rootCardId?: string,
  ): void {
    const schema = workingSchema(builderId)
    const rootId = rootCardId ?? schema.rootCardId
    const root = schema.cards[rootId] as unknown as Record<string, unknown>
    if (!root || root.cardType !== "tab-list") {
      return
    }
    const list = [...((root.tabList as string[] | undefined) ?? [])]
    if (
      fromIndex < 0 ||
      fromIndex >= list.length ||
      toIndex < 0 ||
      toIndex >= list.length ||
      fromIndex === toIndex
    ) {
      return
    }
    const [moved] = list.splice(fromIndex, 1)
    list.splice(toIndex, 0, moved!)
    updateCard(builderId, rootId, {
      tabList: list,
    } as Partial<DisplayCard>)
  }

  /** Remove an element by id from the schema and from its parent's list. */
  function removeElement(builderId: string, elementId: string): void {
    const schema = workingSchema(builderId)
    if (!schema.elements[elementId]) {
      return
    }
    // Cascade-delete children when removing a card-type element
    const elBag = schema.elements[elementId] as unknown as Record<string, unknown>
    if (typeof elBag.type === "string" && (elBag.type as string).startsWith("card.")) {
      for (const childId of (elBag.elementList as string[] | undefined) ?? []) {
        delete schema.elements[childId]
      }
      // card.tab keeps its children inside per-tab element lists.
      for (const tab of (elBag.tabs as Array<Record<string, unknown>> | undefined) ?? []) {
        for (const childId of (tab.elementList as string[] | undefined) ?? []) {
          delete schema.elements[childId]
        }
      }
    }
    delete schema.elements[elementId]
    // Remove from card containers
    for (const card of Object.values(schema.cards)) {
      const bag = card as unknown as Record<string, unknown>
      const list = bag.elementList as string[] | undefined
      if (list?.includes(elementId)) {
        bag.elementList = list.filter((id) => id !== elementId)
      }
    }
    // Remove from element containers (card.* elements) and their tabs
    for (const el of Object.values(schema.elements)) {
      const bag = el as unknown as Record<string, unknown>
      if (typeof bag.type === "string" && (bag.type as string).startsWith("card.")) {
        const list = bag.elementList as string[] | undefined
        if (list?.includes(elementId)) {
          bag.elementList = list.filter((id) => id !== elementId)
        }
        for (const tab of (bag.tabs as Array<Record<string, unknown>> | undefined) ?? []) {
          const tabList = tab.elementList as string[] | undefined
          if (tabList?.includes(elementId)) {
            tab.elementList = tabList.filter((id) => id !== elementId)
          }
        }
      }
    }
    const sel = selections.value[builderId]
    if (sel && sel.kind === "element" && sel.id === elementId) {
      selections.value[builderId] = { kind: null, id: null }
    }
  }

  /** Remove a card (and its descendants) recursively. */
  function removeCard(builderId: string, cardId: string): void {
    const schema = workingSchema(builderId)
    if (cardId === schema.rootCardId) {
      return // never remove the root
    }
    const card = schema.cards[cardId]
    if (!card) {
      return
    }
    const bag = card as unknown as Record<string, unknown>
    for (const elId of (bag.elementList as string[]) ?? []) {
      delete schema.elements[elId]
    }
    for (const childId of (bag.cardList as string[]) ?? []) {
      removeCard(builderId, childId)
    }
    for (const childId of (bag.tabList as string[]) ?? []) {
      removeCard(builderId, childId)
    }
    delete schema.cards[cardId]
    for (const other of Object.values(schema.cards)) {
      const obag = other as unknown as Record<string, unknown>
      for (const key of ["cardList", "tabList"] as const) {
        const list = obag[key] as string[] | undefined
        if (list?.includes(cardId)) {
          obag[key] = list.filter((id) => id !== cardId)
        }
      }
    }
    const sel = selections.value[builderId]
    if (sel && sel.kind === "card" && sel.id === cardId) {
      selections.value[builderId] = { kind: null, id: null }
    }
  }

  /** Move an element within a container's elementList (drag-reorder). */
  function reorderElement(
    builderId: string,
    containerId: string,
    fromIndex: number,
    toIndex: number,
  ): void {
    const schema = workingSchema(builderId)
    const bag = getContainerBag(schema, containerId)
    if (!bag) return
    const list = (bag.elementList as string[] | undefined) ?? []
    if (
      fromIndex < 0 ||
      fromIndex >= list.length ||
      toIndex < 0 ||
      toIndex >= list.length
    ) {
      return
    }
    const next = [...list]
    const [moved] = next.splice(fromIndex, 1)
    if (moved !== undefined) {
      next.splice(toIndex, 0, moved)
      bag.elementList = next
    }
  }

  /** Move an element from one container to another (cross-container drag). */
  function moveElement(
    builderId: string,
    fromContainerId: string,
    toContainerId: string,
    elementId: string,
    toIndex: number,
  ): void {
    if (fromContainerId === toContainerId) return
    const schema = workingSchema(builderId)
    const fromBag = getContainerBag(schema, fromContainerId)
    const toBag = getContainerBag(schema, toContainerId)
    if (!fromBag || !toBag) return
    const fromList = (fromBag.elementList as string[] | undefined) ?? []
    if (!fromList.includes(elementId)) return
    fromBag.elementList = fromList.filter((id) => id !== elementId)
    const toList = (toBag.elementList as string[] | undefined) ?? []
    const clamped = Math.max(0, Math.min(toIndex, toList.length))
    const next = [...toList]
    next.splice(clamped, 0, elementId)
    toBag.elementList = next
  }

  /** Patch an element's properties (used by the Inspector on every input). */
  function updateElement(
    builderId: string,
    elementId: string,
    patch: Partial<CardElement>,
  ): void {
    const schema = workingSchema(builderId)
    const current = schema.elements[elementId]
    if (!current) {
      return
    }
    schema.elements[elementId] = {
      ...current,
      ...(patch as Record<string, unknown>),
    } as CardElement
  }

  /** Patch a card's properties. */
  function updateCard(
    builderId: string,
    cardId: string,
    patch: Partial<DisplayCard>,
  ): void {
    const schema = workingSchema(builderId)
    const current = schema.cards[cardId]
    if (!current) {
      return
    }
    schema.cards[cardId] = {
      ...current,
      ...(patch as Record<string, unknown>),
    } as DisplayCard
  }

  /* ---- card.tab tab management ---------------------------------------- */

  type TabEntry = { id: string; label: string; elementList?: string[] }

  function tabsOf(
    schema: BuilderSchema,
    elementId: string,
  ): TabEntry[] | null {
    const bag = schema.elements[elementId] as unknown as
      | Record<string, unknown>
      | undefined
    if (!bag || bag.type !== "card.tab") return null
    if (!Array.isArray(bag.tabs)) bag.tabs = []
    return bag.tabs as TabEntry[]
  }

  /** Append a new tab to a card.tab element. Returns the new tab id. */
  function addTab(builderId: string, elementId: string, label?: string): string {
    const schema = workingSchema(builderId)
    const tabs = tabsOf(schema, elementId)
    if (!tabs) return ""
    const id = nextId("tab")
    const next = [
      ...tabs,
      { id, label: label ?? `Tab ${tabs.length + 1}`, elementList: [] },
    ]
    updateElement(builderId, elementId, { tabs: next } as Partial<CardElement>)
    return id
  }

  /** Patch a tab's label (or other props). */
  function updateTab(
    builderId: string,
    elementId: string,
    tabId: string,
    patch: Partial<Omit<TabEntry, "id">>,
  ): void {
    const schema = workingSchema(builderId)
    const tabs = tabsOf(schema, elementId)
    if (!tabs) return
    const next = tabs.map((t) => (t.id === tabId ? { ...t, ...patch } : t))
    updateElement(builderId, elementId, { tabs: next } as Partial<CardElement>)
  }

  /** Remove a tab and cascade-delete the elements it contained. */
  function removeTab(builderId: string, elementId: string, tabId: string): void {
    const schema = workingSchema(builderId)
    const tabs = tabsOf(schema, elementId)
    if (!tabs) return
    const target = tabs.find((t) => t.id === tabId)
    for (const childId of target?.elementList ?? []) {
      delete schema.elements[childId]
    }
    const next = tabs.filter((t) => t.id !== tabId)
    updateElement(builderId, elementId, { tabs: next } as Partial<CardElement>)
  }

  /** Move a tab from one index to another. */
  function reorderTab(
    builderId: string,
    elementId: string,
    fromIndex: number,
    toIndex: number,
  ): void {
    const schema = workingSchema(builderId)
    const tabs = tabsOf(schema, elementId)
    if (!tabs) return
    if (
      fromIndex < 0 ||
      fromIndex >= tabs.length ||
      toIndex < 0 ||
      toIndex >= tabs.length ||
      fromIndex === toIndex
    ) {
      return
    }
    const next = [...tabs]
    const [moved] = next.splice(fromIndex, 1)
    next.splice(toIndex, 0, moved!)
    updateElement(builderId, elementId, { tabs: next } as Partial<CardElement>)
  }

  /* ---- snippets ------------------------------------------------------- */

  /** All snippet keys defined on the schema. */
  function listSnippets(builderId: string): string[] {
    const main = ensureBuilder(builderId)
    return Object.keys(main.snippets ?? {})
  }

  /** Return a snippet definition (or null). */
  function getSnippet(
    builderId: string,
    key: string,
  ): SnippetDefinition | null {
    return ensureBuilder(builderId).snippets?.[key] ?? null
  }

  /**
   * Create an empty snippet definition under `key`. `kind` controls its root
   * (element-list for a field snippet, card-list for a card snippet). No-op
   * if a snippet with that key already exists. Returns the new root id.
   */
  function addSnippet(
    builderId: string,
    key: string,
    opts?: { kind?: "element" | "card"; label?: string },
  ): string {
    const main = ensureBuilder(builderId)
    const snippets = ensureSnippets(main)
    if (snippets[key]) {
      return snippets[key]!.rootCardId
    }
    const rootId = nextId("snippet-root")
    const cardType = opts?.kind === "card" ? "card-list" : "element-list"
    const root = {
      id: rootId,
      cardType,
      title: opts?.label ?? key,
    } as unknown as Record<string, unknown>
    if (cardType === "card-list") {
      root.cardList = []
    } else {
      root.elementList = []
    }
    snippets[key] = {
      rootCardId: rootId,
      cards: { [rootId]: root as unknown as DisplayCard },
      elements: {},
      label: opts?.label ?? key,
    }
    return rootId
  }

  /** Scan a schema's id lists for any placement token of snippet `key`. */
  function schemaUsesSnippetKey(schema: BuilderSchema, key: string): boolean {
    const listUsesKey = (list: string[] | undefined): boolean =>
      Boolean(
        list?.some(
          (id) => isSnippetToken(id) && parseSnippetToken(id).key === key,
        ),
      )
    for (const card of Object.values(schema.cards)) {
      const bag = card as unknown as Record<string, unknown>
      for (const listKey of ["elementList", "cardList", "tabList"] as const) {
        if (listUsesKey(bag[listKey] as string[] | undefined)) {
          return true
        }
      }
    }
    for (const el of Object.values(schema.elements)) {
      const bag = el as unknown as Record<string, unknown>
      if (listUsesKey(bag.elementList as string[] | undefined)) {
        return true
      }
      for (const tab of (bag.tabs as Array<Record<string, unknown>> | undefined) ??
        []) {
        if (listUsesKey(tab.elementList as string[] | undefined)) {
          return true
        }
      }
    }
    return false
  }

  /** True when a placement of snippet `key` appears anywhere in the schema. */
  function isSnippetUsed(builderId: string, key: string): boolean {
    const main = ensureBuilder(builderId)
    if (schemaUsesSnippetKey(main, key)) {
      return true
    }
    for (const [k, def] of Object.entries(main.snippets ?? {})) {
      if (k === key) {
        continue
      }
      if (schemaUsesSnippetKey(def as BuilderSchema, key)) {
        return true
      }
    }
    return false
  }

  /**
   * Remove a snippet definition. Refuses (returns false) if the snippet is
   * still referenced anywhere in the form.
   */
  function removeSnippet(builderId: string, key: string): boolean {
    const main = ensureBuilder(builderId)
    if (!main.snippets?.[key]) {
      return false
    }
    if (isSnippetUsed(builderId, key)) {
      return false
    }
    delete main.snippets[key]
    if (snippetScopes.value[builderId] === key) {
      snippetScopes.value[builderId] = null
    }
    return true
  }

  /**
   * Place a snippet reference (`snippet#<key>`) into a container's element
   * list in the current working scope, at `index` (default end).
   */
  function placeSnippet(
    builderId: string,
    containerId: string,
    key: string,
    index?: number,
  ): void {
    const schema = workingSchema(builderId)
    const bag = getContainerBag(schema, containerId)
    if (!bag) {
      return
    }
    const list = (bag.elementList as string[] | undefined) ?? []
    const next = [...list]
    const at = index ?? next.length
    next.splice(
      Math.max(0, Math.min(at, next.length)),
      0,
      snippetToken(key, nextId("snip")),
    )
    bag.elementList = next
  }

  /** Read a snippet placement's per-instance override values (or `{}`). */
  function getSnippetOverride(
    builderId: string,
    token: string,
  ): Record<string, unknown> {
    const { instanceId } = parseSnippetToken(token)
    if (!instanceId) {
      return {}
    }
    return workingSchema(builderId).snippetOverrides?.[instanceId] ?? {}
  }

  /**
   * Merge `patch` into a snippet placement's per-instance override values
   * (e.g. a resized `colSpan`). Stored in the working scope's
   * `snippetOverrides`, keyed by the token's instance id.
   */
  function updateSnippetOverride(
    builderId: string,
    token: string,
    patch: Record<string, unknown>,
  ): void {
    const { instanceId } = parseSnippetToken(token)
    if (!instanceId) {
      return
    }
    const schema = workingSchema(builderId)
    if (!schema.snippetOverrides) {
      schema.snippetOverrides = {}
    }
    schema.snippetOverrides[instanceId] = {
      ...(schema.snippetOverrides[instanceId] ?? {}),
      ...patch,
    }
  }

  /**
   * Remove a snippet placement token from every container list in the current
   * working scope (the snippet definition itself is left intact).
   */
  function removeSnippetPlacement(builderId: string, token: string): void {
    const schema = workingSchema(builderId)
    const drop = (list: string[] | undefined): string[] | undefined =>
      list?.includes(token) ? list.filter((id) => id !== token) : list
    for (const card of Object.values(schema.cards)) {
      const bag = card as unknown as Record<string, unknown>
      bag.elementList = drop(bag.elementList as string[] | undefined)
      bag.cardList = drop(bag.cardList as string[] | undefined)
      bag.tabList = drop(bag.tabList as string[] | undefined)
    }
    for (const el of Object.values(schema.elements)) {
      const bag = el as unknown as Record<string, unknown>
      bag.elementList = drop(bag.elementList as string[] | undefined)
      for (const tab of (bag.tabs as Array<Record<string, unknown>> | undefined) ??
        []) {
        tab.elementList = drop(tab.elementList as string[] | undefined)
      }
    }
    // Drop the placement's overrides (instance ids are unique per placement).
    const { instanceId } = parseSnippetToken(token)
    if (instanceId && schema.snippetOverrides) {
      delete schema.snippetOverrides[instanceId]
    }
    const sel = selections.value[builderId]
    if (sel && sel.id === token) {
      selections.value[builderId] = { kind: null, id: null }
    }
  }

  /** Collect an element id plus all descendant element ids (card.* + tabs). */
  function collectElementSubtree(
    schema: BuilderSchema,
    elementId: string,
    acc: Set<string> = new Set(),
  ): Set<string> {
    if (acc.has(elementId)) {
      return acc
    }
    acc.add(elementId)
    const bag = schema.elements[elementId] as unknown as
      | Record<string, unknown>
      | undefined
    if (!bag) {
      return acc
    }
    for (const childId of (bag.elementList as string[] | undefined) ?? []) {
      if (!isSnippetToken(childId)) {
        collectElementSubtree(schema, childId, acc)
      }
    }
    for (const tab of (bag.tabs as Array<Record<string, unknown>> | undefined) ??
      []) {
      for (const childId of (tab.elementList as string[] | undefined) ?? []) {
        if (!isSnippetToken(childId)) {
          collectElementSubtree(schema, childId, acc)
        }
      }
    }
    return acc
  }

  /** Replace the first occurrence of an id in any container list with a token. */
  function replaceIdWithToken(
    schema: BuilderSchema,
    oldId: string,
    token: string,
  ): void {
    const swap = (list: string[] | undefined): string[] | undefined => {
      if (!list) {
        return list
      }
      const idx = list.indexOf(oldId)
      if (idx === -1) {
        return list
      }
      const next = [...list]
      next[idx] = token
      return next
    }
    for (const card of Object.values(schema.cards)) {
      const bag = card as unknown as Record<string, unknown>
      bag.elementList = swap(bag.elementList as string[] | undefined)
      bag.cardList = swap(bag.cardList as string[] | undefined)
      bag.tabList = swap(bag.tabList as string[] | undefined)
    }
    for (const el of Object.values(schema.elements)) {
      const bag = el as unknown as Record<string, unknown>
      bag.elementList = swap(bag.elementList as string[] | undefined)
      for (const tab of (bag.tabs as Array<Record<string, unknown>> | undefined) ??
        []) {
        tab.elementList = swap(tab.elementList as string[] | undefined)
      }
    }
  }

  /**
   * Convert the element subtree rooted at `elementId` (in the current working
   * scope) into a snippet definition stored under `key`, and replace its
   * placement with a `snippet#<key>` token. No-op (returns false) if `key`
   * already exists or the element isn't found.
   */
  function convertElementToSnippet(
    builderId: string,
    elementId: string,
    key: string,
    label?: string,
  ): boolean {
    const main = ensureBuilder(builderId)
    const schema = workingSchema(builderId)
    const snippets = ensureSnippets(main)
    if (snippets[key] || !schema.elements[elementId]) {
      return false
    }
    const ids = collectElementSubtree(schema, elementId)
    const defElements: Record<string, CardElement> = {}
    for (const id of ids) {
      const el = schema.elements[id]
      if (el) {
        defElements[id] = el
      }
    }
    const rootId = nextId("snippet-root")
    snippets[key] = {
      rootCardId: rootId,
      cards: {
        [rootId]: {
          id: rootId,
          cardType: "element-list",
          title: label ?? key,
          elementList: [elementId],
        } as unknown as DisplayCard,
      },
      elements: defElements,
      label: label ?? key,
    }
    // Remove the lifted elements and point the placement at the snippet token.
    for (const id of ids) {
      delete schema.elements[id]
    }
    replaceIdWithToken(schema, elementId, snippetToken(key, nextId("snip")))
    const sel = selections.value[builderId]
    if (sel && sel.id === elementId) {
      selections.value[builderId] = { kind: null, id: null }
    }
    return true
  }

  /** Deep-clone an element subtree from `src` into `dest`, assigning fresh ids. */
  function cloneElementSubtree(
    src: Record<string, CardElement>,
    srcId: string,
    dest: Record<string, CardElement>,
  ): string {
    const original = src[srcId]
    if (!original) {
      return ""
    }
    const clone = structuredCloneSafe(original) as unknown as Record<
      string,
      unknown
    >
    const newId = nextId("el")
    clone.id = newId
    const type = String(clone.type ?? "")
    if (type.startsWith("card.")) {
      if (Array.isArray(clone.elementList)) {
        clone.elementList = (clone.elementList as string[]).map((cid) =>
          isSnippetToken(cid) ? cid : cloneElementSubtree(src, cid, dest),
        )
      }
      if (Array.isArray(clone.tabs)) {
        clone.tabs = (clone.tabs as Array<Record<string, unknown>>).map((t) => ({
          ...t,
          id: nextId("tab"),
          elementList: ((t.elementList as string[] | undefined) ?? []).map(
            (cid) => (isSnippetToken(cid) ? cid : cloneElementSubtree(src, cid, dest)),
          ),
        }))
      }
    }
    dest[newId] = clone as unknown as CardElement
    return newId
  }

  /** Replace a token in any elementList (cards + card.* elements/tabs) with ids. */
  function replaceTokenWithIds(
    schema: BuilderSchema,
    token: string,
    ids: string[],
  ): void {
    const swap = (list: string[] | undefined): string[] | undefined => {
      if (!list) {
        return list
      }
      const idx = list.indexOf(token)
      if (idx === -1) {
        return list
      }
      const next = [...list]
      next.splice(idx, 1, ...ids)
      return next
    }
    for (const card of Object.values(schema.cards)) {
      const bag = card as unknown as Record<string, unknown>
      bag.elementList = swap(bag.elementList as string[] | undefined)
    }
    for (const el of Object.values(schema.elements)) {
      const bag = el as unknown as Record<string, unknown>
      bag.elementList = swap(bag.elementList as string[] | undefined)
      for (const tab of (bag.tabs as Array<Record<string, unknown>> | undefined) ??
        []) {
        tab.elementList = swap(tab.elementList as string[] | undefined)
      }
    }
  }

  /**
   * Replace a snippet placement with an independent, editable COPY of the
   * snippet's current definition content. Clones the definition's root
   * children (with fresh ids) into the working scope and splices them in where
   * the token was — detaching this placement from the shared snippet. Returns
   * the new top-level element ids.
   */
  function inlineSnippet(builderId: string, token: string): string[] {
    const main = ensureBuilder(builderId)
    const schema = workingSchema(builderId)
    const { key, instanceId } = parseSnippetToken(token)
    const def = main.snippets?.[key]
    const root = def?.cards[def.rootCardId] as unknown as
      | Record<string, unknown>
      | undefined
    if (!def || !root) {
      return []
    }
    const childIds = (root.elementList as string[] | undefined) ?? []
    const newIds: string[] = []
    for (const cid of childIds) {
      if (isSnippetToken(cid)) {
        // Nested snippet placement: re-instance it (+ copy its override).
        const nested = parseSnippetToken(cid)
        const newInstance = nextId("snip")
        const ov = nested.instanceId
          ? def.snippetOverrides?.[nested.instanceId]
          : undefined
        if (ov) {
          if (!schema.snippetOverrides) {
            schema.snippetOverrides = {}
          }
          schema.snippetOverrides[newInstance] = { ...ov }
        }
        newIds.push(snippetToken(nested.key, newInstance))
        continue
      }
      const newId = cloneElementSubtree(def.elements, cid, schema.elements)
      if (newId) {
        newIds.push(newId)
      }
    }
    replaceTokenWithIds(schema, token, newIds)
    if (instanceId && schema.snippetOverrides) {
      delete schema.snippetOverrides[instanceId]
    }
    const sel = selections.value[builderId]
    if (sel && sel.id === token) {
      selections.value[builderId] = { kind: null, id: null }
    }
    return newIds
  }

  /** Enter (key) / leave (null) a snippet-editing scope. */
  function setActiveSnippet(builderId: string, key: string | null): void {
    snippetScopes.value[builderId] = key
    selections.value[builderId] = { kind: null, id: null }
  }

  function getActiveSnippet(builderId: string): string | null {
    return snippetScopes.value[builderId] ?? null
  }

  function select(
    builderId: string,
    kind: "element" | "card" | null,
    id: string | null,
  ): void {
    selections.value[builderId] = { kind, id }
  }

  function getSelection(builderId: string): BuilderSelection {
    return selections.value[builderId] ?? { kind: null, id: null }
  }

  /** Convenience getter returning the (full main) schema for export. */
  function getSchema(builderId: string): BuilderSchema {
    return ensureBuilder(builderId)
  }

  /** The schema the UI currently edits (main, or the active snippet's). */
  function getWorkingSchema(builderId: string): BuilderSchema {
    return workingSchema(builderId)
  }

  return {
    builders,
    selections,
    ensureBuilder,
    loadSchema,
    resetBuilder,
    addElement,
    addCard,
    createRootCard,
    initRootTabs,
    addRootTab,
    reorderRootTab,
    removeElement,
    removeCard,
    reorderElement,
    moveElement,
    updateElement,
    updateCard,
    addTab,
    updateTab,
    removeTab,
    reorderTab,
    select,
    getSelection,
    getSchema,
    getWorkingSchema,
    listSnippets,
    getSnippet,
    addSnippet,
    removeSnippet,
    isSnippetUsed,
    placeSnippet,
    removeSnippetPlacement,
    getSnippetOverride,
    updateSnippetOverride,
    convertElementToSnippet,
    inlineSnippet,
    setActiveSnippet,
    getActiveSnippet,
  }
})

/** Composable wrapping store access for a single builder instance. */
export function useFormBuilder(builderId: string) {
  const store = useFormBuilderStore()
  store.ensureBuilder(builderId)
  return {
    schema: computed<BuilderSchema>(() => store.getSchema(builderId)),
    /** Schema for the active scope (main, or the snippet being edited). */
    workingSchema: computed<BuilderSchema>(() =>
      store.getWorkingSchema(builderId),
    ),
    activeSnippet: computed<string | null>(() =>
      store.getActiveSnippet(builderId),
    ),
    selection: computed<BuilderSelection>(() => store.getSelection(builderId)),
    addElement: (parentId: string, type: ElementType | ElementCategory) =>
      store.addElement(builderId, parentId, type),
    addCard: (parentId: string, cardType: DisplayCard["cardType"]) =>
      store.addCard(builderId, parentId, cardType),
    createRootCard: (
      cardId: string,
      cardType: DisplayCard["cardType"],
      opts?: { title?: string },
    ) => store.createRootCard(builderId, cardId, cardType, opts),
    initRootTabs: () => store.initRootTabs(builderId),
    addRootTab: (title?: string, icon?: string, rootCardId?: string) =>
      store.addRootTab(builderId, title, icon, rootCardId),
    reorderRootTab: (from: number, to: number, rootCardId?: string) =>
      store.reorderRootTab(builderId, from, to, rootCardId),
    removeElement: (id: string) => store.removeElement(builderId, id),
    removeCard: (id: string) => store.removeCard(builderId, id),
    reorderElement: (containerId: string, from: number, to: number) =>
      store.reorderElement(builderId, containerId, from, to),
    moveElement: (fromContainerId: string, toContainerId: string, elementId: string, toIndex: number) =>
      store.moveElement(builderId, fromContainerId, toContainerId, elementId, toIndex),
    updateElement: (id: string, patch: Partial<CardElement>) =>
      store.updateElement(builderId, id, patch),
    updateCard: (id: string, patch: Partial<DisplayCard>) =>
      store.updateCard(builderId, id, patch),
    addTab: (elementId: string, label?: string) =>
      store.addTab(builderId, elementId, label),
    updateTab: (
      elementId: string,
      tabId: string,
      patch: Partial<{ label: string; elementList: string[] }>,
    ) => store.updateTab(builderId, elementId, tabId, patch),
    removeTab: (elementId: string, tabId: string) =>
      store.removeTab(builderId, elementId, tabId),
    reorderTab: (elementId: string, from: number, to: number) =>
      store.reorderTab(builderId, elementId, from, to),
    select: (kind: "element" | "card" | null, id: string | null) =>
      store.select(builderId, kind, id),
    loadSchema: (schema: BuilderSchema) => store.loadSchema(builderId, schema),
    reset: () => store.resetBuilder(builderId),
    // Snippets
    listSnippets: () => store.listSnippets(builderId),
    getSnippet: (key: string) => store.getSnippet(builderId, key),
    addSnippet: (key: string, opts?: { kind?: "element" | "card"; label?: string }) =>
      store.addSnippet(builderId, key, opts),
    removeSnippet: (key: string) => store.removeSnippet(builderId, key),
    isSnippetUsed: (key: string) => store.isSnippetUsed(builderId, key),
    placeSnippet: (containerId: string, key: string, index?: number) =>
      store.placeSnippet(builderId, containerId, key, index),
    removeSnippetPlacement: (token: string) =>
      store.removeSnippetPlacement(builderId, token),
    getSnippetOverride: (token: string) =>
      store.getSnippetOverride(builderId, token),
    updateSnippetOverride: (token: string, patch: Record<string, unknown>) =>
      store.updateSnippetOverride(builderId, token, patch),
    convertElementToSnippet: (elementId: string, key: string, label?: string) =>
      store.convertElementToSnippet(builderId, elementId, key, label),
    inlineSnippet: (token: string) => store.inlineSnippet(builderId, token),
    setActiveSnippet: (key: string | null) =>
      store.setActiveSnippet(builderId, key),
  }
}
