/*
 * SPEC: PJ2026-010405 云端控制台；PJ2026-010103 HWPOD 服务。
 * Implementation reference: draft-2026-07-13-p0-cloud-console。
 * Responsibility: 由 Cloud API HWPOD domain 生成有界 typed Node/设备 topology，不让浏览器拼装业务真相。
 */

export const HWPOD_TOPOLOGY_CONTRACT_VERSION = "hwpod-topology-v1";

const DEFAULT_PAGE_LIMIT = 50;
const MAX_PAGE_LIMIT = 100;

export const HWPOD_NODE_READINESS_DEFINITIONS = [
  { id: "downloaded", label: "已下载", authority: "user-device", nextAction: { label: "从节点接入页下载并校验已发布 Python 单文件", entrypoint: "cloud-web-hwpod-onboarding" } },
  { id: "installed", label: "已安装", authority: "python-node-runtime", nextAction: { label: "按 owning YAML 选择的交互用户与原生 Python 启动节点", entrypoint: "yaml-managed-hwpod-node" } },
  { id: "desktop-visible", label: "桌面可见", authority: "python-node-runtime", nextAction: { label: "在目标交互登录会话确认窗口或托盘可见", entrypoint: "hwlab-node-ui" } },
  { id: "registered", label: "已注册", authority: "websocket-registry", nextAction: { label: "保持节点运行并通过受控节点状态入口检查注册", entrypoint: "hwpod-node-status" } },
  { id: "workspace-ready", label: "工作区就绪", authority: "hwpod-spec-probe", nextAction: { label: "通过显式 readiness 探测检查 owning workspace", entrypoint: "hwpod-readiness-probe" } },
  { id: "hwpod-ready", label: "HWPOD 就绪", authority: "hwpod-topology", nextAction: { label: "检查挂载 HWPOD 的 blocker、能力与占用状态", entrypoint: "hwpod-topology" } }
] as const;

export function buildHwpodTopologyReadModel(specs: any[], registrySnapshot: any = {}, options: any = {}) {
  const observedAt = text(options.observedAt) || new Date().toISOString();
  const onlineNodes = normalizeOnlineNodes(registrySnapshot);
  const invalidSpecs = specs.filter((spec) => spec?.ok === false).map((spec) => invalidSpecSummary(spec));
  const validSpecs = specs.filter((spec) => spec?.ok !== false && text(spec?.nodeId) && text(spec?.hwpodId ?? spec?.name));
  const devices = validSpecs.map((spec) => buildDevice(spec, onlineNodes.get(text(spec.nodeId)), observedAt));
  const nodeIds = new Set([...onlineNodes.keys(), ...validSpecs.map((spec) => text(spec.nodeId)).filter(Boolean)]);
  const nodes = [...nodeIds].map((nodeId) => buildNode(nodeId, onlineNodes.get(nodeId), devices.filter((device) => device.nodeId === nodeId), observedAt));
  const resource = text(options.resource) === "device" ? "device" : "node";
  const sourceItems = resource === "device" ? devices : nodes;
  const filteredItems = filterItems(sourceItems, options);
  const sortedItems = sortItems(filteredItems, options);
  const page = paginate(sortedItems, options);
  const partial = invalidSpecs.length > 0;

  return {
    ok: true,
    status: partial ? "partial" : "ready",
    contractVersion: HWPOD_TOPOLOGY_CONTRACT_VERSION,
    route: "/v1/hwpod/topology",
    owner: "cloud-api-hwpod-domain",
    resource,
    service: {
      ready: true,
      status: partial ? "partial" : "ready",
      websocketMode: text(registrySnapshot?.mode) || "hwpod-node-outbound-native-ws",
      connectedNodeCount: onlineNodes.size,
      pendingOperationCount: integer(registrySnapshot?.pendingCount),
      invalidSpecCount: invalidSpecs.length
    },
    summary: {
      nodeCount: nodes.length,
      onlineNodeCount: nodes.filter((node) => node.online).length,
      offlineNodeCount: nodes.filter((node) => !node.online).length,
      unconfiguredNodeCount: nodes.filter((node) => node.deviceCount === 0).length,
      deviceCount: devices.length,
      availableDeviceCount: devices.filter((device) => device.available).length,
      busyDeviceCount: devices.filter((device) => device.busy).length,
      capabilityMismatchCount: devices.filter((device) => device.capabilityMismatch).length,
      invalidSpecCount: invalidSpecs.length
    },
    items: page.items,
    page: page.page,
    invalidSpecs,
    valuesRedacted: true,
    observedAt
  };
}

function normalizeOnlineNodes(snapshot: any) {
  const map = new Map<string, any>();
  for (const raw of array(snapshot?.nodes)) {
    const nodeId = text(raw?.nodeId);
    if (!nodeId) continue;
    const actualCapabilities = uniqueStrings(raw?.capabilities);
    const inFlightCount = integer(raw?.inFlightCount ?? raw?.pendingCount);
    const maxInFlight = positiveInteger(raw?.maxInFlight, 1);
    map.set(nodeId, {
      nodeId,
      name: text(raw?.name) || nodeId,
      online: raw?.status !== "offline",
      platform: text(raw?.platform ?? raw?.labels?.platform) || "unknown",
      version: text(raw?.version ?? raw?.labels?.version) || null,
      runtimeKind: text(raw?.runtimeKind ?? raw?.runtime?.kind ?? raw?.labels?.runtimeKind) || null,
      installed: booleanOrNull(raw?.installed ?? raw?.runtime?.installed),
      desktopVisible: booleanOrNull(raw?.desktopVisible ?? raw?.runtime?.desktopVisible),
      actualCapabilities,
      inFlightCount,
      maxInFlight,
      busy: raw?.busy === true || inFlightCount >= maxInFlight,
      firstSeenAt: text(raw?.firstSeenAt) || null,
      lastSeenAt: text(raw?.lastSeenAt) || null,
      ageMs: integer(raw?.ageMs),
      latestOperation: operationSummary(raw?.latestOperation),
      latestReadinessProbe: operationSummary(raw?.latestReadinessProbe),
      diagnostic: diagnosticSummary(raw?.lastDiagnostic)
    });
  }
  return map;
}

function buildDevice(spec: any, node: any, observedAt: string) {
  const document = object(spec.document);
  const body = object(document.spec);
  const declaredCapabilities = declaredCapabilitiesFor(body);
  const actualCapabilities = node?.actualCapabilities ?? [];
  const missingCapabilities = node ? declaredCapabilities.filter((capability) => !actualCapabilities.includes(capability)) : declaredCapabilities;
  const capabilityCoverage = buildCapabilityCoverage(declaredCapabilities, actualCapabilities);
  const availability = availabilitySummary(spec.availability);
  const online = Boolean(node?.online);
  const busy = Boolean(node?.busy);
  const capabilityMismatch = online && missingCapabilities.length > 0;
  const blockers = deviceBlockers({ online, busy, capabilityMismatch, missingCapabilities, availability });
  const status = online ? "online" : "offline";
  const available = online && !busy && !capabilityMismatch && availability.ok === true;
  return {
    resourceType: "hwpod",
    hwpodId: text(spec.hwpodId ?? spec.name),
    uid: text(spec.uid) || null,
    name: text(spec.name ?? spec.hwpodId),
    nodeId: text(spec.nodeId),
    nodeName: node?.name ?? text(spec.nodeId),
    status,
    online,
    available,
    busy,
    capabilityMismatch,
    declaredCapabilities,
    actualCapabilities,
    missingCapabilities,
    capabilityCoverage,
    elements: hwpodElements(body),
    availability,
    operation: node?.latestOperation?.hwpodId === text(spec.hwpodId ?? spec.name) ? node.latestOperation : null,
    diagnostic: node?.diagnostic ?? null,
    blockers,
    source: {
      authority: text(spec.authority) || "workspace-or-registry",
      kind: text(spec.source?.kind) || "workspace-or-registry",
      specPath: text(spec.specPath) || null
    },
    observedAt
  };
}

function buildNode(nodeId: string, onlineNode: any, devices: any[], observedAt: string) {
  const declaredCapabilities = uniqueStrings(devices.flatMap((device) => device.declaredCapabilities));
  const actualCapabilities = onlineNode?.actualCapabilities ?? [];
  const missingCapabilities = onlineNode ? declaredCapabilities.filter((capability) => !actualCapabilities.includes(capability)) : declaredCapabilities;
  const capabilityCoverage = buildCapabilityCoverage(declaredCapabilities, actualCapabilities);
  const busy = Boolean(onlineNode?.busy);
  const online = Boolean(onlineNode?.online);
  const capabilityMismatch = online && missingCapabilities.length > 0;
  const hasSpec = devices.length > 0;
  const status = online ? "online" : "offline";
  const blockers = nodeBlockers({ online, hasSpec, busy, capabilityMismatch, missingCapabilities, devices });
  const workspaceReady = devices.some((device) => device.availability.ok === true);
  const hwpodReady = devices.some((device) => device.available);
  return {
    resourceType: "hwpod-node",
    nodeId,
    name: onlineNode?.name ?? nodeId,
    status,
    online,
    platform: onlineNode?.platform ?? "unknown",
    version: onlineNode?.version ?? null,
    runtimeKind: onlineNode?.runtimeKind ?? null,
    firstSeenAt: onlineNode?.firstSeenAt ?? null,
    lastSeenAt: onlineNode?.lastSeenAt ?? null,
    ageMs: onlineNode?.ageMs ?? null,
    deviceCount: devices.length,
    hwpods: devices.map((device) => ({
      hwpodId: device.hwpodId,
      name: device.name,
      status: device.status,
      busy: device.busy,
      capabilityMismatch: device.capabilityMismatch
    })),
    declaredCapabilities,
    actualCapabilities,
    missingCapabilities,
    capabilityCoverage,
    capabilityMismatch,
    inFlight: {
      count: onlineNode?.inFlightCount ?? 0,
      max: onlineNode?.maxInFlight ?? 1,
      busy
    },
    operation: onlineNode?.latestOperation ?? null,
    readinessProbe: onlineNode?.latestReadinessProbe ?? null,
    diagnostic: onlineNode?.diagnostic ?? null,
    readiness: nodeReadiness({ onlineNode, online, workspaceReady, hwpodReady, busy, blockers }),
    blockers,
    observedAt
  };
}

function declaredCapabilitiesFor(spec: any) {
  const explicit = uniqueStrings([...array(spec?.capabilities), ...array(spec?.nodeBinding?.capabilities)]);
  if (explicit.length > 0) return explicit;
  const capabilities = ["node.health", "node.version", "node.inventory", "node.diagnostics"];
  if (Object.keys(object(spec?.workspace)).length > 0) {
    capabilities.push("workspace.ls", "workspace.cat", "workspace.rg", "workspace.apply-patch", "workspace.write", "workspace.replace", "workspace.insert-after", "cmd.run");
  }
  if (Object.keys(object(spec?.debugProbe)).length > 0) capabilities.push("debug.build", "debug.download", "debug.reset");
  if (Object.keys(object(spec?.ioProbe?.uart)).length > 0) capabilities.push("io.uart.read", "io.uart.write", "io.uart.jsonrpc");
  return uniqueStrings(capabilities);
}

function hwpodElements(spec: any) {
  const target = object(spec.targetDevice);
  const workspace = object(spec.workspace);
  const debugProbe = object(spec.debugProbe);
  const ioProbe = object(spec.ioProbe);
  const uart = object(ioProbe.uart);
  return {
    target: {
      label: text(target.board ?? target.name ?? target.id) || "未声明目标板",
      mcu: text(target.mcu) || null
    },
    workspace: {
      label: text(workspace.projectRoot ?? workspace.path) || "未声明工作区",
      path: text(workspace.path) || null,
      toolchain: text(workspace.toolchain) || null
    },
    debugProbe: {
      label: text(debugProbe.probeName ?? debugProbe.name ?? debugProbe.type ?? debugProbe.id) || "未声明调试探头",
      type: text(debugProbe.type ?? debugProbe.kind) || null
    },
    ioProbe: {
      label: text(uart.id ?? uart.port ?? ioProbe.name ?? ioProbe.id) || "未声明 IO 探头",
      kind: Object.keys(uart).length > 0 ? "uart" : text(ioProbe.kind) || "unknown"
    }
  };
}

function availabilitySummary(value: any) {
  const availability = object(value);
  const blocker = object(availability.blocker);
  return {
    ok: booleanOrNull(availability.ok),
    status: text(availability.status) || "unprobed",
    checkedAt: text(availability.checkedAt) || null,
    blocker: Object.keys(blocker).length > 0 ? {
      code: text(blocker.code) || "hwpod_unavailable",
      summary: text(blocker.summary ?? blocker.message) || "HWPOD 不可用",
      retryable: blocker.retryable !== false
    } : null
  };
}

function buildCapabilityCoverage(declaredCapabilities: string[], actualCapabilities: string[]) {
  const matched = declaredCapabilities.filter((capability) => actualCapabilities.includes(capability)).length;
  const extra = actualCapabilities.filter((capability) => !declaredCapabilities.includes(capability)).length;
  return {
    matched,
    declared: declaredCapabilities.length,
    actual: actualCapabilities.length,
    missing: declaredCapabilities.length - matched,
    extra
  };
}

function deviceBlockers(input: any) {
  const blockers: any[] = [];
  if (!input.online) blockers.push(blocker("hwpod_node_offline", "声明的 HWPOD Node 当前离线。", true));
  if (input.capabilityMismatch) blockers.push(blocker("hwpod_capability_mismatch", `节点缺少 ${input.missingCapabilities.length} 项声明能力。`, false));
  if (input.busy) blockers.push(blocker("hwpod_node_busy", "节点已达 in-flight 并发上限。", true));
  if (input.availability.blocker) blockers.push(input.availability.blocker);
  return dedupeBlockers(blockers);
}

function nodeBlockers(input: any) {
  const blockers: any[] = [];
  if (!input.online) blockers.push(blocker("hwpod_node_offline", "Owning 配置已声明该节点，但未观测到主动出站 WebSocket。", true));
  if (input.online && !input.hasSpec) blockers.push(blocker("hwpod_node_spec_missing", "节点已在线，但没有任何 HWPOD spec 绑定。", false));
  if (input.capabilityMismatch) blockers.push(blocker("hwpod_capability_mismatch", `节点缺少 ${input.missingCapabilities.length} 项所属 HWPOD 声明能力。`, false));
  if (input.busy) blockers.push(blocker("hwpod_node_busy", "节点已达 in-flight 并发上限。", true));
  for (const device of input.devices) blockers.push(...device.blockers);
  return dedupeBlockers(blockers);
}

function nodeReadiness(input: any) {
  const localObserved = Boolean(input.onlineNode);
  const stageBlocker = input.blockers[0] ?? null;
  return [
    readinessStage("downloaded", input.onlineNode?.installed === true ? "ready" : "unknown", input.onlineNode?.installed === true ? null : blocker("hwpod_download_not_observed", "云端不以浏览器下载动作作为安装事实。", false)),
    readinessStage("installed", input.onlineNode?.installed === true ? "ready" : "unknown", input.onlineNode?.installed === true ? null : blocker("hwpod_install_not_observed", "等待 Python 节点报告安装运行时。", true)),
    readinessStage("desktop-visible", input.onlineNode?.desktopVisible === true ? "ready" : localObserved ? "blocked" : "unknown", input.onlineNode?.desktopVisible === true ? null : blocker("hwpod_desktop_not_visible", "尚未证明图形界面与托盘属于交互登录会话。", true)),
    readinessStage("registered", input.online ? "ready" : "blocked", input.online ? null : blocker("hwpod_node_offline", "未观测到已认证的主动出站 WebSocket。", true)),
    readinessStage("workspace-ready", input.workspaceReady ? "ready" : "blocked", input.workspaceReady ? null : stageBlocker ?? blocker("hwpod_workspace_not_ready", "工作区尚未通过显式 readiness 探测。", true)),
    readinessStage("hwpod-ready", input.hwpodReady && !input.busy ? "ready" : "blocked", input.hwpodReady && !input.busy ? null : stageBlocker ?? blocker("hwpod_not_ready", "尚未观测到可用且空闲的挂载 HWPOD。", true))
  ];
}

function readinessStage(id: string, status: string, stageBlocker: any) {
  const definition = HWPOD_NODE_READINESS_DEFINITIONS.find((stage) => stage.id === id)!;
  return { ...definition, status, blocker: stageBlocker, nextAction: status === "ready" ? null : definition.nextAction };
}

function invalidSpecSummary(spec: any) {
  return {
    specPath: text(spec?.specPath) || null,
    status: "invalid",
    blocker: blocker(text(spec?.error?.code) || "invalid_hwpod_spec", text(spec?.error?.message) || "HWPOD spec 无法解析。", false)
  };
}

function operationSummary(value: any) {
  const operation = object(value);
  if (Object.keys(operation).length === 0) return null;
  return {
    requestId: text(operation.requestId) || null,
    planId: text(operation.planId) || null,
    hwpodId: text(operation.hwpodId) || null,
    status: text(operation.status) || "unknown",
    opCount: integer(operation.opCount),
    startedAt: text(operation.startedAt) || null,
    finishedAt: text(operation.finishedAt) || null,
    blockerCode: text(operation.blockerCode) || null
  };
}

function diagnosticSummary(value: any) {
  const diagnostic = object(value);
  if (Object.keys(diagnostic).length === 0) return null;
  return {
    level: text(diagnostic.level) || "ERROR",
    source: text(diagnostic.source) || "unknown",
    message: redactDiagnosticMessage(diagnostic.message),
    observedAt: text(diagnostic.observedAt ?? diagnostic.receivedAt) || null,
    valuesRedacted: true
  };
}

function redactDiagnosticMessage(value: unknown) {
  return text(value)
    .replace(/\b(Bearer|Basic)\s+\S+/giu, "$1 [REDACTED]")
    .replace(/([?&](?:api[_-]?key|token|secret|password|authorization)=)[^&\s]+/giu, "$1[REDACTED]")
    .replace(/\b(api[_-]?key|token|secret|password|authorization)\s*[:=]\s*\S+/giu, "$1=[REDACTED]")
    .slice(0, 500);
}

function filterItems(items: any[], options: any) {
  const query = text(options.q).toLowerCase();
  const statuses = new Set(uniqueStrings(String(options.status ?? "").split(",")));
  return items.filter((item) => {
    if (statuses.size > 0 && !statuses.has(text(item.status))) return false;
    if (!query) return true;
    const haystack = [item.nodeId, item.name, item.hwpodId, item.status, ...array(item.missingCapabilities)].map(text).join(" ").toLowerCase();
    return haystack.includes(query);
  });
}

function sortItems(items: any[], options: any) {
  const sort = ["name", "status", "lastSeenAt", "deviceCount"].includes(text(options.sort)) ? text(options.sort) : "name";
  const direction = text(options.direction) === "desc" ? -1 : 1;
  return [...items].sort((left, right) => compareValues(left?.[sort], right?.[sort]) * direction);
}

function paginate(items: any[], options: any) {
  const limit = Math.min(positiveInteger(options.limit, DEFAULT_PAGE_LIMIT), MAX_PAGE_LIMIT);
  const offset = decodeCursor(options.cursor);
  const pageItems = items.slice(offset, offset + limit);
  const nextOffset = offset + pageItems.length;
  return {
    items: pageItems,
    page: {
      limit,
      returned: pageItems.length,
      total: items.length,
      cursor: offset > 0 ? encodeCursor(offset) : null,
      previousCursor: offset > 0 ? encodeCursor(Math.max(0, offset - limit)) : null,
      nextCursor: nextOffset < items.length ? encodeCursor(nextOffset) : null
    }
  };
}

function encodeCursor(offset: number) {
  return Buffer.from(`offset:${offset}`, "utf8").toString("base64url");
}

function decodeCursor(value: unknown) {
  const cursor = text(value);
  if (!cursor) return 0;
  try {
    const parsed = Buffer.from(cursor, "base64url").toString("utf8").match(/^offset:(\d+)$/u);
    return parsed ? integer(parsed[1]) : 0;
  } catch {
    return 0;
  }
}

function blocker(code: string, summary: string, retryable: boolean) {
  return { code, summary, retryable };
}

function dedupeBlockers(values: any[]) {
  const seen = new Set<string>();
  return values.filter((value) => {
    const code = text(value?.code);
    if (!code || seen.has(code)) return false;
    seen.add(code);
    return true;
  }).slice(0, 8);
}

function compareValues(left: unknown, right: unknown) {
  if (typeof left === "number" || typeof right === "number") return Number(left ?? 0) - Number(right ?? 0);
  return text(left).localeCompare(text(right), "zh-CN");
}

function uniqueStrings(values: unknown) {
  return [...new Set(array(values).map(text).filter(Boolean))].sort();
}

function array(value: unknown): any[] {
  return Array.isArray(value) ? value : [];
}

function object(value: unknown): any {
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}

function text(value: unknown) {
  return typeof value === "string" && value.trim() ? value.trim() : "";
}

function integer(value: unknown) {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed >= 0 ? Math.trunc(parsed) : 0;
}

function positiveInteger(value: unknown, fallback: number) {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

function booleanOrNull(value: unknown) {
  return typeof value === "boolean" ? value : null;
}
