<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { api } from '../../api/client';
import { usePlanningStore } from '../../stores/planning';
import type { SpecNode } from '../../stores/planning';
import { useTaskStore } from '../../stores/tasks';
import { useUiStore } from '../../stores/ui';

const planning = usePlanningStore();
const taskStore = useTaskStore();
const ui = useUiStore();
const {
  tree, treeProgress, treeIndex, treeLoading,
  focusedSpecPath, focusedIsIndex, focusedTaskId,
} = storeToRefs(planning);

// ── Persisted UI state ─────────────────────────────────────────────
const STATUS_KEY = 'wallfacer-spec-filter';
const EXPANDED_KEY = 'wallfacer-spec-expanded';
const ARCHIVED_KEY = 'wallfacer-spec-show-archived';

function readSet(key: string): Set<string> {
  try {
    return new Set<string>(JSON.parse(localStorage.getItem(key) ?? '[]'));
  } catch {
    return new Set<string>();
  }
}

const expandedPaths = ref<Set<string>>(readSet(EXPANDED_KEY));
const statusFilter = ref<string>(localStorage.getItem(STATUS_KEY) ?? 'all');
const showArchived = ref<boolean>(localStorage.getItem(ARCHIVED_KEY) === 'true');
const textFilter = ref<string>('');
const selectedPaths = ref<Set<string>>(new Set());
const lastCheckedIndex = ref<number>(-1);

// ── Task Prompts virtual section ──────────────────────────────────

const TASK_PROMPTS_EXPANDED_KEY = 'wallfacer-task-prompts-expanded';
const TASK_PROMPTS_WAITING_KEY = 'wallfacer-task-prompts-waiting';

interface TaskPromptEntry {
  task_id: string;
  title: string;
  status: string;
  updated_at: string;
}

const taskPrompts = ref<TaskPromptEntry[]>([]);
const taskPromptsExpanded = ref<boolean>(
  localStorage.getItem(TASK_PROMPTS_EXPANDED_KEY) !== '0',
);
const taskPromptsIncludeWaiting = ref<boolean>(
  localStorage.getItem(TASK_PROMPTS_WAITING_KEY) === '1',
);

async function loadTaskPrompts() {
  const url =
    '/api/explorer/task-prompts' +
    (taskPromptsIncludeWaiting.value ? '?status=backlog,waiting' : '');
  try {
    const data = await api<TaskPromptEntry[]>('GET', url);
    taskPrompts.value = Array.isArray(data) ? data : [];
  } catch {
    taskPrompts.value = [];
  }
}

function toggleTaskPromptsExpanded() {
  taskPromptsExpanded.value = !taskPromptsExpanded.value;
  localStorage.setItem(
    TASK_PROMPTS_EXPANDED_KEY,
    taskPromptsExpanded.value ? '1' : '0',
  );
}

function toggleTaskPromptsWaiting(ev: Event) {
  ev.stopPropagation();
  taskPromptsIncludeWaiting.value = !taskPromptsIncludeWaiting.value;
  localStorage.setItem(
    TASK_PROMPTS_WAITING_KEY,
    taskPromptsIncludeWaiting.value ? '1' : '0',
  );
  void loadTaskPrompts();
}

async function openTaskPrompt(entry: TaskPromptEntry) {
  // Pull the freshest prompt from the in-memory tasks store if available.
  const cached = taskStore.tasks.find(t => t.id === entry.task_id);
  const prompt = cached?.prompt ?? entry.title;
  await planning.openPlanForTask(entry.task_id, entry.title, prompt);
}

function persistExpanded() {
  localStorage.setItem(EXPANDED_KEY, JSON.stringify([...expandedPaths.value]));
}

const STATUS_ICONS: Record<string, string> = {
  complete: '✅',
  validated: '✔',
  drafted: '📝',
  vague: '💭',
  stale: '⚠️',
  archived: '📦',
};

// ── Filtering ──────────────────────────────────────────────────────

function nodeMatches(node: SpecNode, byPath: Map<string, SpecNode>): boolean {
  const spec = node.spec;
  if (!spec) return false;
  if (spec.status === 'archived' && !showArchived.value) return false;

  let statusOk = true;
  if (statusFilter.value !== 'all') {
    statusOk =
      statusFilter.value === 'incomplete'
        ? spec.status !== 'complete' && spec.status !== 'archived'
        : spec.status === statusFilter.value;
  }

  let textOk = true;
  if (textFilter.value) {
    const q = textFilter.value.toLowerCase();
    textOk = (spec.title ?? '').toLowerCase().includes(q) || node.path.toLowerCase().includes(q);
  }

  const selfOk = statusOk && textOk;
  if (node.is_leaf) return selfOk;
  if (selfOk) return true;
  for (const childPath of node.children ?? []) {
    const child = byPath.get(childPath);
    if (child && nodeMatches(child, byPath)) return true;
  }
  return false;
}

interface RenderedNode {
  node: SpecNode;
  depth: number;
  hasChildren: boolean;
  expanded: boolean;
}

interface RenderedTrack {
  name: string;
  expanded: boolean;
  nodes: RenderedNode[];
}

const renderedTracks = computed<RenderedTrack[]>(() => {
  const byPath = new Map<string, SpecNode>();
  for (const n of tree.value) byPath.set(n.path, n);

  // Group root nodes by track.
  const trackOrder: string[] = [];
  const groups: Record<string, SpecNode[]> = {};
  for (const node of tree.value) {
    if (node.depth !== 0) continue;
    const track = node.spec?.track ?? 'other';
    if (!groups[track]) {
      groups[track] = [];
      trackOrder.push(track);
    }
    groups[track].push(node);
  }

  const out: RenderedTrack[] = [];
  for (const track of trackOrder) {
    const trackKey = '__track__' + track;
    const expanded = expandedPaths.value.has(trackKey) || !!textFilter.value;
    const nodes: RenderedNode[] = [];

    function walk(node: SpecNode, depth: number) {
      if (!nodeMatches(node, byPath)) return;
      const hasChildren = (node.children?.length ?? 0) > 0;
      const isExpanded = expandedPaths.value.has(node.path) || !!textFilter.value;
      nodes.push({ node, depth, hasChildren, expanded: isExpanded });
      if (hasChildren && isExpanded) {
        for (const childPath of node.children ?? []) {
          const child = byPath.get(childPath);
          if (child) walk(child, depth + 1);
        }
      }
    }

    if (expanded) {
      for (const root of groups[track]) walk(root, 0);
    }

    out.push({ name: track, expanded, nodes });
  }
  return out;
});

// ── Interactions ───────────────────────────────────────────────────

function toggleTrack(name: string) {
  const key = '__track__' + name;
  if (expandedPaths.value.has(key)) expandedPaths.value.delete(key);
  else expandedPaths.value.add(key);
  expandedPaths.value = new Set(expandedPaths.value);
  persistExpanded();
}

function toggleNode(path: string) {
  if (expandedPaths.value.has(path)) expandedPaths.value.delete(path);
  else expandedPaths.value.add(path);
  expandedPaths.value = new Set(expandedPaths.value);
  persistExpanded();
}

function selectNode(node: SpecNode) {
  planning.focusSpec(node.path);
}

function selectIndex() {
  if (treeIndex.value) planning.focusIndex();
}

function setStatusFilter(v: string) {
  statusFilter.value = v;
  localStorage.setItem(STATUS_KEY, v);
}

function toggleShowArchived() {
  showArchived.value = !showArchived.value;
  localStorage.setItem(ARCHIVED_KEY, String(showArchived.value));
  if (showArchived.value) {
    // Force-collapse archived parents so they don't flood the view.
    for (const n of tree.value) {
      if (n.spec?.status === 'archived' && expandedPaths.value.has(n.path)) {
        expandedPaths.value.delete(n.path);
      }
    }
    expandedPaths.value = new Set(expandedPaths.value);
    persistExpanded();
  }
  if (!showArchived.value && statusFilter.value === 'archived') setStatusFilter('all');
}

// ── Multi-select dispatch ──────────────────────────────────────────

function unmetDeps(node: SpecNode): string[] {
  const deps = node.spec?.depends_on ?? [];
  if (deps.length === 0) return [];
  const byPath = new Map<string, SpecNode>();
  for (const n of tree.value) byPath.set(n.path, n);
  const out: string[] = [];
  for (const dp of deps) {
    const dn = byPath.get(dp);
    if (!dn || dn.spec?.status !== 'complete') {
      out.push(dn?.spec?.title ?? dp);
    }
  }
  return out;
}

function isCheckable(node: SpecNode): boolean {
  return node.spec?.status === 'validated';
}

const flatLeafIndex = computed(() => {
  const list: string[] = [];
  for (const t of renderedTracks.value) for (const n of t.nodes) list.push(n.node.path);
  return list;
});

function onCheckboxChange(ev: Event, node: SpecNode) {
  const target = ev.target as HTMLInputElement;
  const idx = flatLeafIndex.value.indexOf(node.path);
  if ((ev as MouseEvent).shiftKey && lastCheckedIndex.value >= 0 && idx >= 0) {
    const start = Math.min(lastCheckedIndex.value, idx);
    const end = Math.max(lastCheckedIndex.value, idx);
    for (let i = start; i <= end; i++) {
      const path = flatLeafIndex.value[i];
      if (target.checked) selectedPaths.value.add(path);
      else selectedPaths.value.delete(path);
    }
  }
  if (target.checked) selectedPaths.value.add(node.path);
  else selectedPaths.value.delete(node.path);
  selectedPaths.value = new Set(selectedPaths.value);
  lastCheckedIndex.value = idx;
}

const dispatchPending = ref(false);

interface DispatchResp {
  dispatched?: { spec_path: string; task_id: string }[];
  errors?: { spec_path: string; error: string }[];
}

async function dispatchSelected() {
  const paths = [...selectedPaths.value];
  if (paths.length === 0) return;
  if (!confirm(`Dispatch ${paths.length} specs to the task board?`)) return;
  dispatchPending.value = true;
  try {
    const resp = await api<DispatchResp>('POST', '/api/specs/transition', { action: 'dispatch', paths, run: false });
    selectedPaths.value = new Set();
    // Pulse the freshly-created cards when the board next renders them.
    ui.markDispatched((resp.dispatched ?? []).map((d) => d.task_id).filter(Boolean));
    if (resp.errors && resp.errors.length > 0) {
      const lines = resp.errors.map(e => `${e.spec_path}: ${e.error}`).join('\n');
      const dispatched = resp.dispatched?.length ?? 0;
      alert(
        dispatched > 0
          ? `Dispatched ${dispatched}. ${resp.errors.length} failed:\n${lines}`
          : `Dispatch failed:\n${lines}`,
      );
    }
  } catch (e) {
    alert('Dispatch failed: ' + (e instanceof Error ? e.message : String(e)));
  } finally {
    dispatchPending.value = false;
  }
}

// ── Lifecycle ──────────────────────────────────────────────────────
// The spec-tree fetch and SSE subscription are owned by the parent
// PlanPage so they run regardless of which layout (chat-first vs
// three-pane) is currently mounted. This panel only refreshes the
// Task Prompts side-list which lives in its own DOM.

onMounted(() => {
  void loadTaskPrompts();
});

// Keep the Task Prompts list fresh against the SSE-synced task store: reload
// whenever a task is added/removed OR changes status/title (a backlog task
// starting moves it out of the list). Watching only .length missed those
// transitions. Mirrors the legacy spec-explorer SSE subscription.
const taskPromptsSignal = computed(() =>
  taskStore.tasks.map((t) => `${t.id}:${t.status}:${t.title ? 1 : 0}`).join(','),
);
watch(taskPromptsSignal, () => { void loadTaskPrompts(); });

onUnmounted(() => {
  selectedPaths.value = new Set();
});
</script>

<template>
  <aside class="spec-tree-panel">
    <div class="stp-toolbar">
      <input
        v-model="textFilter"
        class="stp-search"
        type="search"
        placeholder="Filter specs…"
      />
      <select :value="statusFilter" class="stp-status" @change="setStatusFilter(($event.target as HTMLSelectElement).value)">
        <option value="all">All</option>
        <option value="incomplete">Incomplete</option>
        <option value="vague">Vague</option>
        <option value="drafted">Drafted</option>
        <option value="validated">Validated</option>
        <option value="complete">Complete</option>
        <option value="stale">Stale</option>
        <option value="archived" :disabled="!showArchived">Archived</option>
      </select>
      <label class="stp-archived-toggle">
        <input type="checkbox" :checked="showArchived" @change="toggleShowArchived" />
        Show archived
      </label>
    </div>

    <div class="stp-body">
      <div class="stp-task-prompts">
        <div
          class="stp-task-prompts-header"
          role="button"
          tabindex="0"
          :aria-expanded="taskPromptsExpanded"
          @click="toggleTaskPromptsExpanded"
          @keydown.enter.prevent="toggleTaskPromptsExpanded"
          @keydown.space.prevent="toggleTaskPromptsExpanded"
        >
          <span class="stp-chev" :class="{ open: taskPromptsExpanded }">▸</span>
          <span class="stp-task-prompts-label">Task Prompts</span>
          <button
            type="button"
            class="stp-task-prompts-waiting"
            :class="{ active: taskPromptsIncludeWaiting }"
            :title="taskPromptsIncludeWaiting ? 'Hide waiting tasks' : 'Show waiting tasks'"
            :aria-pressed="taskPromptsIncludeWaiting"
            @click="toggleTaskPromptsWaiting"
          >{{ taskPromptsIncludeWaiting ? 'W' : 'w' }}</button>
        </div>
        <template v-if="taskPromptsExpanded">
          <div v-if="taskPrompts.length === 0" class="stp-task-prompts-empty">
            No tasks
          </div>
          <button
            v-for="t in taskPrompts"
            :key="t.task_id"
            type="button"
            class="stp-task-prompt-item"
            :class="{ 'stp-task-prompt-item--focused': focusedTaskId === t.task_id }"
            :title="t.title"
            @click="openTaskPrompt(t)"
          >
            <span class="stp-task-prompt-status" :class="'stp-task-prompt-status--' + t.status" />
            <span class="stp-task-prompt-title">{{ t.title }}</span>
          </button>
        </template>
      </div>

      <div v-if="treeLoading" class="stp-empty">Loading specs…</div>
      <template v-else>
        <div
          v-if="treeIndex"
          class="stp-pinned"
          :class="{ 'stp-pinned--focused': focusedIsIndex }"
          role="button"
          tabindex="0"
          @click="selectIndex"
          @keydown.enter.prevent="selectIndex"
          @keydown.space.prevent="selectIndex"
        >
          <span class="stp-pinned-icon">📋</span> Roadmap
        </div>

        <div v-for="track in renderedTracks" :key="track.name" class="stp-track">
          <div class="stp-track-header" @click="toggleTrack(track.name)">
            <span class="stp-chev" :class="{ open: track.expanded }">▸</span>
            <span class="stp-track-name">{{ track.name }}</span>
          </div>
          <template v-if="track.expanded">
            <div
              v-for="rn in track.nodes"
              :key="rn.node.path"
              class="stp-node"
              :class="{
                'stp-node--leaf': rn.node.is_leaf,
                'stp-node--archived': rn.node.spec?.status === 'archived',
                'stp-node--focused': focusedSpecPath === rn.node.path && !focusedIsIndex,
              }"
              :style="{ paddingLeft: 8 + rn.depth * 14 + 'px' }"
              @click="selectNode(rn.node)"
            >
              <span
                v-if="rn.hasChildren"
                class="stp-chev"
                :class="{ open: rn.expanded }"
                @click.stop="toggleNode(rn.node.path)"
              >▸</span>
              <span v-else class="stp-chev-spacer" />
              <input
                v-if="isCheckable(rn.node)"
                type="checkbox"
                class="stp-checkbox"
                :checked="selectedPaths.has(rn.node.path)"
                :disabled="unmetDeps(rn.node).length > 0"
                :title="unmetDeps(rn.node).length > 0 ? 'Blocked by: ' + unmetDeps(rn.node).join(', ') : ''"
                @click.stop
                @change="onCheckboxChange($event, rn.node)"
              />
              <span class="stp-icon">{{ STATUS_ICONS[rn.node.spec?.status ?? ''] ?? '' }}</span>
              <span class="stp-title">{{ rn.node.spec?.title || rn.node.path }}</span>
              <span
                v-if="!rn.node.is_leaf && treeProgress[rn.node.path]"
                class="stp-progress"
              >{{ treeProgress[rn.node.path].Complete }}/{{ treeProgress[rn.node.path].Total }}</span>
            </div>
          </template>
        </div>

        <div v-if="renderedTracks.length === 0" class="stp-empty">No specs match the filter.</div>
      </template>
    </div>

    <div v-if="selectedPaths.size > 0" class="stp-dispatch-bar">
      <button
        type="button"
        class="stp-dispatch-btn"
        :disabled="dispatchPending"
        @click="dispatchSelected"
      >
        {{ dispatchPending ? 'Dispatching…' : `Dispatch Selected (${selectedPaths.size})` }}
      </button>
    </div>
  </aside>
</template>

<style scoped>
.spec-tree-panel {
  width: 280px;
  min-width: 240px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--rule);
  background: var(--bg-card);
  overflow: hidden;
}

.stp-toolbar {
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  border-bottom: 1px solid var(--rule);
}

.stp-search,
.stp-status {
  font-size: 12px;
  padding: 5px 8px;
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--bg);
  color: var(--ink);
}

.stp-search:focus,
.stp-status:focus {
  outline: none;
  border-color: var(--accent);
}

.stp-archived-toggle {
  font-size: 11px;
  color: var(--ink-3);
  display: flex;
  align-items: center;
  gap: 5px;
  cursor: pointer;
}

.stp-body {
  flex: 1;
  overflow-y: auto;
  font-size: 12px;
}

.stp-empty {
  padding: 16px 12px;
  color: var(--ink-4);
  text-align: center;
  font-size: 12px;
}

.stp-pinned {
  padding: 8px 12px;
  font-weight: 500;
  cursor: pointer;
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: center;
  gap: 6px;
}

.stp-pinned:hover {
  background: var(--bg-hover);
}

.stp-pinned--focused {
  background: var(--bg-active);
}

.stp-pinned-icon {
  font-size: 13px;
}

.stp-track-header {
  padding: 6px 10px;
  cursor: pointer;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 10px;
  color: var(--ink-3);
  display: flex;
  align-items: center;
  gap: 6px;
}

.stp-track-header:hover {
  background: var(--bg-hover);
}

.stp-track-name {
  flex: 1;
}

.stp-node {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  cursor: pointer;
  line-height: 1.4;
}

.stp-node:hover {
  background: var(--bg-hover);
}

.stp-node--focused {
  background: var(--bg-active);
}

.stp-node--archived {
  opacity: 0.55;
}

.stp-chev,
.stp-chev-spacer {
  display: inline-block;
  width: 14px;
  text-align: center;
  font-size: 10px;
  color: var(--ink-4);
  flex-shrink: 0;
  transition: transform 0.15s;
}

.stp-chev.open {
  transform: rotate(90deg);
}

.stp-checkbox {
  margin-right: 2px;
  flex-shrink: 0;
}

.stp-icon {
  flex-shrink: 0;
  font-size: 11px;
  width: 14px;
  text-align: center;
}

.stp-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stp-progress {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ink-3);
}

.stp-task-prompts {
  border-bottom: 1px solid var(--rule);
  padding-bottom: 4px;
}

.stp-task-prompts-header {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  cursor: pointer;
  user-select: none;
}

.stp-task-prompts-header:hover {
  background: var(--bg-hover);
}

.stp-task-prompts-label {
  flex: 1;
}

.stp-task-prompts-waiting {
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 2px;
  font-size: 9px;
  font-weight: 700;
  width: 18px;
  height: 16px;
  cursor: pointer;
  color: var(--ink-3);
  padding: 0;
  font-family: var(--font-mono);
}

.stp-task-prompts-waiting:hover { color: var(--ink); }

.stp-task-prompts-waiting.active {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}

.stp-task-prompts-empty {
  padding: 6px 14px;
  color: var(--ink-4);
  font-size: 11px;
  font-style: italic;
}

.stp-task-prompt-item {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 4px 10px 4px 26px;
  font-size: 12px;
  background: transparent;
  border: none;
  color: var(--ink-2);
  cursor: pointer;
  text-align: left;
  font-family: inherit;
}

.stp-task-prompt-item:hover { background: var(--bg-hover); }

.stp-task-prompt-item--focused { background: var(--bg-active); color: var(--ink); }

.stp-task-prompt-status {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ink-4);
  flex-shrink: 0;
}

.stp-task-prompt-status--backlog { background: #8e8a80; }
.stp-task-prompt-status--waiting { background: #c87b1c; }

.stp-task-prompt-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stp-dispatch-bar {
  padding: 8px;
  border-top: 1px solid var(--rule);
}

.stp-dispatch-btn {
  width: 100%;
  padding: 6px 10px;
  font-size: 12px;
  font-weight: 500;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--r-sm);
  cursor: pointer;
}

.stp-dispatch-btn:disabled {
  opacity: 0.5;
  cursor: default;
}
</style>
