import { some } from "@shared/utils/option.ts";
import { None, type Option, Some } from "@shared/utils/option.ts";
import { errAsync, okAsync, ResultAsync } from "@shared/utils/resultasync.ts";

type ResultJSON<T, E> = { tag: "ok"; value: T } | { tag: "err"; error: E };

//#region Ok, Err and Result
interface IResult<T, E> {
    isOk(): this is Ok<T, E>;
    ifOk(fn: (value: T) => void): Result<T, E>;
    isErr(): this is Err<T, E>;
    ifErr(fn: (err: E) => void): Result<T, E>;
    isErrOrNone(): this is Err<None<T>, E>;
    unwrap(): T;
    unwrapOr<U>(defaultValue: U): T | U;
    unwrapOrElse<U>(fn: () => U): T | U;
    match<A, B = A>(ok: (value: T) => A, err: (error: E) => B): A | B;
    map<U>(fn: (value: T) => U): Result<U, E>;
    mapAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<U, E>;
    mapErr<U>(fn: (err: E) => U): Result<T, U>;
    mapErrAsync<U>(fn: (err: E) => Promise<U>): ResultAsync<T, U>;
    andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F>;
    andThenAsync<U, F>(fn: (value: T) => ResultAsync<U, F>): ResultAsync<U, F>;
    flatten(): FlattenResult<Result<T, E>>;
    flattenOption<U>(errFn: () => U): Result<UnwrapOption<T>, U | E>;
    flattenOptionOr<D = UnwrapOption<T>>(
        defaultValue: D,
    ): Result<UnwrapOption<T> | D, E>;
    mapOption<U>(fn: (value: UnwrapOption<T>) => U): Result<Option<U>, E>;
    matchOption<A, B>(
        some: (value: UnwrapOption<T>) => A,
        none: () => B,
    ): Result<A | B, E>;
    toNullable(): T | null;
    toAsync(): ResultAsync<T, E>;
    void(): Result<void, E>;

    toJSON(): ResultJSON<T, E>;
}

export class Ok<T, E> implements IResult<T, E> {
    public readonly tag = "Ok";

    constructor(public readonly value: T) {
        this.value = value;

        Object.defineProperties(this, {
            tag: {
                writable: false,
                enumerable: false,
            },
        });
    }

    isErr(): this is Err<T, E> {
        return false;
    }

    ifErr(fn: (err: E) => void): Result<T, E> {
        return this;
    }

    isErrOrNone(): this is Err<None<T>, E> {
        if (this.value instanceof None) {
            return true;
        }
        return false;
    }

    isOk(): this is Ok<T, E> {
        return true;
    }

    ifOk(fn: (value: T) => void): Result<T, E> {
        fn(this.value);
        return this;
    }

    unwrap(): T {
        return this.value;
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    unwrapOr<U>(defaultValue: U): T {
        return this.value;
    }

    unwrapOrElse<U>(fn: () => U): T | U {
        return this.value;
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    match<A, B = A>(ok: (value: T) => A, err: (error: E) => B): A | B {
        return ok(this.value);
    }

    map<U>(fn: (value: T) => U): Result<U, E> {
        const mappedValue = fn(this.value);

        return new Ok<U, E>(mappedValue);
    }

    mapAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<U, E> {
        return ResultAsync.fromSafePromise(fn(this.value));
    }

    mapOption<U>(fn: (value: UnwrapOption<T>) => U): Result<Option<U>, E> {
        if (this.value instanceof None || this.value instanceof Some) {
            return ok(this.value.map(fn));
        }
        return ok(some(fn(this.value as UnwrapOption<T>)));
    }

    andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F> {
        return fn(this.value) as Result<U, E | F>;
    }

    andThenAsync<U, F>(fn: (value: T) => ResultAsync<U, F>): ResultAsync<U, F> {
        return fn(this.value);
    }

    mapErr<U>(fn: (err: E) => U): Result<T, U> {
        return new Ok<T, U>(this.value);
    }

    mapErrAsync<U>(fn: (err: E) => Promise<U>): ResultAsync<T, U> {
        return okAsync(this.value);
    }

    flatten(): FlattenResult<Result<T, E>> {
        return flattenResult(this);
    }

    flattenOption<U = E>(errFn: () => U): Result<UnwrapOption<T>, E | U> {
        if (this.value instanceof None || this.value instanceof Some) {
            return this.value.okOrElse(errFn);
        }
        return new Ok<UnwrapOption<T>, E | U>(this.value as UnwrapOption<T>);
    }

    flattenOptionOr<D = UnwrapOption<T>>(
        defaultValue: D,
    ): Result<UnwrapOption<T> | D, E> {
        if (this.value instanceof None || this.value instanceof Some) {
            return this.value.unwrapOr(defaultValue);
        }
        return new Ok<UnwrapOption<T> | D, E>(this.value as UnwrapOption<T>);
    }

    matchOption<A, B>(
        some: (value: UnwrapOption<T>) => A,
        none: () => B,
    ): Result<A | B, E> {
        if (this.value instanceof None || this.value instanceof Some) {
            return ok(this.value.match(some, none));
        }
        return ok(some(this.value as UnwrapOption<T>));
    }

    toNullable(): T | null {
        return this.value;
    }

    toAsync(): ResultAsync<T, E> {
        return okAsync(this.value);
    }

    void(): Result<void, E> {
        return ok();
    }

    toJSON(): ResultJSON<T, E> {
        return {
            tag: "ok",
            value: this.value,
        };
    }
}

export class Err<T, E> implements IResult<T, E> {
    public readonly tag = "Err";

    constructor(public readonly error: E) {
        this.error = error;

        Object.defineProperties(this, {
            tag: {
                writable: false,
                configurable: false,
                enumerable: false,
            },
        });
    }
    isErr(): this is Err<T, E> {
        return true;
    }
    ifErr(fn: (err: E) => void): Result<T, E> {
        fn(this.error);
        return this;
    }
    isOk(): this is Ok<T, E> {
        return false;
    }
    ifOk(fn: (value: T) => void): Result<T, E> {
        return this;
    }
    isErrOrNone(): this is Err<None<T>, E> {
        return true;
    }
    unwrap(): T {
        const message = `Tried to unwrap error: ${
            getMessageFromError(this.error)
        }`;
        throw new Error(message);
    }
    unwrapOr<U>(defaultValue: U): U {
        return defaultValue;
    }
    unwrapOrElse<U>(fn: () => U): T | U {
        return fn();
    }
    match<A, B = A>(ok: (value: T) => A, err: (error: E) => B): A | B {
        return err(this.error);
    }
    map<U>(fn: (value: T) => U): Result<U, E> {
        return new Err<U, E>(this.error);
    }
    mapAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<U, E> {
        return errAsync(this.error);
    }
    mapErr<U>(fn: (err: E) => U): Result<T, U> {
        const mappedError = fn(this.error);
        return new Err<T, U>(mappedError);
    }
    mapErrAsync<U>(fn: (err: E) => Promise<U>): ResultAsync<T, U> {
        return ResultAsync.fromPromise(
            new Promise(() => {
                throw "";
            }),
            () => {
                return fn(this.error);
            },
        );
    }
    mapOption<U>(fn: (value: UnwrapOption<T>) => U): Result<Option<U>, E> {
        return err(this.error);
    }
    andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F> {
        return new Err<U, E | F>(this.error);
    }
    flatten(): FlattenResult<Result<T, E>> {
        return flattenResult(this);
    }
    flattenOption<U>(errFn: () => U): Result<UnwrapOption<T>, E | U> {
        return new Err<UnwrapOption<T>, E | U>(this.error);
    }
    flattenOptionOr<D = UnwrapOption<T>>(
        defaultValue: D,
    ): Result<D | UnwrapOption<T>, E> {
        return new Err<UnwrapOption<T> | D, E>(this.error);
    }

    matchOption<A, B>(
        some: (value: UnwrapOption<T>) => A,
        none: () => B,
    ): Result<A | B, E> {
        return err<A | B, E>(this.error);
    }
    toNullable(): T | null {
        return null;
    }
    toAsync(): ResultAsync<T, E> {
        return errAsync(this.error);
    }
    void(): Result<void, E> {
        return err(this.error);
    }
    toJSON(): { tag: "ok"; value: T } | { tag: "err"; error: E } {
        return {
            tag: "err",
            error: this.error,
        };
    }
}

export type Result<T, E> = Ok<T, E> | Err<T, E>;
//#endregion

//#region Ok and Err factory functions
export function ok<T, E = never>(val: T): Ok<T, E>;
export function ok<T extends void = void, E = never>(val: void): Ok<void, E>;
export function ok<T, E = never>(val: T): Ok<T, E> {
    return new Ok(val) as Ok<T, E>;
}

export function err<T = never, E extends string = string>(err: E): Err<T, E>;
export function err<T = never, E = unknown>(err: E): Err<T, E>;
export function err<T = never, E extends void = void>(err: void): Err<T, void>;
export function err<T, E>(err: E): Err<T, E> {
    return new Err(err) as Err<T, E>;
}

//#endregion

export function fromThrowable<Fn extends (...args: readonly any[]) => any, E>(
    fn: Fn,
    errorMapper?: (e: unknown) => E,
): (...args: Parameters<Fn>) => Result<ReturnType<Fn>, E> {
    return (...args) => {
        try {
            const result = fn(...args);
            return ok(result);
        } catch (e) {
            return err(errorMapper ? errorMapper(e) : e);
        }
    };
}

/**
 * utility function to get an error message from an thrown unknown type
 */
export function getMessageFromError(e: unknown): string {
    if (e instanceof Error) {
        if (e.message) {
            return e.message;
        }

        if ("code" in e && typeof e.code === "string") {
            return e.code;
        }

        return "An unknown error occurred";
    }

    if (typeof e === "string") {
        return e;
    }

    if (typeof e === "object" && e !== null && "message" in e) {
        // If e is an object with a message property (could be a custom error-like object)
        const obj = e as { message: unknown };
        return typeof obj.message === "string"
            ? obj.message
            : String(obj.message);
    }

    return "An unknown error occurred";
}

export function flattenResult<R extends Result<any, any>>(
    nestedResult: R,
): FlattenResult<R> {
    let currentResult = nestedResult;

    while (currentResult instanceof Ok) {
        currentResult = currentResult.value;
    }

    return currentResult as FlattenResult<R>;
}

export type UnwrapOption<T> = T extends Option<infer V> ? V : T;

export type FlattenResult<R> = R extends Result<infer T, infer E>
    ? T extends Result<any, any>
        ? FlattenResult<T> extends Result<infer V, infer innerE>
            ? Result<V, E | innerE>
        : never
    : R
    : never;

class FailedToParseResult extends Error {
    constructor(json: string) {
        super(`Failed to parse ${json} as result`);
    }
}

export function ResultFromJSON<T = unknown, E = unknown>(
    input: string | unknown,
): Result<T, E | FailedToParseResult> {
    let data: unknown;

    if (typeof input === "string") {
        try {
            data = JSON.parse(input);
        } catch (e) {
            return err(
                new FailedToParseResult(getMessageFromError(e)),
            );
        }
    } else {
        data = input;
    }

    if (typeof data !== "object" || data === null) {
        return err(
            new FailedToParseResult(
                "Expected an object but received type ${typeof data}.",
            ),
        );
    }

    const resultObj = data as ResultJSON<T, E>;
    if ("tag" in resultObj) {
        switch (resultObj.tag) {
            case "ok": {
                if ("value" in resultObj) {
                    return ok(resultObj.value as T);
                }
                break;
            }
            case "err": {
                if ("error" in resultObj) {
                    return err(resultObj.error as E);
                }
                break;
            }
        }
    }

    return err(
        new FailedToParseResult(
            "Object does not contain 'tag' and 'value' or 'error' property",
        ),
    );
}
