// Consumable from non-Node environments (browsers) at the type level only. Calling
// `createContract*()` at runtime executes `new AsyncLocalStorage()` (below) and webpack
// cannot tree-shake the `node:async_hooks` import — bundle crashes at module-load.
// Browser-bundled consumers (`wordpress-plugins/*`, `frontends/*`) call routes through
// `createFetchClient<Routes, Version>(...)` (`./client/fetch-client.ts`), which derives
// path / method / request / response from the contract types alone. The contract
// factories are never invoked on the consumer side.
/// <reference lib="ES2022" />
/// <reference types="node" />
import { AsyncLocalStorage } from "node:async_hooks";
import { ZodVoid, z } from "zod";

import { createLiteralErrors } from "./http-response.js";

import type { ContractGuard } from "./contract-guard.js";
import type {
    EHttpStatusCodeClientError,
    EHttpStatusCodeServerError,
    EHttpStatusCodeSuccess,
} from "./http-status-code.js";
import type { IRouteLocationInterface, IRouteParamsInterface, IRouteRequestInterface } from "./route.js";
import type { RefinedZodObject, createRefinableSchema } from "../utility-types/create-refinable-schema.js";
import type { TFunction } from "i18next";
import type { ZodCustom, ZodObject } from "zod";

type ContractResponses = Partial<
    Record<EHttpStatusCodeSuccess | EHttpStatusCodeClientError | EHttpStatusCodeServerError, ContractResponse<any>>
>;

type ContractVersion = `${number}.${number}.${number}` | `v${number}` | null;
interface CreateContractRouteDetails {
    shortDescription?: string;
    longDescription?: string;
    groups?: Array<
        | string
        | {
              name: string;
              description: string;
              /**
               * Allows to set one additional tag as parent. Only one parent is supported and only one level of nesting is supported.
               *
               * @see https://guides.scalar.com/scalar/scalar-api-references/openapi#openapi-specification__x-taggroups
               */
              parent?: string;
          }
    >;
    /**
     * The trigger of the contract. This is oriented to the OpenAPI specification as OpenAPI supports
     * requests and webhooks.
     *
     * @default "request"
     * @see https://learn.openapis.org/examples/v3.1/webhook-example.html
     */
    trigger?: "request" | "webhook";
}

type ContractRoute = {
    /**
     * Used for contract profiles: recommends "internal" but allows any string.
     */
    profiles?: Array<(readonly ["internal"])[number] | (string & {})>;
};

/**
 * See `createContract` for more information.
 */
type Contract<
    Route extends IRouteLocationInterface & ContractRoute,
    ZodRequest extends RefinedZodObject<ZodObject> | ZodVoid = RefinedZodObject<ZodObject> | ZodVoid,
    ZodParams extends RefinedZodObject<ZodObject> | ZodVoid = RefinedZodObject<ZodObject> | ZodVoid,
    ZodHeaders extends RefinedZodObject<ZodObject> | ZodVoid = RefinedZodObject<ZodObject> | ZodVoid,
    Response extends ContractResponses = ContractResponses,
    Guards extends Record<string, ContractGuard> = Record<string, ContractGuard>,
    RouteDetails extends CreateContractRouteDetails = CreateContractRouteDetails,
    ZodInstance extends typeof z = typeof z,
    Versions extends ContractVersion[] = ContractVersion[],
> = {
    z: ZodInstance;
    versions: Versions;
    route: Readonly<Route>;
    request: () => ZodRequest;
    params: () => ZodParams & ReturnType<Guards[keyof Guards]["params"]>;
    headers: () => ZodHeaders & ReturnType<Guards[keyof Guards]["headers"]>;
    response: () => Response;
    guards: () => Readonly<Guards>;
    routeDetails: () => RouteDetails;
    storage: CreateContractContext;
    /**
     * This property is never exposed by the returned object, it is just for getting the type. As alternative
     * you could use the `InferContractResponse` type for example.
     */
    types?: {
        request: z.infer<ZodRequest>;
        params: z.infer<ZodParams> & Guards[keyof Guards]["types"]["params"];
        headers: z.infer<ZodHeaders> & Guards[keyof Guards]["types"]["headers"];
        response: Response;
        guards: Guards;
        routeDetails: RouteDetails;
        versions: Versions;
    };
};

type OmitKeysStartingWith<T, P extends string> = {
    [K in keyof T as `${Extract<K, string | number>}` extends `${P}${string}` ? never : K]: T[K];
};

type CreateContractContextData = {
    t: TFunction;
};

/**
 * Context that is available within your schema definitions. It allows you to e.g. use the `t` function to translate error messages.
 * See also the `createContract` for more information.
 */
type CreateContractContext = AsyncLocalStorage<CreateContractContextData>;

/**
 * Creates a contract that defines the structure of a REST API endpoint.
 * The contract specifies the route, request body schema, URL parameters schema,
 * and response type, but does not contain any implementation logic.
 *
 * Attention: When your contract or any schema is using data from the `CreateContractContext` storage, e.g. `t`,
 * you need to use the `storage.run` method first before you can use the schema. In our schema, make sure to use
 * closures to use the storage data. For example, for error messages use some thing like this:
 *
 * ```ts
 * createContract({
 *     // [...]
 *     request: (store) => z.object({
 *         // Wrong:
 *         // email: z.string().email({ error: () => store.getStore().t("Invalid email address.") }),
 *         // Correct:
 *         email: z.string().email({ error: () => store.getStore().t("Invalid email address.") }),
 *         // The same applies to refinements
 *         name: z.string().refine(name => {
 *             return name.length > 0;
 *         }, {
 *             error: (store) => store.getStore().t("Name must be longer than 0 characters."),
 *         }),
 *     }),
 * });
 * ```
 *
 * If you want to learn more about the contracts and how they work please read the [README.md](../../README.md) file.
 *
 * @see https://github.com/colinhacks/zod/issues/148#issuecomment-2791588354
 * @see https://nodejs.org/docs/latest-v18.x/api/async_context.html
 */
function createContract<
    Route extends IRouteLocationInterface & ContractRoute,
    ZodRequest extends RefinedZodObject<ZodObject> | ZodVoid,
    ZodParams extends RefinedZodObject<ZodObject> | ZodVoid,
    ZodHeaders extends RefinedZodObject<ZodObject> | ZodVoid,
    Response extends ContractResponses,
    Guards extends Record<string, ContractGuard> | unknown,
    RouteDetails extends CreateContractRouteDetails,
    ZodInstance extends typeof z = typeof z,
    Versions extends ContractVersion[] = ContractVersion[],
>({
    z,
    versions,
    route,
    request,
    params,
    headers,
    response,
    guards,
    routeDetails,
}: {
    /**
     * We need to pass the Zod instance to the contract 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 schemes.
     */
    z: ZodInstance;
    versions: Versions;
    route: Readonly<Route>;
    request?: (context: CreateContractContext) => ZodRequest;
    params?: (context: CreateContractContext) => ZodParams;
    headers?: (context: CreateContractContext) => ZodHeaders;
    response: (context: CreateContractContext) => Response;
    guards?: (context: CreateContractContext) => Readonly<Guards>;
    routeDetails?: ((context: CreateContractContext) => RouteDetails) | RouteDetails;
}): Contract<
    Readonly<Route>,
    ZodRequest,
    ZodParams,
    ZodHeaders,
    Guards extends Record<string, ContractGuard>
        ? OmitKeysStartingWith<Readonly<Guards>[keyof Readonly<Guards>]["types"]["response"], "2"> & Response
        : Response,
    Guards extends Record<string, ContractGuard> ? Guards : Record<string, ContractGuard>,
    RouteDetails,
    ZodInstance,
    Versions
> {
    const storage: CreateContractContext = new AsyncLocalStorage();

    // 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 (): R => {
            if (!memoized) {
                memoized = fnOrReturnValue(storage);
            }
            return memoized;
        };
    };

    type UseGuards =
        Readonly<Guards> extends Record<string, ContractGuard> ? Readonly<Guards> : Record<string, ContractGuard>;

    const useGuards: () => UseGuards = wrapAndMemoizeFunction(
        typeof guards === "function" ? (guards as any) : () => guards || ({} as UseGuards),
    );

    const useParams = params || (() => z.object({}) as RefinedZodObject<ZodObject>);
    const useHeaders = headers || (() => z.object({}) as RefinedZodObject<ZodObject>);

    const mergedParams: () => ZodParams & ReturnType<UseGuards[keyof UseGuards]["params"]> = wrapAndMemoizeFunction(
        () => {
            let zodParams = useParams(storage) as Exclude<
                ZodParams & ReturnType<UseGuards[keyof UseGuards]["params"]>,
                ZodVoid
            >;
            for (const guard of Object.values(useGuards())) {
                const guardParams = guard.params(storage);
                if (guardParams instanceof ZodVoid) {
                    continue;
                }

                zodParams = z.object({
                    ...zodParams.shape,
                    ...guardParams.shape,
                }) as typeof zodParams;
            }
            return zodParams;
        },
    );

    const mergedHeaders: () => ZodHeaders & ReturnType<UseGuards[keyof UseGuards]["headers"]> = wrapAndMemoizeFunction(
        () => {
            let zodHeaders = useHeaders(storage) as Exclude<
                ZodHeaders & ReturnType<UseGuards[keyof UseGuards]["headers"]>,
                ZodVoid
            >;
            for (const guard of Object.values(useGuards())) {
                const guardHeaders = guard.headers(storage);
                if (guardHeaders instanceof ZodVoid) {
                    continue;
                }

                zodHeaders = z.object({
                    ...zodHeaders.shape,
                    ...guardHeaders.shape,
                }) as typeof zodHeaders;
            }
            return zodHeaders;
        },
    );

    return {
        z,
        versions,
        route,
        request: wrapAndMemoizeFunction(request || (() => z.void() as ZodRequest)),
        params: mergedParams,
        headers: mergedHeaders,
        response: wrapAndMemoizeFunction(response as any),
        guards: useGuards,
        routeDetails: wrapAndMemoizeFunction(
            typeof routeDetails === "function" ? routeDetails : () => routeDetails || ({} as RouteDetails),
        ),
        storage,
    };
}

/**
 * Create a response from and for a contract.
 *
 * You can define the response from a schema (like Zod, pass with `schema` property) with the `fromSchema` method
 * or from a TypeScript type with the `create` method. There is also another helper method to create a response from literal errors
 * with the `fromLiteralErrors` method.
 *
 * When not using a schema, keep in mind that the response is undocumented in the OpenAPI specification.
 *
 * The class cannot be initialized directly, you need to use the `create`, `fromSchema` or `fromLiteralErrors` methods (as TypeScript does not
 * support partial generic types with infer-support and always falls back to `any` if generic is inferred from optional constructor arguments).
 *
 * @see https://github.com/microsoft/TypeScript/issues/26242
 */
class ContractResponse<Response = void> {
    public response: Response;
    public status: EHttpStatusCodeSuccess | EHttpStatusCodeClientError | EHttpStatusCodeServerError;
    public readonly schema: { _zod: { output: Response } };
    public readonly description: string;

    private constructor({
        status,
        response,
        schema,
        description,
    }: {
        status?: ContractResponse<any>["status"];
        response?: Response;
        schema?: ContractResponse<Response>["schema"];
        description?: ContractResponse<Response>["description"];
        // TypeScript does not support excess / strict property checks, therefore we want
        // to avoid that consumers pass a complete zod schema instead of using the `schema` property.
        _zod?: never;
    } = {}) {
        this.status = status;
        this.response = response;
        this.schema = schema || (z.void() as unknown as { _zod: { output: Response } });
        this.description = description;
    }

    public fill(status: ContractResponse<any>["status"], response: Response): this {
        this.status = status;
        this.response = response;
        return this;
    }

    public static fromSchema<Response>({
        schema,
        ...rest
    }: {
        schema: { _zod: { output: Response } };
    } & Partial<Pick<ContractResponse<Response>, "description">>): ContractResponse<Response> {
        return new ContractResponse({
            schema,
            ...rest,
        });
    }

    public static fromLiteralErrors<Response, const Errors extends Parameters<typeof createLiteralErrors>[0]>({
        errors,
        ...rest
    }: { errors: Errors } & Partial<Pick<ContractResponse<Response>, "description">>): ContractResponse<
        z.infer<ReturnType<typeof createLiteralErrors<Errors>>>
    > {
        return new ContractResponse({
            schema: createLiteralErrors(errors) as unknown as {
                _zod: { output: Response };
            },
            ...rest,
        }) as ContractResponse<z.infer<ReturnType<typeof createLiteralErrors<Errors>>>>;
    }

    public static create(): ContractResponse<void>;
    public static create<Response>(): ContractResponse<Response>;
    public static create(options: Partial<Pick<ContractResponse<void>, "description">>): ContractResponse<void>;
    public static create<Response>(
        options: Partial<Pick<ContractResponse<Response>, "description">>,
    ): ContractResponse<Response>;
    public static create<Response = void>(options?: Partial<Pick<ContractResponse<Response>, "description">>) {
        return new ContractResponse<Response>({
            ...(options ?? {}),
        });
    }
}

type InferContract<CreateContractFunction extends (...args: any[]) => Contract<any, any, any, any, ContractResponses>> =
    ReturnType<CreateContractFunction>;

type InferContractRequest<
    CreateContractFunction extends (...args: any[]) => Contract<any, any, any, any, ContractResponses>,
> = Exclude<InferContract<CreateContractFunction>["types"]["request"], void>;

type InferContractParams<
    CreateContractFunction extends (...args: any[]) => Contract<any, any, any, any, ContractResponses>,
> = Exclude<InferContract<CreateContractFunction>["types"]["params"], void>;

type InferContractHeaders<
    CreateContractFunction extends (...args: any[]) => Contract<any, any, any, any, ContractResponses>,
> = Exclude<InferContract<CreateContractFunction>["types"]["headers"], void>;

type InferContractResponse<
    CreateContractFunction extends (...args: any[]) => Contract<any, any, any, any, ContractResponses>,
> = Exclude<InferContract<CreateContractFunction>["types"]["response"], void>;

type InferContractGuards<
    CreateContractFunction extends (...args: any[]) => Contract<any, any, any, any, ContractResponses>,
> = InferContract<CreateContractFunction>["types"]["guards"];

type InferSchemaFromRefinableSchema<CreateRefineableSchemaFunction extends ReturnType<typeof createRefinableSchema>> =
    z.infer<Parameters<Parameters<CreateRefineableSchemaFunction>[0]>[0]>;

type InferContractResponseClasses<C extends Contract<any>> = C["types"]["response"][keyof C["types"]["response"]];

/**
 * Flattened body shape of a contract response for a given status. Use this on the consumer side
 * (frontend fetch helpers, server-side action results, …) where the discriminated `{ 200: …, 4xx: … }`
 * map from `InferContractResponse` is unwrapped into the raw success body the runtime returns.
 *
 * Defaults to status `200`, which covers the common case. For routes that return a different
 * success status (e.g. `201 Created`, `204 No Content`), pass the status explicitly:
 *
 *     type Body = InferContractResponseBody<typeof createContractFoo, 201>;
 */
type InferContractResponseBody<
    CreateContractFunction extends (...args: any[]) => Contract<any, any, any, any, ContractResponses>,
    Status extends keyof InferContractResponse<CreateContractFunction> = 200,
> = InferContractResponse<CreateContractFunction>[Status] extends ContractResponse<infer T> ? T : never;

// -- Legacy route conversion --

type InterfaceToZod<T> = ZodObject<
    Readonly<{
        [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: ZodCustom<
            T[K]
        >;
    }>
>;
type ConcreteProps<T> = {
    [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K];
};
type HasProps<T> = keyof ConcreteProps<T> extends never ? false : true;

/**
 * Convert a legacy route to a contract so it can be used with the `createFetchClient` function.
 *
 * This does not register the contract in the contract registry and therefore is not available in
 * the OpenAPI specification.
 */
type ContractFromLegacyRoute<
    Route extends IRouteLocationInterface,
    Request extends IRouteRequestInterface,
    Params extends IRouteParamsInterface,
    Response extends ContractResponses,
    Versions extends ContractVersion[] = ContractVersion[],
> = Contract<
    Readonly<Route>,
    HasProps<Request> extends true ? InterfaceToZod<Request> : ZodVoid,
    HasProps<Params> extends true ? InterfaceToZod<Params> : ZodVoid,
    InterfaceToZod<Headers>,
    Response,
    Record<string, ContractGuard>,
    CreateContractRouteDetails,
    typeof z,
    Versions
>;

export {
    type ContractResponses,
    type Contract,
    type ContractRoute,
    type CreateContractContextData,
    type CreateContractContext,
    type CreateContractRouteDetails,
    createContract,
    ContractResponse,
    type InferContract,
    type InferContractRequest,
    type InferContractParams,
    type InferContractHeaders,
    type InferContractResponse,
    type InferContractGuards,
    type InferSchemaFromRefinableSchema,
    type InferContractResponseClasses,
    type InferContractResponseBody,
    type InterfaceToZod,
    type HasProps,
    type ConcreteProps,
    type ContractFromLegacyRoute,
    type ContractVersion,
};
