/* eslint-disable max-classes-per-file -- execution boundary errors live with their schemas. */
import { Effect, Schema } from "effect";

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

const HttpUrl = Schema.String.check(
  Schema.makeFilter(
    (value) => {
      try {
        const url = new URL(value);
        return url.protocol === "http:" || url.protocol === "https:";
      } catch {
        return false;
      }
    },
    { expected: "an HTTP URL" }
  )
);

export const GitProvider = Schema.Literals(["github", "gitea"]);
export type GitProvider = typeof GitProvider.Type;

export const GitCredentialKind = Schema.Literals(["oauth", "token"]);
export type GitCredentialKind = typeof GitCredentialKind.Type;

export const GitConnectionInput = Schema.Struct({
  credential: Text,
  credentialKind: GitCredentialKind,
  provider: GitProvider,
  serverUrl: HttpUrl,
  username: Schema.optional(Text),
});
export type GitConnectionInput = typeof GitConnectionInput.Type;

export const GitConnectionView = Schema.Struct({
  connectedAt: Schema.Number,
  credentialKind: GitCredentialKind,
  id: Text,
  provider: GitProvider,
  serverUrl: HttpUrl,
  username: Schema.optional(Text),
});
export type GitConnectionView = typeof GitConnectionView.Type;

export const RepositoryExecutionAuth = Schema.Struct({
  credential: Text,
  provider: GitProvider,
  serverUrl: HttpUrl,
  username: Schema.optional(Text),
});
export type RepositoryExecutionAuth = typeof RepositoryExecutionAuth.Type;

export const ExecutionEventKind = Schema.Literals([
  "runtime.preparing",
  "repository.cloning",
  "repository.ready",
  "harness.started",
  "harness.progress",
  "harness.log",
  "repository.changed",
  "runtime.completed",
  "runtime.failed",
  "runtime.cancelled",
]);
export type ExecutionEventKind = typeof ExecutionEventKind.Type;

export const ExecutionEvent = Schema.Struct({
  kind: ExecutionEventKind,
  message: Text,
  metadata: Schema.Record(Schema.String, Schema.String),
  occurredAt: Schema.Number,
  sequence: Schema.Int,
});
export type ExecutionEvent = typeof ExecutionEvent.Type;

export const WorkAttemptExecutionInput = Schema.Struct({
  attemptId: Text,
  auth: RepositoryExecutionAuth,
  baseBranch: Text,
  prompt: Text,
  repositoryUrl: HttpUrl,
  runId: Text,
  workId: Text,
  workspaceKey: Text,
});
export type WorkAttemptExecutionInput = typeof WorkAttemptExecutionInput.Type;

export const WorkAttemptExecutionResult = Schema.Struct({
  baseRevision: Text,
  candidateRevision: Text,
  changedFiles: Schema.Array(Text),
  diff: Schema.String,
  environmentId: Text,
  events: Schema.Array(ExecutionEvent),
  summary: Text,
});
export type WorkAttemptExecutionResult = typeof WorkAttemptExecutionResult.Type;

export const WorkAttemptExecutionErrorReason = Schema.Literals([
  "Authentication",
  "Cancelled",
  "HarnessFailed",
  "InvalidInput",
  "ProviderUnavailable",
  "RepositoryFailed",
  "Timeout",
]);
export type WorkAttemptExecutionErrorReason =
  typeof WorkAttemptExecutionErrorReason.Type;

export class WorkAttemptExecutionError extends Schema.TaggedErrorClass<WorkAttemptExecutionError>()(
  "WorkAttemptExecutionError",
  {
    message: Schema.String,
    reason: WorkAttemptExecutionErrorReason,
    retryable: Schema.Boolean,
  }
) {}

export const WorkAttemptExecutionFailure = Schema.Struct({
  error: Schema.Struct({
    message: Text,
    reason: WorkAttemptExecutionErrorReason,
    retryable: Schema.Boolean,
  }),
});
export type WorkAttemptExecutionFailure =
  typeof WorkAttemptExecutionFailure.Type;

const invalidInput = (message: string) =>
  new WorkAttemptExecutionError({
    message,
    reason: "InvalidInput",
    retryable: false,
  });

export const decodeGitConnectionInput = (input: unknown) =>
  Schema.decodeUnknownEffect(GitConnectionInput)(input).pipe(
    Effect.mapError((cause) => invalidInput(cause.message))
  );

export const decodeWorkAttemptExecutionInput = (input: unknown) =>
  Schema.decodeUnknownEffect(WorkAttemptExecutionInput)(input).pipe(
    Effect.mapError((cause) => invalidInput(cause.message))
  );

export const decodeWorkAttemptExecutionResult = (input: unknown) =>
  Schema.decodeUnknownEffect(WorkAttemptExecutionResult)(input).pipe(
    Effect.mapError((cause) => invalidInput(cause.message))
  );

export const decodeWorkAttemptExecutionFailure = (input: unknown) =>
  Schema.decodeUnknownEffect(WorkAttemptExecutionFailure)(input).pipe(
    Effect.mapError((cause) => invalidInput(cause.message))
  );

export interface SandboxRuntime {
  readonly name: string;
  readonly execute: (
    input: WorkAttemptExecutionInput,
    signal?: AbortSignal
  ) => Effect.Effect<WorkAttemptExecutionResult, WorkAttemptExecutionError>;
  readonly cancel: (
    workspaceKey: string,
    attemptId: string
  ) => Effect.Effect<void, WorkAttemptExecutionError>;
}
