/**
 * v2 → v1 schema adapter.
 *
 * Bidirectional pair of `v1ToV2`. Lets consumers serialise the
 * library's internal v2 shape back into the v1 docs-format JSON for
 * round-tripping with legacy backend endpoints / form-builder export.
 *
 * The conversion is lossy by design — v2 carries strictly more
 * information than v1 (style discriminator, separated element map).
 * For elements that have no clean v1 equivalent (`code/git-editor`,
 * `builder/object-builder`, ...) we fall back to the v2 `style`
 * string so the round-trip preserves enough to re-parse later.
 */

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

import { V2_TO_V1_CARD_TYPE } from "./cardTypeMap"
import { v2ToV1FieldType } from "./fieldTypeMap"
import { v2ColumnsToV1, v2ConditionParamToV1 } from "./conditionMap"
import {
  isSnippetToken,
  parseSnippetToken,
  v1SnippetNode,
  type V1SnippetNode,
} from "./snippets"

/** Per-placement snippet override values, keyed by instance id. */
type SnippetOverrides = Record<string, Record<string, unknown>>

/** Decode a `snippet#<key>[:<instanceId>]` token into a v1 snippet node. */
function decodeSnippetToken(
  id: string,
  snippetOverrides?: SnippetOverrides,
): V1SnippetNode {
  const { key, instanceId } = parseSnippetToken(id)
  const overrides =
    instanceId && snippetOverrides ? snippetOverrides[instanceId] : undefined
  return v1SnippetNode(key, overrides, instanceId)
}
import type {
  V1CardConfig,
  V1FieldConfig,
  V1PageConfig,
  V1TabConfig,
  V2PageConfig,
} from "./v1ToV2"

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

  const snippetOverrides = config.snippetOverrides

  if (config.tabsList?.length) {
    out.tabsList = config.tabsList.map((tab) => ({
      title: tab.title,
      tabId: tab.tabId,
      cards: tab.cards.map((c) =>
        cardToV1(c, config.cards, config.elements, snippetOverrides),
      ),
    }))
  } else if (Object.keys(config.cards).length) {
    out.cards = Object.values(config.cards).map((c) =>
      cardToV1(c, config.cards, config.elements, snippetOverrides),
    )
  }

  return out
}

function cardToV1(
  card: DisplayCard,
  allCards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1CardConfig {
  const v1Type = V2_TO_V1_CARD_TYPE[card.cardType] ?? card.cardType

  const v1: V1CardConfig = {
    type: v1Type,
    title: card.title,
    subtitle: card.subtitle,
    showCard: card.showCard,
    classList: card.classList,
    ...v2ColumnsToV1(card),
    conditionParams: conditionParamsToV1(card.conditionParams),
  }
  if (card.id !== undefined) {
    v1.id = card.id
  }

  // element-list → fields_list (snippet tokens + card.* containers handled)
  const elementList = (card as unknown as Record<string, unknown>).elementList as
    | string[]
    | undefined
  if (elementList?.length) {
    v1.fieldsList = elementListToV1Fields(elementList, elements, snippetOverrides)
  }

  // card-list / nested cards (snippet tokens decode to v1 snippet nodes)
  const cardList = (card as unknown as Record<string, unknown>).cardList as
    | string[]
    | undefined
  if (cardList?.length) {
    v1.cards = cardList
      .map((id) => {
        if (isSnippetToken(id)) {
          return decodeSnippetToken(id, snippetOverrides) as unknown as V1CardConfig
        }
        const c = allCards[id]
        return c ? cardToV1(c, allCards, elements, snippetOverrides) : undefined
      })
      .filter((c): c is V1CardConfig => Boolean(c))
  }

  // tab-list → tabs_list
  const tabList = (card as unknown as Record<string, unknown>).tabList as
    | string[]
    | undefined
  if (tabList?.length) {
    v1.tabsList = tabList
      .map((id) => allCards[id])
      .filter((c): c is DisplayCard => Boolean(c))
      .map((c) => ({
        title: c.title ?? "",
        tabId: c.id,
        cards: [cardToV1(c, allCards, elements, snippetOverrides)],
      }))
  }

  // datatable passthrough fields
  const bag = card as unknown as Record<string, unknown>
  for (const passthrough of [
    "headers",
    "options",
    "listKey",
    "createConfig",
    "updateConfig",
    "deleteConfig",
    "tableTitle",
  ] as const) {
    if (bag[passthrough] !== undefined) {
      ;(v1 as Record<string, unknown>)[passthrough] = bag[passthrough]
    }
  }

  return v1
}

function elementToV1(element: CardElement): V1FieldConfig {
  const v1Type = v2ToV1FieldType(element.type)
  const { id, type, name, label, defaultValue } = element

  const cleaned: Record<string, unknown> = {
    ...(element as unknown as Record<string, unknown>),
  }
  delete cleaned.type
  delete cleaned.columns
  delete cleaned.columnsSm
  delete cleaned.columnsMd
  delete cleaned.columnsLg
  delete cleaned.columnsXl
  delete cleaned.columnsXxl
  delete cleaned.conditionParams

  // selectOptions → options (only when it's actually a list of choices;
  // some elements carry a non-array `selectOptions`, which we leave as-is)
  if (Array.isArray(cleaned.selectOptions) && cleaned.options === undefined) {
    cleaned.options = (cleaned.selectOptions as unknown[]).map((o) => {
      if (typeof o !== "object" || o === null) {
        return o
      }
      const opt = o as Record<string, unknown>
      // Keep both `label` AND `text` to round-trip without information loss.
      if (opt.text === undefined && opt.label !== undefined) {
        return { ...opt, text: opt.label }
      }
      return opt
    })
    delete cleaned.selectOptions
  }
  void type

  const out: V1FieldConfig = {
    id,
    type: v1Type,
    name: name ?? undefined,
    label,
    defaultValue,
    ...v2ColumnsToV1(element),
    conditionParams: conditionParamsToV1(element.conditionParams),
    ...cleaned,
  }
  return out
}

/**
 * `card.*` layout element styles ↔ their v1 structural node type. A tab
 * container is a `tabs_list`; every other layout style is a card type the
 * legacy renders (`fields_list`, `data_table`, `grid_view`, `info_card`).
 */
const CARD_ELEMENT_TO_V1_TYPE: Record<string, string> = {
  "card.list": "fields_list",
  "card.tab": "tabs_list",
  "card.datatable": "data_table",
  "card.grid": "grid_view",
  "card.info": "info_card",
}

/** True for a `card.*` layout-container element type. */
function isCardLayoutType(type: unknown): type is string {
  return typeof type === "string" && type.startsWith("card.")
}

/**
 * Convert a `card.*` container ELEMENT (a `CardLayoutElement`) to its v1
 * structural node. `card.tab` becomes a `tabs_list` (each tab's children
 * inlined as `fieldsList`); `card.list` becomes a `fields_list` (children
 * inlined); the data/grid/info styles map to their v1 card type with their
 * config passed through. Children (referenced by id in v2) are inlined so the
 * v1 form is self-contained and round-trips back to the same element.
 */
function cardElementToV1(
  element: CardElement,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1FieldConfig {
  const bag = element as unknown as Record<string, unknown>
  const v1 = elementToV1(element)
  // Re-emit container structure as v1 — drop the builder-only id arrays.
  delete (v1 as Record<string, unknown>).tabs
  delete (v1 as Record<string, unknown>).elementList
  delete (v1 as Record<string, unknown>).nestedElements
  v1.type = CARD_ELEMENT_TO_V1_TYPE[String(bag.type)] ?? "fields_list"
  if (bag.type === "card.tab") {
    ;(v1 as Record<string, unknown>).tabsList = (
      (bag.tabs as Array<Record<string, unknown>> | undefined) ?? []
    ).map((t) => ({
      id: t.id,
      tabId: t.id,
      title: t.label ?? "",
      fieldsList: elementListToV1Fields(
        (t.elementList as string[] | undefined) ?? [],
        elements,
        snippetOverrides,
      ),
    }))
  } else if (bag.type === "card.list") {
    v1.fieldsList = elementListToV1Fields(
      (bag.elementList as string[] | undefined) ?? [],
      elements,
      snippetOverrides,
    )
  }
  // data/grid/info carry their own config (passed through by elementToV1) and
  // have no nested element-id children to inline.
  return v1
}

/**
 * Convert a list of element ids (a card's `elementList` or a tab's) into v1
 * fields, handling snippet tokens, `card.*` container elements, and leaves.
 */
function elementListToV1Fields(
  ids: string[],
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1FieldConfig[] {
  return ids
    .map((id) => {
      if (isSnippetToken(id)) {
        return decodeSnippetToken(id, snippetOverrides) as unknown as V1FieldConfig
      }
      const el = elements[id]
      if (!el) {
        return undefined
      }
      const type = (el as unknown as Record<string, unknown>).type
      if (isCardLayoutType(type)) {
        return cardElementToV1(el, elements, snippetOverrides)
      }
      return elementToV1(el)
    })
    .filter((f): f is V1FieldConfig => Boolean(f))
}

function conditionParamsToV1(
  params: ConditionParam[] | undefined,
): V1FieldConfig["conditionParams"] {
  if (!params || params.length === 0) {
    return undefined
  }
  return params.map(v2ConditionParamToV1)
}

/* -------------------------------------------------------------------------
 * Fragment converters (v2 → v1)
 *
 * Bidirectional partners of the v1→v2 fragment converters. The card/tab
 * converters take the flat `cards` / `elements` id-maps so referenced
 * children can be resolved; element converters are self-contained.
 * ----------------------------------------------------------------------- */

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

/** Convert a list of v2 elements into v1 fields. */
export function v2ElementsToV1(elements: CardElement[]): V1FieldConfig[] {
  return elements.map(elementToV1)
}

/**
 * Convert one v2 card into a v1 card. Accepts either a full `DisplayCard`
 * object (mirroring `v1CardToV2`) or a card id to resolve from `cards`.
 *
 * The `cards` / `elements` maps are always required — a v2 card references
 * its children (`elementList` / `cardList` / `tabList`) by id, so nested
 * cards/elements are resolved from the maps regardless of how the top-level
 * card is supplied. (v1 cards inline their children, so `v1CardToV2` needs
 * no maps — hence the shape difference.)
 */
export function v2CardToV1(
  card: string | DisplayCard,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1CardConfig | undefined {
  const resolved = typeof card === "string" ? cards[card] : card
  return resolved ? cardToV1(resolved, cards, elements, snippetOverrides) : undefined
}

/**
 * Convert v2 cards into v1 cards. Accepts full `DisplayCard` objects (like
 * `v1CardsToV2`) or card ids to resolve from `cards`; the two forms may be
 * mixed. A single id/card (not wrapped in an array) is also accepted and
 * treated as a one-element list.
 */
export function v2CardsToV1(
  cardsOrIds: string | DisplayCard | Array<string | DisplayCard>,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1CardConfig[] {
  const list = Array.isArray(cardsOrIds) ? cardsOrIds : [cardsOrIds]
  return list
    .map((c) => (typeof c === "string" ? cards[c] : c))
    .filter((c): c is DisplayCard => Boolean(c))
    .map((c) => cardToV1(c, cards, elements, snippetOverrides))
}

/**
 * A single tab accepted by the v2→v1 tab converters. It can be:
 *   - a tab **card id** (string) referenced from a tab-list's `tabList`,
 *   - a tab **card object** (`DisplayCard`) — e.g. an element-list "tab"
 *     card from the FormBuilder rootTabsContainer schema, or
 *   - a page-level **tab descriptor** `{ title, tabId, cards: DisplayCard[] }`.
 */
export type V2TabInput =
  | string
  | DisplayCard
  | { title?: string; tabId?: string; cards: DisplayCard[] }

/**
 * Convert a v2 "tab card" into a v1 tab. For backward compatibility every
 * tab is emitted as a `card_list` (its `type`), so legacy consumers always
 * see tabs that hold cards:
 *   - `element-list` → a `card_list` tab whose single body card is the
 *     converted `fields_list` (the tab's content). `v1TabsToV2` reads that
 *     first card back as the tab's element list ("the new way").
 *   - `card-list`    → a `card_list` tab holding its unwrapped nested cards.
 *   - anything else  → a `card_list` tab holding the converted card.
 */
function tabCardToV1(
  card: DisplayCard,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1TabConfig {
  const v1Card = cardToV1(card, cards, elements, snippetOverrides)
  const tabCards: V1CardConfig[] =
    card.cardType === "card-list"
      ? ((v1Card.cards as V1CardConfig[] | undefined) ?? [])
      : // The wrapped fields_list card must NOT reuse the tab's id, or the
        // tab and its only child collide. Derive a distinct id from it.
        [{ ...v1Card, id: card.id ? `${card.id}-fields` : v1Card.id }]
  return {
    title: card.title ?? "",
    tabId: card.id,
    type: V2_TO_V1_CARD_TYPE["card-list"],
    cards: tabCards,
  } as V1TabConfig
}

/**
 * Convert one resolved v2 card into v1 tab(s). A `tab-list` card is
 * **expanded** into one tab per `tabList` entry (so callers can pass the
 * root tab-list card directly); every other card maps to a single tab.
 */
function cardToTabs(
  card: DisplayCard,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1TabConfig[] {
  if (card.cardType === "tab-list") {
    const tabList =
      ((card as unknown as Record<string, unknown>).tabList as
        | string[]
        | undefined) ?? []
    return tabList
      .map((id) => cards[id])
      .filter((c): c is DisplayCard => Boolean(c))
      .map((c) => tabCardToV1(c, cards, elements, snippetOverrides))
  }
  return [tabCardToV1(card, cards, elements, snippetOverrides)]
}

function tabInputToV1(
  tab: V2TabInput,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1TabConfig[] {
  // Tab card id → resolve from the cards map (a tab-list id expands).
  if (typeof tab === "string") {
    const card = cards[tab]
    return card ? cardToTabs(card, cards, elements, snippetOverrides) : []
  }
  const obj = tab as unknown as Record<string, unknown>
  // Tab card object (has a cardType) — a tab-list expands into its tabs.
  if (typeof obj.cardType === "string") {
    return cardToTabs(tab as DisplayCard, cards, elements, snippetOverrides)
  }
  // Page-level tab descriptor with inline card objects.
  if (Array.isArray(obj.cards)) {
    return [
      {
        title: obj.title as string | undefined,
        tabId: obj.tabId as string | undefined,
        cards: (obj.cards as DisplayCard[]).map((c) =>
          cardToV1(c, cards, elements, snippetOverrides),
        ),
      },
    ]
  }
  return []
}

/**
 * Convert v2 tabs into v1 tabs. Accepts an array (or single) of:
 *   - tab card ids or tab card objects (element-list / card-list),
 *   - a **tab-list card** (or its id) — expanded into one tab per child, or
 *   - page-level tab descriptors `{ title, tabId, cards }`.
 * The forms may be mixed. Tab cards reference children by id, so the
 * `cards` / `elements` maps are required to resolve them.
 */
export function v2TabsToV1(
  tabs: V2TabInput | V2TabInput[],
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1TabConfig[] {
  const list = Array.isArray(tabs) ? tabs : [tabs]
  return list.flatMap((t) => tabInputToV1(t, cards, elements, snippetOverrides))
}

/** Convert a single v2 tab (id, card object, or descriptor) into a v1 tab. */
export function v2TabToV1(
  tab: V2TabInput,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1TabConfig | undefined {
  return v2TabsToV1(tab, cards, elements, snippetOverrides)[0]
}
