import { AsyncLocalStorage } from 'node:async_hooks';
import { createHash, randomBytes, randomUUID } from 'node:crypto';

/**
 * Request-scoped trace context (P9 observability). A module-level AsyncLocalStorage
 * so singleton services can read the current request's trace id via `currentTraceId()`
 * WITHOUT threading it through every method or constructor. The middleware runs each
 * request inside `runWithTrace(...)`; async projectors run outside a request and
 * simply see `undefined` (their events already carry the trace from the emitting call).
 */
export interface TraceStore {
  traceId: string;
  correlationId?: string;
}

const als = new AsyncLocalStorage<TraceStore>();

export function runWithTrace<T>(traceId: string, fn: () => T): T {
  return als.run({ traceId }, fn);
}

export function currentTraceId(): string | undefined {
  return als.getStore()?.traceId;
}

/** A fresh trace id (used when no inbound trace context is present). */
export function newTraceId(): string {
  return randomUUID();
}

/** W3C traceparent for a trace id: 00-<32 hex>-<16 hex span>-01. */
export function toTraceparent(traceId: string): string {
  const clean = traceId.replace(/-/g, '').toLowerCase();
  // A UUID (32 hex after dropping dashes) is used as-is; any other id is hashed to 32 hex.
  const trace = /^[0-9a-f]{32}$/.test(clean) ? clean : createHash('sha256').update(traceId).digest('hex').slice(0, 32);
  const span = randomBytes(8).toString('hex');
  return `00-${trace}-${span}-01`;
}

/** Extract a trace id from an inbound traceparent header (the 2nd field), else undefined. */
export function traceIdFromTraceparent(traceparent?: string): string | undefined {
  if (!traceparent) return undefined;
  const parts = traceparent.split('-');
  return parts.length >= 3 && /^[0-9a-f]{32}$/i.test(parts[1]) ? parts[1] : undefined;
}
