import { ZodOptional, z } from "zod";

import { EHttpStatusCodeServerError } from "./http-status-code.js";
import { createRefinableSchema } from "../utility-types/create-refinable-schema.js";

import type { InferSchemaFromRefinableSchema } from "./contract.js";
import type { EHttpStatusCodeClientError, EHttpStatusCodeSuccess } from "./http-status-code.js";
import type { ZodArray, ZodDiscriminatedUnion, ZodLiteral, ZodObject } from "zod";
import type { util } from "zod/v4/core";

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
type ObjectType = {};

// Create Zod schema for IError
const createErrorSchema = createRefinableSchema({
    schema: () =>
        z
            .object({
                code: z.string(),
                message: z.string(),
                description: z.string().optional(),
                entityName: z.string().optional(), // reference to entity name
                fieldName: z.string().optional(), // reference to field name
                index: z.number().optional(), // used for arrays or positions
                data: z.record(z.string(), z.any()).optional(),
            })
            .meta({
                // Workaround for https://github.com/scalar/scalar/issues/6757 to show example in the documentation
                // for all available unions in an array.
                example: {
                    code: "string",
                    message: "string",
                    description: "string",
                    entityName: "string",
                    fieldName: "string",
                    index: 1,
                    data: { "propertyName*": "any" },
                },
            }),
});

/**
 * A literal error schema defines a single, specific error by error code. It removes all optional fields from the base error schema.
 * If you want to add back e.g. the `description` field, you can do so by passing the `additionalFields` parameter.
 */
const createLiteralError = <Code extends string, AdditionalFields extends ZodObject = ZodObject<ObjectType>>(
    code: Code,
    additionalFields?: AdditionalFields,
): z.ZodObject<
    util.Extend<
        util.Extend<
            {
                [K in keyof SingleErrorSchemaShape as SingleErrorSchemaShape[K] extends ZodOptional
                    ? never
                    : K]: SingleErrorSchemaShape[K] extends ZodOptional ? never : SingleErrorSchemaShape[K];
            },
            AdditionalFields["shape"]
        >,
        z.ZodObject<{ code: ZodLiteral<Code> }>["shape"]
    >
> =>
    createErrorSchema((errorSchema) => {
        const errorSchemaExample = { ...(errorSchema.meta().example as Record<string, any>) };
        const optionalFields = Object.keys(errorSchema.shape)
            .filter((key) => errorSchema.shape[key] instanceof ZodOptional)
            .reduce(
                (acc, key) => {
                    acc[key] = true;
                    delete errorSchemaExample[key];
                    return acc;
                },
                {} as Record<string, true>,
            );

        return z
            .object({
                code: z.literal<Code>(code),
                ...errorSchema.omit({ code: true, ...optionalFields }).shape,
                ...(additionalFields ? additionalFields.shape : {}),
            })
            .meta({
                example: {
                    ...errorSchemaExample,
                    code,
                    ...(additionalFields ? ((additionalFields.meta()?.example || {}) as Record<string, any>) : {}),
                },
            });
    }) as any;

/**
 * The same as `createLiteralError`, but for an array of errors. It additionally adds the base error schema to the union as it is the
 * fallback for all errors, e.g. an internal server error.
 */
const createLiteralErrors = <
    const Errors extends Readonly<{ readonly code: string; readonly additionalFields?: ZodObject<ObjectType> }[]>,
>(
    errors: Errors,
): ZodArray<
    ZodDiscriminatedUnion<
        [
            ...{
                [K in keyof Errors]: ReturnType<
                    typeof createLiteralError<Errors[K]["code"], Errors[K]["additionalFields"]>
                >;
            },
            // Base error schema is the fallback for all errors, e.g. an internal server error.
            Parameters<Parameters<typeof createErrorSchema>[0]>[0],
        ]
    >
> => {
    const baseErrorSchema = createErrorSchema();
    const baseErrorSchemaExample = baseErrorSchema.meta().example as Record<string, any>;
    const errorSchemas: any[] = [];
    const errorSchemasExamples: any[] = [];

    for (const { code, additionalFields } of errors) {
        const error = createLiteralError(code, additionalFields);
        errorSchemas.push(error);
        errorSchemasExamples.push(error.meta().example);
    }

    return z.array(z.discriminatedUnion("code", [baseErrorSchema, ...errorSchemas])).meta({
        example: [...errorSchemasExamples, baseErrorSchemaExample],
    }) as any;
};

type SingleErrorSchemaShape = Parameters<Parameters<typeof createErrorSchema>[0]>[0]["shape"];

interface IError extends InferSchemaFromRefinableSchema<typeof createErrorSchema> {}

interface IValidationError extends IError {}

class ServiceError extends Error {
    public readonly error: IError[];

    public readonly status: EHttpStatusCodeServerError | EHttpStatusCodeClientError;

    public constructor(
        error: IError[] | IError,
        status:
            | EHttpStatusCodeServerError
            | EHttpStatusCodeClientError = EHttpStatusCodeServerError.InternalServerError,
    ) {
        super(Array.isArray(error) ? error.map((e) => e.message).join(", ") : error.message);
        this.error = Array.isArray(error) ? error : [error];
        this.status = status;
    }
}

type TEntityValidationException = string;

interface IHttpResponseSuccess<Body> {
    status: EHttpStatusCodeSuccess;
    body?: Body;
    file?: string;
    csv?: object[];
}

interface IHttpResponseClientError {
    status: EHttpStatusCodeClientError;
    body?: undefined;
    errors?: IError[];
}

interface IHttpResponseServerError {
    status: EHttpStatusCodeServerError;
    body?: undefined;
    errors?: IError[] | Error | Error[];
}

type IHttpResponse<Body> = IHttpResponseSuccess<Body> | IHttpResponseClientError | IHttpResponseServerError;

export {
    createErrorSchema,
    createLiteralError,
    createLiteralErrors,
    type IValidationError,
    type TEntityValidationException,
    type IHttpResponse,
    ServiceError,
};
