import type { StandardSchemaV1 } from "@standard-schema/spec";
import type {
  HttpContractConfig,
  InferOutput,
  Success2xxKeys,
} from "../contracts/index.js";
import { inferSoleSuccessStatus } from "../contracts/index.js";
import type { ContractLike, ResolveContract } from "./contract-like.js";
import type {
  AddedCtxFromHooks,
  Handler,
  InferBody,
  InferHeaders,
  InferPath,
  InferQuery,
  RouteHook,
} from "./http.js";

/**
 * Structural shape of a finalized use case accepted by the route binder.
 *
 * This intentionally mirrors `UseCaseDef` from `@beignet/core/application`
 * without importing it, so the server runtime stays decoupled from the
 * application builder at runtime.
 */
export type AnyUseCaseLike = {
  /**
   * Stable use-case name, used in binder diagnostics.
   */
  name: string;
  /**
   * Input schema declared with `.input(...)`.
   */
  inputSchema: StandardSchemaV1;
  /**
   * Output schema declared with `.output(...)`.
   */
  outputSchema: StandardSchemaV1;
  /**
   * Execute the use case with application context and typed input.
   */
  run: (args: never) => Promise<unknown>;
};

type UseCaseRouteCtx<UC> = UC extends {
  run: (args: { ctx: infer Ctx; input: infer _Input }) => Promise<infer _Out>;
}
  ? Ctx
  : never;

/**
 * Input type accepted by a bound use case's `run(...)`.
 */
export type UseCaseRouteInput<UC> = UC extends {
  run: (args: { ctx: infer _Ctx; input: infer Input }) => Promise<infer _Out>;
}
  ? Input
  : never;

type UseCaseRouteOutput<UC> = UC extends {
  run: (args: { ctx: infer _Ctx; input: infer _Input }) => Promise<infer Out>;
}
  ? Out
  : never;

type ResponseBodyForSchema<S> = S extends null
  ? // biome-ignore lint/suspicious/noConfusingVoidType: void accepts z.void() use case outputs for null response schemas
    void | undefined
  : S extends StandardSchemaV1
    ? InferOutput<S>
    : unknown;

type ResponseForStatus<
  TResponses,
  TStatus extends number,
> = TStatus extends keyof TResponses
  ? TResponses[TStatus]
  : `${TStatus}` extends keyof TResponses
    ? TResponses[`${TStatus}`]
    : never;

type SuccessBodyFromKeys<TResponses, K> = [K] extends [never]
  ? unknown
  : K extends number
    ? ResponseBodyForSchema<ResponseForStatus<TResponses, K>>
    : unknown;

type UnionToIntersection<T> = (
  T extends unknown
    ? (value: T) => void
    : never
) extends (value: infer I) => void
  ? I
  : never;

type BinderStatusFromKeys<K> = [K] extends [never]
  ? {
      /**
       * Success status for the use case result. Required because the contract
       * does not declare exactly one 2xx response.
       */
      status: number;
    }
  : [K] extends [UnionToIntersection<K>]
    ? {
        /**
         * Success status for the use case result. Optional because the
         * contract declares exactly one 2xx response.
         */
        status?: K;
      }
    : {
        /**
         * Success status for the use case result. Required because the
         * contract declares multiple 2xx responses.
         */
        status: K;
      };

/**
 * `status` option for a binder route.
 *
 * Optional and typed to the sole declared 2xx status when the contract
 * declares exactly one, required (typed to the union of declared 2xx
 * statuses) otherwise.
 */
export type BinderStatusOption<C extends HttpContractConfig> =
  BinderStatusFromKeys<Success2xxKeys<C["responses"]>>;

/**
 * Parsed request parts passed to a binder route's `input` mapper.
 */
export type UseCaseRouteInputParts<C extends HttpContractConfig> = {
  /**
   * Parsed path parameters.
   */
  path: InferPath<C>;
  /**
   * Parsed query parameters.
   */
  query: InferQuery<C>;
  /**
   * Parsed request headers.
   */
  headers: InferHeaders<C>;
  /**
   * Parsed request body.
   */
  body: InferBody<C>;
};

/**
 * Constraint that checks a use case against the route that binds it.
 *
 * Produces a readable branded mismatch object on the `useCase` property when
 * the use case requires a context the server does not provide, or when its
 * output does not match the contract's declared success response schema.
 */
export type UseCaseFitsRoute<Ctx, C extends HttpContractConfig, UC> = [
  Ctx,
] extends [UseCaseRouteCtx<UC>]
  ? [UseCaseRouteOutput<UC>] extends [
      SuccessBodyFromKeys<C["responses"], Success2xxKeys<C["responses"]>>,
    ]
    ? unknown
    : {
        "~beignetError": "useCase output does not match the contract's success response schema";
      }
  : {
      "~beignetError": "useCase requires a context this server does not provide";
    };

type UseCaseRouteShape<
  HandlerCtx,
  CLike extends ContractLike,
  C extends HttpContractConfig,
  UC extends AnyUseCaseLike,
  Hooks,
> = {
  /**
   * Contract builder or plain contract config for this route.
   */
  contract: CLike;
  /**
   * Route-scoped hooks that run after group hooks and before the use case.
   */
  hooks?: Hooks;
  /**
   * Use case bound directly to the contract.
   */
  useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
  /**
   * Map parsed request parts to the use case input.
   *
   * Defaults to `defaultBinderInput`, which merges query, body, and path
   * objects (path wins collisions) and never merges headers.
   */
  input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
  handle?: never;
} & BinderStatusOption<C>;

/**
 * Route registration that binds a contract directly to a use case.
 *
 * The server synthesizes the handler: it maps parsed request parts to the use
 * case input, runs the use case, and returns its output as the success
 * response body. Use a full `handle` route for headers, streaming, native
 * `Response` values, or multi-status handling.
 */
export type UseCaseRouteDef<
  Ctx,
  CLike extends ContractLike,
  UC extends AnyUseCaseLike,
  Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
> = UseCaseRouteShape<
  Ctx & AddedCtxFromHooks<Hooks>,
  CLike,
  ResolveContract<CLike>,
  UC,
  Hooks
>;

/**
 * Structural check that a use case accepts the context this route provides.
 *
 * Enforced through `run` parameter contravariance so it applies even at loose
 * collection boundaries where contract types are erased.
 */
export type UseCaseAcceptsCtx<Ctx> = {
  run: (args: { ctx: Ctx; input: never }) => Promise<unknown>;
};

/**
 * Loosely typed binder route used at collection boundaries where contract and
 * use case types are erased. The use case's context requirement is still
 * checked against the server context.
 */
export type AnyUseCaseRouteDef<
  Ctx,
  CLike extends ContractLike = ContractLike,
  Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
    Ctx,
    object
  >[],
> = {
  contract: CLike;
  hooks?: Hooks;
  useCase: AnyUseCaseLike & UseCaseAcceptsCtx<Ctx & AddedCtxFromHooks<Hooks>>;
  // biome-ignore lint/suspicious/noExplicitAny: request part types are erased at collection boundaries
  input?: (parts: any) => unknown;
  status?: number;
  handle?: never;
};

type HooksOf<E> = E extends { hooks: infer H extends readonly unknown[] }
  ? H
  : readonly [];

/**
 * Per-element binder validation applied where route tuples are inferred, such
 * as an app-bound `defineRouteGroup({ ... })`, so contract/use-case mismatches are
 * reported on the individual route literal.
 */
export type ValidatedRouteInput<Ctx, E> = E extends {
  contract: infer CL extends ContractLike;
  useCase: infer UC extends AnyUseCaseLike;
}
  ? ResolveContract<CL> extends infer C extends HttpContractConfig
    ? {
        contract: CL;
        hooks?: HooksOf<E>;
        useCase: UC &
          UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
        input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
        handle?: never;
      } & BinderStatusOption<C>
    : unknown
  : unknown;

/**
 * Element-wise binder validation for a route input list.
 */
export type ValidatedRouteInputs<Ctx, R extends readonly unknown[]> = {
  [K in keyof R]: ValidatedRouteInput<Ctx, R[K]>;
};

/**
 * Trusted run key shared with `@beignet/core/application` via the global
 * symbol registry, so the binder never imports the application builder at
 * runtime.
 */
const USE_CASE_TRUSTED_RUN_KEY: unique symbol = Symbol.for(
  "beignet.useCase.trustedRun",
);

const USE_CASE_OUTPUT_VALIDATED_KEY: unique symbol = Symbol.for(
  "beignet.useCase.outputValidated",
);

type RuntimeUseCase = AnyUseCaseLike & {
  run: (args: { ctx: unknown; input: unknown }) => Promise<unknown>;
  [USE_CASE_TRUSTED_RUN_KEY]?: (args: {
    ctx: unknown;
    input: unknown;
  }) => Promise<unknown>;
  [USE_CASE_OUTPUT_VALIDATED_KEY]?: boolean;
};

/**
 * Loosely typed binder route definition consumed by route registration.
 */
export type RuntimeUseCaseRouteDef = {
  useCase: RuntimeUseCase;
  input?: (parts: {
    path: unknown;
    query: unknown;
    headers: unknown;
    body: unknown;
  }) => unknown;
  status?: number;
};

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

/**
 * Default input mapping for binder routes.
 *
 * Merges parsed query, body, and path objects into one input object. Path
 * keys win all collisions, then body keys, then query keys. Headers are never
 * merged: parsed headers include every raw request header, so merging them
 * would poison the use case input. Non-object bodies (text, arrays, scalars)
 * are excluded. Routes that need headers or non-object bodies declare an
 * explicit `input` mapper.
 */
export function defaultBinderInput(parts: {
  path: unknown;
  query: unknown;
  body: unknown;
}): Record<string, unknown> {
  return {
    ...(isPlainObject(parts.query) ? parts.query : {}),
    ...(isPlainObject(parts.body) ? parts.body : {}),
    ...(isPlainObject(parts.path) ? parts.path : {}),
  };
}

/**
 * Whether a route definition is a binder route.
 */
export function isUseCaseRouteDef(route: {
  handle?: unknown;
  useCase?: unknown;
}): route is RuntimeUseCaseRouteDef {
  return route.useCase !== undefined && route.useCase !== null;
}

function computeTrustedInput(
  contract: HttpContractConfig,
  def: RuntimeUseCaseRouteDef,
): boolean {
  if (def.input) return false;

  const sources = [contract.pathParams, contract.query, contract.body].filter(
    (schema) => schema !== null && schema !== undefined,
  );
  return sources.length === 1 && sources[0] === def.useCase.inputSchema;
}

function computeResponseExemption(
  contract: HttpContractConfig,
  def: RuntimeUseCaseRouteDef,
  status: number,
): number | undefined {
  return def.useCase[USE_CASE_OUTPUT_VALIDATED_KEY] === true &&
    contract.responses[status] === def.useCase.outputSchema
    ? status
    : undefined;
}

/**
 * Synthesize the route handler for a binder route at registration time.
 *
 * Resolves the success status, decides whether the validated request parts can
 * skip the use case's input parse, and computes whether server-side response
 * validation is redundant for the success status.
 */
export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
  contract: C,
  def: RuntimeUseCaseRouteDef,
): {
  handler: Handler<Ctx, C>;
  responseValidationExemptStatus?: number;
} {
  const status = def.status ?? inferSoleSuccessStatus(contract);
  if (status === undefined) {
    throw new Error(
      `Route binder for contract "${contract.name}" cannot infer a success ` +
        `status: the contract declares ${
          Object.keys(contract.responses).length === 0
            ? "no responses"
            : "zero or multiple 2xx responses"
        }. Declare exactly one 2xx response or pass an explicit status.`,
    );
  }

  const mapInput = def.input ?? defaultBinderInput;
  const trustedRun = computeTrustedInput(contract, def)
    ? def.useCase[USE_CASE_TRUSTED_RUN_KEY]
    : undefined;
  const run = trustedRun ?? def.useCase.run;

  const handler: Handler<Ctx, C> = async ({
    ctx,
    path,
    query,
    headers,
    body,
  }) => ({
    status,
    body: await run.call(def.useCase, {
      ctx,
      input: mapInput({ path, query, headers, body }),
    }),
  });

  return {
    handler,
    responseValidationExemptStatus: computeResponseExemption(
      contract,
      def,
      status,
    ),
  };
}
