/**
 * Auto-detecting v1 ↔ v2 converters.
 *
 * The fragment converters in `v1ToV2` / `v2ToV1` each handle one specific
 * shape (an element, a card, a set of tabs). These two entry points sit on
 * top of them and *figure out the shape for you* — so callers can hand over
 * "whatever they've got" (a single card, a single tab, a tab-list root, an
 * array, or a whole `{ cards, elements }` builder schema) without first
 * having to decide which converter to call. This mirrors the legacy
 * `m1ConvertToM2` / `m2ConvertToM1` helpers.
 *
 * Both directions stay in THIS library's shapes: v2 elements use the
 * dot-notation `type` (`"input.text"`), v1 fields use the simplified
 * `{ id, defaultValue, cols }` form — i.e. exactly what the other adapters
 * here produce. (They do NOT emit the legacy experience-portal
 * `fieldId`/`default`/`type`+`style` shape.)
 *
 * Cardinality rule for `convertV2ToV1`: it mirrors the input. A single
 * card/tab in → a single converted object out; an array in → an array out.
 * A single card-list "root container" (style `tab-container`, or the root of
 * a passed schema) is *unwrapped* — its children are converted and, if that
 * resolves to exactly one child, that one object is returned (so a
 * `card-list` root holding a single `tab-list` returns one `tabs_list`).
 */

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

import { V1_TO_V2_CARD_TYPE } from "./cardTypeMap"
import {
  v1CardToV2,
  v1CardsToV2,
  v1TabsToV2,
  type V1CardConfig,
  type V1FieldConfig,
  type V1TabConfig,
} from "./v1ToV2"
import {
  v2CardToV1,
  v2ElementToV1,
  v2TabToV1,
  v2TabsToV1,
} from "./v2ToV1"
import {
  isSnippetToken,
  parseSnippetToken,
  v1SnippetNode,
} from "./snippets"

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

/* =========================================================================
 * Shared types
 * ===================================================================== */

/**
 * A self-contained v2 builder schema: a flat `cards` / `elements` id-map plus
 * the id of the root card. This is what `convertV1ToV2` always returns and
 * what ShowPage / FormBuilder consume.
 */
export interface V2BuilderSchema {
  /** Id (within `cards`) of the root card. */
  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>>
}

/** Anything `convertV2ToV1` accepts as the thing-to-convert. */
export type V2ConvertInput =
  | string
  | DisplayCard
  | V2BuilderSchema
  | { rootCardId?: string; root?: string | DisplayCard; cards?: Record<string, DisplayCard>; elements?: Record<string, CardElement> }
  | { title?: string; tabId?: string; cards: DisplayCard[] }
  | Array<string | DisplayCard | { title?: string; tabId?: string; cards: DisplayCard[] }>

/** What `convertV2ToV1` returns — a single converted node, or a list. */
export type V1ConvertResult =
  | V1CardConfig
  | V1TabConfig
  | Array<V1CardConfig | V1TabConfig>

/* =========================================================================
 * v2 → v1 (auto-detecting)
 * ===================================================================== */

function isObject(v: unknown): v is Record<string, unknown> {
  return typeof v === "object" && v !== null && !Array.isArray(v)
}

/** A `{ cards, elements }`-style schema wrapper (cards is a non-array object). */
function isSchema(v: unknown): v is V2BuilderSchema {
  return (
    isObject(v) &&
    !("cardType" in v) &&
    isObject((v as Record<string, unknown>).cards)
  )
}

/** A page-level tab descriptor `{ title?, tabId?, cards: [...] }`. */
function isTabDescriptor(
  v: unknown,
): v is { title?: string; tabId?: string; cards: DisplayCard[] } {
  return (
    isObject(v) &&
    !("cardType" in v) &&
    Array.isArray((v as Record<string, unknown>).cards)
  )
}

/**
 * Convert one resolved v2 card into its v1 form. tab-list cards become a
 * `tabs_list` whose `tabsList` entries are produced by `v2TabsToV1` (each tab
 * a `card_list` holding its content) — the structure legacy consumers expect.
 */
function singleCardToV1(
  card: DisplayCard,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1CardConfig {
  const base = v2CardToV1(card, cards, elements, snippetOverrides)
  if (!base) {
    return { type: card.cardType } as unknown as V1CardConfig
  }
  if (card.cardType === "tab-list") {
    return {
      ...base,
      type: "tabs_list",
      tabsList: v2TabsToV1(card, cards, elements, snippetOverrides),
    }
  }
  return base
}

/** Convert one array item (id / card object / tab descriptor) to v1. */
function itemToV1(
  item: string | DisplayCard | { cards: DisplayCard[] },
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1CardConfig | V1TabConfig | undefined {
  if (typeof item === "string") {
    if (isSnippetToken(item)) {
      const { key, instanceId } = parseSnippetToken(item)
      return v1SnippetNode(
        key,
        instanceId ? snippetOverrides?.[instanceId] : undefined,
        instanceId,
      ) as unknown as V1CardConfig
    }
    const resolved = cards[item]
    return resolved
      ? singleCardToV1(resolved, cards, elements, snippetOverrides)
      : undefined
  }
  if (isTabDescriptor(item)) {
    return v2TabToV1(item, cards, elements, snippetOverrides)
  }
  if (isObject(item) && "cardType" in item) {
    return singleCardToV1(
      item as unknown as DisplayCard,
      cards,
      elements,
      snippetOverrides,
    )
  }
  return undefined
}

/**
 * Convert a single "root" card. A `card-list` acting as a container (it's the
 * schema root, or carries the `tab-container` style) is unwrapped into its
 * children; everything else converts to a single node.
 */
function convertRootCard(
  card: DisplayCard,
  cards: Record<string, DisplayCard>,
  elements: Record<string, CardElement>,
  isRoot: boolean,
  snippetOverrides?: SnippetOverrides,
): V1ConvertResult {
  const bag = card as unknown as Record<string, unknown>
  const isContainer =
    card.cardType === "card-list" && (isRoot || bag.style === "tab-container")

  if (isContainer) {
    const childIds = (bag.cardList as string[] | undefined) ?? []
    const results = childIds
      .map((id) => itemToV1(id, cards, elements, snippetOverrides))
      .filter((c): c is V1CardConfig | V1TabConfig => Boolean(c))
    return results.length === 1 ? results[0]! : results
  }

  return singleCardToV1(card, cards, elements, snippetOverrides)
}

/**
 * Convert v2 → v1, detecting the input shape automatically.
 *
 * Accepts any of:
 *   - a single card object (or card id, with `cards`/`elements` supplied),
 *   - a tab descriptor `{ title, tabId, cards }`,
 *   - a builder schema `{ rootCardId | root, cards, elements }`,
 *   - an array of any of the above.
 *
 * Returns a single converted node when the input is a single thing, or an
 * array when the input is an array (or a container root resolving to several
 * children). See the cardinality rule in the module doc-comment.
 */
export function convertV2ToV1(
  input: V2ConvertInput,
  cards?: Record<string, DisplayCard>,
  elements?: Record<string, CardElement>,
  snippetOverrides?: SnippetOverrides,
): V1ConvertResult {
  // 1. Arrays → convert each item, always return an array.
  if (Array.isArray(input)) {
    const cardsMap = cards ?? {}
    const elementsMap = elements ?? {}
    return input
      .map((item) => itemToV1(item, cardsMap, elementsMap, snippetOverrides))
      .filter((c): c is V1CardConfig | V1TabConfig => Boolean(c))
  }

  // 2. Schema wrapper → resolve its root, use its maps (+ its overrides).
  if (isSchema(input) && cards === undefined) {
    const schema = input as V2BuilderSchema & { root?: string | DisplayCard }
    const cardsMap = schema.cards
    const elementsMap = schema.elements ?? {}
    const overrides = schema.snippetOverrides ?? snippetOverrides
    const rootRef = schema.rootCardId ?? schema.root
    const rootCard =
      typeof rootRef === "string"
        ? cardsMap[rootRef]
        : (rootRef as DisplayCard | undefined)
    if (!rootCard) {
      return []
    }
    return convertRootCard(rootCard, cardsMap, elementsMap, true, overrides)
  }

  const cardsMap = cards ?? {}
  const elementsMap = elements ?? {}

  // 3. Tab descriptor → a single tab.
  if (isTabDescriptor(input)) {
    return v2TabToV1(input, cardsMap, elementsMap, snippetOverrides) ?? []
  }

  // 4. A card id string.
  if (typeof input === "string") {
    const resolved = cardsMap[input]
    return resolved
      ? convertRootCard(resolved, cardsMap, elementsMap, false, snippetOverrides)
      : []
  }

  // 5. A single card object.
  if (isObject(input) && "cardType" in input) {
    return convertRootCard(
      input as DisplayCard,
      cardsMap,
      elementsMap,
      false,
      snippetOverrides,
    )
  }

  // 6. Fallback: treat as a single element.
  return v2ElementToV1(input as unknown as CardElement) as unknown as V1CardConfig
}

/* =========================================================================
 * v1 → v2 (auto-detecting)
 * ===================================================================== */

let rootCounter = 0
function nextRootId(seen: Record<string, unknown>, preferred?: string): string {
  if (preferred && !(preferred in seen)) {
    return preferred
  }
  let id = preferred ?? `root-${(rootCounter += 1)}`
  while (id in seen) {
    id = `root-${(rootCounter += 1)}`
  }
  return id
}

/** Test-only: reset the root-id counter so generated ids are deterministic. */
export function __resetAutoRootCounter(): void {
  rootCounter = 0
}

/** True for objects that are v1 *cards* (a known card type, or a v2 cardType). */
function isCardObject(v: unknown): boolean {
  if (!isObject(v)) {
    return false
  }
  if ("cardType" in v) {
    return true
  }
  const type = v.type
  return typeof type === "string" && type in V1_TO_V2_CARD_TYPE
}

/** True for a v1 `tabs_list` card (carries a `tabsList`). */
function isTabsListCard(v: unknown): boolean {
  return (
    isObject(v) &&
    (v.type === "tabs_list" ||
      (v as Record<string, unknown>).cardType === "tab-list" ||
      Array.isArray((v as Record<string, unknown>).tabsList))
  )
}

function emptySchema(rootKey = "root"): V2BuilderSchema {
  return {
    rootCardId: rootKey,
    cards: {
      [rootKey]: {
        id: rootKey,
        cardType: "element-list",
        title: "",
        elementList: [],
      } as unknown as DisplayCard,
    },
    elements: {},
  }
}

/** Attach a snippet-overrides map to a schema only when it's non-empty. */
function attachOverrides(
  schema: V2BuilderSchema,
  overrides: SnippetOverrides,
): V2BuilderSchema {
  if (Object.keys(overrides).length > 0) {
    schema.snippetOverrides = overrides
  }
  return schema
}

/**
 * Convert v1 → v2, detecting the input shape automatically. Always returns a
 * self-contained builder schema `{ rootCardId, cards, elements }`:
 *
 *   - a `tabs_list` card / tab descriptors / array of tabs → a `tab-list` root,
 *   - a single card → that converted card as the root,
 *   - several cards → a `card-list` root referencing them,
 *   - fields (no card structure) → an `element-list` root holding them.
 */
export function convertV1ToV2(
  input:
    | V1CardConfig
    | V1TabConfig
    | V1FieldConfig
    | Array<V1CardConfig | V1TabConfig | V1FieldConfig>
    | null
    | undefined,
  options?: { rootKey?: string },
): V2BuilderSchema {
  const rootKey = options?.rootKey ?? "root"
  if (input == null) {
    return emptySchema(rootKey)
  }

  // --- Single object forms ---
  if (!Array.isArray(input)) {
    // A tabs_list card → tab-list root built from its tabs (keep its id).
    if (isTabsListCard(input)) {
      const bag = input as unknown as Record<string, unknown>
      const tabs = (bag.tabsList as V1TabConfig[] | undefined) ?? []
      const preferredRoot =
        (typeof bag.id === "string" ? bag.id : undefined) ??
        (typeof bag.tabId === "string" ? bag.tabId : undefined) ??
        rootKey
      const bundle = v1TabsToV2(tabs, { rootKey: preferredRoot })
      return attachOverrides(
        {
          rootCardId: bundle.rootCardId,
          cards: bundle.cards,
          elements: bundle.elements,
        },
        bundle.snippetOverrides,
      )
    }

    // A single card → that card becomes the root.
    if (isCardObject(input)) {
      const bundle = v1CardToV2(input as V1CardConfig)
      const rootCardId = bundle.ids[0]
      if (rootCardId) {
        return attachOverrides(
          { rootCardId, cards: bundle.cards, elements: bundle.elements },
          bundle.snippetOverrides,
        )
      }
      return emptySchema(rootKey)
    }

    // A bare tab descriptor → tab-list root with one tab.
    if (isTabDescriptor(input)) {
      const bundle = v1TabsToV2([input as unknown as V1TabConfig], { rootKey })
      return attachOverrides(
        {
          rootCardId: bundle.rootCardId,
          cards: bundle.cards,
          elements: bundle.elements,
        },
        bundle.snippetOverrides,
      )
    }

    // Otherwise a single field → element-list root.
    return wrapFieldsAsRoot([input as V1FieldConfig], rootKey)
  }

  // --- Array forms ---
  const sample = input.find(Boolean)
  if (!sample) {
    return emptySchema(rootKey)
  }

  // Array of tab descriptors → tab-list root.
  if (isTabDescriptor(sample) && !isCardObject(sample)) {
    const bundle = v1TabsToV2(input as V1TabConfig[], { rootKey })
    return attachOverrides(
      {
        rootCardId: bundle.rootCardId,
        cards: bundle.cards,
        elements: bundle.elements,
      },
      bundle.snippetOverrides,
    )
  }

  // Array of cards → card-list root referencing each converted card.
  if (isCardObject(sample)) {
    const bundle = v1CardsToV2(input as V1CardConfig[])
    const rootId = nextRootId(bundle.cards, rootKey)
    const cards: Record<string, DisplayCard> = {
      ...bundle.cards,
      [rootId]: {
        id: rootId,
        cardType: "card-list",
        title: "",
        cardList: bundle.ids,
      } as unknown as DisplayCard,
    }
    return attachOverrides(
      { rootCardId: rootId, cards, elements: bundle.elements },
      bundle.snippetOverrides,
    )
  }

  // Array of fields → element-list root.
  return wrapFieldsAsRoot(input as V1FieldConfig[], rootKey)
}

function wrapFieldsAsRoot(
  fields: V1FieldConfig[],
  rootKey: string,
): V2BuilderSchema {
  // Wrap as a fields_list card so snippet placements + overrides flow through
  // the shared convertCard path, then use the converted card as the root.
  const bundle = v1CardsToV2([
    { type: "fields_list", fieldsList: fields } as unknown as V1CardConfig,
  ])
  const rootId = bundle.ids[0] ?? nextRootId(bundle.cards, rootKey)
  return attachOverrides(
    { rootCardId: rootId, cards: bundle.cards, elements: bundle.elements },
    bundle.snippetOverrides,
  )
}
