import type { ContractResponses, CreateContractContext } from "./contract.js";
import type { ZodObject, ZodVoid, z } from "zod";

/**
 * See `createContractGuard` for more information.
 */
type ContractGuard<
    Name extends string = string,
    ZodParams extends ZodObject | ZodVoid = ZodObject | ZodVoid,
    ZodHeaders extends ZodObject | ZodVoid = ZodObject | ZodVoid,
    Response extends ContractResponses = ContractResponses,
    ZodInstance extends typeof z = typeof z,
> = {
    z: ZodInstance;
    name: Readonly<Name>;
    params: (context: CreateContractContext) => ZodParams;
    headers: (context: CreateContractContext) => ZodHeaders;
    response: (context: CreateContractContext) => Response;
    /**
     * This property is never exposed by the returned object, it is just for getting the type. As alternative
     * you could use the `InferContractGuardResponse` type for example.
     */
    types?: {
        params: z.infer<ZodParams>;
        headers: z.infer<ZodHeaders>;
        response: Response;
    };
};

/**
 * Creates a contract guard that defines the security of a REST API endpoint. Before a contract gets executed,
 * the contract guard is executed. If the contract guard returns a success status code, the contract is executed.
 * If the contract guard returns a failure status code, the contract is not executed and the request is rejected
 * with the respective status code.
 *
 * The contract guard specifies the request headers and response schema, but does not contain any implementation logic.
 *
 * To create a [security schema](https://openapispec.com/docs/what/what-are-openapi-security-schemes/), you can use
 * the `meta` function to add the `isSecuritySchema` property to the schema.
 *
 * ```ts
 * const createContractGuardWebhookSecret = () =>
 *   createContractGuard({
 *      params: z.object({
 *          secret: z.string().meta({
 *              description: "The secret to access the webhook",
 *              isSecuritySchema: true,
 *          }),
 *      }),
 *  });
 * ```
 */
function createContractGuard<
    Name extends string,
    ZodParams extends ZodObject | ZodVoid,
    ZodHeaders extends ZodObject | ZodVoid,
    Response extends ContractResponses,
    ZodInstance extends typeof z = typeof z,
>({
    z,
    name,
    params,
    headers,
    response,
}: {
    /**
     * We need to pass the Zod instance to the contract guard creation as some of the OpenAPI generation libraries extend the Zod instance.
     * When using a monorepo with multiple Zod instances, you need to pass the instance which you are using for the contract guard schemes.
     */
    z: ZodInstance;
    name: Readonly<Name>;
    params?: (context: CreateContractContext) => ZodParams;
    headers?: (context: CreateContractContext) => ZodHeaders;
    response: (context: CreateContractContext) => Response;
}): ContractGuard<Name, ZodParams, ZodHeaders, Response, ZodInstance> {
    // We should avoid to call zod and constructing schemas in the contract creation too often. A schema is a complex object and should be memoized.
    const wrapAndMemoizeFunction = <R, T extends (...args: any[]) => R>(fnOrReturnValue: T) => {
        let memoized: R;

        return (context: CreateContractContext): R => {
            if (!memoized) {
                memoized = fnOrReturnValue(context);
            }
            return memoized;
        };
    };

    return {
        z,
        name,
        params: wrapAndMemoizeFunction(params || (() => z.object({}) as ZodParams)),
        headers: wrapAndMemoizeFunction(headers || (() => z.object({}) as ZodHeaders)),
        response: wrapAndMemoizeFunction(response),
    };
}

type InferContractGuard<CreateContractFunction extends (...args: any[]) => ContractGuard<any, any, any>> =
    ReturnType<CreateContractFunction>;

type InferContractGuardParams<CreateContractFunction extends (...args: any[]) => ContractGuard<any, any, any>> =
    InferContractGuard<CreateContractFunction>["types"]["params"];

type InferContractGuardHeaders<CreateContractFunction extends (...args: any[]) => ContractGuard<any, any, any>> =
    InferContractGuard<CreateContractFunction>["types"]["headers"];

type InferContractGuardResponse<CreateContractFunction extends (...args: any[]) => ContractGuard<any, any, any>> =
    InferContractGuard<CreateContractFunction>["types"]["response"];

export {
    type ContractGuard,
    createContractGuard,
    type InferContractGuard,
    type InferContractGuardParams,
    type InferContractGuardHeaders,
    type InferContractGuardResponse,
};
