import type { Resource } from '@angular/core';
import { assertNever } from '@shared/kernel/fp';

/**
 * The four mutually-exclusive states of an async fetch, as a tagged union.
 * Crucially the data lives ON the state: only `Failure` has an `error`, only
 * `Success` has a `value`. "Loaded but no value" or "error with stale value"
 * are unrepresentable — Richard Feldman's RemoteData.
 */
export type RemoteData<E, T> =
  | { tag: 'Loading' }
  | { tag: 'Empty' }
  | { tag: 'Failure'; error: E }
  | { tag: 'Success'; value: T };

/** Project Angular's loosely-typed Resource into a RemoteData value. */
export function fromResource<T>(
  r: Resource<T>,
  isEmpty: (v: T) => boolean = () => false,
): RemoteData<Error | undefined, T> {
  if (r.status() === 'error') return { tag: 'Failure', error: r.error() };
  if (r.status() === 'loading') return { tag: 'Loading' };
  if (r.hasValue()) {
    const v = r.value();
    return isEmpty(v) ? { tag: 'Empty' } : { tag: 'Success', value: v };
  }
  return { tag: 'Loading' };
}

/** Exhaustive fold: you must handle every case, checked at compile time. */
export function foldRemote<E, T, R>(
  rd: RemoteData<E, T>,
  h: { loading: () => R; empty: () => R; failure: (e: E) => R; success: (v: T) => R },
): R {
  switch (rd.tag) {
    case 'Loading':
      return h.loading();
    case 'Empty':
      return h.empty();
    case 'Failure':
      return h.failure(rd.error);
    case 'Success':
      return h.success(rd.value);
    default:
      return assertNever(rd);
  }
}

// --- Combinators -----------------------------------------------------------
// Let several independent async sources be treated as one. When you combine
// two streams the result is: a failure if EITHER failed, still loading if
// either is loading, empty if either is empty, and only Success when BOTH
// succeeded. Precedence: Failure > Loading > Empty > Success.

/** Transform the value inside a Success; pass other states through unchanged. */
export function map<E, A, B>(rd: RemoteData<E, A>, f: (a: A) => B): RemoteData<E, B> {
  return rd.tag === 'Success' ? { tag: 'Success', value: f(rd.value) } : rd;
}

/** Combine two sources into one. Use this to merge e.g. a BIG-register call
    and a BRP call into a single state the page can render. */
export function map2<E, A, B, R>(
  a: RemoteData<E, A>,
  b: RemoteData<E, B>,
  f: (a: A, b: B) => R,
): RemoteData<E, R> {
  if (a.tag === 'Failure') return a;
  if (b.tag === 'Failure') return b;
  if (a.tag === 'Loading' || b.tag === 'Loading') return { tag: 'Loading' };
  if (a.tag === 'Empty' || b.tag === 'Empty') return { tag: 'Empty' };
  return { tag: 'Success', value: f(a.value, b.value) };
}

/** Combine three sources (built on map2). */
export function map3<E, A, B, C, R>(
  a: RemoteData<E, A>,
  b: RemoteData<E, B>,
  c: RemoteData<E, C>,
  f: (a: A, b: B, c: C) => R,
): RemoteData<E, R> {
  return map2(map2(a, b, (x, y) => [x, y] as const), c, ([x, y], z) => f(x, y, z));
}

/** Chain a second source that depends on the first one's value. */
export function andThen<E, A, B>(
  rd: RemoteData<E, A>,
  f: (a: A) => RemoteData<E, B>,
): RemoteData<E, B> {
  return rd.tag === 'Success' ? f(rd.value) : rd;
}
