<template>
  <Dialog
    :open="crud.upsertVisible.value"
    @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">{{ titleText }}</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, crud.upsertForm.value)"
                class="vm-crud-upsert__req"
              >*</span>
            </Label>

            <vm-radio
              v-if="itemType(item) === 'radio'"
              :name="`upsert-${item.prop}`"
              :options="resolveOptions(item)"
              :model-value="String(crud.upsertForm.value[item.prop] ?? '')"
              :disabled="isInfo || 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="crud.upsertForm.value[item.prop]"
              :disabled="isInfo || 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="isInfo || item.disabled"
              :aria-invalid="fieldErrors[item.prop] ? true : undefined"
              :model-value="
                crud.upsertForm.value[item.prop] == null
                  ? ''
                  : (crud.upsertForm.value[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="crud.upsertForm.value" />
          <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, crud.upsertForm.value)"
                class="vm-crud-upsert__req"
              >*</span>
            </Label>

            <div class="vm-crud-upsert__textarea-wrap">
              <textarea
                class="vm-crud-upsert__textarea"
                :placeholder="item.placeholder"
                :disabled="isInfo || item.disabled"
                :rows="textareaRows(item)"
                :style="textareaStyle(item)"
                :value="String(crud.upsertForm.value[item.prop] ?? '')"
                @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
              />
              <i
                v-if="textareaClearable(item) && String(crud.upsertForm.value[item.prop] ?? '') !== '' && !isInfo && !item.disabled"
                class="ri-close-circle-fill vm-crud-upsert__textarea-clear"
                role="button"
                tabindex="-1"
                aria-label="清除"
                @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="showFooter" 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="showClose"
            type="button"
            class="vm-crud-shell__btn is-ghost"
            :disabled="submitting"
            @click="onCloseClick"
          >
            {{ closeLabel }}
          </button>
          <button
            v-if="showConfirm"
            type="button"
            class="vm-crud-shell__btn is-primary"
            :disabled="submitting"
            @click="onConfirmClick"
          >
            {{ submitting ? '提交中…' : confirmLabel }}
          </button>
          <slot name="footer-append" v-bind="footerSlotProps" />
        </template>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>

<script setup lang="ts">
import { ref, computed, watch, useSlots, toValue } from 'vue'
import { toast } from 'vue-sonner'
import { useCrud } from './useCrud'
import { injectUpsertOptions } from './key'
import { getCrudStyle, crudLabel } from './style'
import { resolveCrudComponent } from './registry'
import { applySetRules, resolveItemRequired, validateFormFields } from './validate'
import { resolveFormSpan, useFormCols } from './span'
import { preventDialogOutsideClose } from '../lib/dialog-float'
import {
  buildAutoUpsertItems,
  mapDictOptions,
  mergeByProp,
  resolveFormItemValue,
  sortRequiredFirst,
} from './auto-fields'
import type { CrudFormItem } from '../../../typings/admin/comm/crud'
defineOptions({ name: 'vm-upsert' })

const props = withDefaults(
  defineProps<{
    items?: CrudFormItem[]
    title?: string
    /** 弹窗高度，如 800 / '800px'；最高受壳层 800px 约束 */
    height?: string | number
    /** 是否显示确定；未设时 info 模式自动隐藏 */
    confirm?: boolean
    /** 是否显示关闭，默认 true */
    close?: boolean
    /** false 时隐藏整个底栏 */
    footer?: boolean
    confirmText?: string
    closeText?: string
    /** form=栅格表单；fill=纵向铺满（只读列表/文档） */
    layout?: 'form' | 'fill'
  }>(),
  {
    close: undefined,
    confirm: undefined,
    footer: true,
    layout: 'form',
  },
)

const emit = defineEmits<{
  close: []
  confirm: []
}>()

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

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 ?? upsertOpts?.dialog?.height),
)

const mergedItems = computed(() => {
  const manual = props.items?.length ? props.items : upsertOpts?.items || []
  const formPlugins = getCrudStyle().form.plugins as Array<{
    __plugin?: string
    ignoreFields?: string[]
  }>
  const autoPlugin = formPlugins.find((p) => p?.__plugin === 'setAuto')
  const autoEnabled = upsertOpts?.auto !== false && Boolean(autoPlugin)
  let raw = manual
  if (autoEnabled) {
    const pluginIgnore = (autoPlugin?.ignoreFields as string[] | undefined) || []
    const pageIgnore = upsertOpts?.ignoreFields || []
    const auto = buildAutoUpsertItems(crud.service, {
      ignoreFields: [...pluginIgnore, ...pageIgnore],
    })
    raw = mergeByProp(auto, manual)
  }
  raw = sortRequiredFirst(raw)
  const useRules = formPlugins.some((p) => p?.__plugin === 'setRules')
  return useRules ? applySetRules(raw) : raw
})

const titleText = computed(() => {
  if (props.title != null && props.title !== '') return props.title
  if (upsertOpts?.dialog?.title) return upsertOpts.dialog.title
  if (crud.upsertMode.value === 'add') return crudLabel('add', crud.dict.label)
  if (crud.upsertMode.value === 'info') return crudLabel('info', crud.dict.label)
  return crudLabel('update', crud.dict.label)
})

const visibleItems = computed(() =>
  mergedItems.value.filter((it) => {
    if (typeof it.hidden === 'function') return !it.hidden(crud.upsertForm.value)
    return !it.hidden
  }),
)

/** 备注等 textarea 固定渲染在 slot 之后 */
const formItemsBeforeSlot = computed(() =>
  visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
)
const formItemsAfterSlot = computed(() =>
  visibleItems.value.filter((it) => itemType(it) === 'textarea'),
)

const isInfo = computed(() => crud.upsertMode.value === 'info')
/** prop > dialog.confirm > info 自动隐藏 */
const showConfirm = computed(() => {
  if (typeof props.confirm === 'boolean') return props.confirm
  const flag = upsertOpts?.dialog?.confirm
  if (typeof flag === 'boolean') return flag
  return !isInfo.value
})
const showClose = computed(() => {
  if (typeof props.close === 'boolean') return props.close
  const flag = upsertOpts?.dialog?.close
  if (typeof flag === 'boolean') return flag
  return true
})
const showFooter = computed(() => props.footer !== false)
const confirmLabel = computed(
  () =>
    props.confirmText ||
    upsertOpts?.dialog?.confirmText ||
    crudLabel('confirm', crud.dict.label),
)
const closeLabel = computed(
  () =>
    props.closeText ||
    upsertOpts?.dialog?.closeText ||
    crudLabel('close', crud.dict.label),
)
const isBodyEmpty = computed(
  () => !visibleItems.value.length && !slots.default,
)

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

function setField(prop: string, value: unknown) {
  if (isInfo.value) return
  if (fieldErrors.value[prop]) {
    const next = { ...fieldErrors.value }
    delete next[prop]
    fieldErrors.value = next
  }
  crud.upsertForm.value = { ...crud.upsertForm.value, [prop]: value }
}

/** number：把 component.props（type / precision / controls）落到 Input */
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 isSwitchField(item: CrudFormItem) {
  return itemType(item) === 'switch'
}

function isUploadField(item: CrudFormItem) {
  const name = item.component?.name
  return name === 'vm-upload' || name === 'upload'
}

function isMarkdownField(item: CrudFormItem) {
  return item.component?.name === 'vm-markdown'
}

function isJsonEditorField(item: CrudFormItem) {
  return item.component?.name === 'vm-json-editor'
}

function isRichtextField(item: CrudFormItem) {
  return item.component?.name === 'vm-richtext'
}

function fieldClass(item: CrudFormItem) {
  if (isSwitchField(item)) return 'vm-crud-upsert__switch-field'
  if (isUploadField(item)) return 'vm-crud-upsert__upload-field'
  // markdown / json-editor / richtext 自带高度，禁止套用单行 control 高度
  if (isMarkdownField(item)) return 'vm-crud-upsert__markdown-field'
  if (isJsonEditorField(item)) return 'vm-crud-upsert__json-field'
  if (isRichtextField(item)) 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')
  // multiple 须走多选组件；单选 Select 无法展示数组 tagIds
  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)
  // props: dict.get('status') → 直接作 options（switch 等）
  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>)
      : {}),
  }
}

/** options / dict → 渲染时解析 */
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)
}

/** textarea：component.props.rows / height / style */
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>
}

/** textarea 默认可清除；component.props.clearable === false 关闭 */
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() {
  const done = () => crud.upsertClose()
  if (upsertOpts?.onClose) {
    upsertOpts.onClose('close', done)
    emit('close')
    return
  }
  done()
  emit('close')
}

function onConfirmClick() {
  emit('confirm')
  void onSubmit()
}

async function onSubmit() {
  if (isInfo.value) {
    onCloseClick()
    return
  }
  if (submitting.value) return
  const errors = await validateFormFields(
    visibleItems.value,
    crud.upsertForm.value,
  )
  fieldErrors.value = errors
  if (Object.keys(errors).length) return
  submitting.value = true
  try {
    if (upsertOpts?.onSubmit) {
      await upsertOpts.onSubmit(
        { ...crud.upsertForm.value },
        {
          next: async (data) => {
            crud.upsertForm.value = { ...crud.upsertForm.value, ...data }
            await crud.upsertSubmit()
          },
          done: () => {},
          close: () => crud.upsertClose(),
        },
      )
      return
    }
    await crud.upsertSubmit()
  } catch (e) {
    console.error(e)
    const errObj = e as Error & { toasted?: boolean }
    const msg = e instanceof Error && e.message ? e.message : '操作失败'
    if (!errObj.toasted) toast.error(msg.slice(0, 200))
  } finally {
    submitting.value = false
  }
}

watch(
  () => crud.upsertVisible.value,
  (open) => {
    if (!open) {
      submitting.value = false
      fieldErrors.value = {}
      return
    }
    const data = { ...crud.upsertForm.value }
    upsertOpts?.onOpen?.(data)
    for (const it of mergedItems.value) {
      if (data[it.prop] === undefined && it.value !== undefined) {
        const v = resolveFormItemValue(it.value)
        if (v !== undefined) data[it.prop] = v
      }
    }
    crud.upsertForm.value = data
    upsertOpts?.onOpened?.({ ...crud.upsertForm.value })
    const focusPlugin = getCrudStyle().form.plugins.find(
      (p) => (p as { __plugin?: string })?.__plugin === 'setFocus',
    ) as { prop?: string } | undefined
    const prop =
      focusPlugin?.prop ||
      mergedItems.value.find((i) => !i.hidden)?.prop
    requestAnimationFrame(() => {
      const el = document.querySelector(
        `.vm-crud-upsert__field input, .vm-crud-upsert__field textarea`,
      ) as HTMLInputElement | null
      if (prop) {
        const named = document.querySelector(
          `[name="upsert-${prop}"], .vm-crud-upsert__field:has(input) input`,
        ) as HTMLInputElement | null
        ;(named || el)?.focus?.()
      } else {
        el?.focus?.()
      }
    })
  },
)

defineExpose({
  form: crud.upsertForm,
  mode: crud.upsertMode,
  submit: onSubmit,
  close: onCloseClick,
  add: () => crud.rowAdd(),
  append: (data?: Record<string, unknown>) => crud.rowAppend(data),
  edit: (data: Record<string, unknown>) => crud.rowEdit(data),
  info: (data: Record<string, unknown>) => crud.rowInfo(data),
})
</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-user-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>
