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>;
};

type SegmentPathParam<Segment extends string> = Segment extends `:${infer Name}`
  ? Name
  : Segment extends `[${infer Name}]`
    ? Name
    : never;

type PathParamNames<Path extends string> = string extends Path
  ? never
  : Path extends `${infer Segment}/${infer Rest}`
    ? SegmentPathParam<Segment> | PathParamNames<Rest>
    : SegmentPathParam<Path>;

type EmptyBinderInput = Record<never, never>;

declare const UNMERGEABLE_BINDER_INPUT: unique symbol;

type UnmergeableBinderInput = {
  readonly [UNMERGEABLE_BINDER_INPUT]: true;
};

type IsAny<T> = 0 extends 1 & T ? true : false;

type BinderObject<T> =
  IsAny<T> extends true
    ? UnmergeableBinderInput
    : [T] extends [object]
      ? [Extract<T, readonly unknown[]>] extends [never]
        ? T
        : UnmergeableBinderInput
      : UnmergeableBinderInput;

type MergeBinderObjects<LowerPrecedence, HigherPrecedence> = [
  BinderObject<LowerPrecedence>,
] extends [UnmergeableBinderInput]
  ? UnmergeableBinderInput
  : [BinderObject<HigherPrecedence>] extends [UnmergeableBinderInput]
    ? UnmergeableBinderInput
    : Omit<
        BinderObject<LowerPrecedence>,
        keyof BinderObject<HigherPrecedence>
      > &
        BinderObject<HigherPrecedence>;

type InferredPathInput<C extends HttpContractConfig> = string extends C["path"]
  ? UnmergeableBinderInput
  : [PathParamNames<C["path"]>] extends [never]
    ? EmptyBinderInput
    : { [K in PathParamNames<C["path"]>]: string };

type BinderPathInput<C extends HttpContractConfig> =
  C["pathParams"] extends StandardSchemaV1
    ? InferOutput<C["pathParams"]>
    : InferredPathInput<C>;

type BinderQueryInput<C extends HttpContractConfig> =
  C["query"] extends StandardSchemaV1
    ? InferOutput<C["query"]>
    : EmptyBinderInput;

type BinderBodyInput<C extends HttpContractConfig> =
  C["body"] extends StandardSchemaV1
    ? InferOutput<C["body"]>
    : EmptyBinderInput;

type HasPathSchema<C extends HttpContractConfig> =
  C["pathParams"] extends StandardSchemaV1 ? true : false;

type HasQuerySchema<C extends HttpContractConfig> =
  C["query"] extends StandardSchemaV1 ? true : false;

type HasBodySchema<C extends HttpContractConfig> =
  C["body"] extends StandardSchemaV1 ? true : false;

type HasInferredPathInput<C extends HttpContractConfig> =
  string extends C["path"]
    ? true
    : [PathParamNames<C["path"]>] extends [never]
      ? false
      : true;

type MergedBinderInput<C extends HttpContractConfig> = MergeBinderObjects<
  MergeBinderObjects<BinderQueryInput<C>, BinderBodyInput<C>>,
  BinderPathInput<C>
>;

/**
 * Input produced when a binder route omits an explicit `input` mapper.
 *
 * A sole declared request schema passes through unchanged when the literal
 * path has no additional inferred parameters. Every other supported default
 * binding merges object inputs with path over body over query precedence.
 */
type DefaultBinderRouteInput<C extends HttpContractConfig> =
  HasPathSchema<C> extends true
    ? HasQuerySchema<C> extends true
      ? MergedBinderInput<C>
      : HasBodySchema<C> extends true
        ? MergedBinderInput<C>
        : BinderPathInput<C>
    : HasQuerySchema<C> extends true
      ? HasBodySchema<C> extends true
        ? MergedBinderInput<C>
        : HasInferredPathInput<C> extends true
          ? MergedBinderInput<C>
          : BinderQueryInput<C>
      : HasBodySchema<C> extends true
        ? HasInferredPathInput<C> extends true
          ? MergedBinderInput<C>
          : BinderBodyInput<C>
        : BinderPathInput<C>;

type UseCaseRouteInputMode = "default" | "mapped";

/**
 * 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, when its
 * output does not match the contract's declared success response schema, or
 * when the default binder input does not satisfy the use case input.
 */
export type UseCaseFitsRoute<
  Ctx,
  C extends HttpContractConfig,
  UC,
  InputMode extends UseCaseRouteInputMode = "default",
> = [Ctx] extends [UseCaseRouteCtx<UC>]
  ? [UseCaseRouteOutput<UC>] extends [
      SuccessBodyFromKeys<C["responses"], Success2xxKeys<C["responses"]>>,
    ]
    ? InputMode extends "mapped"
      ? unknown
      : [DefaultBinderRouteInput<C>] extends [UseCaseRouteInput<UC>]
        ? unknown
        : {
            "~beignetError": "default binder input does not match the use case input; add an input mapper";
          }
    : {
        "~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;
  handle?: never;
} & (
  | {
      /**
       * Use case bound directly to the contract. The default binder input
       * must satisfy the use case input type.
       */
      useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
      input?: never;
    }
  | {
      /**
       * Use case bound directly to the contract through an explicit input
       * mapper.
       */
      useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC, "mapped">;
      /**
       * Map parsed request parts to the use case input.
       *
       * A sole declared path, query, or body schema is passed through
       * unchanged when no additional path, query, or object body values are
       * present. Otherwise `defaultBinderInput` merges query, body, and path
       * objects (path wins collisions) and never merges headers.
       */
      input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
    }
) &
  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>;
        handle?: never;
      } & (
        | {
            useCase: UC &
              UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
            input?: never;
          }
        | {
            useCase: UC &
              UseCaseFitsRoute<
                Ctx & AddedCtxFromHooks<HooksOf<E>>,
                C,
                UC,
                "mapped"
              >;
            input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
          }
      ) &
        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;
};

type UseCaseInputValidationFailure = Error & {
  name: "UseCaseValidationError";
  phase: "input";
  useCaseName: string;
};

function isUseCaseInputValidationFailure(
  error: unknown,
  useCaseName: string,
): error is UseCaseInputValidationFailure {
  if (!(error instanceof Error)) return false;
  const candidate = error as Partial<UseCaseInputValidationFailure>;
  return (
    candidate.name === "UseCaseValidationError" &&
    candidate.phase === "input" &&
    candidate.useCaseName === useCaseName
  );
}

/**
 * Internal framework error raised when a type-erased binder route produces an
 * input that the bound use case rejects.
 */
export class UseCaseRouteInputValidationError extends Error {
  readonly code = "USE_CASE_INPUT_VALIDATION_ERROR";
  readonly contractName: string;
  readonly useCaseName: string;

  constructor(args: {
    contractName: string;
    useCaseName: string;
    cause: UseCaseInputValidationFailure;
  }) {
    super(
      `Default binder input for contract "${args.contractName}" does not satisfy use case "${args.useCaseName}". Add an explicit input mapper.`,
      { cause: args.cause },
    );
    this.name = "UseCaseRouteInputValidationError";
    this.contractName = args.contractName;
    this.useCaseName = args.useCaseName;
  }
}

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. The route binder passes a sole declared
 * input schema through unchanged when no other object input contains values;
 * this merge handles every other default mapping. Routes that combine a
 * non-object body with another source 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 computeSingleInput(
  contract: HttpContractConfig,
  def: RuntimeUseCaseRouteDef,
): { source: "path" | "query" | "body"; schema: unknown } | undefined {
  if (def.input) return undefined;

  const sources = [
    { source: "path", schema: contract.pathParams },
    {
      source: "query",
      schema: contract.query,
    },
    {
      source: "body",
      schema: contract.body,
    },
  ] as const;
  const present = sources.filter(
    (candidate) => candidate.schema !== null && candidate.schema !== undefined,
  );
  const single = present[0];
  return present.length === 1 && single
    ? { source: single.source, schema: single.schema }
    : undefined;
}

function canPassSingleInput(
  parts: { path: unknown; query: unknown; body: unknown },
  source: "path" | "query" | "body",
): boolean {
  return (["path", "query", "body"] as const).every(
    (candidate) =>
      candidate === source ||
      !isPlainObject(parts[candidate]) ||
      Object.keys(parts[candidate]).length === 0,
  );
}

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 singleInput = computeSingleInput(contract, def);
  const trustedRun =
    singleInput?.schema === def.useCase.inputSchema
      ? def.useCase[USE_CASE_TRUSTED_RUN_KEY]
      : undefined;

  const handler: Handler<Ctx, C> = async ({
    ctx,
    path,
    query,
    headers,
    body,
  }) => {
    const parts = { path, query, headers, body };
    const passSingle =
      singleInput !== undefined &&
      canPassSingleInput(parts, singleInput.source);
    const input = def.input
      ? def.input(parts)
      : passSingle
        ? parts[singleInput.source]
        : defaultBinderInput(parts);
    const run = passSingle && trustedRun ? trustedRun : def.useCase.run;

    try {
      return {
        status,
        body: await run.call(def.useCase, { ctx, input }),
      };
    } catch (error) {
      if (
        !def.input &&
        isUseCaseInputValidationFailure(error, def.useCase.name)
      ) {
        throw new UseCaseRouteInputValidationError({
          contractName: contract.name,
          useCaseName: def.useCase.name,
          cause: error,
        });
      }
      throw error;
    }
  };

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