<template>
  <vm-aside
    :title="onlyTrashed ? crudLabel('deptTrash') : crudLabel('deptOrg')"
    :empty="emptyText"
    :loading="loading"
    :add="canAdd && !onlyTrashed"
    :add-title="crudLabel('deptAddRoot')"
    @refresh="emit('refresh')"
    @add="openAdd(null)"
  >
    <button
      type="button"
      class="vm-dept-tree__node is-root"
      :class="{ 'is-active': modelValue == null }"
      @click="select(null)"
    >
      <i class="ri-building-2-line" />
      <span>{{ rootLabel ?? crudLabel('all') }}</span>
    </button>

    <div
      v-for="row in visibleRows"
      :key="String(row.id)"
      class="vm-dept-tree__node"
      :class="{ 'is-active': modelValue === row.id, 'is-hidden': !row.visible }"
      :style="{ paddingLeft: `${12 + row.depth * 16}px` }"
      @click="select(row.id)"
    >
      <button
        v-if="row.hasChildren"
        type="button"
        class="vm-dept-tree__expand"
        :class="{ 'is-open': expanded.has(row.id) }"
        @click.stop="toggleExpand(row.id)"
      >
        <i class="ri-arrow-right-s-line" />
      </button>
      <span v-else class="vm-dept-tree__expand is-spacer" />
      <span class="vm-dept-tree__label">{{ row.label }}</span>
      <div v-if="onlyTrashed && canTrashOps" class="vm-dept-tree__ops" @click.stop>
        <button
          type="button"
          :title="crudLabel('restore')"
          :disabled="trashBusyId === row.id"
          @click="restoreDept(row)"
        >
          <i class="ri-arrow-go-back-line" />
        </button>
        <button
          type="button"
          :title="crudLabel('forceDelete')"
          :disabled="trashBusyId === row.id"
          @click="forceDeleteDept(row)"
        >
          <i class="ri-delete-bin-2-line" />
        </button>
      </div>
      <div v-else-if="!onlyTrashed && canManage" class="vm-dept-tree__ops" @click.stop>
        <button v-if="canAdd" type="button" :title="crudLabel('deptAddChild')" @click="openAdd(row.id)">
          <i class="ri-add-line" />
        </button>
        <button v-if="canUpdate" type="button" :title="crudLabel('update')" @click="openEdit(row)">
          <i class="ri-edit-line" />
        </button>
        <button v-if="canDelete" type="button" :title="crudLabel('delete')" @click="remove(row)">
          <i class="ri-delete-bin-line" />
        </button>
      </div>
    </div>
  </vm-aside>

  <vm-form-dialog
    v-model:open="formOpen"
    v-model="formData"
    :title="formMode === 'add' ? crudLabel('deptAddTitle') : crudLabel('deptEditTitle')"
    :items="dialogItems"
    @submit="onFormSubmit"
  />
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { flattenTree } from '../lib/tree'
import { vmConfirm } from '../crud/confirm'
import { crudFormat, crudLabel } from '../crud/style'
import { mergeByProp } from '../crud/auto-fields'
import type { CrudFormItem } from '../../../typings/admin/comm/crud'
import type { VmFormDialogSubmitHelpers } from './vm-form-dialog.vue'

defineOptions({ name: 'vm-dept-tree' })

/** 调用方须先组装好的树节点（含 children），组件不再做扁平→树转换 */
export type DeptTreeNode = {
  id: number
  parentId: number | null
  orderNum: number
  name: string
  children?: DeptTreeNode[]
}

type FlatRow = {
  id: number
  label: string
  depth: number
  hasChildren: boolean
  visible: boolean
  parentId: number | null
  orderNum: number
}

export type DeptFormPayload = {
  name: string
  orderNum: number
  parentId: number | null
  id?: number
} & Record<string, unknown>

const props = withDefaults(
  defineProps<{
    /** 已是树形结构的部门数据 */
    data?: DeptTreeNode[]
    loading?: boolean
    modelValue?: number | null
    rootLabel?: string
    /** 回收站 UI：隐藏新增，显示恢复/彻底删除 */
    onlyTrashed?: boolean
    canAdd?: boolean
    canUpdate?: boolean
    canDelete?: boolean
    canRestore?: boolean
    /**
     * 追加/覆盖表单字段（CrudFormItem）；与内置「上级/名称/排序」合并，同 prop 以传入为准。
     * 任意控件：`component: { name: 'vm-select', props: {…} }`
     */
    formItems?: CrudFormItem[]
  }>(),
  {
    data: () => [],
    loading: false,
    modelValue: null,
    rootLabel: undefined,
    onlyTrashed: false,
    canAdd: false,
    canUpdate: false,
    canDelete: false,
    canRestore: false,
    formItems: () => [],
  },
)

const emit = defineEmits<{
  'update:modelValue': [v: number | null]
  change: [v: number | null, meta: { name: string; ids: number[] }]
  refresh: []
  add: [payload: DeptFormPayload]
  update: [payload: DeptFormPayload]
  delete: [payload: { id: number }]
  restore: [payload: { id: number }]
  forceDelete: [payload: { id: number }]
}>()

const expanded = ref(new Set<number>())
const formOpen = ref(false)
const formMode = ref<'add' | 'edit'>('add')
const formData = ref<Record<string, unknown>>({})
const trashBusyId = ref<number | null>(null)

const canManage = computed(
  () => props.canAdd || props.canUpdate || props.canDelete,
)
const canTrashOps = computed(() => props.canRestore || props.canDelete)

const builtinFormItems = computed((): CrudFormItem[] => [
  {
    prop: 'parentLabel',
    label: crudLabel('deptParent'),
    span: 12,
    disabled: true,
  },
  {
    prop: 'name',
    label: crudLabel('fieldName'),
    span: 12,
    required: true,
    placeholder: crudLabel('deptNamePh'),
  },
  {
    prop: 'orderNum',
    label: crudLabel('orderNum'),
    span: 12,
    type: 'number',
    value: 0,
  },
])

const dialogItems = computed(() =>
  mergeByProp(builtinFormItems.value, props.formItems || []),
)

const idNameMap = computed(() => {
  const map = new Map<number, string>()
  const walk = (list: DeptTreeNode[]) => {
    for (const n of list) {
      map.set(n.id, n.name)
      if (n.children?.length) walk(n.children)
    }
  }
  walk(props.data)
  return map
})

const descendantMap = computed(() => {
  const map = new Map<number, number[]>()
  const walk = (node: DeptTreeNode): number[] => {
    let ids: number[] = []
    for (const c of node.children || []) {
      ids.push(c.id)
      ids = ids.concat(walk(c))
    }
    map.set(node.id, ids)
    return ids
  }
  for (const n of props.data) walk(n)
  return map
})

function collectExpandable(list: DeptTreeNode[], into: Set<number>) {
  for (const n of list) {
    if (n.children?.length) {
      into.add(n.id)
      collectExpandable(n.children, into)
    }
  }
}

const visibleRows = computed((): FlatRow[] => {
  const flat = flattenTree(props.data, expanded.value)
  return flat.map((n) => ({
    id: n.id as number,
    label: n.name,
    depth: n.depth,
    hasChildren: n.hasChildren,
    visible: true,
    parentId: n.parentId ?? null,
    orderNum: n.orderNum ?? 0,
  }))
})

const emptyText = computed(() => {
  if (props.loading || visibleRows.value.length) return ''
  return props.onlyTrashed ? crudLabel('trashEmpty') : crudLabel('deptEmpty')
})

function resolveFilterIds(id: number | null) {
  if (id == null) return []
  return [id, ...(descendantMap.value.get(id) || [])]
}

function select(id: number | null) {
  emit('update:modelValue', id)
  const name =
    id == null ? (props.rootLabel ?? '') : idNameMap.value.get(id) || ''
  emit('change', id, { name, ids: resolveFilterIds(id) })
}

function toggleExpand(id: number) {
  const next = new Set(expanded.value)
  if (next.has(id)) next.delete(id)
  else next.add(id)
  expanded.value = next
}

function syncExpandedFromData() {
  const next = new Set<number>()
  collectExpandable(props.data, next)
  expanded.value = next
  if (props.modelValue != null && !idNameMap.value.has(props.modelValue)) {
    select(null)
  }
}

watch(
  () => props.data,
  () => syncExpandedFromData(),
  { deep: true, immediate: true },
)

watch(
  () => props.onlyTrashed,
  () => {
    select(null)
  },
)

function parentLabelOf(parentId: number | null) {
  return parentId == null ? crudLabel('topLevel') : idNameMap.value.get(parentId) || String(parentId)
}

function openAdd(parentId: number | null) {
  formMode.value = 'add'
  formData.value = {
    id: 0,
    name: '',
    orderNum: 0,
    parentId,
    parentLabel: parentLabelOf(parentId),
  }
  formOpen.value = true
}

function openEdit(row: FlatRow) {
  formMode.value = 'edit'
  formData.value = {
    id: row.id,
    name: row.label,
    orderNum: row.orderNum ?? 0,
    parentId: row.parentId,
    parentLabel: parentLabelOf(row.parentId),
  }
  formOpen.value = true
}

function onFormSubmit(data: Record<string, unknown>, helpers: VmFormDialogSubmitHelpers) {
  const { parentLabel: _pl, ...rest } = data
  const payload: DeptFormPayload = {
    ...rest,
    name: String(data.name ?? '').trim(),
    orderNum: Number(data.orderNum ?? 0),
    parentId:
      data.parentId == null || data.parentId === ''
        ? null
        : Number(data.parentId),
  }
  if (formMode.value === 'add') {
    emit('add', payload)
  } else {
    emit('update', { ...payload, id: Number(data.id) })
  }
  helpers.close()
}

async function remove(row: FlatRow) {
  const ok = await vmConfirm({
    title: crudLabel('deptDeleteTitle'),
    message: crudFormat(crudLabel('deptDeleteMsg'), row.label),
    confirmText: crudLabel('delete'),
    cancelText: crudLabel('close'),
  })
  if (!ok) return
  if (props.modelValue === row.id) select(null)
  emit('delete', { id: row.id })
}

async function restoreDept(row: FlatRow) {
  if (trashBusyId.value === row.id) return
  const ok = await vmConfirm({
    title: crudLabel('deptRestoreTitle'),
    message: crudFormat(crudLabel('deptRestoreMsg'), row.label),
    confirmText: crudLabel('restore'),
    cancelText: crudLabel('close'),
  })
  if (!ok) return
  trashBusyId.value = row.id
  if (props.modelValue === row.id) select(null)
  emit('restore', { id: row.id })
  trashBusyId.value = null
}

async function forceDeleteDept(row: FlatRow) {
  if (trashBusyId.value === row.id) return
  const ok = await vmConfirm({
    title: crudLabel('deptForceDeleteTitle'),
    message: crudFormat(crudLabel('deptForceDeleteMsg'), row.label),
    confirmText: crudLabel('forceDelete'),
    cancelText: crudLabel('close'),
  })
  if (!ok) return
  trashBusyId.value = row.id
  if (props.modelValue === row.id) select(null)
  emit('forceDelete', { id: row.id })
  trashBusyId.value = null
}

defineExpose({ idNameMap })
</script>

<style lang="scss" scoped>
.vm-dept-tree__node {
  display: flex;
  min-width: 0;
  min-height: 32px;
  align-items: center;
  gap: 4px;
  padding-right: 6px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: var(--foreground);
  cursor: pointer;
  text-align: left;

  &.is-hidden {
    display: none;
  }

  &:hover {
    background: var(--muted);

    .vm-dept-tree__ops {
      opacity: 1;
    }
  }

  &.is-active {
    background: var(--brand-soft);
    color: var(--brand-deep);
  }

  &.is-root {
    width: 100%;
    padding-left: 10px;
    margin-bottom: 4px;
    font-weight: 600;

    i {
      font-size: 15px;
      color: var(--brand);
    }
  }
}

.vm-dept-tree__expand {
  display: inline-flex;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--muted-foreground);
  cursor: pointer;

  i {
    font-size: 14px;
    transition: transform 0.15s ease;
  }

  &.is-open i {
    transform: rotate(90deg);
  }

  &.is-spacer {
    visibility: hidden;
  }
}

.vm-dept-tree__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
}

.vm-dept-tree__ops {
  display: inline-flex;
  gap: 2px;
  opacity: 0;
  transition: opacity 0.15s ease;

  button {
    display: inline-flex;
    width: 22px;
    height: 22px;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 6px;
    background: transparent;
    color: var(--muted-foreground);
    cursor: pointer;

    &:hover {
      background: var(--card);
      color: var(--brand);
    }

    &:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }
  }
}
</style>
