<script setup lang="ts">
/**
 * Renders a single CardElement by dispatching its JSON definition into
 * the matching composition under `components/compositions/elements/`.
 *
 * The renderer is the bridge layer:
 *   - resolves the composition by `element.type` via the registry
 *   - evaluates the element's visibility (`shouldRender`)
 *   - applies responsive layout + classList chrome at the wrapper level
 *   - binds `useFormData(element.name)` to the form-data store and
 *     forwards `modelValue` + `errors` + the `@update:modelValue`
 *     handler to the composition
 *   - strips schema-only keys (type, conditionParams, columns*, ...)
 *     and passes the rest through as Vue props
 *
 * Compositions don't know about the form-data store, the JSON schema,
 * or this renderer's layout — they receive normal Vue props and emit
 * `update:modelValue`. They're directly mountable in any app.
 */
import { computed } from "vue"
import type { CardElement } from "@/definitions/elements"
import { cn } from "@/lib/utils"
import { getElementComponent } from "./useElementLoader"
import {
  shouldRender,
  type EvaluationContext,
} from "./useDisplayConditions"
import { useFormData } from "./useFormData"
import { applyLegacyRemaps } from "./legacyPropRemap"

interface Props {
  element: CardElement
  item?: Record<string, unknown>
  evaluationContext?: EvaluationContext
}

const props = withDefaults(defineProps<Props>(), {
  item: () => ({}),
  evaluationContext: () => ({}),
})

const visible = computed(() =>
  shouldRender(props.element, props.item, props.evaluationContext),
)

const resolved = computed(() => getElementComponent(props.element.type))

const bindings = computed(() => useFormData(props.element.name ?? ""))

/**
 * Keys that live on every CardElement and are NOT meant to flow
 * through to the composition as props. These are either consumed by
 * this renderer (`columns*`, `conditionParams`, ...) or used only by
 * the engine (`type`, `id` — well, `id` IS passed through, see below).
 */
const STRIPPED_KEYS = new Set<string>([
  "type",
  "conditionParams",
  "conditionGroupLogic",
  "condition",
  "features",
  "permissions",
  "columns",
  "columnsSm",
  "columnsMd",
  "columnsLg",
  "columnsXl",
  "columnsXxl",
  "wrapperClassList",
  "columnClassList",
  "labelClassList",
  "classList",
  "previewMode",
  "order",
  "defaultValue",
  "modelValue",
])

/**
 * Compute the props object to pass to the resolved composition.
 *
 * - `modelValue` comes from the form-data store, falling back to
 *   `item[element.name]`, then `element.defaultValue`. Compositions
 *   declare `modelValue` and Vue picks it up.
 * - `errors` comes from the form-data store.
 * - All other element keys (label, placeholder, required, ...) are
 *   passed through; Vue's prop validation silently drops anything the
 *   composition doesn't declare.
 */
const compositionProps = computed<Record<string, unknown>>(() => {
  const el = props.element as unknown as Record<string, unknown>
  const result: Record<string, unknown> = {}
  for (const [key, value] of Object.entries(el)) {
    if (!STRIPPED_KEYS.has(key)) {
      result[key] = value
    }
  }

  // Resolve the effective modelValue.
  const fieldName = props.element.name ?? ""
  const storeValue = fieldName ? bindings.value.value.value : undefined
  const itemValue = fieldName ? props.item[fieldName] : undefined
  const defaultValue = (el as { defaultValue?: unknown }).defaultValue
  if (storeValue !== undefined && storeValue !== null) {
    result.modelValue = storeValue
  } else if (itemValue !== undefined && itemValue !== null) {
    result.modelValue = itemValue
  } else if (defaultValue !== undefined && defaultValue !== null) {
    result.modelValue = defaultValue
  } else {
    result.modelValue = ""
  }

  result.errors = bindings.value.errors.value

  // Legacy JSON → new composition prop names + value aliases.
  return applyLegacyRemaps(props.element.type, result)
})

function onUpdateModelValue(value: unknown): void {
  if (!props.element.name) {
    return
  }
  bindings.value.setValue(value)
}

const wrapperClass = computed(() =>
  cn(
    "gxp-element",
    `gxp-element--${props.element.type.replace(/\./g, "-")}`,
    columnClass("col", props.element.columns),
    columnClass("col-sm", props.element.columnsSm),
    columnClass("col-md", props.element.columnsMd),
    columnClass("col-lg", props.element.columnsLg),
    columnClass("col-xl", props.element.columnsXl),
    normalizeClasses(props.element.columnClassList),
    normalizeClasses(props.element.wrapperClassList),
  ),
)

function columnClass(prefix: string, n?: number): string | undefined {
  if (typeof n !== "number") {
    return undefined
  }
  // 12-column grid — render width as a Tailwind percentage utility so
  // we don't depend on Bootstrap's col-* classes.
  const pct = Math.max(0, Math.min(12, n)) / 12
  return prefix === "col"
    ? `basis-[${(pct * 100).toFixed(4)}%]`
    : `${prefix}:basis-[${(pct * 100).toFixed(4)}%]`
}

function normalizeClasses(value: string | string[] | undefined): string {
  if (!value) {
    return ""
  }
  return Array.isArray(value) ? value.join(" ") : value
}
</script>

<template>
  <div
    v-if="visible"
    :class="wrapperClass"
    :data-element-id="element.id"
    :data-element-type="element.type"
  >
    <component
      :is="resolved"
      v-bind="compositionProps"
      @update:model-value="onUpdateModelValue"
    />
  </div>
</template>
