import { AsyncLocalStorage } from "node:async_hooks";
import type { ActivityActor, ActivityTenant } from "../ports/audit.js";

/**
 * Ambient correlation values for the request currently being handled.
 *
 * The server enters this context before user hooks run so instrumentation
 * sinks can correlate events recorded anywhere in the request lifecycle.
 * Identity values (`actor`, `tenant`) are refreshed by the server whenever
 * hooks finalize a new context, so wrappers such as `createAmbientAuditLog`
 * observe the elevated identity at record time.
 */
export interface ActiveRequestContext {
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
  tracestate?: string;
  /**
   * Actor for the current request or service execution, when known.
   */
  actor?: ActivityActor;
  /**
   * Tenant scope for the current request or service execution, when known.
   */
  tenant?: ActivityTenant;
}

const activeRequestContext = new AsyncLocalStorage<
  ActiveRequestContext | undefined
>();

/**
 * Enter the ambient request context for the current async execution.
 */
export function enterActiveRequestContext(context: ActiveRequestContext): void {
  activeRequestContext.enterWith(context);
}

/**
 * Clear the ambient request context for the current async execution.
 */
export function clearActiveRequestContext(): void {
  activeRequestContext.enterWith(undefined);
}

/**
 * Run a function inside a scoped ambient request context frame.
 *
 * Internal to the server runtime — not part of the public package surface.
 * `server.runServiceContext(...)` uses this `AsyncLocalStorage.run` form
 * instead of `enterWith` because resuming an `enterWith` frame across
 * top-level await crashes Bun 1.3.x in plain scripts.
 */
export function runWithActiveRequestContext<T>(
  context: ActiveRequestContext,
  fn: () => T,
): T {
  return activeRequestContext.run(context, fn);
}

/**
 * Read the ambient request context, when one is active.
 */
export function getActiveRequestContext(): ActiveRequestContext | undefined {
  return activeRequestContext.getStore();
}

/**
 * Read a normalized actor from an app context object, when present.
 */
export function readContextActor(ctx: unknown): ActivityActor | undefined {
  if (!ctx || typeof ctx !== "object") return undefined;
  const actor = (ctx as { actor?: unknown }).actor;
  if (!actor || typeof actor !== "object") return undefined;
  return typeof (actor as { type?: unknown }).type === "string"
    ? (actor as ActivityActor)
    : undefined;
}

/**
 * Read a normalized tenant from an app context object, when present.
 */
export function readContextTenant(ctx: unknown): ActivityTenant | undefined {
  if (!ctx || typeof ctx !== "object") return undefined;
  const tenant = (ctx as { tenant?: unknown }).tenant;
  if (!tenant || typeof tenant !== "object") return undefined;
  return typeof (tenant as { id?: unknown }).id === "string"
    ? (tenant as ActivityTenant)
    : undefined;
}

/**
 * Update identity fields on the active ambient request context in place.
 *
 * The server calls this after hooks finalize a new request context so the
 * elevated actor/tenant become visible to ambient consumers, including async
 * frames that captured the context object before the update. No-op when no
 * ambient context is active.
 */
export function setActiveRequestIdentity(identity: {
  actor?: ActivityActor;
  tenant?: ActivityTenant;
}): void {
  const context = activeRequestContext.getStore();
  if (!context) return;
  if (identity.actor) context.actor = identity.actor;
  if (identity.tenant) context.tenant = identity.tenant;
}

/**
 * Fill missing correlation fields on an event from the ambient request
 * context.
 */
export function inheritActiveRequestContext<
  Event extends {
    requestId?: string;
    traceId?: string;
    spanId?: string;
    parentSpanId?: string;
    traceparent?: string;
    tracestate?: string;
  },
>(event: Event): Event {
  const context = getActiveRequestContext();
  if (!context) return event;

  return {
    ...event,
    requestId: event.requestId ?? context.requestId,
    traceId: event.traceId ?? context.traceId,
    spanId: event.spanId ?? context.spanId,
    parentSpanId: event.parentSpanId ?? context.parentSpanId,
    traceparent: event.traceparent ?? context.traceparent,
    tracestate: event.tracestate ?? context.tracestate,
  };
}
