import type { HttpContractConfig } from "../contracts/index.js";
import type {
  AnyPorts,
  BoundGate,
  GatePort,
  PolicyDefinition,
} from "../ports/index.js";
import type { TraceContext } from "../tracing/index.js";
import type { HttpRequestLike, MaybePromise } from "./http.js";
import type { TrustedRequestInfo } from "./trusted-proxy.js";

/**
 * Arguments passed to the `context.request` factory after a route is matched.
 */
export type RequestContextArgs<Ports extends AnyPorts = AnyPorts> = {
  /**
   * Framework-neutral request.
   */
  req: HttpRequestLike;
  /**
   * Request metadata resolved from the server's explicit trusted-proxy policy.
   * Forwarding headers never affect this value unless
   * `createServer({ trustedProxy })` opts into them.
   */
  requestInfo: TrustedRequestInfo;
  /**
   * Final app ports, including ports contributed by providers.
   */
  ports: Ports;
  /**
   * Matched contract, when the request resolved to a registered route.
   */
  contract?: HttpContractConfig;
  /**
   * Request correlation ID resolved by the server from the configured request
   * ID header, or generated when the request did not send one.
   */
  requestId: string;
  /**
   * W3C trace context resolved by the server from the incoming `traceparent`
   * header, or generated when the request did not send one. Spread this into
   * the context (`...trace`) to correlate downstream activity.
   */
  trace: TraceContext;
};

/**
 * Arguments passed to the `context.service` factory.
 */
export type ServiceContextArgs<
  Ports extends AnyPorts = AnyPorts,
  ServiceInput = void,
> = {
  /**
   * Final app ports, including ports contributed by providers.
   */
  ports: Ports;
  /**
   * Caller-provided input such as the service actor or tenant.
   */
  input: ServiceInput;
  /**
   * Fresh correlation ID generated for this service context.
   */
  requestId: string;
  /**
   * Fresh W3C trace context generated for this service context. Spread this
   * into the context (`...trace`) to correlate downstream activity.
   */
  trace: TraceContext;
};

/**
 * App context without its `gate` property.
 *
 * Context factories return seeds; the server attaches the gate declared by
 * the blueprint's `gate` selector, so hand-binding `gate` in a factory is a
 * type error.
 */
export type ContextSeed<Ctx> = Omit<Ctx, "gate"> & { gate?: never };

type ContextGateOption<Ctx, Ports extends AnyPorts> = [Ctx] extends [
  { gate: BoundGate<infer TPolicies extends readonly PolicyDefinition[]> },
]
  ? {
      /**
       * Select the gate port the server attaches to every context it builds.
       */
      gate: (ports: Ports) => GatePort<ContextSeed<Ctx>, TPolicies>;
    }
  : {
      /**
       * Gate selection is only available when the context type declares a
       * `gate` property.
       */
      gate?: never;
    };

/**
 * Context blueprint accepted by `createServer(...)`.
 *
 * The server owns context assembly: `request` and `service` return context
 * seeds, and the server attaches the gate declared by `gate` so identity
 * changes can never authorize against a stale context.
 */
export type ServerContextOptions<
  Ctx,
  Ports extends AnyPorts = AnyPorts,
  ServiceInput = void,
> = {
  /**
   * Build the per-request context seed.
   */
  request: (args: RequestContextArgs<Ports>) => MaybePromise<ContextSeed<Ctx>>;
  /**
   * Build a service context seed for schedules, outbox drains, tasks, and
   * background work. Required before `server.createServiceContext(...)` can
   * be called.
   */
  service?: (
    args: ServiceContextArgs<Ports, ServiceInput>,
  ) => MaybePromise<ContextSeed<Ctx>>;
} & ContextGateOption<Ctx, Ports>;

/**
 * Context configuration accepted by `createServer(...)`.
 *
 * Contexts without a `gate` property may use the plain request-factory
 * shorthand. Contexts with a `gate` must use the blueprint form so the server
 * owns gate attachment.
 */
export type ServerContextConfig<
  Ctx,
  Ports extends AnyPorts = AnyPorts,
  ServiceInput = void,
> = [Ctx] extends [{ gate: unknown }]
  ? ServerContextOptions<Ctx, Ports, ServiceInput>
  :
      | ((args: RequestContextArgs<Ports>) => MaybePromise<Ctx>)
      | ServerContextOptions<Ctx, Ports, ServiceInput>;

/**
 * Argument tuple for `createServiceContext(...)`.
 *
 * Servers without a declared service input are callable with no arguments;
 * optional inputs stay optional at the call site.
 */
export type ServiceContextInputArgs<ServiceInput> = [ServiceInput] extends [
  // biome-ignore lint/suspicious/noConfusingVoidType: void marks "no declared service input" here
  void,
]
  ? []
  : undefined extends ServiceInput
    ? [input?: ServiceInput]
    : [input: ServiceInput];

type AnyGateSelector<Ports extends AnyPorts> = (
  ports: Ports,
) => Pick<GatePort<unknown>, "attach">;

/**
 * Normalized runtime view of a server context configuration.
 */
export type ResolvedServerContext<Ctx, Ports extends AnyPorts, ServiceInput> = {
  request: (
    args: RequestContextArgs<Ports>,
  ) => MaybePromise<ContextSeed<Ctx> | Ctx>;
  service?: (
    args: ServiceContextArgs<Ports, ServiceInput>,
  ) => MaybePromise<ContextSeed<Ctx>>;
  gate?: AnyGateSelector<Ports>;
};

/**
 * Normalize a server context configuration into its runtime parts.
 */
export function resolveServerContext<Ctx, Ports extends AnyPorts, ServiceInput>(
  config: ServerContextConfig<Ctx, Ports, ServiceInput>,
): ResolvedServerContext<Ctx, Ports, ServiceInput> {
  const value = config as
    | ((args: RequestContextArgs<Ports>) => MaybePromise<Ctx>)
    | (ResolvedServerContext<Ctx, Ports, ServiceInput> & {
        gate?: AnyGateSelector<Ports>;
      });

  if (typeof value === "function") {
    return { request: value };
  }

  return {
    request: value.request,
    service: value.service,
    gate: value.gate,
  };
}

/**
 * Create the context finalizer for a resolved server context.
 *
 * When the blueprint declares a gate, the finalizer strips hand-assigned
 * `gate` values from seeds and attaches the live gate. Without a gate
 * declaration the seed is the context.
 */
export function createContextFinalizer<
  Ctx,
  Ports extends AnyPorts,
  ServiceInput,
>(
  resolved: ResolvedServerContext<Ctx, Ports, ServiceInput>,
  getPorts: () => Ports,
): (seed: ContextSeed<Ctx> | Ctx) => Ctx {
  const gateSelector = resolved.gate;
  if (!gateSelector) {
    return (seed) => seed as Ctx;
  }

  return (seed) => {
    const target = seed as object;
    const existing = Object.getOwnPropertyDescriptor(target, "gate");
    if (existing && existing.get === undefined) {
      if (process.env.NODE_ENV !== "production") {
        console.warn(
          "[beignet] Ignoring a hand-assigned ctx.gate value. The server context blueprint owns gate attachment; remove `gate` from context factories and hook additions.",
        );
      }
      delete (target as { gate?: unknown }).gate;
    }

    return gateSelector(getPorts()).attach(target) as Ctx;
  };
}
