import type { GoalId, PlanRevisionId, TaskId } from "@awp/contracts";
import { unsatisfiedDependencies, validateTaskDependencies } from "./dependencies.js";
import type { Task } from "./model.js";
export type QueueReadiness =
  | "RUNNING"
  | "READY"
  | "BLOCKED_DEPENDENCY"
  | "BLOCKED_POLICY"
  | "WAITING_USER"
  | "PAUSED"
  | "DONE"
  | "CANCELLED";
export interface QueueEntry {
  readonly taskId: TaskId;
  readonly planRevisionId: PlanRevisionId;
  readonly goalIds: readonly GoalId[];
  readonly position: number;
  readonly readinessState: QueueReadiness;
  readonly unsatisfiedDependencyIds: readonly TaskId[];
  readonly policyBlocked?: boolean;
  readonly waitingUser?: boolean;
  readonly paused?: boolean;
}
export interface LegalPlacementRange {
  readonly earliestLegalPosition: number;
  readonly latestLegalPosition: number;
  readonly blockingDependencies: readonly TaskId[];
  readonly blockingDependents: readonly TaskId[];
}
export class QueueOrderConstraintError extends Error {
  readonly code = "QUEUE_ORDER_CONSTRAINT";
  constructor(
    readonly entryId: TaskId,
    readonly desiredPosition: number,
    readonly legalRange: LegalPlacementRange,
  ) {
    super(
      `Task ${entryId} cannot move to ${desiredPosition}; legal range is ${legalRange.earliestLegalPosition}-${legalRange.latestLegalPosition}`,
    );
  }
}
const readiness = (task: Task, tasks: readonly Task[]): QueueReadiness => {
  if (task.status === "completed") return "DONE";
  if (task.status === "cancelled") return "CANCELLED";
  if (task.status === "executing") return "RUNNING";
  if (task.status === "waiting") return "WAITING_USER";
  const missing = unsatisfiedDependencies(task, tasks);
  return missing.length ? "BLOCKED_DEPENDENCY" : "READY";
};
export function buildQueue(
  tasks: readonly Task[],
  requestedOrder: readonly TaskId[],
): readonly QueueEntry[] {
  validateTaskDependencies(tasks);
  const byId = new Map(tasks.map((t) => [t.id, t] as const));
  if (requestedOrder.length !== tasks.length || new Set(requestedOrder).size !== tasks.length)
    throw new Error("Queue order must contain every Task exactly once");
  requestedOrder.forEach((id) => {
    if (!byId.has(id)) throw new Error(`Unknown queued Task ${id}`);
  });
  const pos = new Map(requestedOrder.map((id, i) => [id, i]));
  for (const task of tasks)
    for (const dep of task.dependencyIds)
      if ((pos.get(dep) ?? -1) > (pos.get(task.id) ?? -1))
        throw new QueueOrderConstraintError(
          task.id,
          pos.get(task.id)!,
          legalPlacementRange(task, requestedOrder, tasks),
        );
  return requestedOrder.map((id, position) => {
    const task = byId.get(id)!;
    return {
      taskId: id,
      planRevisionId: task.planRevisionId,
      goalIds: task.goalIds ?? [],
      position,
      readinessState: readiness(task, tasks),
      unsatisfiedDependencyIds: unsatisfiedDependencies(task, tasks),
    };
  });
}
export function legalPlacementRange(
  task: Task,
  order: readonly TaskId[],
  tasks: readonly Task[],
): LegalPlacementRange {
  const positions = new Map(order.map((id, i) => [id, i]));
  const deps = task.dependencyIds;
  const dependents = tasks.filter((t) => t.dependencyIds.includes(task.id)).map((t) => t.id);
  const depPositions = deps
    .map((id) => positions.get(id))
    .filter((n): n is number => n !== undefined);
  const dependentPositions = dependents
    .map((id) => positions.get(id))
    .filter((n): n is number => n !== undefined);
  return {
    earliestLegalPosition: depPositions.length ? Math.max(...depPositions) + 1 : 0,
    latestLegalPosition: dependentPositions.length
      ? Math.min(...dependentPositions) - 1
      : order.length - 1,
    blockingDependencies: deps,
    blockingDependents: dependents,
  };
}
export function moveTaskToPosition(
  order: readonly TaskId[],
  taskId: TaskId,
  desiredPosition: number,
  tasks: readonly Task[],
): readonly TaskId[] {
  const task = tasks.find((t) => t.id === taskId);
  if (!task) throw new Error(`Unknown Task ${taskId}`);
  const without = order.filter((id) => id !== taskId);
  const bounded = Math.max(0, Math.min(desiredPosition, without.length));
  const candidate = [...without.slice(0, bounded), taskId, ...without.slice(bounded)];
  const range = legalPlacementRange(task, candidate, tasks);
  if (bounded < range.earliestLegalPosition || bounded > range.latestLegalPosition)
    throw new QueueOrderConstraintError(taskId, bounded, range);
  buildQueue(tasks, candidate);
  return candidate;
}
export function isDispatchEligible(
  task: Task,
  tasks: readonly Task[],
  options: {
    approvalsSatisfied?: boolean;
    executionProfileResolved?: boolean;
    providerCapacityAvailable?: boolean;
    paused?: boolean;
    policyAllows?: boolean;
  } = {},
): boolean {
  return (
    unsatisfiedDependencies(task, tasks).length === 0 &&
    (options.approvalsSatisfied ?? true) &&
    (options.executionProfileResolved ?? true) &&
    (options.providerCapacityAvailable ?? true) &&
    !(options.paused ?? false) &&
    (options.policyAllows ?? true) &&
    ["ready", "queued", "planned", "blocked"].includes(task.status)
  );
}
