<!-- SPEC: MDTODO 三栏任务工作区. -->
<!-- Responsibility: 高密度、可键盘导航的层级任务索引。 -->

<script setup lang="ts">
import { ChevronRight, Search, UnfoldVertical } from "lucide-vue-next";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import type { MdtodoTaskPage, MdtodoTaskRecord } from "@/api";
import RefreshBoundary from "@/components/common/RefreshBoundary.vue";
import StatusBadge from "@/components/common/StatusBadge.vue";
import SectionFrame from "@/components/layout/SectionFrame.vue";
import {
  taskParentRefs,
  treeKeyAction,
  visibleTasks,
} from "@/composables/mdtodo/mdtodoTaskTreeModel";
import { statusLabel } from "@/composables/mdtodo/useMdtodoDisplay";
import type { MdtodoTaskStatusFilter } from "@/composables/mdtodo/useMdtodoViewState";

const props = defineProps<{
  tasks: MdtodoTaskRecord[];
  selectedTaskRef: string | null;
  collapsedTaskRefs: Set<string>;
  search: string;
  status: MdtodoTaskStatusFilter;
  page: MdtodoTaskPage | null;
  loading: boolean;
  fileScopedTaskCount: number;
}>();

const emit = defineEmits<{
  select: [taskRef: string];
  "update:collapsedTaskRefs": [value: Set<string>];
  updateSearch: [value: string];
  updateStatus: [value: MdtodoTaskStatusFilter];
  loadMore: [];
}>();

const rows = ref<HTMLElement[]>([]);
const tree = ref<HTMLElement | null>(null);
const viewportTaskRefs = ref<Set<string>>(new Set());
const firstViewportTaskRef = ref<string | null>(null);
let viewportObserver: ResizeObserver | null = null;
const items = computed(() =>
  visibleTasks(props.tasks, props.collapsedTaskRefs, props.selectedTaskRef),
);
const tabStopTaskRef = computed(() =>
  props.selectedTaskRef && viewportTaskRefs.value.has(props.selectedTaskRef)
    ? props.selectedTaskRef
    : firstViewportTaskRef.value || items.value[0]?.taskRef || null,
);

function updateViewportTaskRefs(): void {
  if (!tree.value) return;
  const viewport = tree.value.getBoundingClientRect();
  const refs = new Set<string>();
  let first: string | null = null;
  rows.value.forEach((row, index) => {
    const task = items.value[index];
    if (!row || !task) return;
    const bounds = row.getBoundingClientRect();
    if (bounds.bottom <= viewport.top || bounds.top >= viewport.bottom) return;
    refs.add(task.taskRef);
    first ||= task.taskRef;
  });
  viewportTaskRefs.value = refs;
  firstViewportTaskRef.value = first;
}

onMounted(() => {
  void nextTick(updateViewportTaskRefs);
  if (tree.value && typeof ResizeObserver !== "undefined") {
    viewportObserver = new ResizeObserver(updateViewportTaskRefs);
    viewportObserver.observe(tree.value);
  } else if (typeof window !== "undefined") {
    window.addEventListener("resize", updateViewportTaskRefs, { passive: true });
  }
});

onBeforeUnmount(() => {
  viewportObserver?.disconnect();
  if (typeof window !== "undefined") window.removeEventListener("resize", updateViewportTaskRefs);
});

watch(items, async () => {
  rows.value = [];
  await nextTick();
  updateViewportTaskRefs();
});

function depth(task: MdtodoTaskRecord): number {
  return task.depth ?? taskParentRefs(task, props.tasks).length;
}

function children(task: MdtodoTaskRecord): MdtodoTaskRecord[] {
  return props.tasks.filter((item) => item.parentTaskRef === task.taskRef);
}

function updateCollapse(task: MdtodoTaskRecord, collapse: boolean): void {
  const next = new Set(props.collapsedTaskRefs);
  collapse ? next.add(task.taskRef) : next.delete(task.taskRef);
  emit("update:collapsedTaskRefs", next);
}

function expandAll(): void {
  emit("update:collapsedTaskRefs", new Set());
}

function focus(index: number): void {
  void nextTick(() =>
    rows.value[Math.max(0, Math.min(index, rows.value.length - 1))]?.focus(),
  );
}

function keydown(
  event: KeyboardEvent,
  task: MdtodoTaskRecord,
  index: number,
): void {
  const hasChildren = children(task).length > 0;
  const expanded = !props.collapsedTaskRefs.has(task.taskRef);
  const action = treeKeyAction(event.key, expanded, hasChildren);
  if (action === "none") return;
  event.preventDefault();
  if (action === "previous") focus(index - 1);
  if (action === "next") focus(index + 1);
  if (action === "expand") updateCollapse(task, false);
  if (action === "collapse") updateCollapse(task, true);
  if (action === "parent") {
    const parentRef = taskParentRefs(task, props.tasks).at(-1);
    const parentIndex = items.value.findIndex(
      (item) => item.taskRef === parentRef,
    );
    if (parentIndex >= 0) focus(parentIndex);
  }
}
</script>

<template>
  <SectionFrame
    class="task-index"
    title="任务树"
    :description="`${items.length} 可见 / ${fileScopedTaskCount} 文件任务`"
    :body-padding="false"
  >
    <template #actions>
      <button
        class="icon-button"
        type="button"
        title="展开全部任务"
        aria-label="展开全部任务"
        :disabled="collapsedTaskRefs.size === 0"
        @click="expandAll"
      >
        <UnfoldVertical :size="15" aria-hidden="true" />
      </button>
    </template>

    <div class="task-index-tools">
      <label class="task-search">
        <Search :size="15" aria-hidden="true" />
        <span class="sr-only">搜索任务</span>
        <input
          data-testid="mdtodo-task-search"
          type="search"
          :value="search"
          placeholder="搜索 Rxx 或标题"
          @input="emit('updateSearch', ($event.target as HTMLInputElement).value)"
        />
      </label>
      <select
        data-testid="mdtodo-task-status-filter"
        aria-label="任务状态"
        :value="status"
        @change="emit('updateStatus', ($event.target as HTMLSelectElement).value as MdtodoTaskStatusFilter)"
      >
        <option value="all">全部状态</option>
        <option value="open">待办</option>
        <option value="in_progress">进行中</option>
        <option value="completed">完成</option>
      </select>
    </div>

    <RefreshBoundary
      class="task-index-boundary"
      :loading="loading"
      mode="persistent"
      :has-content="items.length > 0"
      label="读取任务树"
      compact
    >
      <ol
        ref="tree"
        class="task-tree"
        role="tree"
        aria-label="MDTODO 任务树"
        data-testid="mdtodo-task-tree"
        @scroll.passive="updateViewportTaskRefs"
      >
        <li
          v-for="(task, index) in items"
          :key="task.taskRef"
          role="none"
          :style="{ '--task-depth': depth(task) }"
        >
          <button
            :ref="(element) => { if (element) rows[index] = element as HTMLElement; }"
            class="task-row"
            type="button"
            role="treeitem"
            :tabindex="task.taskRef === tabStopTaskRef ? 0 : -1"
            :aria-current="task.taskRef === selectedTaskRef ? 'true' : undefined"
            :aria-selected="task.taskRef === selectedTaskRef"
            :aria-expanded="children(task).length ? !collapsedTaskRefs.has(task.taskRef) : undefined"
            :data-testid="`mdtodo-task-${task.taskId || task.rxxId}`"
            :data-task-id="task.taskId || task.rxxId"
            :data-task-ref="task.taskRef"
            @click="emit('select', task.taskRef)"
            @keydown="keydown($event, task, index)"
          >
            <span class="tree-branch" aria-hidden="true">
              <ChevronRight
                v-if="children(task).length"
                :size="14"
                :data-open="!collapsedTaskRefs.has(task.taskRef)"
              />
            </span>
            <code>{{ task.taskId || task.rxxId || "R?" }}</code>
            <span class="task-copy">
              <strong>{{ task.title || "未命名任务" }}</strong>
              <small>{{ task.linkCount || 0 }} 报告 · L{{ task.lineNumber || "-" }}</small>
            </span>
            <StatusBadge :status="task.status" :label="statusLabel(task.status)" />
          </button>
        </li>
      </ol>
      <template #empty>
        <div class="task-empty">
          {{ loading ? "读取任务树…" : "当前筛选没有任务。" }}
        </div>
      </template>
    </RefreshBoundary>

    <button
      v-if="page?.hasMore"
      class="load-more"
      type="button"
      :disabled="loading"
      data-testid="mdtodo-load-more"
      @click="emit('loadMore')"
    >
      {{ loading ? "读取中…" : "加载更多任务" }}
    </button>
  </SectionFrame>
</template>

<style scoped>
.task-index {
  min-width: 0;
  min-height: 0;
  height: 100%;
  border: 0;
  border-right: 1px solid var(--console-border, #b9c8cf);
  border-radius: 0;
}
.task-index :deep(.console-section-body) {
  display: grid;
  min-height: 0;
  grid-template-rows: auto minmax(0, 1fr) auto;
}
.task-index-tools {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 112px;
  gap: 6px;
  border-bottom: 1px solid var(--console-border, #c6d2d8);
  padding: 8px;
  background: var(--console-surface-subtle, #f4f7f8);
}
.task-search {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 6px;
  border: 1px solid var(--console-border-strong, #a8bbc4);
  background: var(--console-surface, #fff);
  padding: 0 8px;
}
.task-search input,
.task-index-tools select {
  min-width: 0;
  height: 30px;
  border: 0;
  background: transparent;
  color: var(--console-text, #172830);
}
.task-index-tools select {
  border: 1px solid var(--console-border-strong, #a8bbc4);
  background: var(--console-surface, #fff);
}
.task-index-boundary,
.task-tree {
  min-height: 0;
}
.task-index-boundary {
  overflow: hidden;
}
.task-tree {
  height: 100%;
  overflow: auto;
  margin: 0;
  padding: 4px 0;
  list-style: none;
}
.task-row {
  display: grid;
  width: 100%;
  min-height: 48px;
  grid-template-columns: 16px 46px minmax(0, 1fr) auto;
  align-items: center;
  gap: 6px;
  border: 0;
  border-left: 3px solid transparent;
  border-bottom: 1px solid var(--console-border-soft, #d8e0e4);
  background: transparent;
  padding: 5px 8px 5px calc(8px + var(--task-depth) * 14px);
  color: var(--console-text, #172830);
  text-align: left;
}
.task-row:hover {
  background: var(--console-surface-hover, #eef5f7);
}
.task-row[aria-current="true"] {
  border-left-color: var(--console-accent, #1f718c);
  background: var(--console-selection, #deedf2);
}
.tree-branch {
  display: grid;
  place-items: center;
  color: var(--console-text-muted, #607680);
}
.tree-branch svg[data-open="true"] {
  transform: rotate(90deg);
}
.task-row code {
  color: var(--console-accent, #1f718c);
  font-size: 11px;
  font-weight: 850;
}
.task-copy {
  display: grid;
  min-width: 0;
  gap: 2px;
}
.task-copy strong,
.task-copy small {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.task-copy strong {
  font-size: 12px;
}
.task-copy small {
  color: var(--console-text-muted, #607680);
  font: 10px ui-monospace, monospace;
}
.task-empty {
  padding: 24px 14px;
  color: var(--console-text-muted, #607680);
  text-align: center;
}
.load-more {
  min-height: 34px;
  border: 0;
  border-top: 1px solid var(--console-border, #c6d2d8);
  background: var(--console-surface-subtle, #f4f7f8);
  color: var(--console-accent, #1f718c);
  font-weight: 750;
}
@media (max-width: 960px) {
  .task-index-tools {
    grid-template-columns: 1fr;
  }
}
</style>
