import { err, Result } from "@shared/utils/result.ts";
import { ok } from "@shared/utils/index.ts";
import { None, none, Option, some } from "@shared/utils/option.ts";
// ── Error Types ─────────────────────────────────────────────────────
export type ValidationErrorDetail =
    | {
        kind: "typeMismatch";
        expected: string;
        received: string;
        msg?: string;
    }
    | {
        kind: "propertyValidation";
        property: string;
        detail: ValidationErrorDetail;
        msg?: string;
    }
    | {
        kind: "missingProperties" | "unexpectedProperties";
        keys: string[];
        msg?: string;
    }
    | {
        kind: "unionValidation";
        details: ValidationErrorDetail[];
        msg?: string;
    }
    | {
        kind: "arrayElement";
        index: number;
        detail: ValidationErrorDetail;
        msg?: string;
    }
    | { kind: "general"; mark?: string; msg: string };

export class SchemaValidationError extends Error {
    public readonly type = "SchemaValiationError";

    constructor(
        public readonly input: unknown,
        public readonly detail: ValidationErrorDetail,
    ) {
        super(
            SchemaValidationError.getBestMsg(detail) ||
                "Schema validation error",
        );
        this.name = "SchemaValidationError";
    }

    public format(): Record<string, unknown> {
        return {
            input: this.input,
            error: SchemaValidationError.formatDetail(this.detail),
        };
    }

    get info(): string {
        return SchemaValidationError.getBestMsg(this.detail);
    }

    private static getBestMsg(detail: ValidationErrorDetail): string {
        switch (detail.kind) {
            case "typeMismatch":
            case "unexpectedProperties":
            case "missingProperties":
            case "general":
            case "unionValidation":
                return SchemaValidationError.formatMsg(detail);
            case "propertyValidation":
            case "arrayElement":
                return detail.msg ||
                    SchemaValidationError.getBestMsg(detail.detail);
            default:
                return "Unknown error";
        }
    }

    private static formatDetail(detail: ValidationErrorDetail): any {
        switch (detail.kind) {
            case "general":
            case "typeMismatch":
                return SchemaValidationError.formatMsg(detail);
            case "propertyValidation":
                return {
                    [detail.property]: detail.msg ||
                        this.formatDetail(detail.detail!),
                };
            case "unexpectedProperties":
            case "missingProperties": {
                const msg = detail.msg ||
                    (detail.kind === "unexpectedProperties"
                        ? "Property is not allowed in a strict schema object"
                        : "Property is required, but missing");

                return detail.keys.reduce<Record<string, string>>(
                    (acc, key) => {
                        acc[key] = msg;
                        return acc;
                    },
                    {},
                );
            }
            case "arrayElement": {
                const detailObj: Record<string, string> = {};
                if (detail.msg) {
                    detailObj["msg"] = detail.msg;
                }
                detailObj[`index_${detail.index}`] = this.formatDetail(
                    detail.detail,
                );
                return detailObj;
            }
            case "unionValidation": {
                const arr: unknown[] = detail.details?.map(
                    (err): unknown => this.formatDetail(err),
                );
                if (detail.msg) {
                    arr.unshift("Msg: " + detail.msg);
                }
                return arr;
            }
            default:
                return "Unknown error type";
        }
    }

    private static formatMsg(detail: ValidationErrorDetail): string {
        if (detail.msg || detail.kind === "general") {
            return detail.msg || "Unknown error";
        }
        switch (detail.kind) {
            case "typeMismatch":
                return `Expected ${detail.expected}, but received ${detail.received}`;
            case "unexpectedProperties":
                return `Properties not allowed: ${detail.keys.join(", ")}`;
            case "missingProperties":
                return `Missing required properties: ${detail.keys.join(", ")}`;
            case "unionValidation":
                return `Input did not match any union member`;
            default:
                return "Unknown error";
        }
    }
}

export function createValidationError(
    input: unknown,
    error: ValidationErrorDetail,
) {
    return new SchemaValidationError(input, error);
}

type PrimitiveTypeName =
    | "string"
    | "number"
    | "bigint"
    | "boolean"
    | "symbol"
    | "undefined"
    | "object"
    | "function";

// ── Core Schema Types ───────────────────────────────────────────────
export interface Schema<T> {
    parse(input: unknown): Result<T, SchemaValidationError>;
}

type ValidationCheck<T> = (value: T) => SchemaValidationError | void;

export abstract class BaseSchema<T> implements Schema<T> {
    protected checks: ValidationCheck<T>[] = [];

    constructor(public readonly msg?: string) {}

    public parse(input: unknown): Result<T, SchemaValidationError> {
        return this.validateInput(input).andThen((value) =>
            this.applyValidationChecks(value)
        );
    }

    protected abstract validateInput(
        input: unknown,
    ): Result<T, SchemaValidationError>;

    protected static validatePrimitive<U>(
        input: unknown,
        expectedType: PrimitiveTypeName,
        msg?: string,
    ): Result<U, SchemaValidationError> {
        const receivedType = typeof input;
        return receivedType === expectedType ? ok(input as U) : err(
            createValidationError(input, {
                kind: "typeMismatch",
                expected: expectedType,
                received: receivedType,
                msg: msg,
            }),
        );
    }

    public addCheck(check: ValidationCheck<T>): this {
        this.checks.push(check);
        return this;
    }

    protected applyValidationChecks(
        value: T,
    ): Result<T, SchemaValidationError> {
        for (const check of this.checks) {
            const error = check(value);
            if (error) {
                return err(error);
            }
        }
        return ok(value);
    }

    protected static isNullishSchema(schema: Schema<any>): boolean {
        return schema.parse(null).isOk() || schema.parse(undefined).isOk();
    }
}

export class StringSchema extends BaseSchema<string> {
    private static readonly emailRegex =
        /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; // https://stackoverflow.com/questions/46155/how-can-i-validate-an-email-address-in-javascript

    private static readonly ipRegex =
        /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; // https://stackoverflow.com/questions/4460586/javascript-regular-expression-to-check-for-ip-addresses

    protected override validateInput(
        input: unknown,
    ): Result<string, SchemaValidationError> {
        return BaseSchema.validatePrimitive<string>(
            input,
            "string",
            this.msg,
        );
    }

    public max(len: number, msg?: string): this {
        return this.addCheck((value) => {
            if (value.length > len) {
                return createValidationError(value, {
                    kind: "general",
                    msg: msg || `String must be at most ${len} characters long`,
                });
            }
        });
    }

    public min(len: number, msg?: string): this {
        return this.addCheck((value) => {
            if (value.length < len) {
                return createValidationError(value, {
                    kind: "general",
                    msg: msg || `String must be at most ${len} characters long`,
                });
            }
        });
    }

    public regex(pattern: RegExp, msg?: string): this {
        return this.addCheck((value) => {
            if (!pattern.test(value)) {
                return createValidationError(value, {
                    kind: "general",
                    msg: msg || `String must match pattern ${String(pattern)}`,
                });
            }
        });
    }

    public email(msg?: string): this {
        return this.regex(
            StringSchema.emailRegex,
            msg || "String must be a valid email address",
        );
    }

    public ip(msg?: string): this {
        return this.regex(
            StringSchema.ipRegex,
            msg || "String must be a valid ip address",
        );
    }
}

export class LiteralSchema<L extends string> extends BaseSchema<L> {
    constructor(
        public readonly literal: L,
        msg?: string,
    ) {
        super(msg);
    }

    protected override validateInput(
        input: unknown,
    ): Result<L, SchemaValidationError> {
        return BaseSchema.validatePrimitive<string>(
            input,
            "string",
            this.msg,
        )
            .andThen((str) =>
                str === this.literal ? ok(str as L) : err(
                    createValidationError(
                        input,
                        {
                            kind: "typeMismatch",
                            expected: this.literal,
                            received: str,
                            msg: this.msg,
                        },
                    ),
                )
            );
    }
}

class NumberSchema extends BaseSchema<number> {
    protected override validateInput(
        input: unknown,
    ): Result<number, SchemaValidationError> {
        return BaseSchema.validatePrimitive<number>(
            input,
            "number",
            this.msg,
        );
    }

    public gt(
        num: number,
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (value <= num) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg || `Number must be greater than ${num}`,
                    },
                );
            }
        });
    }

    public gte(
        num: number,
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (value < num) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg ||
                            `Number must be greater than or equal to ${num}`,
                    },
                );
            }
        });
    }

    public lt(
        num: number,
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (value >= num) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg || `Number must be less than ${num}`,
                    },
                );
            }
        });
    }

    public lte(
        num: number,
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (value > num) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg ||
                            `Number must be less than or equal to ${num}`,
                    },
                );
            }
        });
    }

    public int(
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (!Number.isInteger(value)) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg ||
                            `Number must be an integer`,
                    },
                );
            }
        });
    }

    public positive(
        msg?: string,
    ): this {
        return this.gt(0, msg || "Number must be positive");
    }

    public nonnegative(
        msg?: string,
    ) {
        return this.gte(0, msg || "Number must be nonnegative");
    }

    public negative(
        msg?: string,
    ): this {
        return this.lt(0, msg || "Number must be negative");
    }

    public nonpositive(
        msg?: string,
    ) {
        return this.lte(0, msg || "Number must be nonpositive");
    }

    public finite(
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (!Number.isFinite(value)) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg ||
                            `Number must be an integer`,
                    },
                );
            }
        });
    }

    public safe(
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (!Number.isSafeInteger(value)) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg ||
                            `Number must be an integer`,
                    },
                );
            }
        });
    }

    public multipleOf(
        num: number,
        msg?: string,
    ): this {
        return this.addCheck((value) => {
            if (value % num !== 0) {
                return createValidationError(
                    value,
                    {
                        kind: "general",
                        msg: msg || `Number must be a multiple of ${num}`,
                    },
                );
            }
        });
    }
}

class BigintSchema extends BaseSchema<bigint> {
    protected override validateInput(
        input: unknown,
    ): Result<bigint, SchemaValidationError> {
        return BaseSchema.validatePrimitive<bigint>(
            input,
            "bigint",
            this.msg,
        );
    }
}

class BooleanSchema extends BaseSchema<boolean> {
    protected override validateInput(
        input: unknown,
    ): Result<boolean, SchemaValidationError> {
        return BaseSchema.validatePrimitive<boolean>(
            input,
            "boolean",
            this.msg,
        );
    }
}

class DateSchema extends BaseSchema<Date> {
    protected override validateInput(
        input: unknown,
    ): Result<Date, SchemaValidationError> {
        return BaseSchema.validatePrimitive<object>(
            input,
            "object",
            this.msg,
        )
            .andThen((o) => {
                if (o instanceof Date) return ok(o as Date);

                const received = o?.constructor?.name ?? "unknown";
                return err(createValidationError(
                    input,
                    {
                        kind: "typeMismatch",
                        expected: "Date instance",
                        received,
                        msg: this.msg,
                    },
                ));
            });
    }
}

class SymbolSchema extends BaseSchema<symbol> {
    protected override validateInput(
        input: unknown,
    ): Result<symbol, SchemaValidationError> {
        return BaseSchema.validatePrimitive<symbol>(
            input,
            "symbol",
            this.msg,
        );
    }
}

class UndefinedSchema extends BaseSchema<undefined> {
    protected override validateInput(
        input: unknown,
    ): Result<undefined, SchemaValidationError> {
        return BaseSchema.validatePrimitive<undefined>(
            input,
            "undefined",
            this.msg,
        );
    }
}

class NullSchema extends BaseSchema<null> {
    protected override validateInput(
        input: unknown,
    ): Result<null, SchemaValidationError> {
        if (input === null) return ok(input);

        const received = typeof input === "object"
            ? input?.constructor?.name ?? "unknown"
            : typeof input;

        return err(
            createValidationError(
                input,
                {
                    kind: "typeMismatch",
                    expected: "null",
                    received,
                    msg: this.msg,
                },
            ),
        );
    }
}

class VoidSchema extends BaseSchema<void> {
    protected override validateInput(
        input: unknown,
    ): Result<void, SchemaValidationError> {
        if (input === undefined || input === null) return ok();

        const received = typeof input === "object"
            ? input?.constructor?.name ?? "unknown"
            : typeof input;

        return err(createValidationError(
            input,
            {
                kind: "typeMismatch",
                expected: "void (undefined/null)",
                received,
                msg: this.msg,
            },
        ));
    }
}

class AnySchema extends BaseSchema<any> {
    protected override validateInput(
        input: unknown,
    ): Result<any, SchemaValidationError> {
        return ok(input);
    }
}

class UnknownSchema extends BaseSchema<unknown> {
    protected override validateInput(
        input: unknown,
    ): Result<unknown, SchemaValidationError> {
        return ok(input);
    }
}

class NeverSchema extends BaseSchema<never> {
    protected override validateInput(
        input: unknown,
    ): Result<never, SchemaValidationError> {
        return err(
            createValidationError(
                input,
                {
                    kind: "typeMismatch",
                    expected: "never",
                    received: typeof input,
                    msg: this.msg,
                },
            ),
        );
    }
}

export class ObjectSchema<S extends Record<string, Schema<any>>>
    extends BaseSchema<{ [K in keyof S]: InferSchemaType<S[K]> }> {
    private strictMode: boolean = false;
    private objectMsg?;

    constructor(
        public readonly shape: S,
        msg?: {
            mismatch?: string;
            nullObject?: string;
            unexpectedProperty?: string;
            propertyValidation?: string;
            missingProperty?: string;
        } | string,
    ) {
        let mismatchMsg: string | undefined;
        let objectMsg;

        if (typeof msg === "string") {
            mismatchMsg = msg;
        } else if (typeof msg === "object") {
            objectMsg = msg;
        }

        super(mismatchMsg);

        this.objectMsg = objectMsg;
    }

    // TODO: Simplify it a bit
    protected override validateInput(
        input: unknown,
    ): Result<
        { [K in keyof S]: InferSchemaType<S[K]> },
        SchemaValidationError
    > {
        return BaseSchema.validatePrimitive<object>(
            input,
            "object",
            this.msg || this.objectMsg?.mismatch,
        )
            .andThen((obj) => {
                if (obj === null) {
                    return err(
                        createValidationError(input, {
                            kind: "typeMismatch",
                            expected: "Non-null object",
                            received: "null",
                            msg: this.msg || this.objectMsg?.nullObject,
                        }),
                    );
                }

                const resultObj: Record<string, unknown> = {};
                const expectedKeys = new Set(Object.keys(this.shape));

                for (const key of Object.keys(obj)) {
                    const schema = this.shape[key];
                    if (schema === undefined) {
                        if (this.strictMode) {
                            const keys = new Set(Object.keys(obj)).difference(
                                new Set(Object.keys(this.shape)),
                            ).keys().toArray();

                            return err(
                                createValidationError(
                                    input,
                                    {
                                        kind: "unexpectedProperties",
                                        keys,
                                        msg: this.msg ||
                                            this.objectMsg
                                                ?.unexpectedProperty,
                                    },
                                ),
                            );
                        }
                        continue;
                    }

                    const result = schema.parse((obj as any)[key]);
                    if (result.isErr()) {
                        return err(
                            createValidationError(
                                input,
                                {
                                    kind: "propertyValidation",
                                    property: key,
                                    detail: result.error.detail,
                                    msg: this.msg ||
                                        this.objectMsg?.propertyValidation,
                                },
                            ),
                        );
                    }
                    expectedKeys.delete(key);
                    resultObj[key] = result.value;
                }

                const missingProperties = expectedKeys.keys().filter(
                    (key) => !BaseSchema.isNullishSchema(this.shape[key]),
                ).toArray();

                if (missingProperties.length > 0) {
                    return err(
                        createValidationError(
                            input,
                            {
                                kind: "missingProperties",
                                keys: missingProperties,
                                msg: this.msg ||
                                    this.objectMsg?.missingProperty,
                            },
                        ),
                    );
                }

                return ok(
                    resultObj as { [K in keyof S]: InferSchemaType<S[K]> },
                );
            });
    }

    public strict(): this {
        this.strictMode = true;
        return this;
    }

    public pick<
        P extends Partial<
            Record<keyof InferSchemaType<this>, boolean>
        >,
    >(
        keys: P,
    ): ObjectPick<this, P> {
        const o: Record<string, Schema<any>> = {};

        for (const key of Object.keys(keys)) {
            if (keys[key as keyof P]) {
                o[key] = this.shape[key];
            }
        }

        return z.obj(o) as unknown as ObjectPick<this, P>;
    }
}

type PickedKeys<T> = {
    [K in keyof T]: T[K] extends true ? K : never;
}[keyof T];

type ObjectPick<
    O extends ObjectSchema<any>,
    P extends Partial<Record<keyof InferSchemaType<O>, boolean>>,
> = O extends ObjectSchema<infer T>
    ? ObjectSchema<{ [K in PickedKeys<P> & keyof T]: T[K] }>
    : never;

type InferUnionSchemaType<U extends Schema<any>[]> = U[number] extends
    Schema<infer T> ? T : never;

class UnionSchema<U extends Schema<any>[]>
    extends BaseSchema<InferUnionSchemaType<U>> {
    private unionMsg?;

    constructor(
        public readonly schemas: U,
        msg?: {
            mismatch?: string;
            unionValidation?: string;
        } | string,
    ) {
        let mismatchMsg: string | undefined;
        let unionMsg;

        if (typeof msg === "string") {
            mismatchMsg = msg;
        } else if (typeof msg === "object") {
            unionMsg = msg;
        }

        super(mismatchMsg);
        this.unionMsg = unionMsg;
    }

    private static getTypeFromSchemaName(name: string): string {
        switch (name) {
            case "StringSchema":
            case "LiteralSchema":
                return "string";
            case "NumberSchema":
                return "number";
            case "BigintSchema":
                return "bigint";
            case "BooleanSchema":
                return "boolean";
            case "UndefinedSchema":
                return "undefined";
            case "SymbolSchema":
                return "symbol";
            default:
                return "object";
        }
    }

    protected validateInput(
        input: unknown,
    ): Result<InferUnionSchemaType<U>, SchemaValidationError> {
        const errors: ValidationErrorDetail[] = [];
        let allTypeMismatch = true;

        for (const schema of this.schemas) {
            const result = schema.parse(input);
            if (result.isOk()) {
                return ok(result.value as InferUnionSchemaType<U>);
            }
            allTypeMismatch = result.error.detail?.kind === "typeMismatch" &&
                allTypeMismatch;
            errors.push(result.error.detail);
        }

        if (allTypeMismatch) {
            return err(createValidationError(input, {
                kind: "typeMismatch",
                expected: this.schemas.map((s) =>
                    UnionSchema.getTypeFromSchemaName(
                        s.constructor.name,
                    )
                ).join(" | "),
                received: typeof input,
                msg: this.msg || this.unionMsg?.mismatch,
            }));
        }

        return err(
            createValidationError(input, {
                kind: "unionValidation",
                msg: this.msg ||
                    this.unionMsg?.unionValidation ||
                    "Input did not match any union member",
                details: errors,
            }),
        );
    }
}

class ArraySchema<S extends Schema<any>>
    extends BaseSchema<InferSchemaType<S>[]> {
    private arrayMsg?;

    constructor(
        private readonly schema: S,
        msg?: {
            mismatch?: string;
            element?: string;
        } | string,
    ) {
        let mismatchMsg: string | undefined;
        let arrayMsg;

        if (typeof msg === "string") {
            mismatchMsg = msg;
        } else if (typeof msg === "object") {
            arrayMsg = msg;
        }

        super(mismatchMsg);
        // TODO: abstract complex schemas in a separate type with thos messages
        this.arrayMsg = arrayMsg;
    }

    protected override validateInput(
        input: unknown,
    ): Result<InferSchemaType<S>[], SchemaValidationError> {
        if (!Array.isArray(input)) {
            return err(createValidationError(input, {
                kind: "typeMismatch",
                expected: "Array",
                received: "Non-array",
                msg: this.msg || this.arrayMsg?.mismatch,
            }));
        }

        for (let i = 0; i < input.length; i++) {
            const result = this.schema.parse(input[i]);

            if (result.isErr()) {
                return err(
                    createValidationError(input, {
                        kind: "arrayElement",
                        index: i,
                        detail: result.error.detail,
                        msg: this.msg || this.arrayMsg?.element,
                    }),
                );
            }
        }

        return ok(input as InferSchemaType<S>[]);
    }
}

class OptionalSchema<S extends Schema<any>>
    extends BaseSchema<InferSchemaType<S> | undefined> {
    constructor(
        public readonly schema: S,
        msg?: string,
    ) {
        super(msg);
    }

    protected override validateInput(
        input: unknown,
    ): Result<InferSchemaType<S> | undefined, SchemaValidationError> {
        if (input === undefined) {
            return ok(input);
        }
        return this.schema.parse(input);
    }
}

class NullableSchema<S extends Schema<any>>
    extends BaseSchema<InferSchemaType<S> | null> {
    constructor(
        public readonly schema: S,
        msg?: string,
    ) {
        super(msg);
    }

    protected override validateInput(
        input: unknown,
    ): Result<InferSchemaType<S> | null, SchemaValidationError> {
        if (input === null) {
            return ok(input);
        }
        return this.schema.parse(input);
    }
}

class NullishSchema<S extends Schema<any>>
    extends BaseSchema<InferSchemaType<S> | undefined | null> {
    constructor(
        public readonly schema: S,
        msg?: string,
    ) {
        super(msg);
    }

    protected override validateInput(
        input: unknown,
    ): Result<InferSchemaType<S> | undefined | null, SchemaValidationError> {
        if (input === undefined || input === null) {
            return ok(input);
        }
        return this.schema.parse(input);
    }
}

export class ResultSchema<T extends Schema<any>, E extends Schema<any>>
    extends BaseSchema<Result<InferSchemaType<T>, InferSchemaType<E>>> {
    constructor(
        private readonly okSchema: T,
        private readonly errSchema: E,
    ) {
        super();
    }

    protected override validateInput(
        input: unknown,
    ): Result<
        Result<InferSchemaType<T>, InferSchemaType<E>>,
        SchemaValidationError
    > {
        return BaseSchema.validatePrimitive<object>(input, "object").andThen(
            (
                obj,
            ): Result<
                Result<InferSchemaType<T>, InferSchemaType<E>>,
                SchemaValidationError
            > => {
                if ("tag" in obj) {
                    switch (obj.tag) {
                        case "ok": {
                            if ("value" in obj) {
                                return this.okSchema.parse(
                                    obj.value,
                                ).match(
                                    (v) => ok(ok(v as InferSchemaType<T>)),
                                    (e) =>
                                        err(createValidationError(input, {
                                            kind: "propertyValidation",
                                            property: "value",
                                            detail: e.detail,
                                        })),
                                );
                            } else if (
                                BaseSchema.isNullishSchema(this.okSchema)
                            ) {
                                return ok(
                                    ok() as Result<
                                        InferSchemaType<T>,
                                        InferSchemaType<E>
                                    >,
                                );
                            }
                            return err(createValidationError(input, {
                                kind: "missingProperties",
                                keys: ["value"],
                                msg: "If tag is set to 'ok', than result must contain a 'value' property",
                            }));
                        }
                        case "err": {
                            if (
                                "error" in obj
                            ) {
                                return this.errSchema.parse(
                                    obj.error,
                                ).match(
                                    (e) => ok(err(e as InferSchemaType<E>)),
                                    (e) =>
                                        err(createValidationError(input, {
                                            kind: "propertyValidation",
                                            property: "error",
                                            detail: e.detail,
                                        })),
                                );
                            } else if (
                                BaseSchema.isNullishSchema(this.errSchema)
                            ) {
                                return ok(
                                    err() as Result<
                                        InferSchemaType<T>,
                                        InferSchemaType<E>
                                    >,
                                );
                            }
                            return err(createValidationError(input, {
                                kind: "missingProperties",
                                keys: ["error"],
                                msg: "If tag is set to 'err', than result must contain a 'error' property",
                            }));
                        }
                        default:
                            return err(createValidationError(input, {
                                kind: "propertyValidation",
                                property: "tag",
                                detail: {
                                    kind: "typeMismatch",
                                    expected: "'ok' or 'err'",
                                    received: `'${obj.tag}'`,
                                },
                            }));
                    }
                } else {
                    return err(createValidationError(input, {
                        kind: "missingProperties",
                        keys: ["tag"],
                        msg: "Result must contain a tag property",
                    }));
                }
            },
        );
    }
}

export class OptionSchema<T extends Schema<any>>
    extends BaseSchema<Option<InferSchemaType<T>>> {
    constructor(
        private readonly schema: T,
    ) {
        super();
    }

    protected override validateInput(
        input: unknown,
    ): Result<Option<InferSchemaType<T>>, SchemaValidationError> {
        return BaseSchema.validatePrimitive<object>(input, "object").andThen(
            (
                obj,
            ): Result<Option<InferSchemaType<T>>, SchemaValidationError> => {
                if ("tag" in obj) {
                    switch (obj.tag) {
                        case "some": {
                            if ("value" in obj) {
                                return this.schema.parse(
                                    obj.value,
                                ).match(
                                    (v) => ok(some(v as InferSchemaType<T>)),
                                    (e) =>
                                        err(createValidationError(input, {
                                            kind: "propertyValidation",
                                            property: "value",
                                            detail: e.detail,
                                        })),
                                );
                            } else if (
                                BaseSchema.isNullishSchema(this.schema)
                            ) {
                                return ok(some() as Option<InferSchemaType<T>>);
                            }
                            return err(createValidationError(input, {
                                kind: "missingProperties",
                                keys: ["value"],
                                msg: "If tag is set to 'some', than option must contain a 'value' property",
                            }));
                        }
                        case "none": {
                            return ok(none);
                        }
                        default:
                            return err(createValidationError(input, {
                                kind: "propertyValidation",
                                property: "tag",
                                detail: {
                                    kind: "typeMismatch",
                                    expected: "'some' or 'none'",
                                    received: `'${obj.tag}'`,
                                },
                            }));
                    }
                } else {
                    return err(createValidationError(input, {
                        kind: "missingProperties",
                        keys: ["tag"],
                        msg: "Option must contain a tag property",
                    }));
                }
            },
        );
    }
}

export class EnumSchema<E extends (number | string)[]>
    extends BaseSchema<E[number]> {
    constructor(
        public readonly entries: E,
        msg?: string,
    ) {
        super(msg);
    }

    protected override validateInput(
        input: unknown,
    ): Result<E[number], SchemaValidationError> {
        for (const entry of this.entries) {
            if (input === entry) {
                return ok(input as E[number]);
            }
        }
        return err(createValidationError(input, {
            kind: "typeMismatch",
            expected: this.entries.map((e) =>
                typeof e === "string" ? `"${e}"` : e
            ).join(" | "),
            received: String(input),
            msg: this.msg,
        }));
    }
}

/* ── Helper Object for Schema Creation (z) ───────────────────────────────────── */

export const z = {
    string: (msg?: string) => new StringSchema(msg),
    literal: <L extends string>(lit: L, msg?: string) =>
        new LiteralSchema<L>(lit, msg),
    number: (msg?: string) => new NumberSchema(msg),
    bigint: (msg?: string) => new BigintSchema(msg),
    boolean: (msg?: string) => new BooleanSchema(msg),
    date: (msg?: string) => new DateSchema(msg),
    symbol: (msg?: string) => new SymbolSchema(msg),
    undefined: (msg?: string) => new UndefinedSchema(msg),
    null: (msg?: string) => new NullSchema(msg),
    void: (msg?: string) => new VoidSchema(msg),
    any: (msg?: string) => new AnySchema(msg),
    unknown: (msg?: string) => new UnknownSchema(msg),
    never: (msg?: string) => new NeverSchema(msg),
    obj: <S extends Record<string, Schema<any>>>(
        schema: S,
        msg?: string | {
            mismatch?: string;
            nullObject?: string;
            unexpectedProperty?: string;
            propertyValidation?: string;
            missingProperty?: string;
        },
    ) => new ObjectSchema<S>(schema, msg),
    union: <U extends Schema<any>[]>(
        schemas: U,
        msg?: string | {
            mismatch?: string;
            unionValidation?: string;
        },
    ) => new UnionSchema<U>(schemas, msg),
    array: <S extends Schema<any>>(
        schema: S,
        msg?: string | { mismatch?: string; element?: string },
    ) => new ArraySchema<S>(schema, msg),
    optional: <S extends Schema<any>>(
        schema: S,
        msg?: string,
    ) => new OptionalSchema<S>(schema, msg),
    nullable: <S extends Schema<any>>(
        schema: S,
        msg?: string,
    ) => new NullableSchema<S>(schema, msg),
    nullish: <S extends Schema<any>>(
        schema: S,
        msg?: string,
    ) => new NullishSchema<S>(schema, msg),
    result: <T extends Schema<any>, E extends Schema<any>>(
        okSchema: T,
        errSchema: E,
    ) => new ResultSchema<T, E>(okSchema, errSchema),
    option: <T extends Schema<any>>(
        schema: T,
    ) => new OptionSchema<T>(schema),
    enum: <E extends (number | string)[]>(e: E, msg?: string) =>
        new EnumSchema(e, msg),
};

export type InferSchemaType<S> = S extends Schema<infer T> ? T : never;
