<!-- SPEC: PJ2026-010102 HWPOD 工具；PJ2026-010405 云端控制台。 -->
<script setup lang="ts">
import { BaseTree } from "@he-tree/vue";
import "@he-tree/vue/style/default.css";
import { ChevronLeft, Download, File, FileCode2, Folder, Hammer, Plug, RefreshCw, Send, Square, Terminal } from "lucide-vue-next";
import { computed, nextTick, onMounted, ref } from "vue";
import ConfirmDialog from "@/components/common/ConfirmDialog.vue";
import { useHwpodStore } from "@/stores/hwpod";
import type { HwpodOperationResponse, HwpodWorkspaceEntry } from "@/types";

type WorkspaceNode = HwpodWorkspaceEntry & { children?: WorkspaceNode[]; loaded?: boolean };

const props = defineProps<{ hwpodId: string; initialPath?: string }>();
const emit = defineEmits<{ back: []; navigate: [path: string] }>();
const hwpod = useHwpodStore();
const tree = ref<WorkspaceNode[]>([]);
const selectedPath = ref(props.initialPath || ".");
const preview = ref("");
const previewMeta = ref<{ sizeBytes: number; truncated: boolean } | null>(null);
const busy = ref<string | null>(null);
const localError = ref<string | null>(null);
const downloadConfirmOpen = ref(false);
const uartPort = ref("uart1");
const uartBaudRate = ref(115200);
const uartData = ref("");
const uartOutput = ref("");
const terminalStatus = computed(() => hwpod.lastOperation?.status ?? "idle");

onMounted(async () => {
  await loadRoot();
  if (props.initialPath && props.initialPath !== ".") await openInitialPath(props.initialPath);
});

async function loadRoot(): Promise<void> {
  const output = await operationOutput("workspace.ls", { path: ".", maxEntries: 1000 });
  tree.value = workspaceEntries(output);
}

async function selectNode(stat: { data: WorkspaceNode; open?: boolean }): Promise<void> {
  const node = stat.data;
  selectedPath.value = node.path;
  emit("navigate", node.path);
  if (node.type === "dir") {
    if (!node.loaded) {
      const output = await operationOutput("workspace.ls", { path: node.path, maxEntries: 1000 });
      node.children = workspaceEntries(output);
      node.loaded = true;
      tree.value = [...tree.value];
      await nextTick();
    }
    stat.open = true;
    preview.value = "";
    previewMeta.value = null;
    return;
  }
  if (node.type !== "file") return;
  const output = await operationOutput("workspace.cat", { path: node.path, maxBytes: 262144 });
  if (output.previewable === false) throw new Error(blockerSummary(output) || "该文件不支持文本预览");
  preview.value = typeof output.content === "string" ? output.content : "";
  previewMeta.value = { sizeBytes: numberValue(output.sizeBytes), truncated: output.truncated === true };
}

async function openInitialPath(path: string): Promise<void> {
  const segments = path.replace(/\\/gu, "/").split("/").filter(Boolean);
  let nodes = tree.value;
  let current = "";
  for (let index = 0; index < segments.length; index += 1) {
    current = current ? `${current}/${segments[index]}` : segments[index];
    const node = nodes.find((entry) => entry.path === current);
    if (!node) return;
    if (index === segments.length - 1) {
      await selectNode({ data: node });
      return;
    }
    if (node.type !== "dir") return;
    if (!node.loaded) {
      const output = await operationOutput("workspace.ls", { path: node.path, maxEntries: 1000 });
      node.children = workspaceEntries(output);
      node.loaded = true;
    }
    nodes = node.children ?? [];
  }
  tree.value = [...tree.value];
}

async function runAction(intent: string, args: Record<string, unknown> = {}): Promise<void> {
  await operationOutput(intent, args);
}

async function confirmDownload(): Promise<void> {
  downloadConfirmOpen.value = false;
  await runAction("debug.download");
}

async function readUart(): Promise<void> {
  const output = await operationOutput("io.uart.read", uartArgs());
  uartOutput.value = typeof output.stdout === "string" ? output.stdout : JSON.stringify(output.response ?? output, null, 2);
}

async function writeUart(): Promise<void> {
  const data = uartData.value;
  if (!data) return;
  await operationOutput("io.uart.write", { ...uartArgs(), data });
  uartData.value = "";
}

function uartArgs(): Record<string, unknown> {
  return { port: uartPort.value, baudRate: uartBaudRate.value };
}

async function operationOutput(intent: string, args: Record<string, unknown>): Promise<Record<string, unknown>> {
  busy.value = intent;
  localError.value = null;
  try {
    const operation = await hwpod.executeOperation(props.hwpodId, intent, args, 900_000);
    const activity = record(operation.result);
    if (activity.ok === false) throw new Error(errorSummary(activity) || `${intent} 执行失败`);
    const response = record(activity.response);
    if (response.ok === false) throw new Error(errorSummary(response) || `${intent} 执行失败`);
    const result = Array.isArray(response.results) ? record(response.results[0]) : {};
    if (result.ok === false) throw new Error(errorSummary(result) || `${intent} 执行失败`);
    return record(result.output);
  } catch (error) {
    localError.value = error instanceof Error ? error.message : String(error);
    throw error;
  } finally {
    busy.value = null;
  }
}

function workspaceEntries(output: Record<string, unknown>): WorkspaceNode[] {
  return Array.isArray(output.entries) ? output.entries.map((entry) => {
    const value = record(entry);
    const type = value.type === "dir" || value.type === "file" ? value.type : "other";
    return {
      name: String(value.name ?? ""),
      path: String(value.path ?? value.name ?? ""),
      type,
      sizeBytes: value.sizeBytes == null ? null : numberValue(value.sizeBytes),
      modifiedAt: typeof value.modifiedAt === "string" ? value.modifiedAt : null,
      ...(type === "dir" ? { children: [], loaded: false } : {}),
    };
  }) : [];
}

function record(value: unknown): Record<string, unknown> { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
function numberValue(value: unknown): number { return Number.isFinite(Number(value)) ? Number(value) : 0; }
function errorSummary(value: Record<string, unknown>): string { return String(record(value.error).message ?? record(value.blocker).summary ?? value.summary ?? ""); }
function blockerSummary(value: Record<string, unknown>): string { return String(record(value.blocker).summary ?? ""); }
function operationLabel(operation: HwpodOperationResponse | null): string { return operation ? `${operation.status} · ${operation.operationId}` : "尚未执行"; }
</script>

<template>
  <section class="operations-workspace" data-testid="hwpod-operations-workspace">
    <header class="workspace-toolbar">
      <button class="icon-command" type="button" title="返回设备资源" @click="emit('back')"><ChevronLeft :size="17" /></button>
      <strong>{{ hwpodId }}</strong>
      <code>{{ selectedPath }}</code>
      <button class="icon-command" type="button" title="刷新工作区" :disabled="Boolean(busy)" @click="loadRoot"><RefreshCw :size="16" /></button>
      <span class="operation-state" :data-status="terminalStatus">{{ operationLabel(hwpod.lastOperation) }}</span>
    </header>

    <div class="workspace-columns">
      <section class="file-pane" aria-label="工作区文件">
        <h2>工作区</h2>
        <BaseTree v-model="tree" class="file-tree" :virtualization="true" :virtualization-prerender-count="30" :default-open="false" aria-label="HWPOD 工作区文件树" @click:node="selectNode">
          <template #default="{ node, stat, indentStyle }">
            <div class="tree-row" :class="{ selected: selectedPath === node.path }" :style="indentStyle">
              <Folder v-if="node.type === 'dir'" :size="15" :fill="stat.open ? 'currentColor' : 'none'" />
              <File v-else :size="15" />
              <span>{{ node.name }}</span>
            </div>
          </template>
        </BaseTree>
      </section>

      <section class="preview-pane" aria-label="文件预览">
        <h2><FileCode2 :size="16" /> 文件预览</h2>
        <div v-if="previewMeta" class="preview-meta"><span>{{ previewMeta.sizeBytes }} bytes</span><span v-if="previewMeta.truncated">预览已截断</span></div>
        <pre v-if="preview">{{ preview }}</pre>
        <div v-else class="empty-preview">选择文件查看内容</div>
      </section>

      <aside class="operation-pane" aria-label="HWPOD 操作">
        <section>
          <h2><Hammer :size="16" /> 调试</h2>
          <div class="action-row">
            <button type="button" :disabled="Boolean(busy)" @click="runAction('debug.build')"><Hammer :size="15" /> 编译</button>
            <button type="button" :disabled="Boolean(busy)" @click="downloadConfirmOpen = true"><Download :size="15" /> 下载</button>
          </div>
        </section>
        <section>
          <h2><Terminal :size="16" /> 串口</h2>
          <label>端口<input v-model="uartPort" /></label>
          <label>波特率<input v-model.number="uartBaudRate" type="number" min="1" /></label>
          <div class="icon-actions">
            <button type="button" title="打开串口" :disabled="Boolean(busy)" @click="runAction('io.uart.open', uartArgs())"><Plug :size="16" /></button>
            <button type="button" title="关闭串口" :disabled="Boolean(busy)" @click="runAction('io.uart.close', uartArgs())"><Square :size="15" /></button>
            <button type="button" title="读取串口" :disabled="Boolean(busy)" @click="readUart"><RefreshCw :size="15" /></button>
          </div>
          <div class="send-row"><input v-model="uartData" aria-label="串口发送内容" @keyup.enter="writeUart" /><button type="button" title="发送串口数据" :disabled="Boolean(busy) || !uartData" @click="writeUart"><Send :size="15" /></button></div>
          <pre class="uart-output">{{ uartOutput || "等待串口数据" }}</pre>
        </section>
        <p v-if="busy" class="operation-message">{{ busy }} · {{ terminalStatus }}</p>
        <p v-if="localError" class="operation-error" role="alert">{{ localError }}</p>
      </aside>
    </div>

    <ConfirmDialog :open="downloadConfirmOpen" title="下载固件" message="下载会写入当前连接的目标板。确认继续？" confirm-label="确认下载" :pending="busy === 'debug.download'" @close="downloadConfirmOpen = false" @confirm="confirmDownload" />
  </section>
</template>

<style scoped>
.operations-workspace { display: grid; height: 100%; min-width: 0; min-height: 0; grid-template-rows: auto minmax(0, 1fr); border: 1px solid var(--console-border-strong); background: var(--console-surface-raised); }
.workspace-toolbar { display: grid; min-width: 0; grid-template-columns: auto auto minmax(80px, 1fr) auto minmax(160px, auto); align-items: center; gap: 8px; min-height: 42px; padding: 5px 8px; border-bottom: 1px solid var(--console-border-strong); }
.workspace-toolbar strong, .workspace-toolbar code, .operation-state { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.workspace-toolbar code, .operation-state { color: var(--console-graphite-600); font-size: 11px; }
.icon-command, .icon-actions button, .send-row button { display: inline-grid; width: 31px; height: 31px; place-items: center; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: #fff; color: var(--console-graphite-800); cursor: pointer; }
.workspace-columns { display: grid; min-width: 0; min-height: 0; grid-template-columns: minmax(210px, .85fr) minmax(300px, 1.5fr) minmax(240px, .9fr); }
.file-pane, .preview-pane, .operation-pane { min-width: 0; min-height: 0; padding: 10px; overflow: auto; }
.file-pane, .preview-pane { border-right: 1px solid var(--console-border-strong); }
h2 { display: flex; align-items: center; gap: 6px; margin: 0 0 9px; color: var(--console-graphite-700); font-size: 12px; }
.file-tree { height: calc(100% - 28px); min-height: 280px; overflow: auto; }
.tree-row { display: flex; min-width: 0; align-items: center; gap: 6px; min-height: 30px; padding-right: 6px; color: var(--console-graphite-700); cursor: pointer; }
.tree-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tree-row.selected { background: var(--console-cyan-100); color: var(--console-cyan-700); }
.preview-pane pre, .uart-output { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--console-font-mono); font-size: 11px; line-height: 1.55; }
.preview-meta { display: flex; gap: 12px; margin-bottom: 8px; color: var(--console-graphite-500); font-size: 10px; }
.empty-preview { display: grid; min-height: 180px; place-items: center; color: var(--console-graphite-500); font-size: 12px; }
.operation-pane { display: grid; align-content: start; gap: 18px; background: var(--console-surface-muted); }
.operation-pane section { display: grid; gap: 8px; }
.operation-pane label { display: grid; gap: 4px; color: var(--console-graphite-600); font-size: 10px; font-weight: 700; }
.operation-pane input { min-width: 0; height: 32px; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: #fff; padding: 0 8px; }
.action-row, .icon-actions, .send-row { display: flex; gap: 6px; }
.action-row button { display: inline-flex; min-height: 32px; align-items: center; gap: 6px; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: #fff; padding: 0 10px; color: var(--console-graphite-800); font-weight: 700; cursor: pointer; }
.send-row input { flex: 1; }
button:disabled { opacity: .45; cursor: wait; }
.uart-output { max-height: 190px; min-height: 88px; padding: 8px; overflow: auto; border: 1px solid var(--console-border-strong); background: #171c1a; color: #d8eee5; }
.operation-message { color: var(--console-cyan-700); font-size: 11px; }
.operation-error { margin: 0; padding: 8px; border-left: 3px solid var(--console-red-700); background: var(--console-red-100); color: var(--console-red-700); font-size: 11px; }
@media (max-width: 980px) { .workspace-columns { grid-template-columns: minmax(190px, .8fr) minmax(280px, 1.2fr); } .operation-pane { grid-column: 1 / -1; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--console-border-strong); } }
@media (max-width: 680px) { .workspace-toolbar { grid-template-columns: auto minmax(0, 1fr) auto; } .workspace-toolbar code, .operation-state { grid-column: 1 / -1; } .workspace-columns { display: block; overflow: auto; } .file-pane, .preview-pane { min-height: 300px; border-right: 0; border-bottom: 1px solid var(--console-border-strong); } .operation-pane { display: grid; grid-template-columns: 1fr; } }
</style>
