<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue';
import { VueDraggable } from 'vue-draggable-plus';
import { Calendar, Flag, Loading, Lock, User } from '@element-plus/icons-vue';
import { RDMS_REQ_PRIORITY_DICT_CODE } from '@/constants/dict';
import DictTag from '@/components/custom/dict-tag.vue';
import { formatDateRange, getProgressText, getTaskStatusName, getTaskStatusTagType } from '../shared';
import { useTaskActions } from '../composables/use-task-actions';
import {
  type BoardFetcherParams,
  type UseTaskBoardColumnsOptions,
  useTaskBoardColumns
} from '../composables/use-task-board-columns';
import { isTaskDraggable, resolveTaskDragAction } from '../composables/use-task-drag';

defineOptions({ name: 'ProjectExecutionTaskBoardView' });

interface Props {
  canLoad: boolean;
  statusBoard: Api.Project.StatusBoard | null;
  /** 看板 fetcher：内部按"单执行 / 跨执行"分流；composable 只负责列状态管理 */
  fetcher: UseTaskBoardColumnsOptions['fetcher'];
  /**
   * 跨执行模式：卡片显示「所属执行 pill」(可点切换执行);
   * 已完成执行的任务卡片灰显（文档建议：跨执行场景下 executionStatusCode 表达执行生命周期）
   */
  crossExecutionMode?: boolean;
  /**
   * 父组件 StatusActionDialog 的可见状态。
   * 拖拽触发的状态变更走和按钮同一路径(emit status-action),期间该卡片显示 pending 视觉;
   * dialog 从 true → false 时(无论 submit 还是 cancel)清掉 pending,把卡片视觉还回去。
   */
  statusActionVisible?: boolean;
}

interface Emits {
  (e: 'detail', row: Api.Project.ProjectTask): void;
  (e: 'edit', row: Api.Project.ProjectTask): void;
  (e: 'report', row: Api.Project.ProjectTask): void;
  (e: 'delete', row: Api.Project.ProjectTask): void;
  (
    e: 'status-action',
    row: Api.Project.ProjectTask,
    action: Api.Project.LifecycleAction<Api.Project.ProjectTaskActionCode> | null
  ): void;
}

const props = withDefaults(defineProps<Props>(), {
  crossExecutionMode: false,
  statusActionVisible: false
});
const emit = defineEmits<Emits>();

const DRAG_GROUP = 'project-task-board';

/** 拖拽触发后等待 dialog 反馈的任务 id;同一时刻最多一个(dialog 是模态) */
const pendingTaskId = ref<string | null>(null);
let pendingTimer: ReturnType<typeof setTimeout> | null = null;
const PENDING_FALLBACK_MS = 8000;

interface DragEndEvent {
  oldIndex?: number;
  newIndex?: number;
  from: HTMLElement;
  to: HTMLElement;
}

function clearPending() {
  pendingTaskId.value = null;
  if (pendingTimer) {
    clearTimeout(pendingTimer);
    pendingTimer = null;
  }
}

/**
 * 设 pending 并启动兜底 timer。
 * 兜底场景:父组件 handleStatusAction 在 await getTaskDetail 后因 availableActions 为空 / complete 进度
 * 不足等原因 warning return,dialog 从未打开 → watch statusActionVisible 永远不会触发清零。
 * timer 到时间强制还原卡片视觉(实际正常路径上 dialog 关闭信号会先于 timer 触发)。
 */
function startPending(taskId: string) {
  pendingTaskId.value = taskId;
  if (pendingTimer) clearTimeout(pendingTimer);
  pendingTimer = setTimeout(() => {
    if (pendingTaskId.value === taskId) clearPending();
  }, PENDING_FALLBACK_MS);
}

const { columns, loadMore } = useTaskBoardColumns({
  canLoad: toRef(props, 'canLoad'),
  // statusBoard 在此仅作"刷新事件源"：workspace 任何变更后都会重新拉它，ref 引用变即触发本看板重拉首屏
  refreshSignal: toRef(props, 'statusBoard'),
  fetcher: (params: BoardFetcherParams) => props.fetcher(params)
});

function isExecutionLocked(task: Api.Project.ProjectTask): boolean {
  if (!props.crossExecutionMode) return false;
  return task.executionStatusCode === 'completed' || task.executionStatusCode === 'cancelled';
}

function isCardDraggable(task: Api.Project.ProjectTask): boolean {
  if (pendingTaskId.value === task.id) return false;
  return isTaskDraggable(task, { executionLocked: isExecutionLocked(task) });
}

/**
 * 卡片"为什么不可拖"——给视觉标识和拖时 toast 复用同一份文案。
 * pending 卡片是临时态(提交中),不算"无权",不显示锁标识。
 */
function getLockedReason(task: Api.Project.ProjectTask): string | null {
  if (pendingTaskId.value === task.id) return null;
  if (isExecutionLocked(task)) return '所属执行已结束，任务不可变更状态';
  const hasExposedAction = task.availableActions.some(action => action.actionCode !== 'auto_start');
  if (!hasExposedAction) return '你暂无该任务的状态变更权限';
  return null;
}

/**
 * 尝试拖 locked 卡片时 SortableJS 触发 onFilter,弹 toast 解释原因。
 * 同任务 1.2s 内不重复弹(防 mousedown 重复触发刷屏)。
 * 详情入口已收敛到标题点击,不再需要"filter 后抑制 click"那套兼容。
 */
let lastFilterTaskId: string | null = null;
let lastFilterAt = 0;
function handleDragFilter(event: { item: HTMLElement }) {
  const taskId = event.item?.dataset?.taskId;
  if (!taskId) return;
  const now = Date.now();
  if (lastFilterTaskId === taskId && now - lastFilterAt < 1200) return;
  lastFilterTaskId = taskId;
  lastFilterAt = now;

  let task: Api.Project.ProjectTask | undefined;
  for (const col of columns.value) {
    task = col.tasks.find(item => item.id === taskId);
    if (task) break;
  }
  if (!task) return;

  const reason = getLockedReason(task);
  if (reason) window.$message?.warning(reason);
}

function resolveStatusCodeFromEl(el: HTMLElement | null | undefined): string | undefined {
  // vue-draggable-plus 通过 h() 手动构造 vnode 不可靠地透传 attrs,所以 data 属性挂在外层 wrapper 上,
  // SortableJS 的 from/to 指向内部 VueDraggable 根 div,需要向上 closest 查找
  return el?.closest<HTMLElement>('[data-board-column]')?.dataset.boardColumn;
}

function handleDragEnd(event: DragEndEvent) {
  const { from, to, oldIndex, newIndex } = event;
  if (oldIndex === undefined || newIndex === undefined) return;

  const fromStatusCode = resolveStatusCodeFromEl(from);
  const toStatusCode = resolveStatusCodeFromEl(to);
  if (!fromStatusCode || !toStatusCode) return;

  const fromCol = columns.value.find(item => item.statusCode === fromStatusCode);
  const toCol = columns.value.find(item => item.statusCode === toStatusCode);
  if (!fromCol || !toCol) return;

  // 同列拖动:看板不支持手动排序,还原顺序后静默
  if (fromCol === toCol) {
    if (oldIndex !== newIndex) {
      const [moved] = toCol.tasks.splice(newIndex, 1);
      toCol.tasks.splice(oldIndex, 0, moved);
    }
    return;
  }

  // 跨列:vue-draggable-plus 已经把任务从 from splice 到 to;先无条件还原,把"是否真正变更"完全交给后端结果
  const [task] = toCol.tasks.splice(newIndex, 1);
  if (!task) return;
  fromCol.tasks.splice(oldIndex, 0, task);

  const resolution = resolveTaskDragAction(task, toStatusCode);
  if (!resolution.ok) {
    if (resolution.reason) window.$message?.warning(resolution.reason);
    return;
  }

  // 合法:走和按钮点击同一路径,父组件打开 StatusActionDialog 收 reason
  startPending(task.id);
  emit('status-action', task, resolution.action);
}

// dialog 关闭(submit 完成或 cancel 都会触发) → 清 pending,把卡片视觉还回去
// submit 成功时父组件会重拉 statusBoard → 看板首屏重渲染,卡片自然到位
watch(
  () => props.statusActionVisible,
  (next, prev) => {
    // false → true:dialog 真正打开,撤掉兜底 timer,关闭信号接管
    if (!prev && next && pendingTimer) {
      clearTimeout(pendingTimer);
      pendingTimer = null;
    }
    // true → false:正常关闭(submit/cancel)
    if (prev && !next) clearPending();
  }
);

// 看板卡片操作按钮（与表格操作列同语义）。
const { createActions } = useTaskActions({
  edit: row => emit('edit', row),
  report: row => emit('report', row),
  remove: row => emit('delete', row),
  statusAction: (row, action) => emit('status-action', row, action)
});

// 每个列对应一个底部 sentinel，IntersectionObserver 观测到入视即触发 loadMore。
// IO 的 root 用 null（视口）即可：列的 overflow 裁剪不影响 IO 的 boundingClientRect 判定，
// sentinel 实际是否能"被滚到"由 column 滚动条决定。
const sentinelRegistry = new Map<HTMLElement, string>();
let observer: IntersectionObserver | null = null;

function handleSentinelRef(el: Element | null, statusCode: string) {
  if (el instanceof HTMLElement) {
    sentinelRegistry.set(el, statusCode);
    observer?.observe(el);
    return;
  }
  // el === null：sentinel 卸载（hasMore 变 false）—— 按 statusCode 反查清理，避免 observer 持有失效 DOM
  for (const [existing, code] of sentinelRegistry) {
    if (code === statusCode) {
      observer?.unobserve(existing);
      sentinelRegistry.delete(existing);
      break;
    }
  }
}

onMounted(() => {
  observer = new IntersectionObserver(
    entries => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) return;
        const statusCode = sentinelRegistry.get(entry.target as HTMLElement);
        if (statusCode) {
          loadMore(statusCode);
        }
      });
    },
    { rootMargin: '200px 0px' }
  );
  sentinelRegistry.forEach((_, el) => observer!.observe(el));
});

onBeforeUnmount(() => {
  observer?.disconnect();
  observer = null;
  sentinelRegistry.clear();
  if (pendingTimer) {
    clearTimeout(pendingTimer);
    pendingTimer = null;
  }
});
</script>

<template>
  <ElCard class="task-board-card" body-class="task-board-card__body">
    <div class="task-board">
      <section v-for="column in columns" :key="column.statusCode" class="task-board-column">
        <header class="task-board-column__header">
          <strong>{{ column.title }}</strong>
          <ElTag effect="plain" size="small" round>{{ column.total }}</ElTag>
        </header>

        <ElScrollbar class="task-board-column__scrollbar">
          <ElEmpty
            v-if="column.tasks.length === 0 && !column.loading"
            :description="`暂无${column.title}任务`"
            :image-size="64"
          />
          <div :data-board-column="column.statusCode" class="task-board-column__list-wrap">
            <VueDraggable
              v-model="column.tasks"
              :group="DRAG_GROUP"
              :animation="180"
              filter=".task-board-card-item--locked"
              :prevent-on-filter="false"
              drag-class="task-board-card-item--dragging"
              ghost-class="task-board-card-item--ghost"
              class="task-board-column__list"
              @end="handleDragEnd"
              @filter="handleDragFilter"
            >
              <article
                v-for="task in column.tasks"
                :key="task.id"
                class="task-board-card-item"
                :class="{
                  'is-dimmed': isExecutionLocked(task),
                  'task-board-card-item--locked': !isCardDraggable(task),
                  'task-board-card-item--pending': pendingTaskId === task.id
                }"
                :data-task-id="task.id"
              >
                <div class="task-board-card-item__top">
                  <strong
                    class="task-board-card-item__title"
                    role="button"
                    tabindex="0"
                    @click.stop="emit('detail', task)"
                    @keydown.enter.stop.prevent="emit('detail', task)"
                  >
                    {{ task.taskTitle || '未命名任务' }}
                  </strong>
                  <div class="task-board-card-item__top-tags">
                    <DictTag
                      :dict-code="RDMS_REQ_PRIORITY_DICT_CODE"
                      :value="task.priority"
                      effect="light"
                      size="small"
                    />
                    <ElTag :type="getTaskStatusTagType(task.statusCode)" effect="light" size="small">
                      {{ getTaskStatusName(task) }}
                    </ElTag>
                    <ElTooltip
                      v-if="getLockedReason(task)"
                      :content="getLockedReason(task) ?? ''"
                      placement="top"
                      :show-after="120"
                    >
                      <ElIcon class="task-board-card-item__lock" @click.stop>
                        <Lock />
                      </ElIcon>
                    </ElTooltip>
                  </div>
                </div>

                <div v-if="crossExecutionMode && task.executionId" class="task-board-card-item__exec">
                  <ElTag
                    size="small"
                    type="info"
                    effect="plain"
                    class="task-board-card-item__exec-tag"
                    :title="task.executionName || ''"
                  >
                    {{ task.executionName || '未命名执行' }}
                  </ElTag>
                </div>

                <div class="task-board-card-item__meta">
                  <span>
                    <ElIcon><User /></ElIcon>
                    {{ task.ownerNickname || task.ownerId || '未设置负责人' }}
                  </span>
                  <span>
                    <ElIcon><Flag /></ElIcon>
                    计划 {{ formatDateRange(task.plannedStartDate, task.plannedEndDate) }}
                  </span>
                  <span v-if="!crossExecutionMode">
                    <ElIcon><Calendar /></ElIcon>
                    实际 {{ formatDateRange(task.actualStartDate, task.actualEndDate) }}
                  </span>
                </div>

                <div class="task-board-card-item__progress">
                  <span>进度 {{ getProgressText(task.progressRate) }}</span>
                  <ElProgress :percentage="task.progressRate" :stroke-width="6" :show-text="false" />
                </div>

                <div
                  v-if="createActions(task).length"
                  class="task-board-card-item__actions"
                  @click.stop
                  @pointerdown.stop
                  @mousedown.stop
                >
                  <ElTooltip v-for="action in createActions(task)" :key="action.key" :content="action.tooltip">
                    <ElButton link :type="action.type" class="task-action-btn" @click="action.onClick()">
                      <component :is="action.icon" class="text-15px" />
                    </ElButton>
                  </ElTooltip>
                </div>

                <div v-if="pendingTaskId === task.id" class="task-board-card-item__pending-overlay">
                  <ElIcon class="is-loading"><Loading /></ElIcon>
                  <span>提交中…</span>
                </div>
              </article>
            </VueDraggable>
          </div>

          <div
            v-if="column.hasMore"
            :ref="el => handleSentinelRef(el as Element | null, column.statusCode)"
            class="task-board-column__sentinel"
          >
            <span v-if="column.loading">加载中…</span>
            <span v-else class="task-board-column__sentinel-idle">下拉加载</span>
          </div>
          <div v-else-if="column.tasks.length > 0" class="task-board-column__footer">已加载全部</div>
        </ElScrollbar>
      </section>
    </div>
  </ElCard>
</template>

<style scoped lang="scss">
.task-board-card {
  min-height: 0;
  flex: 1;
}

:deep(.task-board-card__body) {
  display: flex;
  min-height: 0;
  height: 100%;
}

.task-board {
  display: grid;
  grid-template-columns: repeat(5, minmax(220px, 1fr));
  gap: 12px;
  width: 100%;
  min-height: 0;
  overflow-x: auto;
}

.task-board-column {
  display: flex;
  min-width: 220px;
  min-height: 0;
  flex-direction: column;
  border: 1px solid rgb(226 232 240 / 92%);
  border-radius: 8px;
  background-color: rgb(248 250 252 / 82%);
}

.task-board-column__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 42px;
  padding: 10px 12px;
  border-bottom: 1px solid rgb(226 232 240 / 92%);
  color: rgb(15 23 42 / 94%);
}

.task-board-column__scrollbar {
  min-height: 0;
  flex: 1;
  padding: 10px;
}

.task-board-column__list {
  display: block;
  min-height: 12px;
}

.task-board-card-item {
  position: relative;
  padding: 10px;
  border: 1px solid rgb(226 232 240 / 92%);
  border-radius: 6px;
  margin-bottom: 10px;
  background-color: #fff;
  cursor: grab;
  transition:
    border-color 0.16s ease,
    box-shadow 0.16s ease;
}

.task-board-card-item:active {
  cursor: grabbing;
}

.task-board-card-item:hover {
  border-color: rgb(148 163 184 / 76%);
  box-shadow: 0 6px 18px rgb(15 23 42 / 8%);
}

.task-board-card-item.is-dimmed {
  opacity: 0.55;
  background-color: rgb(248 250 252 / 80%);
}

.task-board-card-item.is-dimmed:hover {
  opacity: 0.8;
}

// 不可拖卡片(灰显执行 / 无可推进 action):光标提示 + 取消"可抓"
.task-board-card-item--locked {
  cursor: pointer;
}

.task-board-card-item--locked:active {
  cursor: pointer;
}

// 拖拽中:跟手卡片样式(SortableJS dragClass)
.task-board-card-item--dragging {
  opacity: 0.92;
  box-shadow: 0 12px 28px rgb(15 23 42 / 18%);
  transform: rotate(1.2deg);
}

// 原位置占位(SortableJS ghostClass)
.task-board-card-item--ghost {
  opacity: 0.36;
  background-color: rgb(241 245 249);
  border-style: dashed;
}

// 提交中:卡片整体淡化,中央 overlay 显示 spinner
.task-board-card-item--pending {
  cursor: progress;
  pointer-events: none;
}

.task-board-card-item--pending:active {
  cursor: progress;
}

.task-board-card-item__pending-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border-radius: 6px;
  background-color: rgb(255 255 255 / 72%);
  backdrop-filter: blur(1px);
  color: rgb(71 85 105);
  font-size: 12px;
  font-weight: 600;
}

.task-board-card-item__pending-overlay .is-loading {
  animation: task-board-pending-spin 0.9s linear infinite;
  font-size: 16px;
}

@keyframes task-board-pending-spin {
  to {
    transform: rotate(360deg);
  }
}

.task-board-card-item__exec {
  margin-top: 8px;
}

.task-board-card-item__exec-tag {
  max-width: 100%;
}

.task-board-card-item__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}

.task-board-card-item__top-tags {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
}

.task-board-card-item__lock {
  font-size: 13px;
  color: rgb(148 163 184);
  cursor: help;
}

.task-board-card-item__title {
  min-width: 0;
  color: rgb(15 23 42 / 94%);
  font-size: 14px;
  line-height: 1.5;
  word-break: break-word;
  cursor: pointer;
  transition: color 0.16s ease;
}

.task-board-card-item__title:hover {
  color: var(--el-color-primary);
}

.task-board-card-item__title:focus-visible {
  outline: 2px solid var(--el-color-primary);
  outline-offset: 2px;
  border-radius: 2px;
}

.task-board-card-item__meta {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 10px;
  color: rgb(100 116 139 / 94%);
  font-size: 12px;
}

.task-board-card-item__meta span {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

.task-board-card-item__progress {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 10px;
  color: rgb(71 85 105 / 96%);
  font-size: 12px;
}

.task-board-card-item__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
  margin-top: 12px;
  padding-top: 8px;
  border-top: 1px dashed rgb(226 232 240 / 92%);
}

.task-board-card-item__actions :deep(.el-button + .el-button) {
  margin-left: 0;
}

:deep(.task-action-btn) {
  padding: 3px;
  min-width: auto;
  height: auto;
  line-height: 1;
}

.task-board-column__sentinel,
.task-board-column__footer {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 0;
  color: rgb(148 163 184 / 92%);
  font-size: 12px;
}

.task-board-column__sentinel-idle {
  opacity: 0.6;
}
</style>
