import { AsyncResult, Result } from ".";
import {
  elsePipe,
  elsePipeAsync,
  errPipe,
  thenPipe,
  thenPipeAsync,
  yieldOrElsePipe,
  yieldOrPipe,
  yieldPipe,
  yieldPipeAsync,
} from "./pipe";

export type Resultified<T, E> = Promise<Result<T, E>> & {
  and<U>(r: Result<U, E>): Resultified<U, E>;

  andThen<U>(f: (t: T) => Result<U, E>): Resultified<U, E>;

  andThenAsync<U>(f: (t: T) => AsyncResult<U, E>): Resultified<U, E>;

  or<F>(r: Result<T, F>): Resultified<T, F>;

  orElse<F>(f: (e: E) => Result<T, F>): Resultified<T, F>;

  orElseAsync<F>(f: (e: E) => AsyncResult<T, F>): Resultified<T, F>;

  yield<U>(f: (t: T) => U): Resultified<U, E>;

  yieldAsync<U>(f: (t: T) => Promise<U>): Resultified<U, E>;

  yieldErr<F>(f: (e: E) => F): Resultified<T, F>;

  yieldOr<U>(u: U, f: (t: T) => U): Resultified<U, E>;

  yieldOrElse<U>(u: (e: E) => U, f: (t: T) => U): Resultified<U, E>;
};

/**
 * Consumes a {@type Result<T, E>} or {@type Promise<Result<T, E>>} and patches
 * it so that the methods of {@type Result<T, E>} can be called on the outer
 * {@type Promise}. If the argument is a {@type Result<T, E>}, this function
 * wraps it in an immediately resolved {@type Promise}; this behavior
 * simplifies starting a call chain with a call to a synchronous, {@type
 * Result} returning function. This function will not re-patch an already
 * modified promise meaning it is safe to call again with the same promise if
 * unsure.
 */
export function resultify<T, E>(
  resultOrPromise: Result<T, E> | Promise<Result<T, E>>,
): Resultified<T, E> {
  const promise = intoPromise(resultOrPromise);
  const resultified = promise as Resultified<T, E>;
  if (!resultOrPromise || !resultified) {
    throw new Error(
      "Promise was undefined or null! If this error occurs during testing, make sure your mocks return a Promise<Result>",
    );
  }
  if (resultified.andThen && typeof resultified.andThen === "function") {
    return resultified;
  }

  resultified.and = <U>(r: Result<U, E>): Resultified<U, E> =>
    resultify(promise.then(() => r));

  resultified.andThen = <U>(f: (t: T) => Result<U, E>): Resultified<U, E> =>
    resultify(promise.then(thenPipe(f)));

  resultified.andThenAsync = <U>(
    f: (t: T) => AsyncResult<U, E>,
  ): Resultified<U, E> => resultify(promise.then(thenPipeAsync(f)));

  resultified.or = <F>(r: Result<T, F>): Resultified<T, F> =>
    resultify(promise.then(() => r));

  resultified.orElse = <F>(f: (e: E) => Result<T, F>): Resultified<T, F> =>
    resultify(promise.then(elsePipe(f)));

  resultified.orElseAsync = <F>(
    f: (e: E) => AsyncResult<T, F>,
  ): Resultified<T, F> => resultify(promise.then(elsePipeAsync(f)));

  resultified.yield = <U>(f: (t: T) => U): Resultified<U, E> =>
    resultify(promise.then(yieldPipe(f)));

  resultified.yieldAsync = <U>(f: (t: T) => Promise<U>): Resultified<U, E> =>
    resultify(promise.then(yieldPipeAsync(f)));

  resultified.yieldErr = <F>(f: (e: E) => F): Resultified<T, F> =>
    resultify(promise.then(errPipe(f)));

  resultified.yieldOr = <U>(u: U, f: (t: T) => U): Resultified<U, E> =>
    resultify(promise.then(yieldOrPipe(u, f)));

  resultified.yieldOrElse = <U>(
    u: (e: E) => U,
    f: (t: T) => U,
  ): Resultified<U, E> => resultify(promise.then(yieldOrElsePipe(u, f)));

  return resultified;
}

function intoPromise<T, E>(
  resultOrPromise: Result<T, E> | Promise<Result<T, E>>,
): Promise<Result<T, E>> {
  return isResult(resultOrPromise)
    ? Promise.resolve(resultOrPromise)
    : resultOrPromise;
}

function isResult<T, E>(
  resultOrPromise: Result<T, E> | Promise<Result<T, E>>,
): resultOrPromise is Result<T, E> {
  return (
    resultOrPromise instanceof Result.Ok ||
    resultOrPromise instanceof Result.Err
  );
}
