import type { ResourceCeilings, RunnerPolicy } from "./types.js";

export const DEFAULT_RUNNER_POLICY: RunnerPolicy = Object.freeze({
  isolation: Object.freeze({
    network: "none",
    rootFilesystem: "read-only",
    capabilities: Object.freeze([]),
    noNewPrivileges: true,
    identity: Object.freeze({ runAsUser: 65_532, runAsGroup: 65_532, requireNonRoot: true }),
  }),
  resources: Object.freeze({
    cpuTimeMs: 5 * 60_000,
    memoryBytes: 2 * 1024 ** 3,
    swapBytes: 0,
    pids: 256,
    tempBytes: 2 * 1024 ** 3,
    outputBytes: 1024 ** 3,
    outputFiles: 20_000,
    wallTimeMs: 10 * 60_000,
  }),
  cancellation: Object.freeze({
    scope: "cgroup",
    initialSignal: "SIGTERM",
    graceMs: 5_000,
    finalSignal: "SIGKILL",
  }),
  workDirectory: Object.freeze({ perJob: true, cleanup: "every-terminal-state" }),
});

const POSITIVE_RESOURCE_KEYS = [
  "cpuTimeMs",
  "memoryBytes",
  "pids",
  "tempBytes",
  "outputBytes",
  "outputFiles",
  "wallTimeMs",
] as const satisfies readonly (keyof ResourceCeilings)[];

export class InvalidRunnerPolicyError extends Error {
  readonly issues: readonly string[];

  constructor(issues: readonly string[]) {
    super(`Invalid runner policy: ${issues.join("; ")}`);
    this.name = "InvalidRunnerPolicyError";
    this.issues = issues;
  }
}

/** Validates values before they are translated to a container/runtime request. */
export function validateRunnerPolicy(policy: RunnerPolicy): void {
  const issues: string[] = [];
  const { isolation, resources, cancellation } = policy;

  if (isolation.network !== "none") issues.push("network must be disabled");
  if (isolation.rootFilesystem !== "read-only") issues.push("root filesystem must be read-only");
  if (isolation.noNewPrivileges !== true) issues.push("no-new-privileges must be enabled");
  if (isolation.identity.requireNonRoot !== true) issues.push("non-root identity must be required");
  if (!Number.isSafeInteger(isolation.identity.runAsUser) || isolation.identity.runAsUser <= 0) {
    issues.push("runAsUser must be a positive non-root integer");
  }
  if (!Number.isSafeInteger(isolation.identity.runAsGroup) || isolation.identity.runAsGroup <= 0) {
    issues.push("runAsGroup must be a positive non-root integer");
  }
  if (isolation.capabilities.length !== 0) issues.push("all capabilities must be dropped");

  for (const key of POSITIVE_RESOURCE_KEYS) {
    const value = resources[key];
    if (!Number.isSafeInteger(value) || value <= 0) issues.push(`${key} must be a positive safe integer`);
  }
  if (!Number.isSafeInteger(resources.swapBytes) || resources.swapBytes < 0) {
    issues.push("swapBytes must be a non-negative safe integer");
  }
  if (cancellation.scope !== "cgroup") issues.push("cancellation scope must be the job cgroup");
  if (cancellation.initialSignal !== "SIGTERM" || cancellation.finalSignal !== "SIGKILL") {
    issues.push("cancellation must use SIGTERM followed by SIGKILL");
  }
  if (policy.workDirectory.perJob !== true || policy.workDirectory.cleanup !== "every-terminal-state") {
    issues.push("a per-job directory with terminal cleanup is required");
  }
  if (!Number.isSafeInteger(cancellation.graceMs) || cancellation.graceMs < 0) {
    issues.push("graceMs must be a non-negative safe integer");
  }

  if (issues.length > 0) throw new InvalidRunnerPolicyError(issues);
}
