// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel.
// Responsibility: Best-effort OTLP span emitter for web-probe sentinel scheduler, cadence and quick-verify events.
import { randomBytes } from "node:crypto";

export interface SentinelOtelContext {
  readonly node: string;
  readonly lane: string;
  readonly sentinelId: string;
  readonly namespace?: string | null;
  readonly runtime?: Record<string, unknown>;
  readonly cicd?: Record<string, unknown>;
}

export function emitWebProbeSentinelSpan(context: SentinelOtelContext, name: string, attributes: Record<string, unknown> = {}, ok = true): void {
  const config = resolveOtelConfig(context);
  if (!config.enabled || config.endpoint === null) return;
  const start = BigInt(Date.now()) * 1_000_000n;
  const end = start + 1_000_000n;
  const traceId = randomHex(16);
  const spanId = randomHex(8);
  const payload = {
    resourceSpans: [{
      resource: {
        attributes: otelAttributes({
          "service.name": config.serviceName,
          "deployment.environment": context.lane,
          "unidesk.node": context.node,
          "hwlab.lane": context.lane,
          "k8s.namespace.name": context.namespace ?? stringAtNullable(context.runtime, "namespace"),
          "unidesk.values_redacted": true,
        }),
      },
      scopeSpans: [{
        scope: { name: "unidesk.web_probe_sentinel", version: "PJ2026-01060508" },
        spans: [{
          traceId,
          spanId,
          name,
          kind: 1,
          startTimeUnixNano: start.toString(),
          endTimeUnixNano: end.toString(),
          attributes: otelAttributes({
            "unidesk.node": context.node,
            "hwlab.lane": context.lane,
            "sentinelId": context.sentinelId,
            "valuesRedacted": true,
            ...attributes,
          }),
          status: { code: ok ? 1 : 2 },
        }],
      }],
    }],
  };
  void fetch(config.endpoint, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(payload),
  }).catch(() => undefined);
}

export function webProbeSentinelOtelSummary(context: SentinelOtelContext): Record<string, unknown> {
  const config = resolveOtelConfig(context);
  return {
    enabled: config.enabled,
    endpointConfigured: config.endpoint !== null,
    serviceName: config.serviceName,
    coverage: config.enabled && config.endpoint !== null ? "best-effort-otlp-spans" : "instrumentation-gap",
    expectedSpans: [
      "web_probe_sentinel.scheduler.heartbeat",
      "web_probe_sentinel.cadence.expected",
      "web_probe_sentinel.cadence.cronjob_rendered",
      "web_probe_sentinel.cadence.cronjob_observed",
      "web_probe_sentinel.manual_trigger",
      "web_probe_sentinel.quick_verify.job_start",
      "web_probe_sentinel.quick_verify.job_finish",
      "web_probe_sentinel.record_run",
      "web_probe_sentinel.scheduler_gap.detected",
    ],
    valuesRedacted: true,
  };
}

function resolveOtelConfig(context: SentinelOtelContext): { readonly enabled: boolean; readonly endpoint: string | null; readonly serviceName: string } {
  const runtime = context.runtime ?? {};
  const cicd = context.cicd ?? {};
  const enabledFromYaml = booleanAtNullable(runtime, "observability.otel.enabled")
    ?? booleanAtNullable(cicd, "observability.otel.enabled");
  const disabledByEnv = /^(1|true)$/iu.test(process.env.OTEL_SDK_DISABLED ?? "");
  const endpoint = stringAtNullable(runtime, "observability.otel.tracesEndpoint")
    ?? stringAtNullable(runtime, "observability.otel.endpoint")
    ?? stringAtNullable(cicd, "observability.otel.tracesEndpoint")
    ?? stringAtNullable(cicd, "observability.otel.endpoint")
    ?? nonEmptyString(process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT);
  const serviceName = stringAtNullable(runtime, "observability.otel.serviceName")
    ?? stringAtNullable(cicd, "observability.otel.serviceName")
    ?? nonEmptyString(process.env.OTEL_SERVICE_NAME)
    ?? `hwlab-web-probe-sentinel-${context.node.toLowerCase()}`;
  return {
    enabled: !disabledByEnv && (enabledFromYaml === true || endpoint !== null),
    endpoint,
    serviceName,
  };
}

function otelAttributes(values: Record<string, unknown>): readonly Record<string, unknown>[] {
  return Object.entries(values)
    .filter(([, value]) => value !== undefined && value !== null)
    .map(([key, value]) => ({ key, value: otelValue(value) }));
}

function otelValue(value: unknown): Record<string, unknown> {
  if (typeof value === "boolean") return { boolValue: value };
  if (typeof value === "number" && Number.isFinite(value)) {
    return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
  }
  return { stringValue: typeof value === "string" ? value : JSON.stringify(value) };
}

function randomHex(bytes: number): string {
  return randomBytes(bytes).toString("hex");
}

function stringAtNullable(value: unknown, path: string): string | null {
  const found = valueAtPath(value, path);
  return typeof found === "string" && found.length > 0 ? found : null;
}

function booleanAtNullable(value: unknown, path: string): boolean | null {
  const found = valueAtPath(value, path);
  return typeof found === "boolean" ? found : null;
}

function nonEmptyString(value: unknown): string | null {
  return typeof value === "string" && value.length > 0 ? value : null;
}

function valueAtPath(value: unknown, path: string): unknown {
  let current: unknown = value;
  for (const segment of path.split(".")) {
    if (typeof current !== "object" || current === null || Array.isArray(current)) return undefined;
    current = (current as Record<string, unknown>)[segment];
  }
  return current;
}
