<template>
  <Dialog
    :open="open"
    @update:open="(v) => (!v ? onCloseClick() : undefined)"
  >
    <DialogContent
      shell="crud"
      :show-close-button="false"
      class="vm-crud-upsert"
      :class="{
        'is-empty': isBodyEmpty,
        'is-height': !!heightCss,
      }"
      :style="heightCss ? { '--vm-dialog-height': heightCss } : undefined"
      @pointer-down-outside="preventDialogOutsideClose"
      @focus-outside="preventDialogOutsideClose"
      @interact-outside="preventDialogOutsideClose"
    >
      <DialogHeader class="vm-crud-shell__head">
        <DialogTitle class="vm-crud-shell__title">{{ title }}</DialogTitle>
        <slot name="title-extra" />
        <DialogClose class="vm-crud-shell__close">
          <i class="ri-close-line" />
          <span class="sr-only">Close</span>
        </DialogClose>
      </DialogHeader>
      <div
        class="vm-crud-upsert__body"
        :class="{ 'is-fill': layout === 'fill' }"
      >
        <template v-if="!isBodyEmpty">
          <div
            v-for="item in formItemsBeforeSlot"
            :key="item.prop"
            class="vm-crud-upsert__field"
            :class="{ 'is-error': fieldErrors[item.prop] }"
            :style="{ gridColumn: `span ${itemSpan(item)}` }"
          >
            <Label class="vm-crud-upsert__label">
              {{ item.label }}
              <span
                v-if="resolveItemRequired(item, form)"
                class="vm-crud-upsert__req"
              >*</span>
            </Label>

            <vm-radio
              v-if="itemType(item) === 'radio'"
              :name="`form-dialog-${item.prop}`"
              :options="resolveOptions(item)"
              :model-value="String(form[item.prop] ?? '')"
              :disabled="readonly || item.disabled"
              @update:model-value="(v) => setField(item.prop, v)"
            />

            <component
              :is="resolveField(item)"
              v-else-if="resolveField(item)"
              :class="fieldClass(item)"
              v-bind="fieldProps(item)"
              :model-value="form[item.prop]"
              :disabled="readonly || item.disabled"
              @update:model-value="(v: unknown) => setField(item.prop, v)"
            />

            <Input
              v-else
              class="vm-crud-upsert__control"
              v-bind="inputBind(item)"
              :placeholder="item.placeholder"
              :disabled="readonly || item.disabled"
              :aria-invalid="fieldErrors[item.prop] ? true : undefined"
              :model-value="
                form[item.prop] == null ? '' : (form[item.prop] as string | number)
              "
              @update:model-value="(v) => setField(item.prop, v)"
            />
            <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
              {{ fieldErrors[item.prop] }}
            </p>
          </div>
          <slot :form="form" />
          <div
            v-for="item in formItemsAfterSlot"
            :key="item.prop"
            class="vm-crud-upsert__field"
            :class="{ 'is-error': fieldErrors[item.prop] }"
            :style="{ gridColumn: `span ${itemSpan(item)}` }"
          >
            <Label class="vm-crud-upsert__label">
              {{ item.label }}
              <span
                v-if="resolveItemRequired(item, form)"
                class="vm-crud-upsert__req"
              >*</span>
            </Label>

            <div class="vm-crud-upsert__textarea-wrap">
              <textarea
                class="vm-crud-upsert__textarea"
                :placeholder="item.placeholder"
                :disabled="readonly || item.disabled"
                :rows="textareaRows(item)"
                :style="textareaStyle(item)"
                :value="String(form[item.prop] ?? '')"
                @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
              />
              <i
                v-if="
                  textareaClearable(item) &&
                  String(form[item.prop] ?? '') !== '' &&
                  !readonly &&
                  !item.disabled
                "
                class="ri-close-circle-fill vm-crud-upsert__textarea-clear"
                role="button"
                tabindex="-1"
                :aria-label="crudLabel('clear')"
                @mousedown.prevent
                @click="setField(item.prop, '')"
              />
            </div>
            <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
              {{ fieldErrors[item.prop] }}
            </p>
          </div>
        </template>
        <div v-else class="vm-crud-upsert__empty">
          <vm-empty kind="暂无内容" />
        </div>
      </div>
      <DialogFooter v-if="footer !== false" class="vm-crud-shell__foot">
        <slot
          v-if="slots.footer"
          name="footer"
          v-bind="footerSlotProps"
        />
        <template v-else>
          <slot name="footer-prepend" v-bind="footerSlotProps" />
          <button
            v-if="close !== false"
            type="button"
            class="vm-crud-shell__btn is-ghost"
            :disabled="submitting"
            @click="onCloseClick"
          >
            {{ closeText || crudLabel('close') }}
          </button>
          <button
            v-if="confirm !== false && !readonly"
            type="button"
            class="vm-crud-shell__btn is-primary"
            :disabled="submitting"
            @click="onConfirmClick"
          >
            {{ submitting ? crudLabel('submitting') : confirmText || crudLabel('confirm') }}
          </button>
          <slot name="footer-append" v-bind="footerSlotProps" />
        </template>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>

<script setup lang="ts">
import { crudLabel } from '../crud/style'
import { ref, computed, watch, useSlots, toValue } from 'vue'
import { preventDialogOutsideClose } from '../lib/dialog-float'
import { resolveCrudComponent } from '../crud/registry'
import { resolveItemRequired, validateFormFields } from '../crud/validate'
import { resolveFormSpan, useFormCols } from '../crud/span'
import { mapDictOptions, resolveFormItemValue } from '../crud/auto-fields'
import type { CrudFormItem } from '../../../typings/admin/comm/crud'

defineOptions({ name: 'vm-form-dialog' })

export type VmFormDialogSubmitHelpers = {
  /** 关闭弹窗并复位 submitting */
  close: () => void
  /** 仅复位 submitting（弹窗保持打开） */
  done: () => void
}

const props = withDefaults(
  defineProps<{
    open?: boolean
    /** 表单数据（v-model） */
    modelValue?: Record<string, unknown>
    title?: string
    items?: CrudFormItem[]
    height?: string | number
    confirm?: boolean
    close?: boolean
    footer?: boolean
    confirmText?: string
    closeText?: string
    layout?: 'form' | 'fill'
    /** 只读：隐藏确定、控件禁用 */
    readonly?: boolean
  }>(),
  {
    open: false,
    modelValue: () => ({}),
    title: '',
    items: () => [],
    confirm: true,
    close: true,
    footer: true,
    layout: 'form',
    readonly: false,
  },
)

const emit = defineEmits<{
  'update:open': [v: boolean]
  'update:modelValue': [v: Record<string, unknown>]
  close: []
  /** 校验通过后触发；业务自行请求，完成后调用 helpers.close() */
  submit: [data: Record<string, unknown>, helpers: VmFormDialogSubmitHelpers]
}>()

const slots = useSlots()
const formCols = useFormCols()
const submitting = ref(false)
const fieldErrors = ref<Record<string, string>>({})

const form = computed({
  get: () => props.modelValue || {},
  set: (v) => emit('update:modelValue', v),
})

function normalizeHeight(h?: string | number) {
  if (h == null || h === '') return ''
  if (typeof h === 'number') return `${h}px`
  return String(h)
}

const heightCss = computed(() => normalizeHeight(props.height))

const visibleItems = computed(() =>
  (props.items || []).filter((it) => {
    if (typeof it.hidden === 'function') return !it.hidden(form.value)
    return !it.hidden
  }),
)

const formItemsBeforeSlot = computed(() =>
  visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
)
const formItemsAfterSlot = computed(() =>
  visibleItems.value.filter((it) => itemType(it) === 'textarea'),
)

const isBodyEmpty = computed(
  () => !visibleItems.value.length && !slots.default,
)

const footerSlotProps = computed(() => ({
  close: onCloseClick,
  submit: onConfirmClick,
  confirm: onConfirmClick,
  submitting: submitting.value,
  form: form.value,
}))

function setField(prop: string, value: unknown) {
  if (props.readonly) return
  if (fieldErrors.value[prop]) {
    const next = { ...fieldErrors.value }
    delete next[prop]
    fieldErrors.value = next
  }
  form.value = { ...form.value, [prop]: value }
}

function inputBind(item: CrudFormItem) {
  if (itemType(item) !== 'number') return {}
  const raw = toValue(item.component?.props)
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
    return raw as Record<string, unknown>
  }
  return { type: 'number', controls: false }
}

function itemType(item: CrudFormItem) {
  if (item.type) return item.type
  const name = item.component?.name
  if (typeof name !== 'string') return 'input'
  if (name === 'vm-switch') return 'switch'
  if (name === 'vm-select' || name === 'vm-tree-select') return 'select'
  return 'input'
}

function fieldClass(item: CrudFormItem) {
  const name = item.component?.name
  if (itemType(item) === 'switch') return 'vm-crud-upsert__switch-field'
  if (name === 'vm-upload' || name === 'upload') return 'vm-crud-upsert__upload-field'
  if (name === 'vm-markdown') return 'vm-crud-upsert__markdown-field'
  if (name === 'vm-json-editor') return 'vm-crud-upsert__json-field'
  if (name === 'vm-richtext') return 'vm-crud-upsert__richtext-field'
  return 'vm-crud-upsert__control'
}

function resolveField(item: CrudFormItem) {
  if (itemType(item) === 'radio' || itemType(item) === 'textarea') return undefined
  const name = item.component?.name
  if (typeof name === 'string') {
    if (name === 'upload') return resolveCrudComponent('vm-upload')
    const comp = resolveCrudComponent(name)
    if (comp) return comp
  } else if (name) {
    return name
  }
  if (item.type === 'switch') return resolveCrudComponent('vm-switch')
  if (item.type === 'select') {
    if (item.multiple) return resolveCrudComponent('vm-multi-select')
    return resolveCrudComponent('vm-select')
  }
  return undefined
}

function fieldProps(item: CrudFormItem) {
  const raw = item.component?.props
  const unwrapped = toValue(raw)
  if (Array.isArray(unwrapped)) {
    return {
      options: unwrapped,
      placeholder: item.placeholder,
      refreshOnChange: false,
    }
  }
  return {
    options: resolveOptions(item),
    placeholder: item.placeholder,
    refreshOnChange: false,
    ...(unwrapped && typeof unwrapped === 'object'
      ? (unwrapped as Record<string, unknown>)
      : {}),
  }
}

function resolveOptions(item: CrudFormItem | CrudFormItem['options']) {
  if (item && typeof item === 'object' && 'prop' in item) {
    const formItem = item as CrudFormItem
    if (formItem.dict) return mapDictOptions(formItem.dict)
    const list = formItem.options == null ? [] : toValue(formItem.options)
    return Array.isArray(list) ? list : []
  }
  const list = item == null ? [] : toValue(item)
  return Array.isArray(list) ? list : []
}

function itemSpan(item: CrudFormItem) {
  return resolveFormSpan(item.span, formCols.value)
}

function textareaProps(item: CrudFormItem) {
  const raw = toValue(item.component?.props)
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
  return raw as Record<string, unknown>
}

function textareaClearable(item: CrudFormItem) {
  return textareaProps(item).clearable !== false
}

function textareaRows(item: CrudFormItem) {
  const rows = textareaProps(item).rows
  return typeof rows === 'number' || typeof rows === 'string' ? rows : undefined
}

function textareaStyle(item: CrudFormItem) {
  const p = textareaProps(item)
  const style: Record<string, string> = {
    ...((p.style && typeof p.style === 'object'
      ? (p.style as Record<string, string>)
      : {}) as Record<string, string>),
  }
  if (p.height != null && p.height !== '') {
    const h = typeof p.height === 'number' ? `${p.height}px` : String(p.height)
    style.height = h
    if (!style.minHeight) style.minHeight = h
  }
  return Object.keys(style).length ? style : undefined
}

function onCloseClick() {
  submitting.value = false
  fieldErrors.value = {}
  emit('update:open', false)
  emit('close')
}

async function onConfirmClick() {
  if (props.readonly || submitting.value) return
  const errors = await validateFormFields(visibleItems.value, form.value)
  fieldErrors.value = errors
  if (Object.keys(errors).length) return
  submitting.value = true
  const helpers: VmFormDialogSubmitHelpers = {
    close: () => {
      submitting.value = false
      fieldErrors.value = {}
      emit('update:open', false)
      emit('close')
    },
    done: () => {
      submitting.value = false
    },
  }
  emit('submit', { ...form.value }, helpers)
}

watch(
  () => props.open,
  (open) => {
    if (!open) {
      submitting.value = false
      fieldErrors.value = {}
      return
    }
    const data = { ...form.value }
    for (const it of props.items || []) {
      if (data[it.prop] === undefined && it.value !== undefined) {
        const v = resolveFormItemValue(it.value)
        if (v !== undefined) data[it.prop] = v
      }
    }
    form.value = data
  },
)

defineExpose({
  form,
  submit: onConfirmClick,
  close: onCloseClick,
})
</script>

<style lang="scss" scoped>
.vm-crud-upsert__body {
  display: grid;
  flex: 1 1 auto;
  grid-template-columns: repeat(24, minmax(0, 1fr));
  gap: 16px;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  padding: 18px 22px;
  align-content: start;

  &.is-fill {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0;

    > * {
      width: 100%;
      min-width: 0;
    }
  }

  /* slot 内容在宿主 SFC，须 :deep 才能命中 label / control 样式 */
  :deep(.vm-crud-upsert__field) {
    display: grid;
    gap: 6px;
    align-content: start;
    min-width: 0;
  }

  :deep(.vm-crud-upsert__label) {
    font-size: 12px;
    color: var(--muted-foreground);
  }

  :deep(.vm-crud-upsert__req) {
    color: var(--destructive);
  }

  :deep(.vm-crud-upsert__control) {
    width: 100%;
    height: var(--vm-control-height) !important;
    min-height: var(--vm-control-height);
    border-radius: var(--vm-control-radius) !important;
    font-size: var(--vm-control-font-size) !important;
    line-height: 1.4 !important;
  }

  /* Input 外壳：高度交给内部 input，避免 wrap 压扁清除按钮 */
  :deep(.vm-input-wrap.vm-crud-upsert__control) {
    height: auto !important;
    min-height: var(--vm-control-height);
  }

  /* 单行 input：白底；select / tree-select 等：淡蓝底 */
  :deep(input.vm-crud-upsert__control),
  :deep(.vm-crud-upsert__control[data-slot='input']),
  :deep(.vm-input-wrap.vm-crud-upsert__control input) {
    background: var(--card) !important;
  }

  :deep(.vm-crud-select.vm-crud-upsert__control),
  :deep(.vm-group-cascader.vm-crud-upsert__control),
  :deep(.vm-multi-select.vm-crud-upsert__control),
  :deep(.vm-date-picker.vm-crud-upsert__control),
  :deep(.vm-icon-picker.vm-crud-upsert__control) {
    background: var(--muted) !important;
  }

  :deep(.vm-tree-select.vm-crud-upsert__control .vm-tree-select__trigger) {
    background: var(--muted) !important;
  }

  @media (max-width: 768px) {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    flex: 1 1 auto;
    min-height: 0;
    gap: 10px;
    padding: 10px 12px;
  }
}

.vm-crud-upsert__empty {
  grid-column: 1 / -1;
  display: flex;
  min-height: 160px;
  align-items: center;
  justify-content: center;
}

.vm-crud-upsert__field {
  &.is-error :deep(.vm-crud-upsert__control) {
    border-color: var(--destructive) !important;
  }
}

.vm-crud-upsert__error {
  margin: 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--destructive);
}

:deep(.vm-group-cascader.vm-crud-upsert__control) {
  height: auto !important;
  min-height: var(--vm-control-height);
}

/* 上传：勿套用单行高度；多图时铺满 span 以便平铺换行 */
:deep(.vm-upload.vm-crud-upsert__upload-field) {
  width: 100%;
  max-width: 100%;
  height: auto !important;
  min-height: 0;
  background: transparent !important;
}

/* Markdown：保留组件自身 min-height，勿被 control 行高压扁 */
:deep(.vm-markdown.vm-crud-upsert__markdown-field) {
  width: 100%;
  height: auto !important;
  min-height: 0;
  background: transparent !important;
}

/* JSON：与备注 textarea 同视觉，勿套单行 control */
:deep(.vm-json-editor.vm-crud-upsert__json-field) {
  width: 100%;
  height: auto !important;
  min-height: 0;
  background: transparent !important;
}

/* 富文本：保留组件默认 minHeight，勿被 control 行高压扁 */
:deep(.vm-richtext.vm-crud-upsert__richtext-field) {
  width: 100%;
  height: auto !important;
  min-height: 0;
  background: transparent !important;
}

:deep(.vm-crud-upsert__switch-field.vm-switch) {
  width: 40px;
  height: 22px;
  margin-top: 8px;
}

.vm-crud-upsert__textarea-wrap {
  position: relative;
  width: 100%;
  min-width: 0;
}

.vm-crud-upsert__textarea-clear {
  position: absolute;
  top: 10px;
  right: var(--vm-control-padding-x);
  z-index: 1;
  color: var(--muted-foreground);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;

  &:hover {
    color: var(--foreground);
  }
}

.vm-crud-upsert__textarea,
:deep(.vm-crud-upsert__textarea) {
  width: 100%;
  min-height: 72px;
  padding: 8px 28px 8px var(--vm-control-padding-x);
  border: 1px solid var(--input);
  border-radius: var(--vm-control-radius);
  background: var(--muted);
  color: var(--foreground);
  font-size: 14px;
  line-height: 1.5;
  resize: vertical;
  outline: none;
  transition: border-color 0.15s ease;

  &:focus,
  &:focus-visible {
    border-color: var(--focus-border);
    box-shadow: none;
  }
}
</style>
