/**
 * @fileoverview Utilities for creating and managing request contexts.
 *
 * A request context is an immutable-by-convention object that carries a unique
 * request ID, ISO 8601 timestamp, optional tenant ID, and optional auth data.
 * It is the primary carrier for log correlation, distributed tracing, and
 * multi-tenancy isolation throughout the server.
 *
 * Key behaviors:
 * - `requestId` is inherited from a parent context when provided, otherwise a
 *   new unique ID is generated via {@link generateRequestContextId}.
 * - `tenantId` is resolved with the following priority (highest to lowest):
 *   `additionalContext.tenantId` → spread rest params → `parentContext.tenantId`
 *   → AsyncLocalStorage auth store (`authContext`).
 * - OpenTelemetry `traceId` and `spanId` are automatically injected from the
 *   active span when one exists.
 * - Auth data is populated separately via {@link requestContextService.withAuthInfo}
 *   and is NOT stored in AsyncLocalStorage by this module — ALS propagation is
 *   the responsibility of auth middleware.
 *
 * @module src/utils/internal/requestContext
 */
import { trace } from '@opentelemetry/api';

import { authContext as alsAuthContext } from '@/mcp-server/transports/auth/lib/authContext.js';
import type { AuthInfo } from '@/mcp-server/transports/auth/lib/authTypes.js';
import { generateRequestContextId } from '@/utils/security/idGenerator.js';

/** Maps validated token info to the context-facing auth shape. */
function toAuthContext(info: AuthInfo): AuthContext {
  return {
    sub: info.subject ?? info.clientId,
    scopes: info.scopes,
    clientId: info.clientId,
    ...(info.tenantId && { tenantId: info.tenantId }),
    ...(info.token && { token: info.token }),
  };
}

/**
 * Processed authentication data extracted from a validated JWT or OAuth token
 * and attached to a {@link RequestContext} by {@link requestContextService.withAuthInfo}.
 *
 * Fields map directly to standard JWT claims: `clientId` ← `cid`/`client_id`,
 * `scopes` ← `scp`/`scope` (split to array), `sub` ← `sub`, `tenantId` ← `tid`.
 *
 * @example
 * ```typescript
 * const auth: AuthContext = ctx.auth!;
 * if (auth.scopes.includes('tool:my_tool:read')) {
 *   // authorized
 * }
 * ```
 */
export interface AuthContext {
  /** The client application identifier (`cid` or `client_id` JWT claim). */
  clientId: string;
  /** Granted permission scopes derived from the `scp` or `scope` JWT claim. */
  scopes: string[];
  /** Subject identifier — the user or service principal (`sub` JWT claim). Falls back to `clientId` when `sub` is absent. */
  sub: string;
  /** Tenant identifier from the `tid` JWT claim. Present only for multi-tenant tokens. */
  tenantId?: string;
  /**
   * Raw bearer token from the validated request. Forwarded so handlers can
   * relay it to upstream APIs in on-behalf-of / PAT pass-through flows.
   * Redacted by the framework logger's pino-redact paths (`*.token`,
   * `*.*.token`); avoid passing it to telemetry sinks that bypass that layer.
   */
  token?: string;
  /** Additional token payload properties not mapped to named fields. */
  // allow open-indexed-named: forward-extensibility for future JWT claims (post-#121)
  [key: string]: unknown;
}

/**
 * Core context object associated with a single request or operation.
 *
 * Every handler invocation receives a `RequestContext` (wrapped in `Context`).
 * It is the authoritative source for log correlation (`requestId`, `traceId`,
 * `spanId`), multi-tenancy isolation (`tenantId`), and auth (`auth`).
 *
 * Optional fields (`traceId`, `spanId`) are injected automatically when an
 * OpenTelemetry active span exists at context-creation time.
 *
 * @example
 * ```typescript
 * // Read-only access in a handler
 * ctx.log.info('Handling request', { requestId: ctx.requestId, tenantId: ctx.tenantId });
 * ```
 */
export interface RequestContext {
  /**
   * Authentication data populated by {@link requestContextService.withAuthInfo}.
   * `undefined` when the request is unauthenticated or auth mode is `none`.
   */
  auth?: AuthContext | undefined;
  /**
   * Unique identifier for this request, used to correlate log entries across
   * service boundaries. Inherited from a parent context when provided;
   * otherwise generated by {@link generateRequestContextId}.
   */
  requestId: string;

  /**
   * OpenTelemetry span ID from the active span at context-creation time.
   * `undefined` when no active span exists.
   */
  spanId?: string | undefined;

  /**
   * Tenant identifier used for multi-tenancy data isolation.
   * Resolved in priority order: `additionalContext.tenantId` →
   * rest params → `parentContext.tenantId` → AsyncLocalStorage auth store.
   */
  tenantId?: string | undefined;

  /**
   * ISO 8601 UTC timestamp recorded at the moment this context was created.
   */
  timestamp: string;

  /**
   * OpenTelemetry trace ID from the active span at context-creation time.
   * `undefined` when no active span exists.
   */
  traceId?: string | undefined;

  /**
   * Additional arbitrary key-value pairs for operation-specific context.
   * Consumers must type-check before accessing extended properties.
   *
   * Note: optional fields above are typed `?: T | undefined` (not just `?: T`)
   * so the handler-facing `Context` — which carries strict-optional fields
   * under `exactOptionalPropertyTypes` — is structurally assignable to
   * {@link RequestContextLike} (the closed canonical-field projection used by
   * service-layer signatures). `RequestContext` itself remains the open
   * "context bag" that the logger and `createRequestContext` build by spread.
   */
  // allow open-indexed-named: central context bag — handlers attach ad-hoc operation context
  [key: string]: unknown;
}

/**
 * Closed, canonical-field projection of {@link RequestContext} for service
 * signatures. The handler-facing `Context` carries strict-optional fields with
 * `| undefined` and lacks the `[key: string]: unknown` index signature, so it
 * is not assignable to `RequestContext` itself; this projection is what both
 * `Context` and `RequestContext` are structurally assignable to.
 *
 * Service and canvas method signatures accept `RequestContextLike` instead of
 * `RequestContext` so consumers can pass the handler `ctx` directly without a
 * slice helper. Internal context construction (`createRequestContext`,
 * `enriched` log dispatch) still produces `RequestContext`, preserving the
 * open-bag spread pattern.
 */
export type RequestContextLike = {
  auth?: AuthContext | undefined;
  requestId: string;
  spanId?: string | undefined;
  tenantId?: string | undefined;
  timestamp: string;
  traceId?: string | undefined;
};

/**
 * Parameters accepted by {@link requestContextService.createRequestContext}.
 *
 * Named fields (`parentContext`, `additionalContext`, `operation`) receive
 * special handling. All other properties are spread directly onto the resulting
 * {@link RequestContext} as-is, allowing ad-hoc context extension without
 * modifying this interface.
 *
 * @example
 * ```typescript
 * const ctx = requestContextService.createRequestContext({
 *   operation: 'fetchUser',
 *   parentContext: incomingCtx,
 *   additionalContext: { userId: '123' },
 * });
 * ```
 */
export interface CreateRequestContextParams {
  /**
   * Key-value pairs merged into the context after `parentContext` properties
   * but before `requestId`/`timestamp` are pinned. Takes precedence over
   * `parentContext` for overlapping keys. `requestId` and `timestamp` keys in
   * `additionalContext` are silently stripped to prevent overwrites.
   */
  additionalContext?: Record<string, unknown>;

  /**
   * Human-readable label for the operation creating this context.
   * Written onto the context as `context.operation` when provided.
   */
  operation?: string;

  /**
   * Parent context whose properties are inherited as the base. When
   * `parentContext.requestId` is a non-empty string it is reused as-is,
   * preserving the same request ID across a call chain.
   */
  parentContext?: Record<string, unknown> | RequestContext | undefined;

  /** Any additional key-value pairs spread directly onto the resulting context. */
  // allow open-indexed-named: spread params semantics — documented
  [key: string]: unknown;
}

/**
 * Singleton-like service object for managing request context operations.
 * @private
 */
const requestContextServiceInstance = {
  /**
   * Creates a new {@link RequestContext} with a guaranteed unique `requestId`
   * and current ISO 8601 `timestamp`.
   *
   * Merge order (later entries win, except `requestId`/`timestamp` which are
   * always generated fresh or inherited):
   * 1. `parentContext` properties (base)
   * 2. Remaining spread params (everything in `params` except named fields)
   * 3. `additionalContext` properties (`requestId`/`timestamp` stripped)
   * 4. Resolved `requestId` and `timestamp` (canonical, never overwritten)
   * 5. Resolved `tenantId` (see priority order in {@link RequestContext.tenantId})
   * 6. `operation` (written as `context.operation` when provided)
   * 7. OTel `traceId`/`spanId` from the active span (when present)
   *
   * @param params - Context creation parameters. Defaults to `{}`.
   * @returns A fully constructed `RequestContext`.
   *
   * @example
   * ```typescript
   * // Minimal — generates a fresh requestId and timestamp
   * const ctx = requestContextService.createRequestContext();
   *
   * // Propagate an existing request ID across a call chain
   * const childCtx = requestContextService.createRequestContext({
   *   parentContext: incomingCtx,
   *   operation: 'processItem',
   *   additionalContext: { itemId: item.id },
   * });
   * ```
   */
  createRequestContext(params: CreateRequestContextParams = {}): RequestContext {
    const { parentContext, additionalContext, operation, ...rest } = params;

    const inheritedContext =
      parentContext && typeof parentContext === 'object' ? { ...parentContext } : {};

    const authStore = alsAuthContext.getStore();
    const tenantIdFromAuth = authStore?.authInfo?.tenantId;

    // Bridge auth info from ALS into the context so ctx.auth is populated
    // in tool/resource handlers without requiring a separate withAuthInfo() call.
    const authFromStore: AuthContext | undefined = authStore?.authInfo
      ? toAuthContext(authStore.authInfo)
      : undefined;

    const requestId =
      typeof inheritedContext.requestId === 'string' && inheritedContext.requestId
        ? inheritedContext.requestId
        : generateRequestContextId();
    const timestamp = new Date().toISOString();

    const extractTenantId = (obj: Record<string, unknown> | undefined): string | undefined =>
      obj != null && typeof obj.tenantId === 'string' ? obj.tenantId : undefined;

    const resolvedTenantId =
      extractTenantId(additionalContext) ??
      extractTenantId(rest as Record<string, unknown>) ??
      extractTenantId(inheritedContext) ??
      tenantIdFromAuth;

    // Strip system fields from additionalContext to prevent overwriting requestId/timestamp
    const {
      requestId: _r,
      timestamp: _t,
      ...safeAdditional
    } = additionalContext && typeof additionalContext === 'object' ? additionalContext : {};

    const context: RequestContext = {
      ...inheritedContext,
      ...rest, // Spread any other properties from the params object
      ...safeAdditional,
      requestId,
      timestamp,
      ...(resolvedTenantId ? { tenantId: resolvedTenantId } : {}),
      ...(operation && typeof operation === 'string' ? { operation } : {}),
      ...(authFromStore && { auth: authFromStore }),
    };

    // --- OpenTelemetry Integration ---
    const activeSpan = trace.getActiveSpan();
    if (activeSpan && typeof activeSpan.spanContext === 'function') {
      const spanContext = activeSpan.spanContext();
      if (spanContext) {
        context.traceId = spanContext.traceId;
        context.spanId = spanContext.spanId;
      }
    }
    // --- End OpenTelemetry Integration ---

    return context;
  },

  /**
   * Creates a new {@link RequestContext} enriched with authentication information.
   * This method populates the context with auth data from a validated token,
   * including tenant ID, client ID, scopes, and subject.
   *
   * **Note:** This method builds and returns a `RequestContext` object only.
   * It does NOT propagate auth into `AsyncLocalStorage`. ALS propagation is
   * handled by the auth middleware via `authContext.run()`. If you need
   * scope enforcement via `withRequiredScopes()`, ensure the call runs
   * inside the middleware's ALS continuation.
   *
   * @param authInfo - The validated authentication information from JWT/OAuth token.
   * @param parentContext - Optional parent context to inherit properties from.
   * @returns A new `RequestContext` object with auth information populated.
   *
   * @example
   * ```typescript
   * const authInfo = await jwtStrategy.verify(token);
   * const context = requestContextService.withAuthInfo(authInfo);
   * // context now includes: { requestId, timestamp, tenantId, auth: {...}, ... }
   * ```
   */
  withAuthInfo(
    authInfo: AuthInfo,
    parentContext?: Record<string, unknown> | RequestContext,
  ): RequestContext {
    // Inherit the operation from parentContext when provided, so callers that
    // already established an operation name (e.g. 'AutoTaskHandler') keep it.
    const parentOp =
      parentContext && typeof (parentContext as Record<string, unknown>).operation === 'string'
        ? ((parentContext as Record<string, unknown>).operation as string)
        : undefined;

    const baseContext = this.createRequestContext({
      operation: parentOp ?? 'withAuthInfo',
      parentContext,
      additionalContext: {
        tenantId: authInfo.tenantId,
      },
    });

    return {
      ...baseContext,
      auth: toAuthContext(authInfo),
    };
  },
};

/**
 * Service for creating and enriching {@link RequestContext} instances.
 *
 * The two primary methods cover the full lifecycle:
 * - {@link requestContextService.createRequestContext} — general-purpose context
 *   creation with parent propagation and OTel integration.
 * - {@link requestContextService.withAuthInfo} — enriches a context with
 *   structured auth data from a validated token.
 *
 * @example
 * ```typescript
 * import { requestContextService } from '@/utils/internal/requestContext.js';
 *
 * // Create a root context for an incoming request
 * const ctx = requestContextService.createRequestContext({ operation: 'handleRequest' });
 *
 * // Enrich with auth after token verification
 * const authCtx = requestContextService.withAuthInfo(verifiedAuthInfo, ctx);
 * ```
 */
export const requestContextService = requestContextServiceInstance;
