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

// Moved mechanically from scripts/src/platform-infra-observability.ts:150-369 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 { ApplyOptions, CommonOptions, DiagnoseCodeAgentOptions, SearchOptions, TraceOptions } from "./types";
import { apply, plan, status, validate } from "./actions";
import { diagnoseCodeAgent, search, trace } from "./render";

export function observabilityHelp(): Record<string, unknown> {
  return {
    command: "platform-infra observability plan|apply|status|validate|trace|search|diagnose-code-agent",
    output: "text/table by default; trace --full expands bounded diagnostic tables, --raw returns backend JSON",
    configTruth: "config/platform-infra/observability.yaml",
    spec: "PJ2026-01060501 OTel追踪 draft-2026-06-19-p0",
    usage: [
      "bun scripts/cli.ts platform-infra observability plan --target D601",
      "bun scripts/cli.ts platform-infra observability plan --target D518",
      "bun scripts/cli.ts platform-infra observability apply --target D601 --dry-run",
      "bun scripts/cli.ts platform-infra observability apply --target D518 --confirm",
      "bun scripts/cli.ts platform-infra observability status --target D601 [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability status --target D518 [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability validate --target D518 [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability trace --target D518 --trace-id <traceId> [--grep provider-stream-disconnected] [--limit 40] [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability search --target D518 --grep 'no rollout found' [--lookback-minutes 360] [--candidate-limit 80] [--limit 20] [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability search --target D518 --path /v1/workbench/sessions --status 502 [--lookback-minutes 120] [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D518 --business-trace-id <trc_...> [--full|--raw]",
      "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D518 --run-id <run_...> [--command-id <cmd_...>] [--runner-job-id <rjob_...>] [--full|--raw]",
    ],
    boundary: "Prometheus remains the metrics source; this command owns only platform-infra OTel Collector, trace backend readiness, and trace lookup.",
  };
}

export async function runPlatformObservabilityCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
  const [action = "plan"] = args;
  if (action === "plan") return plan(parseCommonOptions(args.slice(1)));
  if (action === "apply") return await apply(config, parseApplyOptions(args.slice(1)));
  if (action === "status") return await status(config, parseCommonOptions(args.slice(1)));
  if (action === "validate") return await validate(config, parseCommonOptions(args.slice(1)));
  if (action === "trace") return await trace(config, parseTraceOptions(args.slice(1)));
  if (action === "search") return await search(config, parseSearchOptions(args.slice(1)));
  if (action === "diagnose-code-agent") return await diagnoseCodeAgent(config, parseDiagnoseCodeAgentOptions(args.slice(1)));
  return { ok: false, error: "unsupported-platform-infra-observability-command", args, help: observabilityHelp() };
}

export function parseCommonOptions(args: string[]): CommonOptions {
  let targetId: string | null = null;
  let full = false;
  let raw = false;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--target") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
      if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("--target must be a simple target id");
      targetId = value;
      index += 1;
    } else if (arg === "--full") {
      full = true;
    } else if (arg === "--raw") {
      raw = true;
      full = true;
    } else {
      throw new Error(`unsupported observability option: ${arg}`);
    }
  }
  return { targetId, full, raw };
}

export function parseApplyOptions(args: string[]): ApplyOptions {
  const commonArgs: string[] = [];
  let confirm = false;
  let dryRun = false;
  let wait = false;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--confirm") confirm = true;
    else if (arg === "--dry-run") dryRun = true;
    else if (arg === "--wait") wait = true;
    else {
      commonArgs.push(arg);
      if (arg === "--target") {
        commonArgs.push(args[index + 1] ?? "");
        index += 1;
      }
    }
  }
  if (confirm && dryRun) throw new Error("observability apply accepts only one of --confirm or --dry-run");
  return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait };
}

export function parseTraceOptions(args: string[]): TraceOptions {
  const commonArgs: string[] = [];
  let traceId: string | null = null;
  let grep: string | null = null;
  let limit = 40;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--trace-id") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--trace-id requires a value");
      if (!/^[0-9a-fA-F]{32}$/u.test(value)) throw new Error("--trace-id must be a 32-character OpenTelemetry trace id encoded as hex");
      traceId = value;
      index += 1;
    } else if (arg === "--grep") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--grep requires a value");
      grep = value;
      index += 1;
    } else if (arg === "--limit") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--limit requires a value");
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) throw new Error("--limit must be an integer from 1 to 500");
      limit = parsed;
      index += 1;
    } else {
      commonArgs.push(arg);
      if (arg === "--target") {
        commonArgs.push(args[index + 1] ?? "");
        index += 1;
      }
    }
  }
  return { ...parseCommonOptions(commonArgs), traceId, grep, limit };
}

export function parseSearchOptions(args: string[]): SearchOptions {
  const commonArgs: string[] = [];
  let grep: string | null = null;
  let query: string | null = null;
  let path: string | null = null;
  let status: number | null = null;
  let limit = 20;
  let candidateLimit = 80;
  let lookbackMinutes = 360;
  let endAt: string | null = null;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--grep") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--grep requires a value");
      grep = value;
      index += 1;
    } else if (arg === "--query" || arg === "--q") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      query = value;
      index += 1;
    } else if (arg === "--path") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--path requires a value");
      if (!value.startsWith("/") || value.includes("\n") || value.length > 300) throw new Error("--path must be an absolute HTTP path up to 300 characters");
      path = value;
      index += 1;
    } else if (arg === "--status") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--status requires a value");
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 100 || parsed > 599) throw new Error("--status must be an integer HTTP status from 100 to 599");
      status = parsed;
      index += 1;
    } else if (arg === "--limit") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--limit requires a value");
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) throw new Error("--limit must be an integer from 1 to 100");
      limit = parsed;
      index += 1;
    } else if (arg === "--candidate-limit") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--candidate-limit requires a value");
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) throw new Error("--candidate-limit must be an integer from 1 to 500");
      candidateLimit = parsed;
      index += 1;
    } else if (arg === "--lookback-minutes" || arg === "--since-minutes") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 1 || parsed > 10080) throw new Error(`${arg} must be an integer from 1 to 10080`);
      lookbackMinutes = parsed;
      index += 1;
    } else if (arg === "--end-at") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--end-at requires an ISO timestamp value");
      const parsed = Date.parse(value);
      if (!Number.isFinite(parsed)) throw new Error("--end-at must be an ISO timestamp parseable by Date.parse");
      endAt = new Date(parsed).toISOString();
      index += 1;
    } else {
      commonArgs.push(arg);
      if (arg === "--target") {
        commonArgs.push(args[index + 1] ?? "");
        index += 1;
      }
    }
  }
  if (grep === null && query === null && path === null && status === null) throw new Error("observability search requires --grep <text>, --query <tempo-query>, --path <route>, or --status <code>");
  return { ...parseCommonOptions(commonArgs), grep, query, path, status, limit, candidateLimit, lookbackMinutes, endAt };
}

export function parseDiagnoseCodeAgentOptions(args: string[]): DiagnoseCodeAgentOptions {
  const commonArgs: string[] = [];
  let businessTraceId: string | null = null;
  let traceId: string | null = null;
  let runId: string | null = null;
  let commandId: string | null = null;
  let runnerJobId: string | null = null;
  let limit = 40;
  let candidateLimit = 300;
  let lookbackMinutes = 10080;
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--business-trace-id" || arg === "--trace") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      if (!/^trc_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like trc_<id>`);
      businessTraceId = value;
      index += 1;
    } else if (arg === "--trace-id" || arg === "--otel-trace-id") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      if (!/^[0-9a-fA-F]{32}$/u.test(value)) throw new Error(`${arg} must be a 32-character OpenTelemetry trace id encoded as hex`);
      traceId = value.toLowerCase();
      index += 1;
    } else if (arg === "--run-id" || arg === "--run") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      if (!/^run_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like run_<id>`);
      runId = value;
      index += 1;
    } else if (arg === "--command-id" || arg === "--command") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      if (!/^cmd_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like cmd_<id>`);
      commandId = value;
      index += 1;
    } else if (arg === "--runner-job-id" || arg === "--runner-job" || arg === "--runnerjob") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      if (!/^rjob_[A-Za-z0-9_-]+$/u.test(value)) throw new Error(`${arg} must look like rjob_<id>`);
      runnerJobId = value;
      index += 1;
    } else if (arg === "--limit") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--limit requires a value");
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 1 || parsed > 200) throw new Error("--limit must be an integer from 1 to 200");
      limit = parsed;
      index += 1;
    } else if (arg === "--candidate-limit") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error("--candidate-limit requires a value");
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 1 || parsed > 500) throw new Error("--candidate-limit must be an integer from 1 to 500");
      candidateLimit = parsed;
      index += 1;
    } else if (arg === "--lookback-minutes" || arg === "--since-minutes") {
      const value = args[index + 1];
      if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
      const parsed = Number(value);
      if (!Number.isInteger(parsed) || parsed < 1 || parsed > 10080) throw new Error(`${arg} must be an integer from 1 to 10080`);
      lookbackMinutes = parsed;
      index += 1;
    } else {
      commonArgs.push(arg);
      if (arg === "--target") {
        commonArgs.push(args[index + 1] ?? "");
        index += 1;
      }
    }
  }
  if (businessTraceId === null && traceId === null && runId === null && commandId === null && runnerJobId === null) {
    throw new Error("observability diagnose-code-agent requires --business-trace-id <trc_...>, --trace-id <otelTraceId>, --run-id <run_...>, --command-id <cmd_...>, or --runner-job-id <rjob_...>");
  }
  return { ...parseCommonOptions(commonArgs), businessTraceId, traceId, runId, commandId, runnerJobId, limit, candidateLimit, lookbackMinutes };
}
