import { err, ok, Result } from "@shared/utils/result.ts";
import { none, Option, some } from "@shared/utils/option.ts";

class ParseError extends Error {
    type = "ParseError";

    public trace: NestedArray<string> = [];

    constructor(
        public input: any,
        trace: NestedArray<string> | string,
        public readonly msg: string,
    ) {
        super(msg);

        if (Array.isArray(trace)) {
            this.trace = trace;
        } else {
            this.trace = [trace];
        }
    }

    stackParseErr(trace: string, input: any): ParseError {
        this.trace = [trace, this.trace];
        this.input = input;
        return this;
    }
}

function pe(input: unknown, trace: NestedArray<string>, msg: string) {
    return new ParseError(input, trace, msg);
}

export interface Schema<T> {
    parse(input: unknown): Result<T, ParseError>;
    checkIfValid(input: unknown): boolean;
    nullable(): NullableSchema<Schema<T>>;
    option(): OptionSchema<Schema<T>>;
    or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]>;
}

type CheckFunction<T> = (input: T) => ParseError | void;

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

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

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

    checkIfValid(input: unknown): boolean {
        return this.parse(input).isOk();
    }

    nullable(): NullableSchema<Schema<T>> {
        return new NullableSchema(this);
    }

    or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]> {
        return new UnionSchema(this, ...schema);
    }

    option(): OptionSchema<Schema<T>> {
        return new OptionSchema(this);
    }

    abstract parse(input: unknown): Result<T, ParseError>;
}

export abstract class PrimitiveSchema<T> extends BaseSchema<T> {
    protected abstract initialCheck(input: unknown): Result<T, ParseError>;

    protected checkPrimitive<U = T>(
        input: unknown,
        type:
            | "string"
            | "number"
            | "boolean"
            | "bigint"
            | "undefined"
            | "object"
            | "symbol"
            | "funciton",
    ): Result<U, ParseError> {
        const inputType = typeof input;

        if (inputType === type) {
            return ok(input as U);
        }
        return err(
            pe(input, `Expected '${type}', received '${inputType}'`),
        );
    }

    public parse(input: unknown): Result<T, ParseError> {
        return this.initialCheck(input).andThen((input) => {
            for (const check of this.checks) {
                const e = check(input);

                if (e) {
                    return err(e);
                }
            }

            return ok(input);
        });
    }
}

export class StringSchema extends PrimitiveSchema<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 initialCheck(
        input: unknown,
    ): Result<string, ParseError> {
        return this.checkPrimitive(input, "string");
    }

    public max(
        length: number,
        msg?: string,
    ): this {
        const trace = `String length must be at most ${length} characters long`;
        return this.addCheck((input) =>
            input.length <= length ? undefined : pe(input, trace, msg)
        );
    }

    public min(
        length: number,
        msg?: string,
    ): this {
        const trace =
            `String length must be at least ${length} characters long`;
        return this.addCheck((input) =>
            input.length >= length ? undefined : pe(input, trace, msg)
        );
    }

    public regex(
        pattern: RegExp,
        msg?: string,
    ): this {
        const trace = `String length must match the pattern ${String(pattern)}`;
        return this.addCheck((input) =>
            pattern.test(input) ? undefined : pe(input, trace, msg)
        );
    }

    public email(
        msg?: string,
    ): this {
        const trace = `String must be a valid email address`;
        return this.addCheck((input) =>
            StringSchema.emailRegex.test(input)
                ? undefined
                : pe(input, trace, msg)
        );
    }

    public ip(
        msg?: string,
    ): this {
        const trace = `String must be a valid ip address`;
        return this.addCheck((input) =>
            StringSchema.ipRegex.test(input) ? undefined : pe(input, trace, msg)
        );
    }
}

export class NumberSchema extends PrimitiveSchema<number> {
    protected override initialCheck(
        input: unknown,
    ): Result<number, ParseError> {
        return this.checkPrimitive(input, "number");
    }

    public gt(
        num: number,
        msg?: string,
    ): this {
        const trace = `Number must be greates than ${num}`;
        return this.addCheck((input) =>
            input > num ? undefined : pe(input, trace, msg)
        );
    }

    public gte(
        num: number,
        msg?: string,
    ): this {
        const trace = `Number must be greates than or equal to ${num}`;
        return this.addCheck((input) =>
            input >= num ? undefined : pe(input, trace, msg)
        );
    }

    public lt(
        num: number,
        msg?: string,
    ): this {
        const trace = `Number must be less than ${num}`;
        return this.addCheck((input) =>
            input < num ? undefined : pe(input, trace, msg)
        );
    }

    public lte(
        num: number,
        msg?: string,
    ): this {
        const trace = `Number must be less than or equal to ${num}`;
        return this.addCheck((input) =>
            input <= num ? undefined : pe(input, trace, msg)
        );
    }

    public int(
        msg?: string,
    ): this {
        const trace = `Number must be an integer`;
        return this.addCheck((input) =>
            Number.isInteger(input) ? undefined : pe(input, trace, msg)
        );
    }

    public positive(
        msg?: string,
    ): this {
        const trace = `Number must be positive`;
        return this.addCheck((input) =>
            input > 0 ? undefined : pe(input, trace, msg)
        );
    }

    public nonnegative(
        msg?: string,
    ): this {
        const trace = `Number must be nonnegative`;
        return this.addCheck((input) =>
            input >= 0 ? undefined : pe(input, trace, msg)
        );
    }

    public negative(
        msg?: string,
    ): this {
        const trace = `Number must be negative`;
        return this.addCheck((input) =>
            input < 0 ? undefined : pe(input, trace, msg)
        );
    }

    public nonpositive(
        msg?: string,
    ): this {
        const trace = `Number must be nonpositive`;
        return this.addCheck((input) =>
            input < 0 ? undefined : pe(input, trace, msg)
        );
    }

    public finite(
        msg?: string,
    ): this {
        const trace = `Number must be finite`;
        return this.addCheck((input) =>
            Number.isFinite(input) ? undefined : pe(input, trace, msg)
        );
    }

    public safe(
        msg?: string,
    ): this {
        const trace = `Number must be a safe integer`;
        return this.addCheck((input) =>
            Number.isSafeInteger(input) ? undefined : pe(input, trace, msg)
        );
    }

    public multipleOf(
        num: number,
        msg?: string,
    ): this {
        const trace = `Number must be a multiple of ${num}`;
        return this.addCheck((input) =>
            input % num === 0 ? undefined : pe(input, trace, msg)
        );
    }
}

export class BigintSchema extends PrimitiveSchema<bigint> {
    protected override initialCheck(
        input: unknown,
    ): Result<bigint, ParseError> {
        return this.checkPrimitive(input, "bigint");
    }

    public gt(
        num: number | bigint,
        msg?: string,
    ): this {
        const trace = `Bigint must be greates than ${num}`;
        return this.addCheck((input) =>
            input > num ? undefined : pe(input, trace, msg)
        );
    }

    public gte(
        num: number | bigint,
        msg?: string,
    ): this {
        const trace = `Bigint must be greates than or equal to ${num}`;
        return this.addCheck((input) =>
            input >= num ? undefined : pe(input, trace, msg)
        );
    }

    public lt(
        num: number | bigint,
        msg?: string,
    ): this {
        const trace = `Bigint must be less than ${num}`;
        return this.addCheck((input) =>
            input < num ? undefined : pe(input, trace, msg)
        );
    }

    public lte(
        num: number | bigint,
        msg?: string,
    ): this {
        const trace = `Bigint must be less than or equal to ${num}`;
        return this.addCheck((input) =>
            input <= num ? undefined : pe(input, trace, msg)
        );
    }

    public int(
        msg?: string,
    ): this {
        const trace = `Bigint must be an integer`;
        return this.addCheck((input) =>
            Number.isInteger(input) ? undefined : pe(input, trace, msg)
        );
    }

    public positive(
        msg?: string,
    ): this {
        const trace = `Bigint must be positive`;
        return this.addCheck((input) =>
            input > 0 ? undefined : pe(input, trace, msg)
        );
    }

    public nonnegative(
        msg?: string,
    ): this {
        const trace = `Bigint must be nonnegative`;
        return this.addCheck((input) =>
            input >= 0 ? undefined : pe(input, trace, msg)
        );
    }

    public negative(
        msg?: string,
    ): this {
        const trace = `Bigint must be negative`;
        return this.addCheck((input) =>
            input < 0 ? undefined : pe(input, trace, msg)
        );
    }

    public nonpositive(
        msg?: string,
    ): this {
        const trace = `Bigint must be nonpositive`;
        return this.addCheck((input) =>
            input < 0 ? undefined : pe(input, trace, msg)
        );
    }

    public finite(
        msg?: string,
    ): this {
        const trace = `Bigint must be finite`;
        return this.addCheck((input) =>
            Number.isFinite(input) ? undefined : pe(input, trace, msg)
        );
    }

    public safe(
        msg?: string,
    ): this {
        const trace = `Bigint must be a safe integer`;
        return this.addCheck((input) =>
            Number.isSafeInteger(input) ? undefined : pe(input, trace, msg)
        );
    }

    public multipleOf(
        num: bigint,
        msg?: string,
    ): this {
        const trace = `Bigint must be a multiple of ${num}`;
        return this.addCheck((input) =>
            input % num === BigInt(0) ? undefined : pe(input, trace, msg)
        );
    }
}

export class BooleanSchema extends PrimitiveSchema<boolean> {
    protected override initialCheck(
        input: unknown,
    ): Result<boolean, ParseError> {
        return this.checkPrimitive(input, "boolean");
    }
}

export class DateSchema extends PrimitiveSchema<object> {
    protected override initialCheck(
        input: unknown,
    ): Result<Date, ParseError> {
        return this.checkPrimitive(input, "object").andThen((obj) => {
            if (obj instanceof Date) {
                return ok(obj);
            }
            return err(
                pe(
                    input,
                    `Expected instance of Date, received ${obj.constructor.name}`,
                ),
            );
        });
    }

    public min(
        date: Date,
        msg?: string,
    ) {
        const trace = `Date must be after ${date.toLocaleString()}`;
        return this.addCheck((input) =>
            input >= date ? undefined : pe(input, trace, msg)
        );
    }

    public max(
        date: Date,
        msg?: string,
    ) {
        const trace = `Date must be before ${date.toLocaleString()}`;
        return this.addCheck((input) =>
            input <= date ? undefined : pe(input, trace, msg)
        );
    }
}

class UndefinedSchema extends PrimitiveSchema<undefined> {
    protected override initialCheck(
        input: unknown,
    ): Result<undefined, ParseError> {
        return this.checkPrimitive(input, "undefined");
    }
}

class NullSchema extends PrimitiveSchema<null> {
    protected override initialCheck(
        input: unknown,
    ): Result<null, ParseError> {
        if (input === null) {
            return ok(input);
        }
        return err(pe(input, "Expected 'null', received '${typeof input}'"));
    }
}

class VoidSchema extends PrimitiveSchema<void> {
    protected override initialCheck(input: unknown): Result<void, ParseError> {
        if (input !== undefined && input !== null) {
            return err(
                pe(input, `Expected 'void', received '${typeof input}'`),
            );
        }

        return ok();
    }
}

class AnySchema extends PrimitiveSchema<any> {
    protected override initialCheck(input: any): Result<any, ParseError> {
        return ok(input);
    }
}

class UnknownSchema extends PrimitiveSchema<unknown> {
    protected override initialCheck(
        input: unknown,
    ): Result<unknown, ParseError> {
        return ok(input);
    }
}

class ObjectSchema<O extends Record<string, Schema<any>>>
    extends PrimitiveSchema<{ [K in keyof O]: InferSchema<O[K]> }> {
    private strict: boolean = false;

    constructor(
        private readonly schema: O,
    ) {
        super();
    }

    protected override initialCheck(
        input: unknown,
    ): Result<{ [K in keyof O]: InferSchema<O[K]> }, ParseError> {
        return this.checkPrimitive<object>(input, "object").andThen(
            (objPrimitive) => {
                let obj = objPrimitive as Record<string, any>;
                let parsedObj: Record<string, any> = {};

                for (const [key, schema] of Object.entries(this.schema)) {
                    const value = obj[key];

                    const checkResult = schema.parse(value);

                    if (checkResult.isErr()) {
                        return err(
                            checkResult.error.stackParseErr(
                                `Failed to parse '${key}' attribute`,
                                input,
                            ),
                        );
                    }

                    parsedObj[key] = checkResult.value;
                }

                return ok(parsedObj as { [K in keyof O]: InferSchema<O[K]> });
            },
        );
    }
}

class NullableSchema<S extends Schema<any>>
    extends PrimitiveSchema<InferSchema<S> | void> {
    private static readonly voidSchema = new VoidSchema();

    constructor(
        private readonly schema: S,
    ) {
        super();
    }

    protected override initialCheck(
        input: unknown,
    ): Result<void | InferSchema<S>, ParseError> {
        if (NullableSchema.voidSchema.checkIfValid(input)) {
            return ok();
        }
        return this.schema.parse(input);
    }
}

class LiteralSchema<L extends string> extends PrimitiveSchema<L> {
    constructor(
        private readonly literal: L,
    ) {
        super();
    }

    protected override initialCheck(input: unknown): Result<L, ParseError> {
        if (input === this.literal) {
            return ok(this.literal);
        }
        return err(pe(input, `Input must match literal '${this.literal}'`));
    }
}

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

class UnionSchema<S extends Schema<any>[]>
    extends PrimitiveSchema<InferSchemaUnion<S>> {
    private static readonly schemasTypes: Partial<
        Record<string, TypeOfString>
    > = {
        StringSchema: "string",
        LiteralSchema: "string",
        NumberSchema: "number",
        BigintSchema: "bigint",
        BooleanSchema: "boolean",
        UndefinedSchema: "undefined",
        VoidSchema: "undefined",
    };
    private readonly primitiveTypesMap: Map<TypeOfString, Schema<any>[]> =
        new Map();
    private readonly othersTypes: Schema<any>[] = [];

    constructor(...schemas: S) {
        super();

        for (const schema of schemas) {
            const type = UnionSchema.schemasTypes[schema.constructor.name];

            if (type !== undefined) {
                if (!this.primitiveTypesMap.has(type)) {
                    this.primitiveTypesMap.set(type, []);
                }
                const schemasForType = this.primitiveTypesMap.get(type);
                schemasForType?.push(schema);
            } else {
                this.othersTypes.push(schema);
            }
        }
    }

    protected override initialCheck(
        input: unknown,
    ): Result<InferSchemaUnion<S>, ParseError> {
        const schemas = this.primitiveTypesMap.get(typeof input) ||
            this.othersTypes;

        const errors: string[] = [];

        for (const schema of schemas) {
            const checkResult = schema.parse(input);

            if (checkResult.isOk()) {
                return ok(checkResult.value);
            }

            errors.push(
                `${schema.constructor.name} - ${
                    checkResult.error.trace.join("\n")
                }`,
            );
        }

        const type = typeof input;
        return err(
            pe(
                input,
                [
                    `UnionSchema (${
                        this.primitiveTypesMap.keys().toArray().join(" | ")
                    }${
                        this.othersTypes.length > 0
                            ? "object"
                            : ""
                    }) - failed to parse input as any of the schemas:`,
                    errors.join("\n"),
                ].join("\n"),
                "Failed to match union",
            ),
        );
    }
}

class ArraySchema<S extends Schema<any>>
    extends PrimitiveSchema<InferSchema<S>[]> {
    constructor(
        private readonly schema: S,
    ) {
        super();
    }

    protected override initialCheck(
        input: unknown[],
    ): Result<InferSchema<S>[], ParseError> {
        const parsed = [];

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

            if (r.isErr()) {
                return err(
                    pe(
                        input,
                        `Array. Failed to parse element at index ${i}:\n${r.error.trace}`,
                    ),
                );
            }

            parsed.push(r.value);
        }

        return ok(parsed);
    }
}

class ResultSchema<T, E> extends PrimitiveSchema<Result<T, E>> {
    private schema;

    constructor(
        private readonly valueSchema: Schema<T>,
        private readonly errorSchema: Schema<E>,
    ) {
        super();

        this.schema = new UnionSchema(
            new ObjectSchema({
                tag: new LiteralSchema("ok"),
                value: valueSchema,
            }),
            new ObjectSchema({
                tag: new LiteralSchema("err"),
                error: errorSchema,
            }),
        );
    }

    protected override initialCheck(
        input: unknown,
    ): Result<Result<T, E>, ParseError> {
        return this.schema.parse(input).map((result) => {
            switch (result.tag) {
                case "ok":
                    return ok(result.value);
                case "err":
                    return err(result.error);
            }
        });
    }
}

class OptionSchema<S extends Schema<any>>
    extends PrimitiveSchema<Option<InferSchema<S>>> {
    private schema;

    constructor(private readonly valueSchema: S) {
        super();

        this.schema = new UnionSchema(
            new ObjectSchema({
                tag: new LiteralSchema("some"),
                value: valueSchema,
            }),
            new ObjectSchema({
                tag: new LiteralSchema("none"),
            }),
        );
    }

    protected override initialCheck(
        input: unknown,
    ): Result<Option<T>, ParseError> {
        return this.schema.parse(input).map((option) => {
            switch (option.tag) {
                case "some":
                    return some(option.value);
                case "none":
                    return none;
            }
        });
    }
}

class Validator {
    string(): StringSchema {
        return new StringSchema();
    }

    literal<L extends string>(literal: L): LiteralSchema<L> {
        return new LiteralSchema(literal);
    }

    number(): NumberSchema {
        return new NumberSchema();
    }

    bigint(): BigintSchema {
        return new BigintSchema();
    }

    boolean(): BooleanSchema {
        return new BooleanSchema();
    }

    date(): DateSchema {
        return new DateSchema();
    }

    undefined(): UndefinedSchema {
        return new UndefinedSchema();
    }

    null(): NullSchema {
        return new NullSchema();
    }

    void(): VoidSchema {
        return new VoidSchema();
    }

    any(): AnySchema {
        return new AnySchema();
    }

    unknown(): UnknownSchema {
        return new UnknownSchema();
    }

    union<S extends Schema<any>[]>(...schemas: S): UnionSchema<S> {
        return new UnionSchema(...schemas);
    }

    array<S extends Schema<any>>(elementSchema: S): ArraySchema<S> {
        return new ArraySchema(elementSchema);
    }

    result<T, E>(
        valueSchema: Schema<T>,
        errorSchema: Schema<E>,
    ): ResultSchema<T, E> {
        return new ResultSchema(valueSchema, errorSchema);
    }
}

const v = new Validator();

const r = v.string().max(4, "too long").or(v.number());

const res = r.parse(some("11234"));

console.log(res);

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

type NestedArray<T> = T | NestedArray<T>[];
