import {
  BEIGNET_ERROR_OWNER_HEADER,
  type HttpContractConfig,
} from "../contracts/index.js";
import type { AnyPorts } from "../ports/index.js";
import { redactValue } from "../ports/redaction.js";
import {
  type ProviderInstrumentationEventInput,
  type ProviderInstrumentationPort,
  resolveProviderInstrumentationPort,
} from "../providers/instrumentation.js";
import {
  createTraceContext,
  parseTraceparent,
  resolveTracingPort,
  type TraceContext,
  type TracingPort,
} from "../tracing/index.js";
import type {
  HttpRequestLike,
  HttpResponseHeaders,
  HttpResponseLike,
  ServerHook,
} from "./http.js";
import {
  clearActiveRequestContext,
  enterActiveRequestContext,
  readContextActor,
  readContextTenant,
} from "./request-context.js";

/**
 * Options for the server-owned request instrumentation pipeline.
 *
 * The server resolves request IDs and W3C trace context before user hooks and
 * context creation run, writes them to response headers, and records request
 * and error events into the resolved provider instrumentation port
 * (`ports.instrumentation`, then `ports.devtools`) after responses are sent.
 *
 * Pass `instrumentation: false` to `createServer(...)` to disable headers and
 * event recording entirely.
 */
export interface ServerInstrumentationOptions<Ctx = unknown> {
  /**
   * Request/response header used for the request correlation ID.
   *
   * Pass `false` to avoid reading or writing a request ID header.
   *
   * @default "x-request-id"
   */
  requestIdHeader?: string | false;

  /**
   * W3C trace context header used to correlate events with distributed
   * traces.
   *
   * Pass `false` to avoid reading or writing a trace context header.
   *
   * @default "traceparent"
   */
  traceContextHeader?: string | false;

  /**
   * Request path prefixes that should not record instrumentation events.
   * Ambient correlation still runs, and enabled response headers are written.
   *
   * Defaults to the devtools dashboard prefix so its polling traffic does not
   * fill the event timeline.
   *
   * @default ["/api/devtools"]
   */
  ignorePaths?: readonly string[];

  /**
   * Apply a custom redactor to events produced by the server. Sink-level
   * redaction (such as the devtools redactor) still runs when events are
   * stored.
   */
  redact?: (
    event: ProviderInstrumentationEventInput,
  ) => ProviderInstrumentationEventInput;

  /**
   * Decide whether to capture a completed request event.
   */
  shouldCapture?: (args: {
    req: HttpRequestLike;
    ctx?: Ctx;
    contract: HttpContractConfig;
    response: HttpResponseLike;
    error?: unknown;
  }) => boolean;
}

/**
 * Correlation values resolved by the server for one request or service
 * context.
 */
export interface RequestCorrelation {
  /**
   * Request correlation ID.
   */
  requestId: string;
  /**
   * W3C trace context.
   */
  trace: TraceContext;
}

/**
 * Internal runtime created by `createServer(...)` from its `instrumentation`
 * option.
 */
export interface ServerInstrumentationRuntime<Ctx> {
  /**
   * Resolve the instrumentation sink once final ports are known.
   */
  attachPorts(ports: AnyPorts): void;
  /**
   * Resolve (and cache per request) the request ID and trace context.
   */
  prepareRequest(req: HttpRequestLike): RequestCorrelation;
  /**
   * Create fresh correlation values for a service context.
   */
  createServiceCorrelation(): RequestCorrelation;
  /**
   * Pipeline hook installed before user hooks. It always owns ambient
   * correlation; response headers and event recording remain configurable.
   */
  hook: ServerHook<Ctx, AnyPorts>;
}

type TraceContextFields = {
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
  tracestate?: string;
};

function getContextRequestId(ctx: unknown): string | undefined {
  if (!ctx || typeof ctx !== "object") return undefined;
  const requestId = (ctx as { requestId?: unknown }).requestId;
  return typeof requestId === "string" ? requestId : undefined;
}

function getContextTraceContext(ctx: unknown): TraceContextFields | undefined {
  if (!ctx || typeof ctx !== "object") return undefined;
  const context = ctx as TraceContextFields;
  if (
    !context.traceId &&
    !context.spanId &&
    !context.parentSpanId &&
    !context.traceparent &&
    !context.tracestate
  ) {
    return undefined;
  }
  return {
    traceId: context.traceId,
    spanId: context.spanId,
    parentSpanId: context.parentSpanId,
    traceparent: context.traceparent,
    tracestate: context.tracestate,
  };
}

function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === "string") return error;
  return "Unknown error";
}

function getErrorStack(error: unknown): string | undefined {
  return error instanceof Error ? error.stack : undefined;
}

function createRequestId(): string {
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
    return crypto.randomUUID();
  }
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function getPathname(req: HttpRequestLike): string {
  try {
    return new URL(req.url).pathname;
  } catch {
    return req.url;
  }
}

function isIgnoredPath(
  pathname: string,
  ignorePaths: readonly string[],
): boolean {
  return ignorePaths.some((ignorePath) => {
    const normalized = ignorePath.replace(/\/+$/, "");
    return pathname === normalized || pathname.startsWith(`${normalized}/`);
  });
}

function requestHeadersToRecord(headers: Headers): Record<string, string> {
  const record: Record<string, string> = {};
  headers.forEach((value, key) => {
    record[key] = value;
  });
  return record;
}

function getResponseHeader(
  headers: HttpResponseHeaders | undefined,
  name: string,
): string | undefined {
  if (!headers) return undefined;
  const direct = headers[name];
  if (direct !== undefined) {
    return typeof direct === "string" ? direct : direct[0];
  }
  const normalized = name.toLowerCase();
  const entry = Object.entries(headers).find(
    ([key]) => key.toLowerCase() === normalized,
  );
  const value = entry?.[1];
  return typeof value === "string" ? value : value?.[0];
}

function getResponseOwner(
  response: HttpResponseLike,
): "route" | "framework" | "transport" | "unknown" {
  return getResponseHeader(response.headers, BEIGNET_ERROR_OWNER_HEADER) ===
    "framework"
    ? "framework"
    : "route";
}

function isWatcherEnabled(
  port: ProviderInstrumentationPort,
  name: string,
): boolean {
  return port.isWatcherEnabled?.(name) ?? true;
}

/**
 * Create the server-owned instrumentation runtime for `createServer(...)`.
 *
 * Correlation values (request ID and trace context) are always resolved so
 * context factories receive stable `requestId`/`trace` arguments, even when
 * instrumentation is disabled. Headers and event recording only run when
 * instrumentation is enabled.
 */
export function createServerInstrumentation<Ctx>(
  options: ServerInstrumentationOptions<Ctx> | false | undefined,
): ServerInstrumentationRuntime<Ctx> {
  const enabled = options !== false;
  const resolvedOptions =
    options === false || options === undefined ? {} : options;
  const requestIdHeader = resolvedOptions.requestIdHeader ?? "x-request-id";
  const traceContextHeader =
    resolvedOptions.traceContextHeader ?? "traceparent";
  const ignorePaths = resolvedOptions.ignorePaths ?? ["/api/devtools"];

  let port: ProviderInstrumentationPort | undefined;
  let tracing: TracingPort | undefined;
  const correlations = new WeakMap<HttpRequestLike, RequestCorrelation>();

  const prepareRequest = (req: HttpRequestLike): RequestCorrelation => {
    const cached = correlations.get(req);
    if (cached) return cached;

    const headerRequestId =
      requestIdHeader === false
        ? undefined
        : (req.headers.get(requestIdHeader) ?? undefined);
    const headerTraceparent =
      traceContextHeader === false
        ? undefined
        : (req.headers.get(traceContextHeader) ?? undefined);
    const parsedTraceparent = parseTraceparent(headerTraceparent);

    const activeTrace = tracing?.current();
    const correlation: RequestCorrelation = {
      requestId: headerRequestId ?? createRequestId(),
      trace:
        activeTrace ??
        createTraceContext({
          traceparent: parsedTraceparent?.traceparent,
          tracestate: parsedTraceparent
            ? (req.headers.get("tracestate") ?? undefined)
            : undefined,
        }),
    };
    correlations.set(req, correlation);
    return correlation;
  };

  const resolveRequestId = (args: {
    req: HttpRequestLike;
    ctx?: unknown;
  }): string =>
    getContextRequestId(args.ctx) ?? prepareRequest(args.req).requestId;

  const resolveTraceContext = (args: {
    req: HttpRequestLike;
    ctx?: unknown;
  }): TraceContext => {
    const activeTrace = tracing?.current();
    if (activeTrace) return activeTrace;

    const contextTrace = getContextTraceContext(args.ctx);
    if (contextTrace) {
      return createTraceContext(contextTrace);
    }
    return prepareRequest(args.req).trace;
  };

  const record = (event: ProviderInstrumentationEventInput) => {
    if (!port) return;

    let prepared = redactValue(event);
    if (resolvedOptions.redact) {
      try {
        prepared = resolvedOptions.redact(prepared);
      } catch (error) {
        try {
          port.record({
            type: "error",
            message: "Server instrumentation redactor failed",
            owner: "framework",
            details: {
              message: getErrorMessage(error),
            },
          });
        } catch {
          // Instrumentation sinks must never affect responses.
        }
        return;
      }
    }

    try {
      port.record(prepared);
    } catch {
      // Instrumentation sinks must never affect responses.
    }
  };

  const enterAmbientContext = (args: {
    req: HttpRequestLike;
    ctx?: unknown;
  }) => {
    const trace = resolveTraceContext(args);
    enterActiveRequestContext({
      requestId: resolveRequestId(args),
      traceId: trace.traceId,
      spanId: trace.spanId,
      parentSpanId: trace.parentSpanId,
      traceparent: trace.traceparent,
      tracestate: trace.tracestate,
      actor: readContextActor(args.ctx),
      tenant: readContextTenant(args.ctx),
    });
  };

  const hook: ServerHook<Ctx, AnyPorts> = {
    name: "beignet.instrumentation",
    onRequest: ({ req }) => {
      enterAmbientContext({ req });
      return undefined;
    },
    beforeHandle: ({ req, ctx }) => {
      // Re-enter with context values so app-owned overrides win for ambient
      // correlation inheritance.
      enterAmbientContext({ req, ctx });
      return undefined;
    },
    beforeSend: ({ req, ctx, response }) => {
      if (
        !enabled ||
        (requestIdHeader === false && traceContextHeader === false)
      ) {
        return undefined;
      }

      const requestId = resolveRequestId({ req, ctx });
      const trace = resolveTraceContext({ req, ctx });
      return {
        ...response,
        headers: {
          ...response.headers,
          ...(requestIdHeader === false
            ? {}
            : { [requestIdHeader]: requestId }),
          ...(traceContextHeader === false
            ? {}
            : { [traceContextHeader]: trace.traceparent }),
        },
      };
    },
    afterSend: ({
      req,
      ctx,
      contract,
      response,
      error,
      durationMs,
      stages,
    }) => {
      try {
        if (!port) return;

        const path = getPathname(req);
        if (isIgnoredPath(path, ignorePaths)) return;

        const shouldCaptureRequest = isWatcherEnabled(port, "requests");
        const shouldCaptureError =
          Boolean(error) && isWatcherEnabled(port, "errors");
        if (!shouldCaptureRequest && !shouldCaptureError) return;

        if (
          resolvedOptions.shouldCapture &&
          !resolvedOptions.shouldCapture({
            req,
            ctx: ctx as Ctx | undefined,
            contract,
            response,
            error,
          })
        ) {
          return;
        }

        const requestId = resolveRequestId({ req, ctx });
        const trace = resolveTraceContext({ req, ctx });
        const responseOwner = getResponseOwner(response);

        if (shouldCaptureRequest) {
          record({
            type: "request",
            requestId,
            traceId: trace.traceId,
            spanId: trace.spanId,
            parentSpanId: trace.parentSpanId,
            traceparent: trace.traceparent,
            tracestate: trace.tracestate,
            method: req.method,
            path,
            contractName: contract.name,
            responseOwner,
            status: response.status,
            durationMs,
            stages,
            details: {
              headers: requestHeadersToRecord(req.headers),
              route: {
                contractName: contract.name,
                method: req.method,
                path,
              },
              response: {
                owner: responseOwner,
                status: response.status,
              },
              hookPhases: [
                "onRequest",
                "beforeHandle",
                "beforeSend",
                "afterSend",
              ],
            },
          });
        }

        if (error && shouldCaptureError) {
          record({
            type: "error",
            requestId,
            traceId: trace.traceId,
            spanId: trace.spanId,
            parentSpanId: trace.parentSpanId,
            traceparent: trace.traceparent,
            tracestate: trace.tracestate,
            message: getErrorMessage(error),
            stack: getErrorStack(error),
            contractName: contract.name,
            owner: responseOwner === "framework" ? "framework" : "route",
          });
        }
      } finally {
        clearActiveRequestContext();
      }
    },
  };

  return {
    attachPorts(ports) {
      tracing = resolveTracingPort(ports);
      if (!enabled) return;
      port = resolveProviderInstrumentationPort(ports);
    },
    prepareRequest,
    createServiceCorrelation: () => ({
      requestId: createRequestId(),
      trace: tracing?.current() ?? createTraceContext(),
    }),
    hook,
  };
}
