/**
 * @since 4.0.0
 */
import * as Arr from "../../Array.js";
import * as Cause from "../../Cause.js";
import * as Context from "../../Context.js";
import * as Data from "../../Data.js";
import * as Effect from "../../Effect.js";
import * as Exit from "../../Exit.js";
import * as Fiber from "../../Fiber.js";
import * as Filter from "../../Filter.js";
import { constFalse, constTrue, dual, identity } from "../../Function.js";
import * as Layer from "../../Layer.js";
import * as Option from "../../Option.js";
import * as Predicate from "../../Predicate.js";
import * as Schema from "../../Schema.js";
import * as Issue from "../../SchemaIssue.js";
import * as Parser from "../../SchemaParser.js";
import * as Tranformation from "../../SchemaTransformation.js";
import * as Scope from "../../Scope.js";
import { makeHashDigest } from "./internal/crypto.js";
const TypeId = "~effect/workflow/Workflow";
const EngineTag = /*#__PURE__*/Context.Service("effect/workflow/WorkflowEngine");
const InstanceTag = /*#__PURE__*/Context.Service("effect/workflow/WorkflowEngine/WorkflowInstance");
/**
 * @since 4.0.0
 * @category Constructors
 */
export const make = options => {
  const makeExecutionId = payload => makeHashDigest(`${options.name}-${options.idempotencyKey(payload)}`);
  const self = {
    [TypeId]: TypeId,
    name: options.name,
    payloadSchema: Schema.isSchema(options.payload) ? options.payload : Schema.Struct(options.payload),
    successSchema: options.success ?? Schema.Void,
    errorSchema: options.error ?? Schema.Never,
    annotations: options.annotations ?? Context.empty(),
    annotate(tag, value) {
      return make({
        ...options,
        annotations: Context.add(self.annotations, tag, value)
      });
    },
    annotateMerge(context) {
      return make({
        ...options,
        annotations: Context.merge(self.annotations, context)
      });
    },
    execute: Effect.fnUntraced(function* (fields, opts) {
      const payload = self.payloadSchema.make(fields);
      const engine = yield* EngineTag;
      const executionId = yield* makeExecutionId(payload);
      yield* Effect.annotateCurrentSpan({
        executionId
      });
      return yield* engine.execute(self, {
        executionId,
        payload,
        discard: opts?.discard,
        suspendedRetrySchedule: options.suspendedRetrySchedule
      });
    }, Effect.withSpan(`${options.name}.execute`, {}, {
      captureStackTrace: false
    })),
    poll: Effect.fnUntraced(function* (executionId) {
      const engine = yield* EngineTag;
      return yield* engine.poll(self, executionId);
    }, (effect, executionId) => Effect.withSpan(effect, `${options.name}.poll`, {
      captureStackTrace: false,
      attributes: {
        executionId
      }
    })),
    interrupt: Effect.fnUntraced(function* (executionId) {
      const engine = yield* EngineTag;
      yield* engine.interrupt(self, executionId);
    }, (effect, executionId) => Effect.withSpan(effect, `${options.name}.interrupt`, {
      captureStackTrace: false,
      attributes: {
        executionId
      }
    })),
    resume: Effect.fnUntraced(function* (executionId) {
      const engine = yield* EngineTag;
      yield* engine.resume(self, executionId);
    }, (effect, executionId) => Effect.withSpan(effect, `${options.name}.resume`, {
      captureStackTrace: false,
      attributes: {
        executionId
      }
    })),
    toLayer: execute => Layer.effectDiscard(Effect.gen(function* () {
      const engine = yield* EngineTag;
      return yield* engine.register(self, execute);
    })),
    executionId: payload => Effect.flatMap(Effect.orDie(self.payloadSchema.makeEffect(payload)), makeExecutionId),
    withCompensation
  };
  return self;
};
const ResultTypeId = "~effect/workflow/Workflow/Result";
/**
 * @since 4.0.0
 * @category Result
 */
export const isResult = u => Predicate.hasProperty(u, ResultTypeId);
/**
 * @since 4.0.0
 * @category Result
 */
export class Complete extends /*#__PURE__*/Data.TaggedClass("Complete") {
  /**
   * @since 4.0.0
   */
  [ResultTypeId] = ResultTypeId;
  /**
   * @since 4.0.0
   */
  static Schema(options) {
    // TODO: extract to a helper function
    const schema = Schema.declareConstructor()([Schema.Exit(options.success, options.error, Schema.Defect)], ([exit]) => (input, ast, options) => {
      if (!(isResult(input) && input._tag === "Complete")) {
        return Effect.fail(new Issue.InvalidType(ast, Option.some(input)));
      }
      return Effect.mapBothEager(Parser.decodeEffect(exit)(input.exit, options), {
        onSuccess: exit => new Complete({
          exit
        }),
        onFailure: issue => new Issue.Composite(ast, Option.some(input), [new Issue.Pointer(["exit"], issue)])
      });
    }, {
      expected: "Workflow.Complete",
      toCodecJson: ([exit]) => Schema.link()(Schema.Struct({
        _tag: Schema.tag("Complete"),
        exit
      }), Tranformation.transform({
        decode: encoded => new Complete({
          exit: encoded.exit
        }),
        encode: result => ({
          _tag: "Complete",
          exit: result.exit
        })
      }))
    });
    return Schema.make(schema.ast, {
      success: options.success,
      error: options.error
    });
  }
}
/**
 * @since 4.0.0
 * @category Result
 */
export class Suspended extends /*#__PURE__*/Schema.Class("effect/workflow/Workflow/Suspended")({
  _tag: /*#__PURE__*/Schema.tag("Suspended"),
  cause: /*#__PURE__*/Schema.optional(/*#__PURE__*/Schema.Cause(Schema.Never, Schema.Defect))
}) {
  /**
   * @since 4.0.0
   */
  [ResultTypeId] = ResultTypeId;
}
/**
 * @since 4.0.0
 * @category Result
 */
export const Result = options => Schema.Union([Complete.Schema(options), Suspended]);
const AnyOrVoid = /*#__PURE__*/Schema.Union([Schema.Any, Schema.Void]);
/**
 * @since 4.0.0
 * @category Result
 */
export const ResultEncoded = /*#__PURE__*/Schema.toEncoded(/*#__PURE__*/Schema.toCodecJson(/*#__PURE__*/Result({
  success: AnyOrVoid,
  error: AnyOrVoid
})));
/**
 * @since 4.0.0
 * @category Result
 */
export const intoResult = effect => Effect.contextWith(context => {
  const instance = Context.get(context, InstanceTag);
  const captureDefects = Context.get(instance.workflow.annotations, CaptureDefects);
  const suspendOnFailure = Context.get(instance.workflow.annotations, SuspendOnFailure);
  return effect.pipe(
  // so we can use external interruption to suspend the workflow
  Effect.forkChild({
    startImmediately: true
  }), Effect.flatMap(fiber => Effect.onInterrupt(Fiber.join(fiber), () => Fiber.interrupt(fiber))), Effect.interruptible, suspendOnFailure ? Effect.catchCause(cause => {
    instance.suspended = true;
    if (!Cause.hasInterruptsOnly(cause)) {
      instance.cause = Cause.die(Cause.squash(cause));
    }
    return Effect.interrupt;
  }) : identity, Effect.scoped, Effect.matchCauseEffect({
    onSuccess: value => Effect.succeed(new Complete({
      exit: Exit.succeed(value)
    })),
    onFailure: cause => {
      const [reasons, interrupts] = Arr.partition(cause.reasons, Filter.fromPredicate(Cause.isInterruptReason));
      const hasInterruptsOnly = interrupts.length === cause.reasons.length;
      const filtered = reasons.length === 0 ? cause : Cause.fromReasons(reasons);
      return instance.suspended && hasInterruptsOnly ? Effect.succeed(new Suspended({
        cause: instance.cause
      })) : !instance.interrupted && hasInterruptsOnly || !captureDefects && Cause.hasDies(cause) ? Effect.failCause(filtered) : Effect.succeed(new Complete({
        exit: Exit.failCause(filtered)
      }));
    }
  }), eff => Effect.onExitPrimitive(eff, exit => {
    if (Exit.isFailure(exit)) {
      return Scope.close(instance.scope, exit);
    } else if (exit.value._tag === "Complete") {
      return Scope.close(instance.scope, exit.value.exit);
    }
    return Effect.void;
  }, true), Effect.uninterruptible);
});
/**
 * @since 4.0.0
 * @category Result
 */
export const wrapActivityResult = (effect, isSuspend) => Effect.contextWith(context => {
  const instance = Context.get(context, InstanceTag);
  const state = instance.activityState;
  if (state.count === 0) state.latch.closeUnsafe();
  state.count++;
  return Effect.onExit(effect, exit => {
    state.count--;
    const isSuspended = Exit.isSuccess(exit) && isSuspend(exit.value);
    if (Exit.isSuccess(exit) && isResult(exit.value) && exit.value._tag === "Suspended" && exit.value.cause) {
      instance.cause = instance.cause ? Cause.combine(instance.cause, exit.value.cause) : exit.value.cause;
    }
    return state.count === 0 ? state.latch.open : isSuspended ? waitForZero(instance) : Effect.void;
  });
});
const waitForZero = /*#__PURE__*/Effect.fnUntraced(function* (instance) {
  const state = instance.activityState;
  while (true) {
    if (state.count > 0) {
      yield* state.latch.await;
      yield* Effect.yieldNow;
      continue;
    }
    yield* Effect.yieldNow;
    if (state.count === 0) return;
  }
});
/**
 * Accesses the workflow scope.
 *
 * The workflow scope is only closed when the workflow execution fully
 * completes.
 *
 * @since 1.0.0
 * @category Scope
 */
export const scope = /*#__PURE__*/Effect.map(InstanceTag, instance => instance.scope);
/**
 * Provides the workflow scope to the given effect.
 *
 * The workflow scope is only closed when the workflow execution fully
 * completes.
 *
 * @since 1.0.0
 * @category Scope
 */
export const provideScope = effect => Effect.flatMap(scope, scope => Scope.provide(effect, scope));
/**
 * @since 1.0.0
 * @category Scope
 */
export const addFinalizer = /*#__PURE__*/Effect.fnUntraced(function* (f) {
  const scope = (yield* InstanceTag).scope;
  const services = yield* Effect.context();
  yield* Scope.addFinalizerExit(scope, exit => Effect.provideContext(f(exit), services));
});
/**
 * Add compensation logic to an effect inside a Workflow. The compensation finalizer will be
 * called if the entire workflow fails, allowing you to perform cleanup or
 * other actions based on the success value and the cause of the workflow failure.
 *
 * NOTE: Compensation will not work for nested activities. Compensation
 * finalizers are only registered for top-level effects in the workflow.
 *
 * @since 1.0.0
 * @category Compensation
 */
export const withCompensation = /*#__PURE__*/dual(2, (effect, compensation) => Effect.uninterruptibleMask(restore => Effect.tap(restore(effect), value => addFinalizer(exit => Exit.isSuccess(exit) ? Effect.void : compensation(value, exit.cause)))));
/**
 * @since 4.0.0
 * @category Result
 */
export const suspend = instance => Effect.interruptible(Effect.callback(() => {
  instance.suspended = true;
  const fiber = Fiber.getCurrent();
  fiber.interruptUnsafe(fiber.id);
}));
/**
 * If you set this annotation to `true` for a workflow, it will capture defects
 * and include them in the result of the workflow or it's activities.
 *
 * By default, this is set to `true`, meaning that defects will be captured.
 *
 * @since 4.0.0
 * @category Annotations
 */
export const CaptureDefects = /*#__PURE__*/Context.Reference("effect/workflow/Workflow/CaptureDefects", {
  defaultValue: constTrue
});
/**
 * If you set this annotation to `true` for a workflow, it will suspend if it
 * encounters any kind of error.
 *
 * You can then manually resume the workflow later with
 * `Workflow.resume(executionId)`.
 *
 * @since 4.0.0
 * @category Annotations
 */
export const SuspendOnFailure = /*#__PURE__*/Context.Reference("effect/workflow/Workflow/SuspendOnFailure", {
  defaultValue: constFalse
});
//# sourceMappingURL=Workflow.js.map