<template>
  <div v-if="visible" class="vm-import-btn">
    <button
      type="button"
      class="vm-crud-toolbar__btn is-ghost"
      :disabled="disabled"
      @click="openDialog"
    >
      <i class="ri-upload-2-line" />
      {{ buttonText }}
    </button>

    <Dialog :open="open" @update:open="onOpenChange">
      <DialogContent
        shell="crud"
        class="vm-import-btn__dialog is-height"
        :style="{ '--vm-dialog-height': '600px' }"
        @pointer-down-outside="preventDialogOutsideClose"
        @focus-outside="preventDialogOutsideClose"
        @interact-outside="preventDialogOutsideClose"
      >
        <DialogHeader class="vm-crud-shell__head">
          <DialogTitle class="vm-crud-shell__title">{{ crudLabel('import') }}</DialogTitle>
        </DialogHeader>

        <div class="vm-crud-shell__body vm-import-btn__body">
          <div class="vm-import-btn__bar">
            <p v-if="tip" class="vm-import-btn__tip">{{ tip }}</p>
            <span v-else-if="file" class="vm-import-btn__file-name">
              {{ file.name }}
            </span>
            <span v-else />
            <div class="vm-import-btn__bar-actions">
              <button
                v-if="previewRows.length"
                type="button"
                class="vm-import-btn__repick"
                :disabled="submitting || parsing"
                @click="onPick"
              >
                {{ crudLabel('reselect') }}
              </button>
              <button
                type="button"
                class="vm-import-btn__tpl"
                :disabled="submitting || downloading || parsing"
                @click="onTemplate"
              >
                {{ downloading ? crudLabel('downloading') : crudLabel('downloadTpl') }}
              </button>
            </div>
          </div>

          <div
            v-if="!previewRows.length"
            class="vm-import-btn__drop"
            :class="{
              'is-drag': dragging,
              'is-disabled': submitting || parsing,
            }"
            @click="onPick"
            @dragenter.prevent="onDragEnter"
            @dragover.prevent
            @dragleave.prevent="onDragLeave"
            @drop.prevent="onDrop"
          >
            <i class="ri-upload-cloud-2-line" />
            <span v-if="parsing">{{ crudLabel('parsing') }}</span>
            <span v-else>
              {{ crudFormat(crudLabel('importDropHint'), maxSizeMb) }}
            </span>
            <span class="vm-import-btn__ext">{{ crudLabel('importExtHint') }}</span>
          </div>

          <div v-if="previewRows.length" class="vm-import-btn__preview">
            <div class="vm-import-btn__table-wrap">
              <table class="vm-import-btn__table">
                <thead>
                  <tr>
                    <th
                      v-for="col in previewHeaders"
                      :key="col"
                      class="vm-import-btn__th"
                    >
                      {{ col }}
                    </th>
                  </tr>
                </thead>
                <tbody>
                  <tr v-for="(row, idx) in pageRows" :key="pageStart + idx">
                    <td
                      v-for="col in previewHeaders"
                      :key="col"
                      class="vm-import-btn__td"
                    >
                      {{ formatCell(row[col]) }}
                    </td>
                  </tr>
                </tbody>
              </table>
            </div>
            <div class="vm-import-btn__pager">
              <span class="vm-import-btn__total">{{ crudFormat(crudLabel('total'), previewRows.length) }}</span>
              <div class="vm-import-btn__pager-actions">
                <button
                  type="button"
                  class="vm-import-btn__page-btn"
                  :disabled="page <= 1 || submitting"
                  @click="goPrevPage"
                >
                  {{ crudLabel('prevPage') }}
                </button>
                <span class="vm-import-btn__page-num">
                  {{ page }} / {{ totalPages }}
                </span>
                <button
                  type="button"
                  class="vm-import-btn__page-btn"
                  :disabled="page >= totalPages || submitting"
                  @click="goNextPage"
                >
                  {{ crudLabel('nextPage') }}
                </button>
              </div>
            </div>
          </div>

          <input
            ref="inputRef"
            type="file"
            class="vm-import-btn__input"
            accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            @change="onFileInput"
          />
        </div>

        <DialogFooter class="vm-crud-shell__foot">
          <button
            type="button"
            class="vm-crud-shell__btn is-ghost"
            :disabled="submitting"
            @click="closeDialog"
          >
            {{ crudLabel('close') }}
          </button>
          <button
            type="button"
            class="vm-crud-shell__btn is-primary"
            :disabled="submitting || parsing || !canSubmit"
            @click="onSubmit"
          >
            {{ submitting ? crudLabel('submitting') : crudLabel('submit') }}
          </button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { toast } from 'vue-sonner'
import { preventDialogOutsideClose } from '../lib/dialog-float'
import {
  downloadImportTemplate,
  parseImportExcelPreview,
  uploadImportExcel,
  uploadImportRows,
  type ImportResult,
} from '../lib/import-excel'
import { useCrud } from './useCrud'
import { injectImportOptions } from './key'
import { crudFormat, crudLabel } from './style'
import type { CrudPermission } from '../../../typings/admin/comm/crud'

defineOptions({ name: 'vm-import-btn' })

/** 与服务端 importTemplate（excelFilename → .xlsx）保持一致 */
const ACCEPT_EXT = '.xlsx'
const MAX_SIZE_MB_DEFAULT = 10
const PAGE_SIZE = 20

const props = withDefaults(
  defineProps<{
    disabled?: boolean
    text?: string
    /** 弹窗左上提示文案 */
    tip?: string
    templateFilename?: string | (() => string)
    /** 文件大小上限（MB） */
    maxSizeMb?: number
    /** 权限键，默认 add */
    permission?: keyof CrudPermission
  }>(),
  {
    disabled: false,
    tip: '',
    maxSizeMb: MAX_SIZE_MB_DEFAULT,
    permission: 'add',
  },
)

const emit = defineEmits<{
  success: [result: { inserted: number; updated: number; failed: number }]
}>()

const crud = useCrud()
const importOpts = injectImportOptions()
const open = ref(false)
const submitting = ref(false)
const downloading = ref(false)
const parsing = ref(false)
const dragging = ref(false)
const file = ref<File | null>(null)
const previewHeaders = ref<string[]>([])
const previewRows = ref<Record<string, unknown>[]>([])
const page = ref(1)
const inputRef = ref<HTMLInputElement | null>(null)
const dragDepth = ref(0)

const visible = computed(() => Boolean(crud.getPermission(props.permission)))

const buttonText = computed(
  () => props.text || crudLabel('import', crud.dict.label),
)

const totalPages = computed(() =>
  Math.max(1, Math.ceil(previewRows.value.length / PAGE_SIZE)),
)

const pageStart = computed(() => (page.value - 1) * PAGE_SIZE)

const pageRows = computed(() =>
  previewRows.value.slice(pageStart.value, pageStart.value + PAGE_SIZE),
)

const canSubmit = computed(
  () => Boolean(file.value) && previewRows.value.length > 0,
)

function goPrevPage() {
  if (page.value <= 1) return
  page.value -= 1
}

function goNextPage() {
  if (page.value >= totalPages.value) return
  page.value += 1
}

function formatCell(v: unknown) {
  if (v == null) return ''
  return String(v)
}

function resolveTemplateName() {
  const raw = props.templateFilename
  const name = typeof raw === 'function' ? raw() : raw
  const base = name || crudLabel('importTpl')
  return base.endsWith(ACCEPT_EXT) ? base : `${base}${ACCEPT_EXT}`
}

function resetPreview() {
  file.value = null
  previewHeaders.value = []
  previewRows.value = []
  page.value = 1
  parsing.value = false
}

function openDialog() {
  if (props.disabled) return
  resetPreview()
  open.value = true
}

function closeDialog() {
  if (submitting.value) return
  open.value = false
  resetPreview()
  dragging.value = false
  dragDepth.value = 0
}

function onOpenChange(v: boolean) {
  if (!v) {
    closeDialog()
    return
  }
  open.value = true
}

function onPick() {
  if (submitting.value || parsing.value || props.disabled) return
  inputRef.value?.click()
}

function isXlsx(f: File) {
  return f.name.toLowerCase().endsWith(ACCEPT_EXT)
}

async function acceptFile(f: File | undefined | null) {
  if (!f || parsing.value || submitting.value) return
  if (!isXlsx(f)) {
    toast.error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
    return
  }
  const limit = props.maxSizeMb * 1024 * 1024
  if (f.size > limit) {
    toast.error(crudFormat(crudLabel('fileSizeLimit'), props.maxSizeMb))
    return
  }

  parsing.value = true
  try {
    const preview = await parseImportExcelPreview(f)
    if (!preview.rows.length) {
      resetPreview()
      toast.error(crudLabel('noImportRows'))
      return
    }
    file.value = f
    previewHeaders.value = preview.headers
    previewRows.value = preview.rows
    page.value = 1
  } catch (e) {
    resetPreview()
    toast.error(e instanceof Error ? e.message : crudLabel('parseExcelFail'))
  } finally {
    parsing.value = false
  }
}

function onFileInput(ev: Event) {
  const input = ev.target as HTMLInputElement
  const f = input.files?.[0]
  input.value = ''
  void acceptFile(f)
}

function onDragEnter() {
  if (submitting.value || parsing.value) return
  dragDepth.value += 1
  dragging.value = true
}

function onDragLeave() {
  dragDepth.value = Math.max(0, dragDepth.value - 1)
  if (dragDepth.value === 0) dragging.value = false
}

function onDrop(ev: DragEvent) {
  dragDepth.value = 0
  dragging.value = false
  if (submitting.value || parsing.value) return
  void acceptFile(ev.dataTransfer?.files?.[0])
}

async function onTemplate() {
  if (downloading.value || submitting.value || props.disabled) return
  const ns = crud.service?.namespace
  if (!ns) {
    toast.error(crudLabel('noServiceTpl'))
    return
  }
  downloading.value = true
  try {
    await downloadImportTemplate(ns, resolveTemplateName())
    toast.success(crudLabel('tplDownloaded'))
  } catch (e) {
    toast.error(e instanceof Error ? e.message : crudLabel('downloadTplFail'))
  } finally {
    downloading.value = false
  }
}

async function finishImport(result: ImportResult) {
  const failN = result.failed?.length || 0
  if (failN) {
    const tipText = result.failed
      .slice(0, 3)
      .map((f) => crudFormat(crudLabel('importRowFail'), f.row, f.message))
      .join('；')
    toast.warning(
      crudFormat(
        crudLabel('importDone'),
        result.inserted,
        result.updated,
        failN,
        tipText ? `（${tipText}）` : '',
      ),
    )
  } else {
    toast.success(
      crudFormat(crudLabel('importSuccess'), result.inserted, result.updated),
    )
  }
  emit('success', {
    inserted: result.inserted,
    updated: result.updated,
    failed: failN,
  })
  await crud.refresh()
  open.value = false
  resetPreview()
}

async function runDefaultImport(uploadFile: File) {
  const ns = crud.service?.namespace
  if (!ns) {
    toast.error(crudLabel('noServiceImport'))
    return null
  }
  const result = await uploadImportExcel(ns, uploadFile)
  await finishImport(result)
  return result
}

async function onSubmit() {
  if (submitting.value || parsing.value || props.disabled || !canSubmit.value) {
    return
  }
  if (!file.value || !isXlsx(file.value)) {
    toast.error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
    return
  }
  if (!previewRows.value.length) {
    toast.error(crudLabel('pickExcelFirst'))
    return
  }

  const ns = crud.service?.namespace
  if (!ns) {
    toast.error(crudLabel('noServiceImport'))
    return
  }

  submitting.value = true
  try {
    if (importOpts?.onSubmit) {
      await importOpts.onSubmit(
        {
          file: file.value,
          rows: previewRows.value,
          headers: previewHeaders.value,
        },
        {
          next: async (payload) => {
            if (Array.isArray(payload)) {
              if (!payload.length) {
                throw new Error(crudLabel('noImportRows'))
              }
              const result = await uploadImportRows(ns, payload)
              await finishImport(result)
              return result
            }
            const f = payload ?? file.value
            if (!f || !isXlsx(f)) {
              throw new Error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
            }
            const result = await uploadImportExcel(ns, f)
            await finishImport(result)
            return result
          },
          done: () => {},
          close: () => {
            open.value = false
            resetPreview()
          },
        },
      )
      return
    }
    await runDefaultImport(file.value)
  } catch (e) {
    toast.error(e instanceof Error ? e.message : crudLabel('importFail'))
  } finally {
    submitting.value = false
  }
}
</script>

<style lang="scss" scoped>
.vm-import-btn {
  display: inline-flex;
  align-items: center;
}

.vm-import-btn__input {
  display: none;
}

/* 导入弹窗：高度 600，宽度沿用默认 CRUD（50vw） */
:deep(.vm-import-btn__dialog[data-slot='dialog-content']),
:deep(.vm-import-btn__dialog.vm-dialog-shell-crud) {
  height: 600px !important;
  max-height: 600px !important;
}

.vm-import-btn__body {
  display: flex;
  min-height: 0;
  flex: 1;
  flex-direction: column;
  gap: 12px;
  overflow: hidden;
}

.vm-import-btn__bar {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.vm-import-btn__bar-actions {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  gap: 8px;
}

.vm-import-btn__file-name {
  overflow: hidden;
  color: var(--muted-foreground);
  font-size: 13px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vm-import-btn__tip {
  margin: 0;
  color: var(--warning);
  font-size: 13px;
  font-weight: 500;
}

.vm-import-btn__repick {
  flex-shrink: 0;
  height: 32px;
  padding: 0 12px;
  border: none;
  border-radius: 10px;
  background: var(--muted);
  color: var(--foreground);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;

  &:hover:not(:disabled) {
    background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
  }

  &:disabled {
    cursor: not-allowed;
    opacity: 0.45;
  }
}

.vm-import-btn__tpl {
  flex-shrink: 0;
  height: 32px;
  padding: 0 12px;
  border: none;
  border-radius: 10px;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, opacity 0.15s ease;

  &:hover:not(:disabled) {
    background: color-mix(in srgb, var(--brand-soft) 70%, var(--brand) 12%);
  }

  &:disabled {
    cursor: not-allowed;
    opacity: 0.45;
  }
}

.vm-import-btn__drop {
  display: grid;
  flex: 1;
  min-height: 0;
  place-items: center;
  place-content: center;
  gap: 8px;
  padding: 20px 16px;
  border: 1px dashed var(--border);
  border-radius: 12px;
  background: var(--muted);
  color: var(--muted-foreground);
  font-size: 13px;
  text-align: center;
  cursor: pointer;
  transition:
    border-color 0.15s ease,
    color 0.15s ease,
    background 0.15s ease;

  i {
    font-size: 32px;
    line-height: 1;
  }

  &:hover:not(.is-disabled),
  &.is-drag {
    border-color: var(--brand);
    background: var(--brand-soft);
    color: var(--brand);
  }

  &.is-disabled {
    cursor: not-allowed;
    opacity: 0.55;
  }
}

.vm-import-btn__ext {
  font-size: 12px;
  opacity: 0.8;
}

.vm-import-btn__preview {
  display: flex;
  min-height: 0;
  flex: 1;
  flex-direction: column;
  gap: 10px;
  overflow: hidden;
}

.vm-import-btn__table-wrap {
  min-height: 0;
  flex: 1;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card);
}

.vm-import-btn__table {
  width: max-content;
  min-width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.vm-import-btn__th,
.vm-import-btn__td {
  max-width: 220px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

.vm-import-btn__th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--muted);
  color: var(--muted-foreground);
  font-weight: 600;
}

.vm-import-btn__pager {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.vm-import-btn__total {
  color: var(--muted-foreground);
  font-size: 13px;
}

.vm-import-btn__pager-actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.vm-import-btn__page-num {
  min-width: 52px;
  color: var(--muted-foreground);
  font-size: 12px;
  text-align: center;
}

.vm-import-btn__page-btn {
  height: 30px;
  padding: 0 12px;
  border: none;
  border-radius: 10px;
  background: var(--muted);
  color: var(--foreground);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;

  &:hover:not(:disabled) {
    background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
  }

  &:disabled {
    cursor: not-allowed;
    opacity: 0.45;
  }
}

.vm-crud-toolbar__btn {
  display: inline-flex;
  height: 36px;
  align-items: center;
  gap: 4px;
  padding: 0 14px;
  border: none;
  border-radius: 12px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;

  &:disabled {
    cursor: not-allowed;
    opacity: 0.45;
  }

  &.is-ghost {
    background: var(--muted);
    color: var(--foreground);

    &:hover:not(:disabled) {
      background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
    }
  }
}
</style>
