/**
 * v1 → v2 schema adapter.
 *
 * Translates the v1 (docs-format / experience-portal) JSON schema into
 * the v2 (internal) shape the engine expects. The two key differences
 * are naming convention (cols/colsMd → columns/columnsMd, conditionParams
 * field/operator → name/logic, snake_case card types → kebab-case) and
 * structure (v1 inlines elements inside cards; v2 keeps them in a flat
 * id-keyed map and references them by id from cards' elementList /
 * cardList / tabList arrays).
 *
 * The adapter is loss-tolerant: any property it doesn't recognise is
 * passed through verbatim so forward-compat consumers don't have to
 * fork the table.
 */

import type { CardElement } from "@/definitions/elements"
import type { DisplayCard } from "@/definitions/cards"
import type { ConditionParamV1 } from "@/definitions/shared/conditions"

import {
  V1_ELEMENT_AS_CARD,
  V1_TO_V2_CARD_TYPE,
} from "./cardTypeMap"
import {
  V1_TO_V2_FIELD_TYPE,
  type V2Discriminator,
} from "./fieldTypeMap"
import {
  v1ColumnsToV2,
  v1ConditionParamsToV2,
} from "./conditionMap"
import {
  isSnippetNode,
  snippetOverridesOf,
  snippetToken,
  type V1SnippetNode,
} from "./snippets"

/** Per-conversion map of snippet instance id → raw override values. */
type SnippetOverrides = Record<string, Record<string, unknown>>

/**
 * Register a v1 snippet reference and return its v2 placement token. When the
 * node carries overrides they're stored under a fresh instance id and the
 * token is `snippet#<key>:<instanceId>`; otherwise the bare `snippet#<key>`.
 */
function registerSnippetRef(
  node: V1SnippetNode,
  snippetOverrides?: SnippetOverrides,
): string {
  // Every placement gets its own instance id so duplicates of one snippet are
  // independently addressable for ordering / removal. Reuse the node's own
  // `id` when present (round-trips the instance id), else generate one.
  const instanceId =
    typeof node.id === "string" && node.id ? node.id : nextId("snip")
  const overrides = snippetOverridesOf(node)
  if (snippetOverrides && Object.keys(overrides).length > 0) {
    snippetOverrides[instanceId] = overrides
  }
  return snippetToken(node.key, instanceId)
}

let idCounter = 0
function nextId(prefix: string): string {
  idCounter += 1
  return `${prefix}-${idCounter}`
}
/** Test-only: reset the id counter so generated ids are deterministic. */
export function __resetIdCounter(): void {
  idCounter = 0
}

/**
 * Return a unique id for a converted node. Prefers the source `preferred`
 * id when it hasn't been used yet; otherwise (or on collision) generates a
 * fresh one. `seen` tracks every id assigned across the whole conversion so
 * cards, tabs, and elements can never share an id. When `seen` is omitted
 * the preferred/generated id is returned without a uniqueness check (used
 * by the single-item converters, where collisions aren't possible).
 */
function ensureUniqueId(
  preferred: string | undefined,
  prefix: string,
  seen?: Set<string>,
): string {
  if (!seen) {
    return preferred ?? nextId(prefix)
  }
  let id = preferred && !seen.has(preferred) ? preferred : nextId(prefix)
  while (seen.has(id)) {
    id = nextId(prefix)
  }
  seen.add(id)
  return id
}

/**
 * v1 page-level config shape (loose — the adapter only inspects keys it
 * needs to translate, everything else passes through).
 */
export interface V1PageConfig {
  title?: string
  breadCrumbs?: { title: string }[]
  section?: string
  tabsList?: V1TabConfig[]
  cards?: V1CardConfig[]
  headers?: unknown
  options?: Record<string, unknown>
  listKey?: string
  createConfig?: unknown
  updateConfig?: unknown
  deleteConfig?: unknown
  eventHandlers?: Record<string, (...args: unknown[]) => unknown>
  [key: string]: unknown
}

export interface V1TabConfig {
  title?: string
  tabId?: string
  cards?: V1CardConfig[]
  nextTab?: { id: string; text: string }
  [key: string]: unknown
}

export interface V1CardConfig {
  type: string
  title?: string
  subtitle?: string
  showCard?: boolean
  fieldsList?: V1FieldConfig[]
  cards?: V1CardConfig[]
  tabsList?: V1TabConfig[]
  buttons?: V1FieldConfig[]
  headers?: unknown
  options?: Record<string, unknown>
  listKey?: string
  cols?: number
  colsSm?: number
  colsMd?: number
  colsLg?: number
  colsXl?: number
  classList?: string[] | string
  conditionParams?: ConditionParamV1[]
  [key: string]: unknown
}

export interface V1FieldConfig {
  type: string
  name?: string
  label?: string
  default?: unknown
  defaultValue?: unknown
  options?: unknown
  cols?: number
  colsSm?: number
  colsMd?: number
  colsLg?: number
  colsXl?: number
  conditionParams?: ConditionParamV1[]
  [key: string]: unknown
}

/**
 * The v2 page shape consumers pass to ShowPage / IndexPage. Cards and
 * elements are kept in flat id-keyed maps so cards can reference each
 * other (and their elements) by id.
 */
export interface V2PageConfig {
  title?: string
  breadCrumbs?: { title: string }[]
  section?: string
  tabsList?: {
    title: string
    tabId: string
    cards: DisplayCard[]
  }[]
  cards: Record<string, DisplayCard>
  elements: Record<string, CardElement>
  /** Per-placement snippet override values, keyed by instance id. */
  snippetOverrides?: Record<string, Record<string, unknown>>
  createConfig?: unknown
  updateConfig?: unknown
  deleteConfig?: unknown
  eventHandlers?: Record<string, (...args: unknown[]) => unknown>
  [key: string]: unknown
}

/**
 * Convert a v1 page config (IndexPage or ShowPage) to the v2 shape.
 */
export function v1ToV2(config: V1PageConfig): V2PageConfig {
  const cards: Record<string, DisplayCard> = {}
  const elements: Record<string, CardElement> = {}
  const snippetOverrides: SnippetOverrides = {}
  const seen = new Set<string>()

  const out: V2PageConfig = {
    title: config.title,
    breadCrumbs: config.breadCrumbs,
    section: config.section,
    cards,
    elements,
  }
  for (const passthrough of [
    "createConfig",
    "updateConfig",
    "deleteConfig",
    "eventHandlers",
  ] as const) {
    if (config[passthrough] !== undefined) {
      ;(out as Record<string, unknown>)[passthrough] = config[passthrough]
    }
  }

  if (config.tabsList?.length) {
    out.tabsList = config.tabsList.map((tab) => ({
      title: tab.title ?? "",
      tabId: ensureUniqueId(tab.tabId, "tab", seen),
      cards: (tab.cards ?? []).map((c) =>
        convertCard(c, cards, elements, seen, snippetOverrides),
      ),
    }))
  }

  if (config.cards?.length) {
    for (const c of config.cards) {
      convertCard(c, cards, elements, seen, snippetOverrides)
    }
  }

  // IndexPage shorthand: top-level headers/options/listKey → synthesise a
  // single datatable card.
  if (
    config.headers !== undefined ||
    config.options !== undefined ||
    config.listKey !== undefined
  ) {
    const id = ensureUniqueId(undefined, "card", seen)
    const card = {
      id,
      cardType: "datatable",
      headers: normaliseHeaders(config.headers),
      options: config.options,
      listKey: config.listKey,
      tableTitle: config.title,
    } as unknown as DisplayCard
    cards[id] = card
  }

  if (Object.keys(snippetOverrides).length > 0) {
    out.snippetOverrides = snippetOverrides
  }

  return out
}

function convertCard(
  v1: V1CardConfig,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  seen?: Set<string>,
  snippetOverrides?: SnippetOverrides,
): DisplayCard {
  if (V1_ELEMENT_AS_CARD.has(v1.type)) {
    // button_group / form_builder are elements in v2 — wrap in a
    // synthetic element-list card containing the converted element.
    return wrapElementAsCard(v1, cards, elements, seen)
  }

  const v2Type =
    V1_TO_V2_CARD_TYPE[v1.type] ?? (v1.type as DisplayCard["cardType"])
  const id = ensureUniqueId(v1.id as string | undefined, "card", seen)

  const card = {
    id,
    cardType: v2Type,
    title: v1.title,
    subtitle: v1.subtitle,
    showCard: v1.showCard,
    classList: v1.classList,
    ...v1ColumnsToV2(v1),
    conditionParams: v1ConditionParamsToV2(v1.conditionParams),
  } as unknown as Record<string, unknown>

  if (v1.fieldsList?.length) {
    // fields_list → element-list. Snippet refs become tokens; nested
    // tabs_list/fields_list nodes become card.tab/card.list container elements.
    card.elementList = fieldsToElementIds(
      v1.fieldsList,
      elements,
      seen,
      snippetOverrides,
    )
  }

  if (v1.cards?.length) {
    // Nested cards: convert each, store in flat map, reference by id.
    const ids: string[] = []
    for (const c of v1.cards) {
      if (isSnippetNode(c)) {
        ids.push(registerSnippetRef(c, snippetOverrides))
        continue
      }
      const childCard = convertCard(c, cards, elements, seen, snippetOverrides)
      cards[childCard.id!] = childCard
      ids.push(childCard.id!)
    }
    card.cardList = ids
  }

  if (v1.tabsList?.length) {
    // tabs_list → tab-list with tabList referencing child card ids.
    const ids: string[] = []
    for (const t of v1.tabsList) {
      // Each tab maps to an element-list card containing the tab's body.
      const tabBodyElements: string[] = []
      for (const tabCard of t.cards ?? []) {
        if (isSnippetNode(tabCard)) {
          ids.push(registerSnippetRef(tabCard, snippetOverrides))
          continue
        }
        const cardOrCards = convertCard(tabCard, cards, elements, seen, snippetOverrides)
        cards[cardOrCards.id!] = cardOrCards
        // Tabs in v1 hold cards; v2 tab-list contains card-id refs.
        ids.push(cardOrCards.id!)
        // For mixed-element-tabs we'd need an intermediate wrapper;
        // leaving as-is for now since tab content is usually one card.
      }
      void tabBodyElements
    }
    card.tabList = ids
  }

  if (v1.buttons?.length) {
    // button_group nested INSIDE a fields_list reaches here via the
    // top-level case; if a non-button_group card unexpectedly has
    // `buttons`, surface them as an action element.
    const el = convertElement(
      {
        type: "button_group",
        items: v1.buttons,
      } as V1FieldConfig,
      seen,
    )
    elements[el.id] = el
    card.elementList = [...(card.elementList as string[] ?? []), el.id]
  }

  // Pass-through datatable specifics. Normalise headers from v1 shape
  // ({text,value}) to v2 shape ({title,key}).
  for (const passthrough of [
    "headers",
    "options",
    "listKey",
    "createConfig",
    "updateConfig",
    "deleteConfig",
    "tableTitle",
  ] as const) {
    if (v1[passthrough] !== undefined) {
      card[passthrough] =
        passthrough === "headers"
          ? normaliseHeaders(v1[passthrough])
          : v1[passthrough]
    }
  }

  cards[id] = card as unknown as DisplayCard
  return card as unknown as DisplayCard
}

function wrapElementAsCard(
  v1: V1CardConfig,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  seen?: Set<string>,
): DisplayCard {
  const cardId = ensureUniqueId(undefined, "card", seen)
  const element = convertElement(v1 as unknown as V1FieldConfig, seen)
  elements[element.id] = element
  const card = {
    id: cardId,
    cardType: "element-list",
    showCard: v1.showCard ?? false,
    elementList: [element.id],
    ...v1ColumnsToV2(v1),
    conditionParams: v1ConditionParamsToV2(v1.conditionParams),
  } as unknown as DisplayCard
  cards[cardId] = card
  return card
}

function convertElement(v1: V1FieldConfig, seen?: Set<string>): CardElement {
  const disc: V2Discriminator =
    V1_TO_V2_FIELD_TYPE[v1.type] ?? {
      type: "input.text",
      extras: { __unknownV1Type: v1.type },
    }

  const id = ensureUniqueId(v1.id as string | undefined, "el", seen)
  const { type, name, label, default: legacyDefault, defaultValue, options } = v1

  // Drop v1-specific structural keys we've already consumed; everything
  // else passes through so things like `placeholder`, `maxLength`, etc.
  // arrive untouched.
  const cleaned: Record<string, unknown> = { ...v1 }
  delete cleaned.type
  delete cleaned.default
  // The source `id` must not override the (possibly regenerated) unique id.
  delete cleaned.id
  delete cleaned.cols
  delete cleaned.colsSm
  delete cleaned.colsMd
  delete cleaned.colsLg
  delete cleaned.colsXl
  delete cleaned.colsXxl
  delete cleaned.conditionParams
  void type

  const element = {
    id,
    type: disc.type,
    name,
    label,
    defaultValue: defaultValue ?? legacyDefault,
    ...v1ColumnsToV2(v1),
    conditionParams: v1ConditionParamsToV2(v1.conditionParams),
    ...cleaned,
    ...(disc.extras ?? {}),
  } as unknown as CardElement

  // v1 used `options` for choices; v2 uses `selectOptions`. Rename
  // when it looks like a select-style field.
  const elementBag = element as unknown as Record<string, unknown>
  if (options !== undefined && elementBag.selectOptions === undefined) {
    elementBag.selectOptions = normaliseOptions(options)
    delete elementBag.options
  }

  return element
}

/**
 * v1 structural node type (in an element/fieldsList context) → the `card.*`
 * layout element it reconstructs. Accepts both `_` and `-` spellings.
 */
const V1_NODE_TO_CARD_ELEMENT: Record<string, string> = {
  tabs_list: "card.tab",
  "tabs-list": "card.tab",
  fields_list: "card.list",
  "fields-list": "card.list",
  data_table: "card.datatable",
  "data-table": "card.datatable",
  grid_view: "card.grid",
  "grid-view": "card.grid",
  info_card: "card.info",
  "info-card": "card.info",
}

/**
 * Convert a list of v1 fields into v2 element ids, registering each into the
 * `elements` map. Handles snippet refs (→ tokens), structural card nodes
 * (`tabs_list`/`fields_list`/`data_table`/`grid_view`/`info_card` → `card.*`
 * container elements), and leaves.
 */
function fieldsToElementIds(
  fields: V1FieldConfig[],
  elements: Record<string, CardElement>,
  seen?: Set<string>,
  snippetOverrides?: SnippetOverrides,
): string[] {
  const ids: string[] = []
  for (const f of fields) {
    if (isSnippetNode(f)) {
      ids.push(registerSnippetRef(f, snippetOverrides))
      continue
    }
    const cardElType = V1_NODE_TO_CARD_ELEMENT[f.type]
    if (cardElType) {
      const el = buildCardElementFromNode(
        f,
        cardElType,
        elements,
        seen,
        snippetOverrides,
      )
      elements[el.id] = el
      ids.push(el.id)
      continue
    }
    const el = convertElement(f, seen)
    elements[el.id] = el
    ids.push(el.id)
  }
  return ids
}

/**
 * Reconstruct a `card.*` container ELEMENT from a v1 structural node found
 * inside a fieldsList (element context). For tab/list styles the children are
 * re-extracted into the flat `elements` map and referenced by id; data/grid/
 * info styles carry their own config. Inverse of `cardElementToV1`.
 */
function buildCardElementFromNode(
  f: V1FieldConfig,
  cardType: string,
  elements: Record<string, CardElement>,
  seen?: Set<string>,
  snippetOverrides?: SnippetOverrides,
): CardElement {
  const id = ensureUniqueId(
    (typeof f.id === "string" ? f.id : undefined) ??
      (typeof f.fieldId === "string" ? (f.fieldId as string) : undefined),
    "el",
    seen,
  )
  const cleaned: Record<string, unknown> = { ...f }
  for (const k of [
    "type",
    "id",
    "fieldId",
    "tabsList",
    "fieldsList",
    "cards",
    "cols",
    "colsSm",
    "colsMd",
    "colsLg",
    "colsXl",
    "colsXxl",
    "conditionParams",
  ]) {
    delete cleaned[k]
  }
  const base = {
    id,
    ...v1ColumnsToV2(f),
    conditionParams: v1ConditionParamsToV2(f.conditionParams),
    ...cleaned,
    type: cardType,
  } as Record<string, unknown>
  if (cardType === "card.tab") {
    base.tabs = (
      ((f as Record<string, unknown>).tabsList as
        | Array<Record<string, unknown>>
        | undefined) ?? []
    ).map((t) => ({
      id: ensureUniqueId(
        (typeof t.id === "string" ? t.id : undefined) ??
          (typeof t.tabId === "string" ? (t.tabId as string) : undefined),
        "tab",
        seen,
      ),
      label: (t.title as string | undefined) ?? "",
      elementList: fieldsToElementIds(
        (t.fieldsList as V1FieldConfig[] | undefined) ?? [],
        elements,
        seen,
        snippetOverrides,
      ),
    }))
  } else if (cardType === "card.list") {
    base.elementList = fieldsToElementIds(
      ((f as Record<string, unknown>).fieldsList as V1FieldConfig[] | undefined) ??
        [],
      elements,
      seen,
      snippetOverrides,
    )
  }
  // data/grid/info styles carry their own config via `cleaned`; no children.
  return base as unknown as CardElement
}

/**
 * v1 headers use `{ text, value }` for column label + data-key. v2 uses
 * `{ key, title }`. Pass-through other keys (sortable, width, type, ...)
 * untouched.
 */
function normaliseHeaders(input: unknown): unknown {
  if (!Array.isArray(input)) {
    return input
  }
  return input.map((h) => {
    if (typeof h !== "object" || h === null) {
      return h
    }
    const header = h as Record<string, unknown>
    const out: Record<string, unknown> = { ...header }
    if (out.key === undefined && header.value !== undefined) {
      out.key = header.value
    }
    if (out.title === undefined && header.text !== undefined) {
      out.title = header.text
    }
    return out
  })
}

function normaliseOptions(input: unknown): unknown {
  if (!Array.isArray(input)) {
    return input
  }
  // v1 often used { value, text } pairs; v2 uses { value, label }.
  return input.map((opt) => {
    if (typeof opt !== "object" || opt === null) {
      return opt
    }
    const o = opt as Record<string, unknown>
    if (o.label === undefined && o.text !== undefined) {
      return { ...o, label: o.text }
    }
    return o
  })
}

/* -------------------------------------------------------------------------
 * Fragment converters (v1 → v2)
 *
 * The full-page `v1ToV2` converts an entire ShowPage/IndexPage config. These
 * helpers convert the individual building blocks — a single element, a card
 * (with its descendants), or a set of tabs — for callers (e.g. the
 * FormBuilder) that only hold a fragment of a page.
 *
 * Because the v2 shape references cards/elements by id from flat maps, the
 * card/tab converters return a self-contained *bundle*: the `cards` /
 * `elements` id-maps plus the top-level id(s) into them. Element converters
 * are self-contained objects and need no maps.
 * ----------------------------------------------------------------------- */

/** A self-contained set of converted v2 cards + the elements they reference. */
export interface V2CardBundle {
  /** Flat id-keyed card map (top-level + nested cards). */
  cards: Record<string, DisplayCard>
  /** Flat id-keyed element map referenced by the cards. */
  elements: Record<string, CardElement>
  /** Ids of the top-level cards within `cards`, in input order. */
  ids: string[]
  /** Per-placement snippet override values, keyed by instance id. */
  snippetOverrides: Record<string, Record<string, unknown>>
}

/**
 * A self-contained v2 builder schema for a set of tabs: a `tab-list` root
 * card whose `tabList` references one tab card per input tab, plus the flat
 * card/element maps. This is exactly the shape the FormBuilder consumes in
 * `rootTabsContainer` mode.
 */
export interface V2TabsBundle {
  /** Id of the root `tab-list` card within `cards`. */
  rootCardId: string
  cards: Record<string, DisplayCard>
  elements: Record<string, CardElement>
  /** Per-placement snippet override values, keyed by instance id. */
  snippetOverrides: Record<string, Record<string, unknown>>
}

/** Convert a single v1 field into a v2 element. */
export function v1ElementToV2(field: V1FieldConfig): CardElement {
  return convertElement(field)
}

/** Coerce a single item / nullish value into an array. */
function asArray<T>(value: T | T[] | null | undefined): T[] {
  if (Array.isArray(value)) return value
  return value == null ? [] : [value]
}

/**
 * Convert v1 fields into v2 elements. Accepts an array or a single field
 * (a single value is treated as a one-element list).
 */
export function v1ElementsToV2(
  fields: V1FieldConfig | V1FieldConfig[],
): CardElement[] {
  const seen = new Set<string>()
  return asArray(fields).map((f) => convertElement(f, seen))
}

/**
 * Convert v1 cards (+ descendants) into a self-contained bundle. Accepts an
 * array or a single card (a single value is treated as a one-element list).
 * All produced card/element ids are guaranteed unique.
 */
export function v1CardsToV2(
  v1Cards: V1CardConfig | V1CardConfig[],
): V2CardBundle {
  const cards: Record<string, DisplayCard> = {}
  const elements: Record<string, CardElement> = {}
  const snippetOverrides: SnippetOverrides = {}
  const seen = new Set<string>()
  const ids: string[] = []
  for (const c of asArray(v1Cards)) {
    if (isSnippetNode(c)) {
      ids.push(registerSnippetRef(c, snippetOverrides))
      continue
    }
    const card = convertCard(c, cards, elements, seen, snippetOverrides)
    if (card.id !== undefined) {
      ids.push(card.id)
    }
  }
  return { cards, elements, ids, snippetOverrides }
}

/** Convert a single v1 card (+ descendants) into a self-contained bundle. */
export function v1CardToV2(v1Card: V1CardConfig): V2CardBundle {
  return v1CardsToV2([v1Card])
}

/**
 * Convert v1 tabs into a self-contained tabs bundle. Accepts an array or a
 * single tab (a single value is treated as a one-element list).
 *
 * A tab body can be expressed two ways and both are supported:
 *   - `tab.cards` — nested v1 cards (the classic tabs_list shape), or
 *   - `tab.fieldsList` / `tab.elementList` — a flat field list directly on
 *     the tab (an "element-list" tab, e.g. the FormBuilder rootTabsContainer
 *     shape). These are wrapped into a synthesized `fields_list` card so the
 *     fields aren't dropped.
 */
export function v1TabsToV2(
  v1Tabs: V1TabConfig | V1TabConfig[],
  options?: { rootKey?: string },
): V2TabsBundle {
  const cards: Record<string, DisplayCard> = {}
  const elements: Record<string, CardElement> = {}
  const snippetOverrides: SnippetOverrides = {}
  const seen = new Set<string>()
  const tabIds: string[] = []

  for (const tab of asArray(v1Tabs)) {
    const tabId = ensureUniqueId(
      (typeof tab.id === "string" ? tab.id : undefined) ?? tab.tabId,
      "tab-card",
      seen,
    )

    const directFields = (tab.fieldsList ?? tab.elementList) as
      | V1FieldConfig[]
      | undefined
    const tabCards = (
      Array.isArray(tab.cards) ? tab.cards : []
    ) as V1CardConfig[]

    // Backward-compat tabs are `card_list` cards. When such a tab's single
    // body card is a `fields_list` (element-list), collapse it back into an
    // element-list tab card — the FormBuilder rootTabsContainer shape.
    const soleFieldsCard =
      tabCards.length === 1 &&
      (V1_TO_V2_CARD_TYPE[tabCards[0]!.type] ?? tabCards[0]!.type) ===
        "element-list"
        ? tabCards[0]!
        : undefined

    if (tabCards.length > 0 && !soleFieldsCard) {
      // Genuine multi-card tab → a card-list tab card.
      const childIds: string[] = []
      for (const c of tabCards) {
        if (isSnippetNode(c)) {
          childIds.push(registerSnippetRef(c, snippetOverrides))
          continue
        }
        const child = convertCard(c, cards, elements, seen, snippetOverrides)
        if (child.id !== undefined) childIds.push(child.id)
      }
      cards[tabId] = {
        id: tabId,
        cardType: "card-list",
        title: tab.title ?? "",
        cardList: childIds,
      } as unknown as DisplayCard
    } else {
      // Element-list tab card — from a direct field list or the sole
      // fields_list body card. Always carries an `elementList` array.
      const fields = soleFieldsCard?.fieldsList ?? directFields ?? []
      const elementIds = fieldsToElementIds(
        fields,
        elements,
        seen,
        snippetOverrides,
      )
      cards[tabId] = {
        id: tabId,
        cardType: "element-list",
        title: tab.title ?? soleFieldsCard?.title ?? "",
        elementList: elementIds,
      } as unknown as DisplayCard
    }
    tabIds.push(tabId)
  }

  const rootCardId = ensureUniqueId(options?.rootKey, "root", seen)
  cards[rootCardId] = {
    id: rootCardId,
    cardType: "tab-list",
    title: "",
    tabList: tabIds,
  } as unknown as DisplayCard

  return { rootCardId, cards, elements, snippetOverrides }
}

/**
 * Convert a single v1 tab into a self-contained tabs bundle (a tab-list root
 * with one tab card).
 */
export function v1TabToV2(
  v1Tab: V1TabConfig,
  options?: { rootKey?: string },
): V2TabsBundle {
  return v1TabsToV2([v1Tab], options)
}

export const __testing = {
  convertCard,
  convertElement,
  normaliseOptions,
  v1ConditionParamsToV2,
}

// Re-export for test convenience.
export { v1ConditionParamsToV2 }
