/**
 * Models one low-level pull step for stream-like consumers.
 *
 * A `Pull<A, E, Done, R>` is an `Effect` that can produce one `A`, fail with an
 * ordinary error `E`, or signal end-of-input with `Cause.Done<Done>`. The
 * separate done signal lets low-level consumers distinguish normal completion
 * from failure. This module includes type extractors and helpers for detecting,
 * filtering, catching, converting, and matching done signals separately from
 * ordinary failures.
 *
 * @since 4.0.0
 */
import * as Cause from "./Cause.ts"
import type { Effect } from "./Effect.ts"
import * as Exit from "./Exit.ts"
import * as Filter from "./Filter.ts"
import { dual } from "./Function.ts"
import * as internalEffect from "./internal/effect.ts"
import * as Result from "./Result.ts"

/**
 * An effectful pull step that either produces a value, fails with `E`, or
 * signals completion with `Cause.Done<Done>`.
 *
 * **When to use**
 *
 * Use to model one low-level pull step when a consumer repeatedly evaluates an
 * effect that may emit a value, fail normally, or signal normal completion
 * through `Cause.Done`.
 *
 * **Details**
 *
 * `Pull` represents completion in the error channel so low-level stream
 * consumers can distinguish ordinary failures from end-of-input and carry a
 * leftover value when needed.
 *
 * @category models
 * @since 4.0.0
 */
export interface Pull<out A, out E = never, out Done = void, out R = never>
  extends Effect<A, E | Cause.Done<Done>, R>
{}

/**
 * Extracts the success type from a Pull type.
 *
 * **When to use**
 *
 * Use to derive the value produced by an existing `Pull` when declaring
 * reusable type aliases, low-level stream helpers, or function signatures.
 *
 * @see {@link Error} for extracting the ordinary failure type
 * @see {@link Leftover} for extracting the completion leftover type
 * @see {@link Services} for extracting the required services type instead
 *
 * @category type extractors
 * @since 4.0.0
 */
export type Success<P> = P extends Effect<infer _A, infer _E, infer _R> ? _A : never

/**
 * Extracts the error type from a Pull type, excluding Done errors.
 *
 * **When to use**
 *
 * Use to derive only the ordinary failure type from a `Pull` when declaring
 * wrappers or APIs that handle completion separately.
 *
 * @see {@link Success} for extracting the pulled value type instead
 * @see {@link Leftover} for extracting the completion leftover type
 * @see {@link Services} for extracting the required services type instead
 * @see {@link ExcludeDone} for excluding `Cause.Done` from an error union
 *
 * @category type extractors
 * @since 4.0.0
 */
export type Error<P> = P extends Effect<infer _A, infer _E, infer _R> ? _E extends Cause.Done<infer _L> ? never : _E
  : never

/**
 * Extracts the leftover type from a Pull type.
 *
 * **When to use**
 *
 * Use to derive the completion leftover type from an existing `Pull` when
 * declaring reusable type aliases or helper signatures that preserve a pull's
 * done value.
 *
 * @see {@link Success} for extracting the pulled value type instead
 * @see {@link Error} for extracting the ordinary failure type, excluding `Cause.Done`
 * @see {@link Services} for extracting the required services type instead
 *
 * @category type extractors
 * @since 4.0.0
 */
export type Leftover<P> = P extends Effect<infer _A, infer _E, infer _R> ? _E extends Cause.Done<infer _L> ? _L : never
  : never

/**
 * Extracts the service requirements (context) type from a Pull type.
 *
 * **When to use**
 *
 * Use to derive the context requirements of a generic or inferred `Pull`
 * without restating its `R` type parameter.
 *
 * @see {@link Success} for extracting the pulled value type instead
 * @see {@link Error} for extracting the ordinary failure type
 * @see {@link Leftover} for extracting the completion leftover type
 *
 * @category type extractors
 * @since 4.0.0
 */
export type Services<P> = P extends Effect<infer _A, infer _E, infer _R> ? _R : never

/**
 * Excludes `Cause.Done` completion signals from an error type union.
 *
 * **When to use**
 *
 * Use to describe the ordinary error type that remains after `Cause.Done`
 * completion signals have been handled or filtered out of an error union.
 *
 * @see {@link Error} for extracting ordinary failures from a `Pull`
 * @see {@link Leftover} for extracting the completion leftover type
 *
 * @category type extractors
 * @since 4.0.0
 */
export type ExcludeDone<E> = Exclude<E, Cause.Done<any>>

// -----------------------------------------------------------------------------
// Done
// -----------------------------------------------------------------------------

/**
 * Handles `Cause.Done` failures in an effect while leaving ordinary failures
 * in the error channel.
 *
 * **When to use**
 *
 * Use to recover from a `Cause.Done` completion signal in an effect, such as
 * turning a pull leftover value into a successful recovery effect while
 * preserving ordinary failures.
 *
 * **Details**
 *
 * The handler receives the done leftover value and may recover with a new
 * effect. Non-done errors are preserved.
 *
 * @see {@link matchEffect} for handling success, ordinary failure, and done outcomes explicitly
 * @see {@link filterDoneLeftover} for extracting a done leftover from an existing `Cause`
 *
 * @category Done
 * @since 4.0.0
 */
export const catchDone: {
  <E, A2, E2, R2>(f: (leftover: Cause.Done.Extract<E>) => Effect<A2, E2, R2>): <A, R>(
    self: Effect<A, E, R>
  ) => Effect<A | A2, ExcludeDone<E> | E2, R | R2>
  <A, R, E, A2, E2, R2>(
    self: Effect<A, E, R>,
    f: (leftover: Cause.Done.Extract<E>) => Effect<A2, E2, R2>
  ): Effect<A | A2, ExcludeDone<E> | E2, R | R2>
} = dual(2, <A, R, E, A2, E2, R2>(
  effect: Effect<A, E, R>,
  f: (leftover: Cause.Done.Extract<E>) => Effect<A2, E2, R2>
): Effect<A | A2, ExcludeDone<E> | E2, R | R2> =>
  internalEffect.catchCauseFilter(effect, filterDoneLeftover as any, (l: any) => f(l)) as any)

/**
 * Checks whether a Cause contains any done errors.
 *
 * **When to use**
 *
 * Use when you need to test whether a pull failure cause represents normal
 * completion and only need a boolean result.
 *
 * @see {@link isDoneFailure} for checking a single `Cause.Reason`
 * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause`
 * @see {@link filterNoDone} for selecting causes with no done failures
 *
 * @category Done
 * @since 4.0.0
 */
export const isDoneCause = <E>(cause: Cause.Cause<E>): boolean => cause.reasons.some(isDoneFailure)

/**
 * Checks whether a `Cause.Reason` is a `Fail` reason whose error is a
 * `Cause.Done` signal.
 *
 * **When to use**
 *
 * Use when you need to identify done completion reasons while traversing
 * `cause.reasons`, before handling ordinary failures.
 *
 * @see {@link isDoneCause} for checking an entire `Cause` for any done reason
 * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause`
 *
 * @category Done
 * @since 4.0.0
 */
export const isDoneFailure = <E>(
  failure: Cause.Reason<E>
): failure is Cause.Fail<E & Cause.Done<any>> => failure._tag === "Fail" && Cause.isDone(failure.error)

/**
 * Finds a `Cause.Done` failure in a `Cause`.
 *
 * **When to use**
 *
 * Use to separate `Cause.Done` completion from ordinary causes while preserving
 * the typed done value.
 *
 * **Details**
 *
 * Returns a successful `Result` with the `Cause.Done` value when one is
 * present, otherwise returns a failed `Result` containing the non-done cause.
 *
 * @category Done
 * @since 4.0.0
 */
export const filterDone: <E>(
  input: Cause.Cause<E>
) => Result.Result<Cause.Done.Only<E>, Cause.Cause<ExcludeDone<E>>> = Filter
  .composePassthrough(
    Cause.findError,
    (e) => Cause.isDone(e) ? Result.succeed(e) : Result.fail(e)
  ) as any

/**
 * Finds a `Cause.Done` failure in a cause whose done value is not used.
 *
 * **When to use**
 *
 * Use to detect `Cause.Done` completion in a `Cause` when the completion value
 * is not part of the downstream logic.
 *
 * **Details**
 *
 * Returns a successful `Result` with the done marker when present, otherwise
 * returns a failed `Result` with the non-done cause.
 *
 * @see {@link filterDone} for preserving the typed `Cause.Done` value when the done payload matters
 * @see {@link filterDoneLeftover} for extracting only the done leftover value
 * @see {@link filterNoDone} for the inverse filter that succeeds only when no done failure is present
 *
 * @category Done
 * @since 4.0.0
 */
export const filterDoneVoid: <E extends Cause.Done>(
  input: Cause.Cause<E>
) => Result.Result<Cause.Done, Cause.Cause<Exclude<E, Cause.Done>>> = Filter.composePassthrough(
  Cause.findError,
  (e) => Cause.isDone(e) ? Result.succeed(e) : Result.fail(e)
) as any

/**
 * Keeps a `Cause` only when it contains no `Cause.Done` failures.
 *
 * **When to use**
 *
 * Use to select ordinary failure causes for handling while leaving `Cause.Done`
 * completion causes outside that handler.
 *
 * **Details**
 *
 * Returns a successful `Result` with the cause when every failure is non-done;
 * otherwise returns a failed `Result` with the original cause.
 *
 * @see {@link filterDone} for the inverse typed done filter
 * @see {@link filterDoneVoid} for done detection when the payload is not needed
 *
 * @category Done
 * @since 4.0.0
 */
export const filterNoDone: <E>(
  input: Cause.Cause<E>
) => Result.Result<
  Cause.Cause<ExcludeDone<E>>,
  Cause.Cause<E>
> = Filter.fromPredicate((cause: Cause.Cause<unknown>) =>
  cause.reasons.every((failure) => !isDoneFailure(failure))
) as any

/**
 * Filters a Cause to extract the leftover value from done errors.
 *
 * **When to use**
 *
 * Use to extract only the leftover value carried by a `Cause.Done` completion
 * signal.
 *
 * @category Done
 * @since 4.0.0
 */
export const filterDoneLeftover: <E>(
  cause: Cause.Cause<E>
) => Result.Result<Cause.Done.Extract<E>, Cause.Cause<ExcludeDone<E>>> = Filter.composePassthrough(
  Cause.findError,
  (e) => Cause.isDone(e) ? Result.succeed(e.value) : Result.fail(e)
) as any

/**
 * Converts a `Cause` into an `Exit`, treating `Cause.Done` as successful
 * completion.
 *
 * **When to use**
 *
 * Use to produce an `Exit` for finalizing a low-level pull workflow when a
 * `Cause.Done` signal should be treated as success and any remaining cause
 * should fail.
 *
 * **Details**
 *
 * If the cause contains a done value, that leftover becomes the successful
 * value. Otherwise the non-done cause becomes the failure cause.
 *
 * @see {@link filterDone} for extracting the done signal without converting the cause to an `Exit`
 * @see {@link matchEffect} for handling `Pull` success, failure, and done outcomes directly
 *
 * @category Done
 * @since 4.0.0
 */
export const doneExitFromCause = <E>(cause: Cause.Cause<E>): Exit.Exit<Cause.Done.Extract<E>, ExcludeDone<E>> => {
  const halt = filterDone(cause)
  return !Result.isFailure(halt) ? Exit.succeed(halt.success.value as any) : Exit.failCause(halt.failure)
}

/**
 * Pattern matches on a Pull, handling success, failure, and done cases.
 *
 * **When to use**
 *
 * Use to handle all three `Pull` outcomes with effectful handlers.
 *
 * **Example** (Matching Pull outcomes)
 *
 * ```ts
 * import { Cause, Effect, Pull } from "effect"
 *
 * const pull = Cause.done("stream ended")
 *
 * const result = Pull.matchEffect(pull, {
 *   onSuccess: (value) => Effect.succeed(`Got value: ${value}`),
 *   onFailure: (cause) => Effect.succeed(`Got error: ${cause}`),
 *   onDone: (leftover) => Effect.succeed(`Stream halted with: ${leftover}`)
 * })
 * ```
 *
 * @category pattern matching
 * @since 4.0.0
 */
export const matchEffect: {
  <A, E, L, AS, ES, RS, AF, EF, RF, AH, EH, RH>(options: {
    readonly onSuccess: (value: A) => Effect<AS, ES, RS>
    readonly onFailure: (failure: Cause.Cause<E>) => Effect<AF, EF, RF>
    readonly onDone: (leftover: L) => Effect<AH, EH, RH>
  }): <R>(self: Pull<A, E, L, R>) => Effect<AS | AF | AH, ES | EF | EH, R | RS | RF | RH>
  <A, E, L, R, AS, ES, RS, AF, EF, RF, AH, EH, RH>(self: Pull<A, E, L, R>, options: {
    readonly onSuccess: (value: A) => Effect<AS, ES, RS>
    readonly onFailure: (failure: Cause.Cause<E>) => Effect<AF, EF, RF>
    readonly onDone: (leftover: L) => Effect<AH, EH, RH>
  }): Effect<AS | AF | AH, ES | EF | EH, R | RS | RF | RH>
} = dual(2, <A, E, L, R, AS, ES, RS, AF, EF, RF, AH, EH, RH>(self: Pull<A, E, L, R>, options: {
  readonly onSuccess: (value: A) => Effect<AS, ES, RS>
  readonly onFailure: (failure: Cause.Cause<E>) => Effect<AF, EF, RF>
  readonly onDone: (leftover: L) => Effect<AH, EH, RH>
}): Effect<AS | AF | AH, ES | EF | EH, R | RS | RF | RH> =>
  internalEffect.matchCauseEffect(self, {
    onSuccess: options.onSuccess,
    onFailure: (cause): Effect<AS | AF | AH, ES | EF | EH, RS | RF | RH> => {
      const halt = filterDone(cause)
      return !Result.isFailure(halt) ? options.onDone(halt.success.value as L) : options.onFailure(halt.failure)
    }
  }))
