import { AsyncResult, Result } from ".";
import { resultify } from "./resultify";

type Arg = unknown;

/**
 * This function is designed to make for comprehensions a little easier to compose without diagonalizing.
 *
 * A for comprehension is a pattern of chaining `Result` with a series of `andThen`/`andThenAsync` calls, passing
 * along incremental plus accumulated results to each next step, then making a final yield call to get the total
 * output from the chain if all were successful.
 *
 * NOTE: Because of limitations of Typescript, incremental results, that is the new value produce by an `andThen`/
 * `andThenAsync`, will be *pre*-pended to the front of the accumulated results. Also, tsc can quickly fail to infer
 * the changes to the accumulated type, a tuple to which each incremental type will be added. Adding a type annotation
 * between the `andThen`/`andThenAsync` and the open parenthesis for the arguments should be sufficient to clear up
 * tsc's confusion.
 *
 * Example, from MQ's side effect handling when a subscription changes:
 *
 *  return await resultify(tryNarrowSubscription(event.data)).andThenAsync(
 *    async ({ object: subscription }) => {
 *      return await resultify(tryGetSubscription(subscription))
 *        .andThen(tryNarrowSubscriptionCustomerId)
 *        // type hint here because `carry` is used to add the determined plan
 *        // to the tuple already containing the retrieved and narrowed subscription
 *        .andThen<[PlanType, SubscriptionWithCustomerId]>(
 *          (subscription) => {
 *            const { price } = subscription.items.data[0];
 *            return tryDeterminePlan(price).yield(carry(subscription));
 *          },
 *        )
 *        // and another link in the chain, adding the user from the database to
 *        // the tuple that has already accumulated the subscription and plan type
 *        .andThenAsync<[User, PlanType, SubscriptionWithCustomerId]>(
 *          async ([planType, subscription]) => {
 *            const { customer: stripeCustomerId } = subscription;
 *            return resultify(
 *              getRepository(User).tryFindStripeCustomer(stripeCustomerId),
 *            ).yield(carry(planType, subscription));
 *          },
 *        )
 *        // this is still part of the for comprehension, it happens to consume some of the state
 *        // and uses a simple yield call without carry to set a constant output, that the event was
 *        // handled
 *        .andThenAsync(async ([user, planType, subscription]) => {
 *          return await resultify(
 *            createMauticContact(
 *              QueueName.MauticStripe,
 *              user.email,
 *              planType,
 *              user?.firstName,
 *              subscription.status,
 *            ),
 *          ).yield(() => EventOutcome.Handled);
 *        })
 *        .orElse(skipUnknownPlan)
 *        // here the final yield is simply doing some logging but in API-land, a more typical pattern
 *        // would be to add the accumulated outputs from the rest of the comprehension into some singular value
 *        // other than the tuple used simply to carry those outputs to this last step
 *        .yield(
 *          logOutcome(
 *            logger,
 *            outcomeMessage("mautic", "Stripe", event.type),
 *            event,
 *          ),
 *        );
 *    },
 *  );
 */
export function carry<T extends Array<Arg>, U>(...ts: T): (u: U) => [U, ...T] {
  return (u: U) => {
    return [u, ...ts];
  };
}

/**
 * An alternative to `carry` that can be used to wrap a closure before passing it to `andThen` so that there is no
 * need to manually add a `.yield(carry(...))` in the closure's logic.
 *
 * NOTE: This currently doesn't work well with longer for comprehensions since it makes the flow of types a little
 * more opaque to tsc than simply using `carry`.
 */
export function yieldWithSync<T extends Array<Arg>, U, E>(
  f: (ts: T) => Result<U, E>,
): (ts: T) => Result<[U, ...T], E> {
  return (ts: T) => {
    return f(ts).yield((u) => [u, ...ts]);
  };
}

/**
 * An alternative to `carry` that can be used to wrap a closure before passing it to `andThen` so that there is no
 * need to manually add a `.yield(carry(...))` in the closure's logic.
 *
 * NOTE: This currently doesn't work well with longer for comprehensions since it makes the flow of types a little
 * more opaque to tsc than simply using `carry`.
 */
export function yieldWith<T extends Array<Arg>, U, E>(
  f: (ts: T) => AsyncResult<U, E>,
): (ts: T) => AsyncResult<[U, ...T], E> {
  return async (ts: T) => {
    return await resultify(f(ts)).yield((u) => [u, ...ts]);
  };
}
