import {
    Err,
    FlattenResult,
    Ok,
    type Result,
    type UnwrapOption,
} from "@shared/utils/result.ts";
import { None, none, Option, Some, some } from "@shared/utils/option.ts";

export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
    constructor(private readonly _promise: Promise<Result<T, E>>) {
        this._promise = _promise;
    }

    static fromPromise<T, E>(
        promise: Promise<T>,
        errorMapper: (error: unknown) => E,
    ) {
        const promiseOfResult: Promise<Result<T, E>> = promise
            .then((value: T): Result<T, E> => {
                return new Ok<T, E>(value);
            })
            .catch((error: unknown): Result<T, E> => {
                return new Err<T, E>(errorMapper(error));
            });

        return new ResultAsync<T, E>(promiseOfResult);
    }

    static fromSafePromise<T>(promise: Promise<T>) {
        const promiseOfResult: Promise<Result<T, never>> = promise.then(
            (value: T): Result<T, never> => {
                return new Ok<T, never>(value);
            },
        );

        return new ResultAsync<T, never>(promiseOfResult);
    }

    static fromThrowable<
        Fn extends (...args: readonly any[]) => Promise<any>,
        Em extends ErrorMapper<any>,
    >(
        fn: Fn,
        errorMapper?: Em,
    ): (
        ...args: Parameters<Fn>
    ) => ResultAsync<
        UnwrapPromise<ReturnType<Fn>>,
        ExtractErrorFromMapper<Em>
    > {
        return (
            ...args: Parameters<Fn>
        ): ResultAsync<
            UnwrapPromise<ReturnType<Fn>>,
            ExtractErrorFromMapper<Em>
        > => {
            return ResultAsync.fromPromise(
                fn(args),
                (e) => errorMapper ? errorMapper(e) : e,
            );
        };
    }

    async unwrap(): Promise<T> {
        const result = await this._promise;
        if (result.isErr()) {
            throw result.error;
        }

        return result.value;
    }

    async match<A, B = A>(
        ok: (value: T) => A,
        err: (err: E) => B,
    ): Promise<A | B> {
        const result = await this._promise;

        if (result.isErr()) {
            return err(result.error);
        }
        return ok(result.value);
    }

    map<U>(fn: (value: T) => U): ResultAsync<U, E> {
        return new ResultAsync(
            this._promise.then((result: Result<T, E>): Result<U, E> => {
                if (result.isErr()) {
                    return new Err<U, E>(result.error);
                }

                return new Ok<U, E>(fn(result.value));
            }),
        );
    }

    mapAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<U, E> {
        return new ResultAsync(
            this._promise.then(
                async (result: Result<T, E>): Promise<Result<U, E>> => {
                    if (result.isErr()) {
                        return errAsync<U, E>(result.error);
                    }

                    return new Ok<U, E>(await fn(result.value));
                },
            ),
        );
    }

    mapErr<U>(fn: (err: E) => U): ResultAsync<T, U> {
        return new ResultAsync(
            this._promise.then((result: Result<T, E>): Result<T, U> => {
                if (result.isErr()) {
                    return new Err<T, U>(fn(result.error));
                }

                return new Ok<T, U>(result.value);
            }),
        );
    }

    mapErrAsync<U>(fn: (value: T) => Promise<U>): ResultAsync<T, U> {
        return new ResultAsync(
            this._promise.then(
                async (result: Result<T, E>): Promise<Result<T, U>> => {
                    if (result.isErr()) {
                        return errAsync<T, U>(await fn(result.error));
                    }

                    return new Ok<T, U>(result.value);
                },
            ),
        );
    }

    andThen<U, F>(fn: (value: T) => Result<U, F>): ResultAsync<U, E | F> {
        return new ResultAsync(
            this._promise.then(
                (result: Result<T, E>): ResultAsync<U, E | F> => {
                    if (result.isErr()) {
                        return errAsync(result.error);
                    }

                    return fn(result.value).toAsync() as ResultAsync<U, E | F>;
                },
            ),
        );
    }

    andThenAsync<U, F>(
        fn: (value: T) => ResultAsync<U, F>,
    ): ResultAsync<U, E | F> {
        return new ResultAsync(
            this._promise.then(
                (result: Result<T, E>): ResultAsync<U, E | F> => {
                    if (result.isErr()) {
                        return errAsync(result.error);
                    }

                    return fn(result.value) as ResultAsync<U, E | F>;
                },
            ),
        );
    }

    nullableToOption(): ResultAsync<Option<NonNullable<T>>, E> {
        return this.map((v) => (v ? some(v) : none));
    }

    flatten(): FlattenResultAsync<ResultAsync<T, E>> {
        return new ResultAsync(
            this._promise.then(
                (result: Result<T, E>): FlattenResult<Result<T, E>> => {
                    return result.flatten();
                },
            ),
        ) as FlattenResultAsync<ResultAsync<T, E>>;
    }

    flattenOption<U = never>(
        errFn: () => U,
    ): ResultAsync<UnwrapOption<T>, E | U> {
        return new ResultAsync(
            this._promise.then(
                (result: Result<T, E>): Result<UnwrapOption<T>, E | U> => {
                    return result.flattenOption(errFn);
                },
            ),
        );
    }

    flattenOptionOrDefault<D = UnwrapOption<T>>(
        defaultValue: D,
    ): ResultAsync<UnwrapOption<T> | D, E> {
        return new ResultAsync(
            this._promise.then(
                (result: Result<T, E>): Result<UnwrapOption<T> | D, E> => {
                    return result.flattenOptionOrDefault(defaultValue);
                },
            ),
        );
    }

    matchOption<A, B = A>(
        some: (value: UnwrapOption<T>) => A,
        none: () => B,
    ): ResultAsync<A | B, E> {
        return new ResultAsync(
            this._promise.then((result: Result<T, E>): Result<A | B, E> => {
                return result.matchOption(some, none);
            }),
        );
    }

    matchOptionAndFlatten<A, B, U, F>(
        some: (value: UnwrapOption<T>) => Result<A, U>,
        none: () => Result<B, F>,
    ): ResultAsync<A | B, E | U | F> {
        return new ResultAsync(
            this._promise.then(
                (result: Result<T, E>): Result<A | B, E | U | F> => {
                    return result.matchOptionAndFlatten(some, none);
                },
            ),
        );
    }

    then<A, B>(
        onFulfilled?: (res: Result<T, E>) => A | PromiseLike<A>,
        onRejected?: (reason: unknown) => B | PromiseLike<B>,
    ): PromiseLike<A | B> {
        return this._promise.then(onFulfilled, onRejected);
    }
}

export function okAsync<T, E = never>(value: T): ResultAsync<T, E>;
export function okAsync<T extends void = void, E = never>(
    value: void,
): ResultAsync<void, E>;
export function okAsync<T, E = never>(value: T): ResultAsync<T, E> {
    return new ResultAsync(Promise.resolve(new Ok<T, E>(value)));
}

export function errAsync<T = never, E extends string = string>(
    err: E,
): ResultAsync<T, E>;
export function errAsync<T = never, E = unknown>(err: E): ResultAsync<T, E>;
export function errAsync<T = never, E extends void = void>(
    err: void,
): ResultAsync<T, void>;
export function errAsync<E, T = never>(err: E): ResultAsync<T, E> {
    return new ResultAsync(Promise.resolve(new Err<T, E>(err)));
}

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

type UnwrapPromise<Pr extends Promise<unknown>> = Pr extends Promise<infer U>
    ? U
    : never;

type ErrorMapper<E> = ((e: unknown) => E) | undefined;

type ExtractErrorFromMapper<Em extends ErrorMapper<unknown>> = Em extends
    (e: unknown) => infer E ? E : unknown;
