/* eslint-disable max-classes-per-file -- each domain failure has a distinct tagged reason. */
import { Effect, Schema } from "effect";

const MeaningfulString = Schema.String.check(
  Schema.makeFilter((value) => value.trim().length > 0, {
    expected: "a non-empty string",
  })
);

// ---------------------------------------------------------------------------
// Branded identifiers
// ---------------------------------------------------------------------------

export const OrbId = MeaningfulString.pipe(Schema.brand("OrbId"));
export type OrbId = typeof OrbId.Type;

export const OrbRunId = MeaningfulString.pipe(Schema.brand("OrbRunId"));
export type OrbRunId = typeof OrbRunId.Type;

export const OrbSessionId = MeaningfulString.pipe(Schema.brand("OrbSessionId"));
export type OrbSessionId = typeof OrbSessionId.Type;

// ---------------------------------------------------------------------------
// Actor identity — one Orb per project/issue/run triple
// ---------------------------------------------------------------------------

export const OrbIdentity = Schema.Struct({
  projectId: MeaningfulString,
  runId: MeaningfulString,
  workUnitId: MeaningfulString,
});
export type OrbIdentity = typeof OrbIdentity.Type;

/** Stable RivetKit actor key derived from the identity triple. */
export const orbActorKey = (identity: OrbIdentity): string =>
  `project:${identity.projectId}:work:${identity.workUnitId}:run:${identity.runId}`;

// ---------------------------------------------------------------------------
// State machines — product state stays separate from run state
// ---------------------------------------------------------------------------

export const OrbState = Schema.Literals([
  "creating",
  "prepared",
  "running",
  "needs-input",
  "completed",
  "failed",
  "cancelled",
  "disposed",
]);
export type OrbState = typeof OrbState.Type;

export const RunState = Schema.Literals([
  "queued",
  "provisioning",
  "preparing",
  "running",
  "verifying",
  "succeeded",
  "failed",
  "cancelled",
]);
export type RunState = typeof RunState.Type;

const ORB_TRANSITIONS: Readonly<Record<OrbState, readonly OrbState[]>> = {
  cancelled: ["disposed"],
  completed: ["disposed"],
  creating: ["prepared", "running", "failed", "cancelled", "disposed"],
  disposed: [],
  failed: ["disposed"],
  "needs-input": ["running", "completed", "failed", "cancelled", "disposed"],
  prepared: ["running", "failed", "cancelled", "disposed"],
  running: ["needs-input", "completed", "failed", "cancelled", "disposed"],
};

const RUN_TRANSITIONS: Readonly<Record<RunState, readonly RunState[]>> = {
  cancelled: [],
  failed: [],
  preparing: ["running", "verifying", "failed", "cancelled"],
  provisioning: ["preparing", "running", "failed", "cancelled"],
  queued: ["provisioning", "preparing", "running", "failed", "cancelled"],
  running: ["verifying", "succeeded", "failed", "cancelled"],
  succeeded: [],
  verifying: ["succeeded", "failed", "cancelled"],
};

// ---------------------------------------------------------------------------
// Tagged errors
// ---------------------------------------------------------------------------

export const OrbStateErrorReason = Schema.Literals([
  "InvalidTransition",
  "AlreadyDisposed",
  "NotRunning",
]);
export type OrbStateErrorReason = typeof OrbStateErrorReason.Type;

export class OrbStateError extends Schema.TaggedErrorClass<OrbStateError>()(
  "OrbStateError",
  {
    from: Schema.String,
    message: Schema.String,
    reason: OrbStateErrorReason,
    to: Schema.String,
  }
) {}

export const OrbSandboxErrorReason = Schema.Literals([
  "DockerUnavailable",
  "ContainerStart",
  "CommandFailed",
  "ContainerCleanup",
]);
export type OrbSandboxErrorReason = typeof OrbSandboxErrorReason.Type;

export class OrbSandboxError extends Schema.TaggedErrorClass<OrbSandboxError>()(
  "OrbSandboxError",
  {
    message: Schema.String,
    reason: OrbSandboxErrorReason,
  }
) {}

export const OrbSessionErrorReason = Schema.Literals([
  "OpenSession",
  "PromptFailed",
  "AgentNotInstalled",
  "SessionNotFound",
  "PermissionDenied",
]);
export type OrbSessionErrorReason = typeof OrbSessionErrorReason.Type;

export class OrbSessionError extends Schema.TaggedErrorClass<OrbSessionError>()(
  "OrbSessionError",
  {
    message: Schema.String,
    reason: OrbSessionErrorReason,
  }
) {}

export const OrbConfigurationErrorReason = Schema.Literals([
  "MissingGateway",
  "MissingIdentity",
  "InvalidModelConfig",
]);
export type OrbConfigurationErrorReason =
  typeof OrbConfigurationErrorReason.Type;

export class OrbConfigurationError extends Schema.TaggedErrorClass<OrbConfigurationError>()(
  "OrbConfigurationError",
  {
    message: Schema.String,
    reason: OrbConfigurationErrorReason,
  }
) {}

// ---------------------------------------------------------------------------
// Configuration types
// ---------------------------------------------------------------------------

export const OrbModelGatewayConfig = Schema.Struct({
  apiKey: MeaningfulString,
  baseUrl: MeaningfulString,
  model: MeaningfulString,
  provider: MeaningfulString,
});
export type OrbModelGatewayConfig = typeof OrbModelGatewayConfig.Type;

export const OrbProjectContext = Schema.Struct({
  artifacts: Schema.Array(
    Schema.Struct({
      content: Schema.String,
      path: MeaningfulString,
    })
  ),
  contextFiles: Schema.Array(
    Schema.Struct({
      content: Schema.String,
      path: MeaningfulString,
    })
  ),
  issueBody: MeaningfulString,
  issueTitle: MeaningfulString,
  repositoryUrl: Schema.UndefinedOr(MeaningfulString),
});
export type OrbProjectContext = typeof OrbProjectContext.Type;

// ---------------------------------------------------------------------------
// Transition helpers
// ---------------------------------------------------------------------------

export const transitionOrbState = Effect.fn("Orb.transitionState")(
  function* transitionOrbState(input: {
    readonly from: OrbState;
    readonly to: OrbState;
  }) {
    if (input.from === input.to) {
      return input.to;
    }
    if (input.from === "disposed") {
      return yield* Effect.fail(
        new OrbStateError({
          from: input.from,
          message: "Orb is already disposed",
          reason: "AlreadyDisposed",
          to: input.to,
        })
      );
    }
    if (!ORB_TRANSITIONS[input.from].includes(input.to)) {
      return yield* Effect.fail(
        new OrbStateError({
          from: input.from,
          message: `Cannot transition orb from ${input.from} to ${input.to}`,
          reason: "InvalidTransition",
          to: input.to,
        })
      );
    }
    return input.to;
  }
);

export const transitionRunState = Effect.fn("Orb.transitionRunState")(
  function* transitionRunState(input: {
    readonly from: RunState;
    readonly to: RunState;
  }) {
    if (input.from === input.to) {
      return input.to;
    }
    if (
      input.from === "succeeded" ||
      input.from === "failed" ||
      input.from === "cancelled"
    ) {
      return yield* Effect.fail(
        new OrbStateError({
          from: input.from,
          message: `Run is already in terminal state ${input.from}`,
          reason: "InvalidTransition",
          to: input.to,
        })
      );
    }
    if (!RUN_TRANSITIONS[input.from].includes(input.to)) {
      return yield* Effect.fail(
        new OrbStateError({
          from: input.from,
          message: `Cannot transition run from ${input.from} to ${input.to}`,
          reason: "InvalidTransition",
          to: input.to,
        })
      );
    }
    return input.to;
  }
);
