// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. manifest module for scripts/src/platform-infra-observability.ts.

// Moved mechanically from scripts/src/platform-infra-observability.ts:983-1299 for #903.

// SPEC: PJ2026-01060501 OTel追踪 draft-2026-06-19-p0.
// Responsibility: YAML-first platform-infra OpenTelemetry tracing control commands.
import { Buffer } from "node:buffer";
import { readFileSync } from "node:fs";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import type { RenderedCliResult } from "../output";
import {
  compactCapture,
  createYamlFieldReader,
  numberField,
  parseJsonOutput,
  redactSensitiveUnknown,
  shQuote,
  capture,
} from "../platform-infra-ops-library";

import type { DiagnoseCodeAgentOptions, ObservabilityTarget, SearchOptions, TraceOptions } from "./types";
import { asPlainRecord, compactSearchTrace } from "./apply-status-scripts";
import { trace } from "./render";
import { asArray } from "./trace-script";

export function traceSpanDurationMs(span: Record<string, unknown>, attrs: Record<string, unknown> | null): number {
  const topLevel = numberFromUnknown(span.durationMs);
  if (Number.isFinite(topLevel)) return topLevel;
  if (attrs === null) return Number.NaN;
  for (const key of ["workbench.ui.value_ms", "workbench.ui.resource_duration_ms", "durationMs"]) {
    const value = numberFromUnknown(attrs[key]);
    if (Number.isFinite(value)) return value;
  }
  return Number.NaN;
}

export function traceSpanImportanceScore(span: Record<string, unknown>): number {
  const attrs = asPlainRecord(span.attributes);
  const statusCode = textValue(span.statusCode);
  const name = textValue(span.name).toLowerCase();
  const hasFailure = spanColumnAttr(attrs, ["failureKind", "errorSummary", "error.message", "exception.message"]) !== "-";
  const terminalStatus = spanColumnAttr(attrs, ["terminalStatus"]);
  const problemScore = statusCode === "STATUS_CODE_ERROR" || statusCode === "2" || name.includes("error") || hasFailure || terminalStatus === "failed" || terminalStatus === "blocked" ? 1_000_000_000 : 0;
  const durationMs = traceSpanDurationMs(span, attrs);
  const durationScore = Number.isFinite(durationMs) ? durationMs : 0;
  return problemScore + durationScore;
}

export function searchTraceStatus(trace: Record<string, unknown>): string {
  if (trace.queryOk === false) return "query-bad";
  if (trace.parseOk === false) return "parse-bad";
  const errorCount = numberFromUnknown(trace.errorSpanCount);
  if (errorCount > 0) return "error";
  const matchedCount = numberFromUnknown(trace.matchedSpanCount);
  if (matchedCount > 0 || trace.rawMatched === true) return "matched";
  return "ok";
}

export function uniqueSpanRecords(values: unknown[]): Record<string, unknown>[] {
  const rows: Record<string, unknown>[] = [];
  const seen = new Set<string>();
  for (const value of values) {
    const span = asPlainRecord(value) ?? {};
    const key = `${textValue(span.name)}\n${textValue(span.service)}\n${spanDetail(span, 200)}`;
    if (seen.has(key)) continue;
    seen.add(key);
    rows.push(span);
  }
  return rows;
}

export function spanColumnAttr(attrs: Record<string, unknown> | null, keys: string[]): string {
  if (attrs === null) return "-";
  for (const key of keys) {
    const value = attrs[key];
    const text = textValue(value);
    if (text !== "-") return text;
  }
  return "-";
}

export function spanDetail(span: Record<string, unknown>, maxLength: number): string {
  const attrs = asPlainRecord(span.attributes);
  const parts = [
    valuePart("durationMs", span.durationMs),
    attrPart(attrs, "failureKind"),
    attrPart(attrs, "errorSummary"),
    attrPart(attrs, "error.message"),
    attrPart(attrs, "exception.message"),
    attrPart(attrs, "terminalStatus"),
    attrPart(attrs, "status"),
    attrPart(attrs, "toolName"),
    attrPart(attrs, "exitCode"),
    attrPart(attrs, "outputSummary"),
    attrPart(attrs, "outputBytes"),
    attrPart(attrs, "durationMs"),
    attrPart(attrs, "cwd"),
    attrPart(attrs, "command"),
    attrPart(attrs, "waitingFor"),
    attrPart(attrs, "lastEventLabel"),
    attrPart(attrs, "idleMs"),
    attrPart(attrs, "idleSeconds"),
    attrPart(attrs, "commandFingerprint"),
    attrPart(attrs, "http.route"),
    attrPart(attrs, "http.response.status_code"),
    attrPart(attrs, "http.response.status_class"),
    attrPart(attrs, "workbench.ui.event_type"),
    attrPart(attrs, "workbench.ui.scope"),
    attrPart(attrs, "workbench.ui.state"),
    attrPart(attrs, "workbench.ui.reason"),
    attrPart(attrs, "workbench.ui.outcome"),
    attrPart(attrs, "workbench.ui.value_ms"),
    attrPart(attrs, "workbench.ui.activity_idle_ms"),
    attrPart(attrs, "workbench.ui.activity_waiting_for"),
    attrPart(attrs, "workbench.ui.activity_last_event_label"),
    attrPart(attrs, "workbench.ui.resource_duration_ms"),
    attrPart(attrs, "workbench.ui.resource_fetch_to_request_ms"),
    attrPart(attrs, "workbench.ui.resource_request_wait_ms"),
    attrPart(attrs, "workbench.ui.resource_response_transfer_ms"),
    attrPart(attrs, "workbench.ui.resource_next_hop_protocol"),
    attrPart(attrs, "workbench.ui.resource_server_timing"),
    attrPart(attrs, "hwlab.http.stage"),
    attrPart(attrs, "hwlab.http.phase"),
    attrPart(attrs, "hwlab.http.phase.outcome"),
    attrPart(attrs, "hwlab.live_builds.service_id"),
    attrPart(attrs, "hwlab.live_builds.service_kind"),
    attrPart(attrs, "hwlab.live_builds.external"),
    attrPart(attrs, "hwlab.live_builds.deploy_manifest_status"),
    attrPart(attrs, "hwlab.live_builds.artifact_catalog_status"),
    attrPart(attrs, "db.system"),
    attrPart(attrs, "db.operation.name"),
    attrPart(attrs, "db.sql.table"),
    attrPart(attrs, "db.query.arg_count"),
    attrPart(attrs, "db.index.expected"),
    attrPart(attrs, "db.pool.max_open"),
    attrPart(attrs, "db.pool.open_connections"),
    attrPart(attrs, "db.pool.in_use"),
    attrPart(attrs, "db.pool.idle"),
    attrPart(attrs, "db.pool.wait_count"),
    attrPart(attrs, "db.pool.wait_duration_ms"),
    attrPart(attrs, "error.code"),
    attrPart(attrs, "error.category"),
    attrPart(attrs, "error.layer"),
  ].filter((item) => item !== null) as string[];
  if (parts.length === 0) return "-";
  return shortenEnd(parts.join(" "), maxLength);
}

export function valuePart(key: string, value: unknown): string | null {
  if (value === null || value === undefined || value === "") return null;
  return `${key}=${textValue(value)}`;
}

export function attrPart(attrs: Record<string, unknown> | null, key: string): string | null {
  if (attrs === null) return null;
  const value = attrs[key];
  if (value === null || value === undefined || value === "") return null;
  return `${key}=${textValue(value)}`;
}

export function httpTableRows(http: Record<string, unknown> | null): string[][] {
  if (http === null) return [];
  const problemRows = asArray(http.problemCounts).map((item) => {
    const row = asPlainRecord(item) ?? {};
    return [
      textValue(row.method),
      shortenEnd(textValue(row.route ?? row.path), 44),
      textValue(row.status ?? row.statusCode),
      textValue(row.count),
    ];
  });
  const statusRows = asArray(http.statusCounts).map((item) => {
    const row = asPlainRecord(item) ?? {};
    return [
      textValue(row.method),
      shortenEnd(textValue(row.route ?? row.path), 44),
      textValue(row.status ?? row.statusCode),
      textValue(row.count),
    ];
  });
  return [...problemRows, ...statusRows].slice(0, 8);
}

export function buildTraceCommand(target: ObservabilityTarget, options: TraceOptions, full: boolean): string {
  const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "trace", "--target", target.id];
  if (options.traceId !== null) parts.push("--trace-id", options.traceId);
  if (options.grep !== null) parts.push("--grep", options.grep);
  parts.push("--limit", String(options.limit));
  if (full) parts.push("--full");
  return parts.map(cliArg).join(" ");
}

export function buildSearchCommand(target: ObservabilityTarget, options: SearchOptions, full: boolean): string {
  const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "search", "--target", target.id];
  if (options.grep !== null) parts.push("--grep", options.grep);
  if (options.query !== null) parts.push("--query", options.query);
  if (options.path !== null) parts.push("--path", options.path);
  if (options.status !== null) parts.push("--status", String(options.status));
  if (options.endAt !== null) parts.push("--end-at", options.endAt);
  parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit));
  if (full) parts.push("--full");
  return parts.map(cliArg).join(" ");
}

export function buildDiagnoseCommand(target: ObservabilityTarget, options: DiagnoseCodeAgentOptions, full: boolean): string {
  const parts = ["bun", "scripts/cli.ts", "platform-infra", "observability", "diagnose-code-agent", "--target", target.id];
  if (options.businessTraceId !== null) parts.push("--business-trace-id", options.businessTraceId);
  if (options.traceId !== null) parts.push("--trace-id", options.traceId);
  if (options.runId !== null) parts.push("--run-id", options.runId);
  if (options.commandId !== null) parts.push("--command-id", options.commandId);
  if (options.runnerJobId !== null) parts.push("--runner-job-id", options.runnerJobId);
  parts.push("--lookback-minutes", String(options.lookbackMinutes), "--candidate-limit", String(options.candidateLimit), "--limit", String(options.limit));
  if (full) parts.push("--full");
  return parts.map(cliArg).join(" ");
}

export function cliArg(value: string): string {
  return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : shQuote(value);
}

export function formatTable(headers: string[], rows: string[][]): string {
  const normalizedRows = rows.map((row) => headers.map((_, index) => row[index] ?? ""));
  const widths = headers.map((header, index) => Math.max(header.length, ...normalizedRows.map((row) => row[index].length)));
  return [
    headers.map((header, index) => header.padEnd(widths[index])).join("  ").trimEnd(),
    ...normalizedRows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join("  ").trimEnd()),
  ].join("\n");
}

export function textValue(value: unknown): string {
  if (value === null || value === undefined || value === "") return "-";
  if (typeof value === "string") return value;
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  return JSON.stringify(value);
}

export function numberValue(value: unknown): string {
  const number = numberFromUnknown(value);
  return Number.isFinite(number) ? String(number) : "-";
}

export function numberFromUnknown(value: unknown): number {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string" && value.trim().length > 0) {
    const parsed = Number(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return Number.NaN;
}

export function isoFromUnixNano(value: unknown): string {
  if (value === null || value === undefined || value === "") return "-";
  try {
    const text = String(value);
    if (!/^\d+$/u.test(text)) return "-";
    const millis = Number(BigInt(text) / 1000000n);
    if (!Number.isFinite(millis) || millis <= 0) return "-";
    return new Date(millis).toISOString().replace(/\.000Z$/u, "Z");
  } catch {
    return "-";
  }
}

export function joinValues(value: unknown, maxLength: number): string {
  const joined = asArray(value).map(textValue).filter((item) => item !== "-").join(",");
  return shortenEnd(joined.length > 0 ? joined : textValue(value), maxLength);
}

export function shortenEnd(value: string, maxLength: number): string {
  if (value.length <= maxLength) return value;
  if (maxLength <= 1) return value.slice(0, maxLength);
  return `${value.slice(0, maxLength - 1)}~`;
}

export function shortenMiddle(value: string, maxLength: number): string {
  if (value.length <= maxLength) return value;
  if (maxLength <= 3) return value.slice(0, maxLength);
  const left = Math.ceil((maxLength - 1) / 2);
  const right = Math.floor((maxLength - 1) / 2);
  return `${value.slice(0, left)}~${value.slice(value.length - right)}`;
}

export function compactSearchResult(value: unknown): Record<string, unknown> {
  const source = asPlainRecord(value) ?? {};
  const traces = asArray(source.traces).map(compactSearchTrace);
  return {
    ok: source.ok === true,
    searchPath: source.searchPath ?? null,
    grep: source.grep ?? null,
    tempoQuery: source.tempoQuery ?? null,
    pathFilter: source.pathFilter ?? null,
    statusFilter: source.statusFilter ?? null,
    limit: source.limit ?? null,
    candidateLimit: source.candidateLimit ?? null,
    searchParseOk: source.searchParseOk ?? null,
    candidateTraceCount: source.candidateTraceCount ?? null,
    scannedTraceCount: source.scannedTraceCount ?? null,
    matchedTraceCount: source.matchedTraceCount ?? null,
    scanStopped: source.scanStopped ?? null,
    matchingActive: source.matchingActive ?? null,
    traces,
    truncated: source.truncated ?? null,
    next: source.next ?? null,
    searchStderrTail: source.searchStderrTail ?? null,
    disclosure: {
      defaultView: "compact trace rows only; span arrays are omitted to keep stdout below config/unidesk-cli.yaml output.maxStdoutBytes",
      expand: "rerun with --full or query a specific trace id with platform-infra observability trace",
    },
  };
}
