import * as combinators from "./combinators";

export { carry, yieldWith, yieldWithSync } from "./carry";
export { isResult } from "./guards";
export { resultify } from "./resultify";
export { passOrYield, tryAsync, trySync } from "./unit";

export function collectErrs<T, E>(rs: Result<T, E>[]): E[] {
  return rs.filter((r) => r.isErr()).map((e) => e.error);
}

/**
 * Interface for the success variant of a result, includes the tag, field, and
 * guards to narrow and access the contained value.
 */
export interface Ok<T> {
  kind: "ok";
  value: T;

  isOk(this: Result<T, unknown>): this is Ok<T>;
  isErr(this: Result<T, unknown>): this is Err<never>;
}

/**
 * Interface for the failure variant of a result, includes the tag, field, and
 * guards to narrow and access the contained value.
 */
export interface Err<E> {
  kind: "err";
  error: E;

  isOk(this: Result<unknown, E>): this is Ok<never>;
  isErr(this: Result<unknown, E>): this is Err<E>;
}

/**
 * Common interface for accessing the inner values of results. In use, one or
 * the other accessor will be of type never. A use of a result will first need
 * to narrow via one of the type guard methods, {@method isOk} and {@method
 * isErr} to narrow the generics correctly and use the methods of this
 * interface.
 */
export interface Unwrap<T, E> {
  get value(): T;

  get error(): E;
}

/**
 * Common interface for all of the useful combinators that allow accessing and
 * working with interior values of {@type Result}.
 */
export interface Combinators<T, E> {
  /**
   * If this result is {@type OK}, return {@param r} otherwise preserve any
   * {@type Err}. If you need to run any logic to produce a new value, use
   * {@method andThen} instead.
   */
  and<U>(r: Result<U, E>): Result<U, E>;

  /**
   * If this result is {@type OK}, evaluates {@param f} and returns, otherwise
   * preserve any {@type Err}. `andThen` is a synonym for `flatMap`.
   */
  andThen<U>(f: (t: T) => Result<U, E>): Result<U, E>;

  /**
   * Variation of {@method andThen} that accepts an asynchronous lambda, useful
   * with {@function thenPipeAsync} to compose {@type Result} resolving async
   * functions.
   */
  andThenAsync<U>(f: (t: T) => AsyncResult<U, E>): AsyncResult<U, E>;

  /**
   * If this result is {@type Err}, return {@param r} otherwise preserve any
   * {@type Ok}. If you need to run any logic to produce a new value, use
   * {@method orElse} instead.
   */
  or<F>(r: Result<T, F>): Result<T, F>;

  /**
   * Accept a lambda to execute in case of an error.
   */
  orElse<F>(f: (e: E) => Result<T, F>): Result<T, F>;

  /**
   * Accept an async lambda to execute in case of an error.
   */
  orElseAsync<F>(f: (e: E) => AsyncResult<T, F>): AsyncResult<T, F>;

  /**
   * Synonym for map, useful at the end of a chain of {@method andThen} calls
   * to adjust the final value before unwrapping.
   */
  yield<U>(f: (t: T) => U): Result<U, E>;

  /**
   * Variation of yield that accepts an asynchronous closure.
   */
  yieldAsync<U>(f: (t: T) => Promise<U>): AsyncResult<U, E>;

  /**
   * A flat map for the error of this {@type Result}.
   */
  yieldErr<F>(f: (e: E) => F): Result<T, F>;

  /**
   * If this result is {@type Err}, return an {@type Ok} containing {@param u},
   * otherwise execute {@param f}. If you need to run some logic to produce the
   * value for the argument, {@param u}, use {@method yieldOrElse} instead.
   */
  yieldOr<U>(u: U, f: (t: T) => U): Result<U, E>;

  /**
   * Accept a lambda to execute and produce a default value in case of an
   * error.
   */
  yieldOrElse<U>(u: (e: E) => U, f: (t: T) => U): Result<U, E>;
}

/**
 * Top type for our Try type.
 *
 * This is based on the approach explained in
 * https://www.jmcelwa.in/posts/rust-like-enums/ and after several attempts is
 * the only way to get the type guard methods of the variants to narrow
 * correctly for all use cases.
 *
 * Using never here with {@type Unwrap} works to ensure only the correct inner
 * values are accessible and avoids the problems that arise using
 * never/any/unknown directly on the implementation classes.
 */
export type Result<T, E> = (
  | (Ok<T> & Unwrap<T, never>)
  | (Err<E> & Unwrap<never, E>)
) &
  Combinators<T, E>;

export function Ok<T, E>(t: T): Result<T, E> {
  return new Result.Ok(t);
}

export function Err<T, E>(e: E): Result<T, E> {
  return new Result.Err(e);
}

/**
 * An alias that makes reading async function signatures that return promises a
 * bit easier.
 */
export type AsyncResult<T, E> = Promise<Result<T, E>>;

/**
 * This namespace allows the concrete implementations to have the same name as
 * their respective key interfaces. Callers should not need to work with the
 * namespace or its contained classes directly.
 */
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Result {
  export class Ok<T> implements Ok<T>, Unwrap<T, never>, Combinators<T, never> {
    kind = "ok" as const;

    constructor(readonly _value: T) {}

    isOk(this: Result<T, unknown>): this is Ok<T> {
      return this.kind === "ok";
    }

    isErr(this: Result<T, unknown>): this is Err<never> {
      return !this.isOk();
    }

    and<U, E>(r: Result<U, E>): Result<U, E> {
      return combinators.and<T, U, E>(this, r);
    }

    andThen<U, E>(f: (t: T) => Result<U, E>): Result<U, E> {
      return f(this.value);
    }

    async andThenAsync<U, E>(
      f: (t: T) => AsyncResult<U, E>,
    ): AsyncResult<U, E> {
      return await f(this.value);
    }

    or<F>(r: Result<T, F>): Result<T, F> {
      return combinators.or(this, r);
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    orElse<F>(_: (e: never) => Result<T, F>): Result<T, F> {
      return this;
    }

    async orElseAsync<F>(
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      _: (e: never) => AsyncResult<T, F>,
    ): AsyncResult<T, F> {
      return this;
    }

    yield<U, E>(f: (t: T) => U): Result<U, E> {
      return combinators.mapSync<T, U, E>(this, f);
    }

    async yieldAsync<U, E>(f: (t: T) => Promise<U>): AsyncResult<U, E> {
      return await combinators.mapAsync<T, U, E>(this, f);
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    yieldErr<E, F>(_: (e: E) => F): Result<T, F> {
      return this;
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    yieldOr<U>(_: U, f: (t: T) => U): Result<U, never> {
      return this.yield(f);
    }

    yieldOrElse<U>(d: (e: never) => U, f: (t: T) => U): Result<U, never> {
      return this.yieldOr(d(this.error), f);
    }

    get value(): T {
      return this._value;
    }

    get error(): never {
      throw new Error(
        "Cannot retrieve error value from OK! Has this result been narrowed using isOk/isErr?",
      );
    }
  }

  export class Err<E>
    implements Err<E>, Unwrap<never, E>, Combinators<never, E>
  {
    kind = "err" as const;

    constructor(readonly _error: E) {}

    isOk(this: Result<unknown, E>): this is Ok<never> {
      return !this.isErr();
    }

    isErr(this: Result<unknown, E>): this is Err<E> {
      return this.kind === "err";
    }

    and<U>(r: Result<U, E>): Result<U, E> {
      return combinators.and(this, r);
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    andThen<T, U>(_: (t: T) => Result<U, E>): Result<U, E> {
      return this;
    }

    async andThenAsync<T, U>(
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      _: (t: T) => AsyncResult<U, E>,
    ): AsyncResult<U, E> {
      return this;
    }

    or<T, F>(r: Result<T, F>): Result<T, F> {
      return combinators.or<T, E, F>(this, r);
    }

    orElse<F>(f: (e: E) => Result<never, F>): Result<never, F> {
      return f(this.error);
    }

    async orElseAsync<F>(
      f: (e: E) => AsyncResult<never, F>,
    ): AsyncResult<never, F> {
      return await f(this.error);
    }

    yield<T, U>(f: (t: T) => U): Result<U, E> {
      return combinators.mapSync(this, f);
    }

    async yieldAsync<T, U>(f: (t: T) => Promise<U>): AsyncResult<U, E> {
      return await combinators.mapAsync(this, f);
    }

    yieldErr<T, F>(f: (e: E) => F): Result<T, F> {
      return new Result.Err(f(this.error));
    }

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    yieldOr<U>(u: U, _: (t: never) => U): Result<U, E> {
      return new Result.Ok(u);
    }

    yieldOrElse<U>(u: (e: E) => U, f: (t: never) => U): Result<U, E> {
      return this.yieldOr(u(this._error), f);
    }

    get value(): never {
      throw new Error(
        "Cannot retrieve value from Err! Has this result been narrowed using isOk/isErr?",
      );
    }

    get error(): E {
      return this._error;
    }
  }
}
