import * as Cause from "../Cause.js"
import * as Clock from "../Clock.js"
import * as Context from "../Context.js"
import * as Duration from "../Duration.js"
import type * as Effect from "../Effect.js"
import type * as Exit from "../Exit.js"
import type { FiberRef } from "../FiberRef.js"
import * as FiberRefsPatch from "../FiberRefsPatch.js"
import type { LazyArg } from "../Function.js"
import { dual, pipe } from "../Function.js"
import * as HashMap from "../HashMap.js"
import type * as Layer from "../Layer.js"
import type * as ManagedRuntime from "../ManagedRuntime.js"
import { pipeArguments } from "../Pipeable.js"
import { hasProperty } from "../Predicate.js"
import type * as Runtime from "../Runtime.js"
import type * as Schedule from "../Schedule.js"
import * as ScheduleDecision from "../ScheduleDecision.js"
import * as Intervals from "../ScheduleIntervals.js"
import * as Scope from "../Scope.js"
import type * as Synchronized from "../SynchronizedRef.js"
import type * as Tracer from "../Tracer.js"
import type * as Types from "../Types.js"
import * as effect from "./core-effect.js"
import * as core from "./core.js"
import * as circular from "./effect/circular.js"
import * as fiberRuntime from "./fiberRuntime.js"
import * as circularManagedRuntime from "./managedRuntime/circular.js"
import * as EffectOpCodes from "./opCodes/effect.js"
import * as OpCodes from "./opCodes/layer.js"
import * as ref from "./ref.js"
import * as runtime from "./runtime.js"
import * as runtimeFlags from "./runtimeFlags.js"
import * as synchronized from "./synchronizedRef.js"
import * as tracer from "./tracer.js"

/** @internal */
const LayerSymbolKey = "effect/Layer"

/** @internal */
export const LayerTypeId: Layer.LayerTypeId = Symbol.for(
  LayerSymbolKey
) as Layer.LayerTypeId

const layerVariance = {
  /* c8 ignore next */
  _RIn: (_: never) => _,
  /* c8 ignore next */
  _E: (_: never) => _,
  /* c8 ignore next */
  _ROut: (_: unknown) => _
}

/** @internal */
export const proto = {
  [LayerTypeId]: layerVariance,
  pipe() {
    return pipeArguments(this, arguments)
  }
}

/** @internal */
const MemoMapTypeIdKey = "effect/Layer/MemoMap"

/** @internal */
export const MemoMapTypeId: Layer.MemoMapTypeId = Symbol.for(
  MemoMapTypeIdKey
) as Layer.MemoMapTypeId

/** @internal */
export type Primitive =
  | ExtendScope
  | Fold
  | Fresh
  | FromEffect
  | Scoped
  | Suspend
  | Locally
  | ProvideTo
  | ZipWith
  | ZipWithPar

/** @internal */
export type Op<Tag extends string, Body = {}> = Layer.Layer<unknown, unknown, unknown> & Body & {
  readonly _op_layer: Tag
}

/** @internal */
export interface ExtendScope extends
  Op<OpCodes.OP_EXTEND_SCOPE, {
    readonly layer: Layer.Layer<unknown>
  }>
{}

/** @internal */
export interface Fold extends
  Op<OpCodes.OP_FOLD, {
    readonly layer: Layer.Layer<unknown>
    failureK(cause: Cause.Cause<unknown>): Layer.Layer<unknown>
    successK(context: Context.Context<unknown>): Layer.Layer<unknown>
  }>
{}

/** @internal */
export interface Fresh extends
  Op<OpCodes.OP_FRESH, {
    readonly layer: Layer.Layer<unknown>
  }>
{}

/** @internal */
export interface FromEffect extends
  Op<OpCodes.OP_FROM_EFFECT, {
    readonly effect: Effect.Effect<unknown, unknown, Context.Context<unknown>>
  }>
{}

/** @internal */
export interface Scoped extends
  Op<OpCodes.OP_SCOPED, {
    readonly effect: Effect.Effect<unknown, unknown, Context.Context<unknown>>
  }>
{}

/** @internal */
export interface Suspend extends
  Op<OpCodes.OP_SUSPEND, {
    evaluate(): Layer.Layer<unknown>
  }>
{}

/** @internal */
export interface Locally extends
  Op<"Locally", {
    readonly self: Layer.Layer<unknown>
    f(_: Effect.Effect<any, any, any>): Effect.Effect<any, any, any>
  }>
{}

/** @internal */
export interface ProvideTo extends
  Op<OpCodes.OP_PROVIDE, {
    readonly first: Layer.Layer<unknown>
    readonly second: Layer.Layer<unknown>
  }>
{}

/** @internal */
export interface ZipWith extends
  Op<OpCodes.OP_PROVIDE_MERGE, {
    readonly first: Layer.Layer<unknown>
    readonly second: Layer.Layer<unknown>
    zipK(left: Context.Context<unknown>, right: Context.Context<unknown>): Context.Context<unknown>
  }>
{}

/** @internal */
export interface ZipWithPar extends
  Op<OpCodes.OP_ZIP_WITH, {
    readonly first: Layer.Layer<unknown>
    readonly second: Layer.Layer<unknown>
    zipK(left: Context.Context<unknown>, right: Context.Context<unknown>): Context.Context<unknown>
  }>
{}

/** @internal */
export const isLayer = (u: unknown): u is Layer.Layer<unknown, unknown, unknown> => hasProperty(u, LayerTypeId)

/** @internal */
export const isFresh = <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>): boolean => {
  return (self as Primitive)._op_layer === OpCodes.OP_FRESH
}

// -----------------------------------------------------------------------------
// MemoMap
// -----------------------------------------------------------------------------

/** @internal */
class MemoMapImpl implements Layer.MemoMap {
  readonly [MemoMapTypeId]: Layer.MemoMapTypeId
  constructor(
    readonly ref: Synchronized.SynchronizedRef<
      Map<
        Layer.Layer<any, any, any>,
        readonly [Effect.Effect<any, any>, Scope.Scope.Finalizer]
      >
    >
  ) {
    this[MemoMapTypeId] = MemoMapTypeId
  }

  /**
   * Checks the memo map to see if a layer exists. If it is, immediately
   * returns it. Otherwise, obtains the layer, stores it in the memo map,
   * and adds a finalizer to the `Scope`.
   */
  getOrElseMemoize<RIn, E, ROut>(
    layer: Layer.Layer<ROut, E, RIn>,
    scope: Scope.Scope
  ): Effect.Effect<Context.Context<ROut>, E, RIn> {
    return pipe(
      synchronized.modifyEffect(this.ref, (map) => {
        const inMap = map.get(layer)
        if (inMap !== undefined) {
          const [acquire, release] = inMap
          const cached: Effect.Effect<Context.Context<ROut>, E> = pipe(
            acquire as Effect.Effect<readonly [FiberRefsPatch.FiberRefsPatch, Context.Context<ROut>], E>,
            core.flatMap(([patch, b]) => pipe(effect.patchFiberRefs(patch), core.as(b))),
            core.onExit(core.exitMatch({
              onFailure: () => core.void,
              onSuccess: () => core.scopeAddFinalizerExit(scope, release)
            }))
          )
          return core.succeed([cached, map] as const)
        }
        return pipe(
          ref.make(0),
          core.flatMap((observers) =>
            pipe(
              core.deferredMake<readonly [FiberRefsPatch.FiberRefsPatch, Context.Context<ROut>], E>(),
              core.flatMap((deferred) =>
                pipe(
                  ref.make<Scope.Scope.Finalizer>(() => core.void),
                  core.map((finalizerRef) => {
                    const resource = core.uninterruptibleMask((restore) =>
                      pipe(
                        fiberRuntime.scopeMake(),
                        core.flatMap((innerScope) =>
                          pipe(
                            restore(core.flatMap(
                              makeBuilder(layer, innerScope, true),
                              (f) => effect.diffFiberRefs(f(this))
                            )),
                            core.exit,
                            core.flatMap((exit) => {
                              switch (exit._tag) {
                                case EffectOpCodes.OP_FAILURE: {
                                  return pipe(
                                    core.deferredFailCause(deferred, exit.effect_instruction_i0),
                                    core.zipRight(core.scopeClose(innerScope, exit)),
                                    core.zipRight(core.failCause(exit.effect_instruction_i0))
                                  )
                                }
                                case EffectOpCodes.OP_SUCCESS: {
                                  return pipe(
                                    ref.set(finalizerRef, (exit) =>
                                      pipe(
                                        core.scopeClose(innerScope, exit),
                                        core.whenEffect(
                                          ref.modify(observers, (n) => [n === 1, n - 1] as const)
                                        ),
                                        core.asVoid
                                      )),
                                    core.zipRight(ref.update(observers, (n) => n + 1)),
                                    core.zipRight(
                                      core.scopeAddFinalizerExit(scope, (exit) =>
                                        pipe(
                                          core.sync(() => map.delete(layer)),
                                          core.zipRight(ref.get(finalizerRef)),
                                          core.flatMap((finalizer) => finalizer(exit))
                                        ))
                                    ),
                                    core.zipRight(core.deferredSucceed(deferred, exit.effect_instruction_i0)),
                                    core.as(exit.effect_instruction_i0[1])
                                  )
                                }
                              }
                            })
                          )
                        )
                      )
                    )
                    const memoized = [
                      pipe(
                        core.deferredAwait(deferred),
                        core.onExit(core.exitMatchEffect({
                          onFailure: () => core.void,
                          onSuccess: () => ref.update(observers, (n) => n + 1)
                        }))
                      ),
                      (exit: Exit.Exit<unknown, unknown>) =>
                        pipe(
                          ref.get(finalizerRef),
                          core.flatMap((finalizer) => finalizer(exit))
                        )
                    ] as const
                    return [
                      resource,
                      isFresh(layer) ? map : map.set(layer, memoized)
                    ] as const
                  })
                )
              )
            )
          )
        )
      }),
      core.flatten
    )
  }
}

/** @internal */
export const makeMemoMap: Effect.Effect<Layer.MemoMap> = core.suspend(() =>
  core.map(
    circular.makeSynchronized<
      Map<
        Layer.Layer<any, any, any>,
        readonly [
          Effect.Effect<any, any>,
          Scope.Scope.Finalizer
        ]
      >
    >(new Map()),
    (ref) => new MemoMapImpl(ref)
  )
)

/** @internal */
export const unsafeMakeMemoMap = (): Layer.MemoMap => new MemoMapImpl(circular.unsafeMakeSynchronized(new Map()))

/** @internal */
export const build = <RIn, E, ROut>(
  self: Layer.Layer<ROut, E, RIn>
): Effect.Effect<Context.Context<ROut>, E, RIn | Scope.Scope> =>
  fiberRuntime.scopeWith((scope) => buildWithScope(self, scope))

/** @internal */
export const buildWithScope = dual<
  (
    scope: Scope.Scope
  ) => <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect.Effect<Context.Context<ROut>, E, RIn>,
  <RIn, E, ROut>(
    self: Layer.Layer<ROut, E, RIn>,
    scope: Scope.Scope
  ) => Effect.Effect<Context.Context<ROut>, E, RIn>
>(2, (self, scope) =>
  core.flatMap(
    makeMemoMap,
    (memoMap) => core.flatMap(makeBuilder(self, scope), (run) => run(memoMap))
  ))

/** @internal */
export const buildWithMemoMap = dual<
  (
    memoMap: Layer.MemoMap,
    scope: Scope.Scope
  ) => <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect.Effect<Context.Context<ROut>, E, RIn>,
  <RIn, E, ROut>(
    self: Layer.Layer<ROut, E, RIn>,
    memoMap: Layer.MemoMap,
    scope: Scope.Scope
  ) => Effect.Effect<Context.Context<ROut>, E, RIn>
>(3, (self, memoMap, scope) => core.flatMap(makeBuilder(self, scope), (run) => run(memoMap)))

const makeBuilder = <RIn, E, ROut>(
  self: Layer.Layer<ROut, E, RIn>,
  scope: Scope.Scope,
  inMemoMap = false
): Effect.Effect<(memoMap: Layer.MemoMap) => Effect.Effect<Context.Context<ROut>, E, RIn>> => {
  const op = self as Primitive
  switch (op._op_layer) {
    case "Locally": {
      return core.sync(() => (memoMap: Layer.MemoMap) => op.f(memoMap.getOrElseMemoize(op.self, scope)))
    }
    case "ExtendScope": {
      return core.sync(() => (memoMap: Layer.MemoMap) =>
        fiberRuntime.scopeWith(
          (scope) => memoMap.getOrElseMemoize(op.layer, scope)
        ) as unknown as Effect.Effect<Context.Context<ROut>, E, RIn>
      )
    }
    case "Fold": {
      return core.sync(() => (memoMap: Layer.MemoMap) =>
        pipe(
          memoMap.getOrElseMemoize(op.layer, scope),
          core.matchCauseEffect({
            onFailure: (cause) => memoMap.getOrElseMemoize(op.failureK(cause), scope),
            onSuccess: (value) => memoMap.getOrElseMemoize(op.successK(value), scope)
          })
        )
      )
    }
    case "Fresh": {
      return core.sync(() => (_: Layer.MemoMap) => pipe(op.layer, buildWithScope(scope)))
    }
    case "FromEffect": {
      return inMemoMap
        ? core.sync(() => (_: Layer.MemoMap) => op.effect as Effect.Effect<Context.Context<ROut>, E, RIn>)
        : core.sync(() => (memoMap: Layer.MemoMap) => memoMap.getOrElseMemoize(self, scope))
    }
    case "Provide": {
      return core.sync(() => (memoMap: Layer.MemoMap) =>
        pipe(
          memoMap.getOrElseMemoize(op.first, scope),
          core.flatMap((env) =>
            pipe(
              memoMap.getOrElseMemoize(op.second, scope),
              core.provideContext(env)
            )
          )
        )
      )
    }
    case "Scoped": {
      return inMemoMap
        ? core.sync(() => (_: Layer.MemoMap) =>
          fiberRuntime.scopeExtend(
            op.effect as Effect.Effect<Context.Context<ROut>, E, RIn>,
            scope
          )
        )
        : core.sync(() => (memoMap: Layer.MemoMap) => memoMap.getOrElseMemoize(self, scope))
    }
    case "Suspend": {
      return core.sync(() => (memoMap: Layer.MemoMap) =>
        memoMap.getOrElseMemoize(
          op.evaluate(),
          scope
        )
      )
    }
    case "ProvideMerge": {
      return core.sync(() => (memoMap: Layer.MemoMap) =>
        pipe(
          memoMap.getOrElseMemoize(op.first, scope),
          core.zipWith(
            memoMap.getOrElseMemoize(op.second, scope),
            op.zipK
          )
        )
      )
    }
    case "ZipWith": {
      return core.sync(() => (memoMap: Layer.MemoMap) =>
        pipe(
          memoMap.getOrElseMemoize(op.first, scope),
          fiberRuntime.zipWithOptions(
            memoMap.getOrElseMemoize(op.second, scope),
            op.zipK,
            { concurrent: true }
          )
        )
      )
    }
  }
}

// -----------------------------------------------------------------------------
// Layer
// -----------------------------------------------------------------------------

/** @internal */
export const catchAll = dual<
  <E, RIn2, E2, ROut2>(
    onError: (error: E) => Layer.Layer<ROut2, E2, RIn2>
  ) => <RIn, ROut>(self: Layer.Layer<ROut, E, RIn>) => Layer.Layer<ROut & ROut2, E2, RIn | RIn2>,
  <RIn, E, ROut, RIn2, E2, ROut2>(
    self: Layer.Layer<ROut, E, RIn>,
    onError: (error: E) => Layer.Layer<ROut2, E2, RIn2>
  ) => Layer.Layer<ROut & ROut2, E2, RIn | RIn2>
>(2, (self, onFailure) => match(self, { onFailure, onSuccess: succeedContext }))

/** @internal */
export const catchAllCause = dual<
  <E, RIn2, E2, ROut2>(
    onError: (cause: Cause.Cause<E>) => Layer.Layer<ROut2, E2, RIn2>
  ) => <RIn, ROut>(self: Layer.Layer<ROut, E, RIn>) => Layer.Layer<ROut & ROut2, E2, RIn | RIn2>,
  <RIn, E, ROut, RIn2, E2, ROut22>(
    self: Layer.Layer<ROut, E, RIn>,
    onError: (cause: Cause.Cause<E>) => Layer.Layer<ROut22, E2, RIn2>
  ) => Layer.Layer<ROut & ROut22, E2, RIn | RIn2>
>(2, (self, onFailure) => matchCause(self, { onFailure, onSuccess: succeedContext }))

/** @internal */
export const die = (defect: unknown): Layer.Layer<unknown> => failCause(Cause.die(defect))

/** @internal */
export const dieSync = (evaluate: LazyArg<unknown>): Layer.Layer<unknown> => failCauseSync(() => Cause.die(evaluate()))

/** @internal */
export const discard = <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>): Layer.Layer<never, E, RIn> =>
  map(self, () => Context.empty())

/** @internal */
export const context = <R>(): Layer.Layer<R, never, R> => fromEffectContext(core.context<R>())

/** @internal */
export const extendScope = <RIn, E, ROut>(
  self: Layer.Layer<ROut, E, RIn>
): Layer.Layer<ROut, E, RIn | Scope.Scope> => {
  const extendScope = Object.create(proto)
  extendScope._op_layer = OpCodes.OP_EXTEND_SCOPE
  extendScope.layer = self
  return extendScope
}

/** @internal */
export const fail = <E>(error: E): Layer.Layer<unknown, E> => failCause(Cause.fail(error))

/** @internal */
export const failSync = <E>(evaluate: LazyArg<E>): Layer.Layer<unknown, E> =>
  failCauseSync(() => Cause.fail(evaluate()))

/** @internal */
export const failCause = <E>(cause: Cause.Cause<E>): Layer.Layer<unknown, E> => fromEffectContext(core.failCause(cause))

/** @internal */
export const failCauseSync = <E>(evaluate: LazyArg<Cause.Cause<E>>): Layer.Layer<unknown, E> =>
  fromEffectContext(core.failCauseSync(evaluate))

/** @internal */
export const flatMap = dual<
  <A, A2, E2, R2>(
    f: (context: Context.Context<A>) => Layer.Layer<A2, E2, R2>
  ) => <E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A2, E | E2, R | R2>,
  <A, E, R, A2, E2, R2>(
    self: Layer.Layer<A, E, R>,
    f: (context: Context.Context<A>) => Layer.Layer<A2, E2, R2>
  ) => Layer.Layer<A2, E | E2, R | R2>
>(2, (self, f) => match(self, { onFailure: fail, onSuccess: f }))

/** @internal */
export const flatten = dual<
  <I, A, E2, R2>(
    tag: Context.Tag<I, Layer.Layer<A, E2, R2>>
  ) => <E, R>(
    self: Layer.Layer<I, E, R>
  ) => Layer.Layer<A, E | E2, R | R2>,
  <I, E, R, A, E2, R2>(
    self: Layer.Layer<I, E, R>,
    tag: Context.Tag<I, Layer.Layer<A, E2, R2>>
  ) => Layer.Layer<A, E | E2, R | R2>
>(2, (self, tag) => flatMap(self, Context.get(tag as any) as any))

/** @internal */
export const fresh = <A, E, R>(self: Layer.Layer<A, E, R>): Layer.Layer<A, E, R> => {
  const fresh = Object.create(proto)
  fresh._op_layer = OpCodes.OP_FRESH
  fresh.layer = self
  return fresh
}

/** @internal */
export const fromEffect = dual<
  <I, S>(
    tag: Context.Tag<I, S>
  ) => <E, R>(
    effect: Effect.Effect<Types.NoInfer<S>, E, R>
  ) => Layer.Layer<I, E, R>,
  <I, S, E, R>(
    tag: Context.Tag<I, S>,
    effect: Effect.Effect<Types.NoInfer<S>, E, R>
  ) => Layer.Layer<I, E, R>
>(2, (a, b) => {
  const tagFirst = Context.isTag(a)
  const tag = (tagFirst ? a : b) as Context.Tag<unknown, unknown>
  const effect = tagFirst ? b : a
  return fromEffectContext(core.map(effect, (service) => Context.make(tag, service)))
})

/** @internal */
export const fromEffectDiscard = <X, E, R>(effect: Effect.Effect<X, E, R>) =>
  fromEffectContext(core.map(effect, () => Context.empty()))

/** @internal */
export function fromEffectContext<A, E, R>(
  effect: Effect.Effect<Context.Context<A>, E, R>
): Layer.Layer<A, E, R> {
  const fromEffect = Object.create(proto)
  fromEffect._op_layer = OpCodes.OP_FROM_EFFECT
  fromEffect.effect = effect
  return fromEffect
}

/** @internal */
export const fiberRefLocally = dual<
  <X>(ref: FiberRef<X>, value: X) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, R>,
  <A, E, R, X>(self: Layer.Layer<A, E, R>, ref: FiberRef<X>, value: X) => Layer.Layer<A, E, R>
>(3, (self, ref, value) => locallyEffect(self, core.fiberRefLocally(ref, value)))

/** @internal */
export const locallyEffect = dual<
  <RIn, E, ROut, RIn2, E2, ROut2>(
    f: (_: Effect.Effect<RIn, E, Context.Context<ROut>>) => Effect.Effect<RIn2, E2, Context.Context<ROut2>>
  ) => (self: Layer.Layer<ROut, E, RIn>) => Layer.Layer<ROut2, E2, RIn2>,
  <RIn, E, ROut, RIn2, E2, ROut2>(
    self: Layer.Layer<ROut, E, RIn>,
    f: (_: Effect.Effect<RIn, E, Context.Context<ROut>>) => Effect.Effect<RIn2, E2, Context.Context<ROut2>>
  ) => Layer.Layer<ROut2, E2, RIn2>
>(2, (self, f) => {
  const locally = Object.create(proto)
  locally._op_layer = "Locally"
  locally.self = self
  locally.f = f
  return locally
})

/** @internal */
export const fiberRefLocallyWith = dual<
  <X>(ref: FiberRef<X>, value: (_: X) => X) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, R>,
  <A, E, R, X>(self: Layer.Layer<A, E, R>, ref: FiberRef<X>, value: (_: X) => X) => Layer.Layer<A, E, R>
>(3, (self, ref, value) => locallyEffect(self, core.fiberRefLocallyWith(ref, value)))

/** @internal */
export const fiberRefLocallyScoped = <A>(self: FiberRef<A>, value: A): Layer.Layer<never> =>
  scopedDiscard(fiberRuntime.fiberRefLocallyScoped(self, value))

/** @internal */
export const fiberRefLocallyScopedWith = <A>(self: FiberRef<A>, value: (_: A) => A): Layer.Layer<never> =>
  scopedDiscard(fiberRuntime.fiberRefLocallyScopedWith(self, value))

/** @internal */
export const fromFunction = <I1, S1, I2, S2>(
  tagA: Context.Tag<I1, S1>,
  tagB: Context.Tag<I2, S2>,
  f: (a: Types.NoInfer<S1>) => Types.NoInfer<S2>
): Layer.Layer<I2, never, I1> => fromEffectContext(core.map(tagA, (a) => Context.make(tagB, f(a))))

/** @internal */
export const launch = <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>): Effect.Effect<never, E, RIn> =>
  fiberRuntime.scopedEffect(
    core.zipRight(
      fiberRuntime.scopeWith((scope) => pipe(self, buildWithScope(scope))),
      core.never
    )
  )

/** @internal */
export const map = dual<
  <A, B>(
    f: (context: Context.Context<A>) => Context.Context<B>
  ) => <E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<B, E, R>,
  <A, E, R, B>(
    self: Layer.Layer<A, E, R>,
    f: (context: Context.Context<A>) => Context.Context<B>
  ) => Layer.Layer<B, E, R>
>(2, (self, f) => flatMap(self, (context) => succeedContext(f(context))))

/** @internal */
export const mapError = dual<
  <E, E2>(f: (error: E) => E2) => <A, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E2, R>,
  <A, E, R, E2>(self: Layer.Layer<A, E, R>, f: (error: E) => E2) => Layer.Layer<A, E2, R>
>(2, (self, f) => catchAll(self, (error) => failSync(() => f(error))))

/** @internal */
export const matchCause = dual<
  <E, A2, E2, R2, A, A3, E3, R3>(
    options: {
      readonly onFailure: (cause: Cause.Cause<E>) => Layer.Layer<A2, E2, R2>
      readonly onSuccess: (context: Context.Context<A>) => Layer.Layer<A3, E3, R3>
    }
  ) => <R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A2 & A3, E2 | E3, R | R2 | R3>,
  <A, E, R, A2, E2, R2, A3, E3, R3>(
    self: Layer.Layer<A, E, R>,
    options: {
      readonly onFailure: (cause: Cause.Cause<E>) => Layer.Layer<A2, E2, R2>
      readonly onSuccess: (context: Context.Context<A>) => Layer.Layer<A3, E3, R3>
    }
  ) => Layer.Layer<A2 & A3, E2 | E3, R | R2 | R3>
>(2, (self, { onFailure, onSuccess }) => {
  const fold = Object.create(proto)
  fold._op_layer = OpCodes.OP_FOLD
  fold.layer = self
  fold.failureK = onFailure
  fold.successK = onSuccess
  return fold
})

/** @internal */
export const match = dual<
  <E, A2, E2, R2, A, A3, E3, R3>(
    options: {
      readonly onFailure: (error: E) => Layer.Layer<A2, E2, R2>
      readonly onSuccess: (context: Context.Context<A>) => Layer.Layer<A3, E3, R3>
    }
  ) => <R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A2 & A3, E2 | E3, R | R2 | R3>,
  <A, E, R, A2, E2, R2, A3, E3, R3>(
    self: Layer.Layer<A, E, R>,
    options: {
      readonly onFailure: (error: E) => Layer.Layer<A2, E2, R2>
      readonly onSuccess: (context: Context.Context<A>) => Layer.Layer<A3, E3, R3>
    }
  ) => Layer.Layer<A2 & A3, E2 | E3, R | R2 | R3>
>(2, (self, { onFailure, onSuccess }) =>
  matchCause(self, {
    onFailure: (cause) => {
      const failureOrCause = Cause.failureOrCause(cause)
      switch (failureOrCause._tag) {
        case "Left": {
          return onFailure(failureOrCause.left)
        }
        case "Right": {
          return failCause(failureOrCause.right)
        }
      }
    },
    onSuccess
  }))

/** @internal */
export const memoize = <RIn, E, ROut>(
  self: Layer.Layer<ROut, E, RIn>
): Effect.Effect<Layer.Layer<ROut, E, RIn>, never, Scope.Scope> =>
  fiberRuntime.scopeWith((scope) =>
    core.map(
      effect.memoize(buildWithScope(self, scope)),
      fromEffectContext
    )
  )

/** @internal */
export const merge = dual<
  <RIn2, E2, ROut2>(
    that: Layer.Layer<ROut2, E2, RIn2>
  ) => <RIn, E1, ROut>(self: Layer.Layer<ROut, E1, RIn>) => Layer.Layer<
    ROut | ROut2,
    E1 | E2,
    RIn | RIn2
  >,
  <RIn, E1, ROut, RIn2, E2, ROut2>(self: Layer.Layer<ROut, E1, RIn>, that: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<
    ROut | ROut2,
    E1 | E2,
    RIn | RIn2
  >
>(2, (self, that) => zipWith(self, that, (a, b) => Context.merge(a, b)))

/** @internal */
export const mergeAll = <Layers extends [Layer.Layer<never, any, any>, ...Array<Layer.Layer<never, any, any>>]>(
  ...layers: Layers
): Layer.Layer<
  { [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number],
  { [k in keyof Layers]: Layer.Layer.Error<Layers[k]> }[number],
  { [k in keyof Layers]: Layer.Layer.Context<Layers[k]> }[number]
> => {
  let final = layers[0]
  for (let i = 1; i < layers.length; i++) {
    final = merge(final, layers[i])
  }
  return final as any
}

/** @internal */
export const orDie = <A, E, R>(self: Layer.Layer<A, E, R>): Layer.Layer<A, never, R> =>
  catchAll(self, (defect) => die(defect))

/** @internal */
export const orElse = dual<
  <A2, E2, R2>(
    that: LazyArg<Layer.Layer<A2, E2, R2>>
  ) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A & A2, E | E2, R | R2>,
  <A, E, R, A2, E2, R2>(
    self: Layer.Layer<A, E, R>,
    that: LazyArg<Layer.Layer<A2, E2, R2>>
  ) => Layer.Layer<A & A2, E | E2, R | R2>
>(2, (self, that) => catchAll(self, that))

/** @internal */
export const passthrough = <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>): Layer.Layer<RIn | ROut, E, RIn> =>
  merge(context<RIn>(), self)

/** @internal */
export const project = dual<
  <I1, S1, I2, S2>(
    tagA: Context.Tag<I1, S1>,
    tagB: Context.Tag<I2, S2>,
    f: (a: Types.NoInfer<S1>) => Types.NoInfer<S2>
  ) => <RIn, E>(self: Layer.Layer<I1, E, RIn>) => Layer.Layer<I2, E, RIn>,
  <RIn, E, I1, S1, I2, S2>(
    self: Layer.Layer<I1, E, RIn>,
    tagA: Context.Tag<I1, S1>,
    tagB: Context.Tag<I2, S2>,
    f: (a: Types.NoInfer<S1>) => Types.NoInfer<S2>
  ) => Layer.Layer<I2, E, RIn>
>(4, (self, tagA, tagB, f) => map(self, (context) => Context.make(tagB, f(Context.unsafeGet(context, tagA)))))

/** @internal */
export const retry = dual<
  <X, E, RIn2>(
    schedule: Schedule.Schedule<X, E, RIn2>
  ) => <ROut, RIn>(
    self: Layer.Layer<ROut, E, RIn>
  ) => Layer.Layer<ROut, E, RIn | RIn2>,
  <ROut, E, RIn, X, RIn2>(
    self: Layer.Layer<ROut, E, RIn>,
    schedule: Schedule.Schedule<X, E, RIn2>
  ) => Layer.Layer<ROut, E, RIn | RIn2>
>(2, (self, schedule) =>
  suspend(() => {
    const stateTag = Context.GenericTag<{ state: unknown }>("effect/Layer/retry/{ state: unknown }")
    return pipe(
      succeed(stateTag, { state: schedule.initial }),
      flatMap((env: Context.Context<{ state: unknown }>) =>
        retryLoop(self, schedule, stateTag, pipe(env, Context.get(stateTag)).state)
      )
    )
  }))

const retryLoop = <ROut, E, RIn, X, RIn2>(
  self: Layer.Layer<ROut, E, RIn>,
  schedule: Schedule.Schedule<X, E, RIn2>,
  stateTag: Context.Tag<{ state: unknown }, { state: unknown }>,
  state: unknown
): Layer.Layer<ROut, E, RIn | RIn2> => {
  return pipe(
    self,
    catchAll((error) =>
      pipe(
        retryUpdate(schedule, stateTag, error, state),
        flatMap((env) => fresh(retryLoop(self, schedule, stateTag, pipe(env, Context.get(stateTag)).state)))
      )
    )
  )
}

const retryUpdate = <X, E, RIn>(
  schedule: Schedule.Schedule<X, E, RIn>,
  stateTag: Context.Tag<{ state: unknown }, { state: unknown }>,
  error: E,
  state: unknown
): Layer.Layer<{ state: unknown }, E, RIn> => {
  return fromEffect(
    stateTag,
    pipe(
      Clock.currentTimeMillis,
      core.flatMap((now) =>
        pipe(
          schedule.step(now, error, state),
          core.flatMap(([state, _, decision]) =>
            ScheduleDecision.isDone(decision) ?
              core.fail(error) :
              pipe(
                Clock.sleep(Duration.millis(Intervals.start(decision.intervals) - now)),
                core.as({ state })
              )
          )
        )
      )
    )
  )
}

/** @internal */
export const scoped = dual<
  <I, S>(
    tag: Context.Tag<I, S>
  ) => <E, R>(
    effect: Effect.Effect<Types.NoInfer<S>, E, R>
  ) => Layer.Layer<I, E, Exclude<R, Scope.Scope>>,
  <I, S, E, R>(
    tag: Context.Tag<I, S>,
    effect: Effect.Effect<Types.NoInfer<S>, E, R>
  ) => Layer.Layer<I, E, Exclude<R, Scope.Scope>>
>(2, (a, b) => {
  const tagFirst = Context.isTag(a)
  const tag = (tagFirst ? a : b) as Context.Tag<unknown, unknown>
  const effect = tagFirst ? b : a
  return scopedContext(core.map(effect, (service) => Context.make(tag, service)))
})

/** @internal */
export const scopedDiscard = <X, E, R>(
  effect: Effect.Effect<X, E, R>
): Layer.Layer<never, E, Exclude<R, Scope.Scope>> => scopedContext(pipe(effect, core.as(Context.empty())))

/** @internal */
export const scopedContext = <A, E, R>(
  effect: Effect.Effect<Context.Context<A>, E, R>
): Layer.Layer<A, E, Exclude<R, Scope.Scope>> => {
  const scoped = Object.create(proto)
  scoped._op_layer = OpCodes.OP_SCOPED
  scoped.effect = effect
  return scoped
}

/** @internal */
export const scope: Layer.Layer<Scope.Scope> = scopedContext(
  core.map(
    fiberRuntime.acquireRelease(
      fiberRuntime.scopeMake(),
      (scope, exit) => scope.close(exit)
    ),
    (scope) => Context.make(Scope.Scope, scope)
  )
)

/** @internal */
export const service = <I, S>(
  tag: Context.Tag<I, S>
): Layer.Layer<I, never, I> => fromEffect(tag, tag)

/** @internal */
export const succeed = dual<
  <I, S>(
    tag: Context.Tag<I, S>
  ) => (
    resource: Types.NoInfer<S>
  ) => Layer.Layer<I>,
  <I, S>(
    tag: Context.Tag<I, S>,
    resource: Types.NoInfer<S>
  ) => Layer.Layer<I>
>(2, (a, b) => {
  const tagFirst = Context.isTag(a)
  const tag = (tagFirst ? a : b) as Context.Tag<unknown, unknown>
  const resource = tagFirst ? b : a
  return fromEffectContext(core.succeed(Context.make(tag, resource)))
})

/** @internal */
export const succeedContext = <A>(
  context: Context.Context<A>
): Layer.Layer<A> => {
  return fromEffectContext(core.succeed(context))
}

/** @internal */
export const empty = succeedContext(Context.empty())

/** @internal */
export const suspend = <RIn, E, ROut>(
  evaluate: LazyArg<Layer.Layer<ROut, E, RIn>>
): Layer.Layer<ROut, E, RIn> => {
  const suspend = Object.create(proto)
  suspend._op_layer = OpCodes.OP_SUSPEND
  suspend.evaluate = evaluate
  return suspend
}

/** @internal */
export const sync = dual<
  <I, S>(
    tag: Context.Tag<I, S>
  ) => (
    evaluate: LazyArg<Types.NoInfer<S>>
  ) => Layer.Layer<I>,
  <I, S>(
    tag: Context.Tag<I, S>,
    evaluate: LazyArg<Types.NoInfer<S>>
  ) => Layer.Layer<I>
>(2, (a, b) => {
  const tagFirst = Context.isTag(a)
  const tag = (tagFirst ? a : b) as Context.Tag<unknown, unknown>
  const evaluate = tagFirst ? b : a
  return fromEffectContext(core.sync(() => Context.make(tag, evaluate())))
})

/** @internal */
export const syncContext = <A>(evaluate: LazyArg<Context.Context<A>>): Layer.Layer<A> => {
  return fromEffectContext(core.sync(evaluate))
}

/** @internal */
export const tap = dual<
  <ROut, XR extends ROut, RIn2, E2, X>(
    f: (context: Context.Context<XR>) => Effect.Effect<X, E2, RIn2>
  ) => <RIn, E>(self: Layer.Layer<ROut, E, RIn>) => Layer.Layer<ROut, E | E2, RIn | RIn2>,
  <RIn, E, ROut, XR extends ROut, RIn2, E2, X>(
    self: Layer.Layer<ROut, E, RIn>,
    f: (context: Context.Context<XR>) => Effect.Effect<X, E2, RIn2>
  ) => Layer.Layer<ROut, E | E2, RIn | RIn2>
>(2, (self, f) => flatMap(self, (context) => fromEffectContext(core.as(f(context), context))))

/** @internal */
export const tapError = dual<
  <E, XE extends E, RIn2, E2, X>(
    f: (e: XE) => Effect.Effect<X, E2, RIn2>
  ) => <RIn, ROut>(self: Layer.Layer<ROut, E, RIn>) => Layer.Layer<ROut, E | E2, RIn | RIn2>,
  <RIn, E, XE extends E, ROut, RIn2, E2, X>(
    self: Layer.Layer<ROut, E, RIn>,
    f: (e: XE) => Effect.Effect<X, E2, RIn2>
  ) => Layer.Layer<ROut, E | E2, RIn | RIn2>
>(2, (self, f) =>
  catchAll(
    self,
    (e) => fromEffectContext(core.flatMap(f(e as any), () => core.fail(e)))
  ))

/** @internal */
export const tapErrorCause = dual<
  <E, XE extends E, RIn2, E2, X>(
    f: (cause: Cause.Cause<XE>) => Effect.Effect<X, E2, RIn2>
  ) => <RIn, ROut>(self: Layer.Layer<ROut, E, RIn>) => Layer.Layer<ROut, E | E2, RIn | RIn2>,
  <RIn, E, XE extends E, ROut, RIn2, E2, X>(
    self: Layer.Layer<ROut, E, RIn>,
    f: (cause: Cause.Cause<XE>) => Effect.Effect<X, E2, RIn2>
  ) => Layer.Layer<ROut, E | E2, RIn | RIn2>
>(2, (self, f) =>
  catchAllCause(
    self,
    (cause) => fromEffectContext(core.flatMap(f(cause as any), () => core.failCause(cause)))
  ))

/** @internal */
export const toRuntime = <RIn, E, ROut>(
  self: Layer.Layer<ROut, E, RIn>
): Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope> =>
  pipe(
    fiberRuntime.scopeWith((scope) => buildWithScope(self, scope)),
    core.flatMap((context) =>
      pipe(
        runtime.runtime<ROut>(),
        core.provideContext(context)
      )
    )
  )

/** @internal */
export const toRuntimeWithMemoMap = dual<
  (
    memoMap: Layer.MemoMap
  ) => <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope>,
  <RIn, E, ROut>(
    self: Layer.Layer<ROut, E, RIn>,
    memoMap: Layer.MemoMap
  ) => Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope>
>(2, (self, memoMap) =>
  core.flatMap(
    fiberRuntime.scopeWith((scope) => buildWithMemoMap(self, memoMap, scope)),
    (context) =>
      pipe(
        runtime.runtime<any>(),
        core.provideContext(context)
      )
  ))

/** @internal */
export const provide = dual<
  {
    <RIn, E, ROut>(
      that: Layer.Layer<ROut, E, RIn>
    ): <RIn2, E2, ROut2>(
      self: Layer.Layer<ROut2, E2, RIn2>
    ) => Layer.Layer<ROut2, E | E2, RIn | Exclude<RIn2, ROut>>
    <const Layers extends [Layer.Layer.Any, ...Array<Layer.Layer.Any>]>(
      that: Layers
    ): <A, E, R>(
      self: Layer.Layer<A, E, R>
    ) => Layer.Layer<
      A,
      E | { [k in keyof Layers]: Layer.Layer.Error<Layers[k]> }[number],
      | { [k in keyof Layers]: Layer.Layer.Context<Layers[k]> }[number]
      | Exclude<R, { [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number]>
    >
  },
  {
    <RIn2, E2, ROut2, RIn, E, ROut>(
      self: Layer.Layer<ROut2, E2, RIn2>,
      that: Layer.Layer<ROut, E, RIn>
    ): Layer.Layer<ROut2, E | E2, RIn | Exclude<RIn2, ROut>>
    <A, E, R, const Layers extends [Layer.Layer.Any, ...Array<Layer.Layer.Any>]>(
      self: Layer.Layer<A, E, R>,
      that: Layers
    ): Layer.Layer<
      A,
      E | { [k in keyof Layers]: Layer.Layer.Error<Layers[k]> }[number],
      | { [k in keyof Layers]: Layer.Layer.Context<Layers[k]> }[number]
      | Exclude<R, { [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number]>
    >
  }
>(2, (
  self: Layer.Layer.Any,
  that: Layer.Layer.Any | ReadonlyArray<Layer.Layer.Any>
) =>
  suspend(() => {
    const provideTo = Object.create(proto)
    provideTo._op_layer = OpCodes.OP_PROVIDE
    provideTo.first = Object.create(proto, {
      _op_layer: { value: OpCodes.OP_PROVIDE_MERGE, enumerable: true },
      first: { value: context(), enumerable: true },
      second: { value: Array.isArray(that) ? mergeAll(...that as any) : that },
      zipK: { value: (a: Context.Context<any>, b: Context.Context<any>) => pipe(a, Context.merge(b)) }
    })
    provideTo.second = self
    return provideTo
  }))

/** @internal */
export const provideMerge = dual<
  <RIn, E, ROut>(
    self: Layer.Layer<ROut, E, RIn>
  ) => <RIn2, E2, ROut2>(
    that: Layer.Layer<ROut2, E2, RIn2>
  ) => Layer.Layer<ROut | ROut2, E2 | E, RIn | Exclude<RIn2, ROut>>,
  <RIn2, E2, ROut2, RIn, E, ROut>(
    that: Layer.Layer<ROut2, E2, RIn2>,
    self: Layer.Layer<ROut, E, RIn>
  ) => Layer.Layer<ROut | ROut2, E2 | E, RIn | Exclude<RIn2, ROut>>
>(2, <RIn2, E2, ROut2, RIn, E, ROut>(that: Layer.Layer<ROut2, E2, RIn2>, self: Layer.Layer<ROut, E, RIn>) => {
  const zipWith = Object.create(proto)
  zipWith._op_layer = OpCodes.OP_PROVIDE_MERGE
  zipWith.first = self
  zipWith.second = provide(that, self)
  zipWith.zipK = (a: Context.Context<ROut>, b: Context.Context<ROut2>): Context.Context<ROut | ROut2> => {
    return pipe(a, Context.merge(b))
  }
  return zipWith
})

/** @internal */
export const zipWith = dual<
  <B, E2, R2, A, C>(
    that: Layer.Layer<B, E2, R2>,
    f: (a: Context.Context<A>, b: Context.Context<B>) => Context.Context<C>
  ) => <E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<C, E | E2, R | R2>,
  <A, E, R, B, E2, R2, C>(
    self: Layer.Layer<A, E, R>,
    that: Layer.Layer<B, E2, R2>,
    f: (a: Context.Context<A>, b: Context.Context<B>) => Context.Context<C>
  ) => Layer.Layer<C, E | E2, R | R2>
>(3, (self, that, f) =>
  suspend(() => {
    const zipWith = Object.create(proto)
    zipWith._op_layer = OpCodes.OP_ZIP_WITH
    zipWith.first = self
    zipWith.second = that
    zipWith.zipK = f
    return zipWith
  }))

/** @internal */
export const unwrapEffect = <A, E1, R1, E, R>(
  self: Effect.Effect<Layer.Layer<A, E1, R1>, E, R>
): Layer.Layer<A, E | E1, R | R1> => {
  const tag = Context.GenericTag<Layer.Layer<A, E1, R1>>("effect/Layer/unwrapEffect/Layer.Layer<R1, E1, A>")
  return flatMap(fromEffect(tag, self), (context) => Context.get(context, tag))
}

/** @internal */
export const unwrapScoped = <A, E1, R1, E, R>(
  self: Effect.Effect<Layer.Layer<A, E1, R1>, E, R>
): Layer.Layer<A, E | E1, R1 | Exclude<R, Scope.Scope>> => {
  const tag = Context.GenericTag<Layer.Layer<A, E1, R1>>("effect/Layer/unwrapScoped/Layer.Layer<R1, E1, A>")
  return flatMap(scoped(tag, self), (context) => Context.get(context, tag))
}

// -----------------------------------------------------------------------------
// logging
// -----------------------------------------------------------------------------

export const annotateLogs = dual<
  {
    (key: string, value: unknown): <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, R>
    (
      values: Record<string, unknown>
    ): <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, R>
  },
  {
    <A, E, R>(self: Layer.Layer<A, E, R>, key: string, value: unknown): Layer.Layer<A, E, R>
    <A, E, R>(self: Layer.Layer<A, E, R>, values: Record<string, unknown>): Layer.Layer<A, E, R>
  }
>(
  (args) => isLayer(args[0]),
  function<A, E, R>() {
    const args = arguments
    return fiberRefLocallyWith(
      args[0] as Layer.Layer<A, E, R>,
      core.currentLogAnnotations,
      typeof args[1] === "string"
        ? HashMap.set(args[1], args[2])
        : (annotations) =>
          Object.entries(args[1] as Record<string, unknown>).reduce(
            (acc, [key, value]) => HashMap.set(acc, key, value),
            annotations
          )
    )
  }
)

// -----------------------------------------------------------------------------
// tracing
// -----------------------------------------------------------------------------

export const annotateSpans = dual<
  {
    (key: string, value: unknown): <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, R>
    (
      values: Record<string, unknown>
    ): <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, R>
  },
  {
    <A, E, R>(self: Layer.Layer<A, E, R>, key: string, value: unknown): Layer.Layer<A, E, R>
    <A, E, R>(self: Layer.Layer<A, E, R>, values: Record<string, unknown>): Layer.Layer<A, E, R>
  }
>(
  (args) => isLayer(args[0]),
  function<A, E, R>() {
    const args = arguments
    return fiberRefLocallyWith(
      args[0] as Layer.Layer<A, E, R>,
      core.currentTracerSpanAnnotations,
      typeof args[1] === "string"
        ? HashMap.set(args[1], args[2])
        : (annotations) =>
          Object.entries(args[1] as Record<string, unknown>).reduce(
            (acc, [key, value]) => HashMap.set(acc, key, value),
            annotations
          )
    )
  }
)

/** @internal */
export const withSpan: {
  (
    name: string,
    options?: Tracer.SpanOptions & {
      readonly onEnd?:
        | ((span: Tracer.Span, exit: Exit.Exit<unknown, unknown>) => Effect.Effect<void>)
        | undefined
    }
  ): <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, Exclude<R, Tracer.ParentSpan>>
  <A, E, R>(
    self: Layer.Layer<A, E, R>,
    name: string,
    options?: Tracer.SpanOptions & {
      readonly onEnd?:
        | ((span: Tracer.Span, exit: Exit.Exit<unknown, unknown>) => Effect.Effect<void>)
        | undefined
    }
  ): Layer.Layer<A, E, Exclude<R, Tracer.ParentSpan>>
} = function() {
  const dataFirst = typeof arguments[0] !== "string"
  const name = dataFirst ? arguments[1] : arguments[0]
  const options = tracer.addSpanStackTrace(dataFirst ? arguments[2] : arguments[1]) as Tracer.SpanOptions & {
    readonly onEnd?:
      | ((span: Tracer.Span, exit: Exit.Exit<unknown, unknown>) => Effect.Effect<void>)
      | undefined
  }
  if (dataFirst) {
    const self = arguments[0]
    return unwrapScoped(
      core.map(
        options?.onEnd
          ? core.tap(
            fiberRuntime.makeSpanScoped(name, options),
            (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit))
          )
          : fiberRuntime.makeSpanScoped(name, options),
        (span) => withParentSpan(self, span)
      )
    )
  }
  return (self: Layer.Layer<any, any, any>) =>
    unwrapScoped(
      core.map(
        options?.onEnd
          ? core.tap(
            fiberRuntime.makeSpanScoped(name, options),
            (span) => fiberRuntime.addFinalizer((exit) => options.onEnd!(span, exit))
          )
          : fiberRuntime.makeSpanScoped(name, options),
        (span) => withParentSpan(self, span)
      )
    )
} as any

/** @internal */
export const withParentSpan = dual<
  (
    span: Tracer.AnySpan
  ) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, E, Exclude<R, Tracer.ParentSpan>>,
  <A, E, R>(self: Layer.Layer<A, E, R>, span: Tracer.AnySpan) => Layer.Layer<A, E, Exclude<R, Tracer.ParentSpan>>
>(2, (self, span) => provide(self, succeedContext(Context.make(tracer.spanTag, span))))

// circular with Effect

const provideSomeLayer = dual<
  <A2, E2, R2>(
    layer: Layer.Layer<A2, E2, R2>
  ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | E2, R2 | Exclude<R, A2>>,
  <A, E, R, A2, E2, R2>(
    self: Effect.Effect<A, E, R>,
    layer: Layer.Layer<A2, E2, R2>
  ) => Effect.Effect<A, E | E2, R2 | Exclude<R, A2>>
>(2, (self, layer) =>
  fiberRuntime.scopedWith((scope) =>
    core.flatMap(
      buildWithScope(layer, scope),
      (context) => core.provideSomeContext(self, context)
    )
  ))

const provideSomeRuntime = dual<
  <R>(context: Runtime.Runtime<R>) => <A, E, R1>(self: Effect.Effect<A, E, R1>) => Effect.Effect<A, E, Exclude<R1, R>>,
  <A, E, R1, R>(self: Effect.Effect<A, E, R1>, context: Runtime.Runtime<R>) => Effect.Effect<A, E, Exclude<R1, R>>
>(2, (self, rt) => {
  const patchRefs = FiberRefsPatch.diff(runtime.defaultRuntime.fiberRefs, rt.fiberRefs)
  const patchFlags = runtimeFlags.diff(runtime.defaultRuntime.runtimeFlags, rt.runtimeFlags)
  return core.uninterruptibleMask((restore) =>
    core.withFiberRuntime((fiber) => {
      const oldContext = fiber.getFiberRef(core.currentContext)
      const oldRefs = fiber.getFiberRefs()
      const newRefs = FiberRefsPatch.patch(fiber.id(), oldRefs)(patchRefs)
      const oldFlags = fiber.currentRuntimeFlags
      const newFlags = runtimeFlags.patch(patchFlags)(oldFlags)
      const rollbackRefs = FiberRefsPatch.diff(newRefs, oldRefs)
      const rollbackFlags = runtimeFlags.diff(newFlags, oldFlags)
      fiber.setFiberRefs(newRefs)
      fiber.currentRuntimeFlags = newFlags
      return fiberRuntime.ensuring(
        core.provideSomeContext(restore(self), Context.merge(oldContext, rt.context)),
        core.withFiberRuntime((fiber) => {
          fiber.setFiberRefs(FiberRefsPatch.patch(fiber.id(), fiber.getFiberRefs())(rollbackRefs))
          fiber.currentRuntimeFlags = runtimeFlags.patch(rollbackFlags)(fiber.currentRuntimeFlags)
          return core.void
        })
      )
    })
  )
})

/** @internal */
export const effect_provide = dual<
  {
    <const Layers extends [Layer.Layer.Any, ...Array<Layer.Layer.Any>]>(
      layers: Layers
    ): <A, E, R>(
      self: Effect.Effect<A, E, R>
    ) => Effect.Effect<
      A,
      E | { [k in keyof Layers]: Layer.Layer.Error<Layers[k]> }[number],
      | { [k in keyof Layers]: Layer.Layer.Context<Layers[k]> }[number]
      | Exclude<R, { [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number]>
    >
    <ROut, E2, RIn>(
      layer: Layer.Layer<ROut, E2, RIn>
    ): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | E2, RIn | Exclude<R, ROut>>
    <R2>(
      context: Context.Context<R2>
    ): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, R2>>
    <R2>(
      runtime: Runtime.Runtime<R2>
    ): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, R2>>
    <E2, R2>(
      managedRuntime: ManagedRuntime.ManagedRuntime<R2, E2>
    ): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E | E2, Exclude<R, R2>>
  },
  {
    <A, E, R, const Layers extends [Layer.Layer.Any, ...Array<Layer.Layer.Any>]>(
      self: Effect.Effect<A, E, R>,
      layers: Layers
    ): Effect.Effect<
      A,
      E | { [k in keyof Layers]: Layer.Layer.Error<Layers[k]> }[number],
      | { [k in keyof Layers]: Layer.Layer.Context<Layers[k]> }[number]
      | Exclude<R, { [k in keyof Layers]: Layer.Layer.Success<Layers[k]> }[number]>
    >
    <A, E, R, ROut, E2, RIn>(
      self: Effect.Effect<A, E, R>,
      layer: Layer.Layer<ROut, E2, RIn>
    ): Effect.Effect<A, E | E2, RIn | Exclude<R, ROut>>
    <A, E, R, R2>(
      self: Effect.Effect<A, E, R>,
      context: Context.Context<R2>
    ): Effect.Effect<A, E, Exclude<R, R2>>
    <A, E, R, R2>(
      self: Effect.Effect<A, E, R>,
      runtime: Runtime.Runtime<R2>
    ): Effect.Effect<A, E, Exclude<R, R2>>
    <A, E, E2, R, R2>(
      self: Effect.Effect<A, E, R>,
      managedRuntime: ManagedRuntime.ManagedRuntime<R2, E2>
    ): Effect.Effect<A, E | E2, Exclude<R, R2>>
  }
>(
  2,
  <A, E, R, ROut>(
    self: Effect.Effect<A, E, R>,
    source:
      | Layer.Layer<ROut, any, any>
      | Context.Context<ROut>
      | Runtime.Runtime<ROut>
      | ManagedRuntime.ManagedRuntime<ROut, any>
      | Array<Layer.Layer.Any>
  ): Effect.Effect<any, any, Exclude<R, ROut>> => {
    if (Array.isArray(source)) {
      // @ts-expect-error
      return provideSomeLayer(self, mergeAll(...source))
    } else if (isLayer(source)) {
      return provideSomeLayer(self, source as Layer.Layer<ROut, any, any>)
    } else if (Context.isContext(source)) {
      return core.provideSomeContext(self, source)
    } else if (circularManagedRuntime.TypeId in source) {
      return core.flatMap(
        (source as ManagedRuntime.ManagedRuntime<ROut, any>).runtimeEffect,
        (rt) => provideSomeRuntime(self, rt)
      )
    } else {
      return provideSomeRuntime(self, source as Runtime.Runtime<ROut>)
    }
  }
)
