import type {
  BatchDeleteRunsRequest,
  BatchDeleteRunsResponse,
  BatchRunLifecycleRequest,
  BatchRunLifecycleResponse,
  BatchRunLifecycleSummary,
  ErrorResponseEntry,
  Run,
  RunControlAction,
} from "@qltysh/fabro-api-client";

import {
  ApiError,
  apiData,
  apiResponse,
  requestSignalOptions,
  runsApi,
} from "./api-client";
import type { RunStatus } from "../data/runs";

export type LifecycleAction =
  | "cancel"
  | "approve"
  | "deny"
  | "archive"
  | "unarchive"
  | "retry";

export interface LifecycleActionError {
  status: number;
  errors: ErrorResponseEntry[];
}

const CANCELABLE_STATUSES = new Set<RunStatus>([
  "submitted",
  "pending",
  "runnable",
  "starting",
  "running",
  "paused",
  "blocked",
]);

const TERMINAL_RUN_STATUSES = new Set<RunStatus>([
  "succeeded",
  "failed",
  "dead",
]);

export async function cancelRun(id: string, request?: Request): Promise<Run> {
  return runLifecycleAction(id, "cancel", request);
}

export async function approveRun(id: string, request?: Request): Promise<Run> {
  return runLifecycleAction(id, "approve", request);
}

export async function denyRun(id: string, request?: Request): Promise<Run> {
  return runLifecycleAction(id, "deny", request);
}

export async function archiveRun(id: string, request?: Request): Promise<Run> {
  return runLifecycleAction(id, "archive", request);
}

export async function unarchiveRun(id: string, request?: Request): Promise<Run> {
  return runLifecycleAction(id, "unarchive", request);
}

// Client-side batch summary for actions that don't have a server-side batch
// endpoint yet (currently: approve). Shape matches the server's batch
// responses so it slots into the same toolbar plumbing.
export interface BatchActionSummary {
  summary: BatchRunLifecycleSummary;
}

export async function archiveRuns(
  runIds: string[],
  request?: Request,
): Promise<BatchRunLifecycleResponse> {
  return batchRunLifecycleAction(runIds, "archive", request);
}

export async function approveRuns(
  runIds: string[],
  request?: Request,
): Promise<BatchActionSummary> {
  const settled = await Promise.allSettled(
    runIds.map((id) => approveRun(id, request)),
  );
  const succeeded = settled.filter((r) => r.status === "fulfilled").length;
  return {
    summary: {
      requested: settled.length,
      succeeded,
      failed:    settled.length - succeeded,
    },
  };
}

export async function unarchiveRuns(
  runIds: string[],
  request?: Request,
): Promise<BatchRunLifecycleResponse> {
  return batchRunLifecycleAction(runIds, "unarchive", request);
}

export async function deleteRuns(
  runIds: string[],
  force = false,
  request?: Request,
): Promise<BatchDeleteRunsResponse> {
  try {
    const body: BatchDeleteRunsRequest = { run_ids: runIds, force };
    return await apiData(() => runsApi.batchDeleteRuns(body, requestSignalOptions(request)));
  } catch (error) {
    throw lifecycleActionErrorFromError(error);
  }
}

export async function retryRun(id: string, request?: Request): Promise<Run> {
  return runLifecycleAction(id, "retry", request);
}

export async function deleteRun(id: string, request?: Request): Promise<void> {
  try {
    await apiResponse(() => runsApi.deleteRun(id, undefined, requestSignalOptions(request)));
  } catch (error) {
    if (error instanceof ApiError && error.status === 404) return;
    throw lifecycleActionErrorFromError(error);
  }
}

export function canCancel(status: string | null | undefined): boolean {
  return !!status && CANCELABLE_STATUSES.has(status as RunStatus);
}

export function canApprove(run: Run | null | undefined): boolean {
  return run?.lifecycle.status.kind === "pending" && run.lifecycle.approval?.state === "pending";
}

export function canArchive(status: string | null | undefined): boolean {
  return isTerminalRunStatus(status);
}

export function canUnarchive(status: string | null | undefined): boolean {
  return status === "archived";
}

export function canRetry(run: Pick<Run, "lifecycle"> | null | undefined): boolean {
  if (!run || run.lifecycle.archived) return false;
  return isTerminalRunStatus(run.lifecycle.status.kind);
}

export function isTerminalRunStatus(
  status: string | null | undefined,
): boolean {
  return !!status && TERMINAL_RUN_STATUSES.has(status as RunStatus);
}

export function canDelete(status: string | null | undefined): boolean {
  return status === "archived";
}

export function isTerminalCancelledRun(run: Pick<Run, "lifecycle">): boolean {
  const status = run.lifecycle.status;
  return status.kind === "failed" && status.reason === "cancelled";
}

export function isCancellationPending(
  run: Pick<Run, "lifecycle"> | null | undefined,
  mutationPending = false,
): boolean {
  return isCancellationPendingState(
    run?.lifecycle.status.kind,
    run?.lifecycle.pending_control,
    mutationPending,
  );
}

export function isCancellationPendingState(
  status: string | null | undefined,
  pendingControl: RunControlAction | null | undefined,
  mutationPending = false,
): boolean {
  return canCancel(status) && (mutationPending || pendingControl === "cancel");
}

export function cancellationSuccessMessage(run: Pick<Run, "lifecycle">): string {
  return isTerminalCancelledRun(run)
    ? "Run cancelled."
    : "Cancellation requested.";
}

export function cancellationActionLabel(pending: boolean): string {
  return pending ? "Cancelling…" : "Cancel";
}

export function deleteErrorMessage(error: unknown): string {
  if (isLifecycleActionError(error)) {
    if (error.status === 409) {
      return "Active runs can't be deleted.";
    }
    const detail = error.errors[0]?.detail?.trim();
    if (detail) return detail;
  }
  return "Couldn't delete the run right now. Try again.";
}

export function mapError(error: unknown, action: LifecycleAction): string {
  if (isLifecycleActionError(error)) {
    if (error.status === 404) {
      return "This run no longer exists.";
    }
    if (error.status === 409) {
      switch (action) {
        case "cancel":
          return "This run can no longer be cancelled.";
        case "approve":
        case "deny":
          return "This run is no longer pending approval.";
        case "archive":
          return "Only terminal runs can be archived.";
        case "unarchive":
          return "Active runs can't be unarchived.";
        case "retry":
          return "This run can no longer be retried.";
      }
    }

    const detail = error.errors[0]?.detail?.trim();
    if (detail) {
      return detail;
    }
  }

  switch (action) {
    case "cancel":
      return "Couldn't cancel the run right now. Try again.";
    case "approve":
      return "Couldn't approve the run right now. Try again.";
    case "deny":
      return "Couldn't deny the run right now. Try again.";
    case "archive":
      return "Couldn't archive the run right now. Try again.";
    case "unarchive":
      return "Couldn't unarchive the run right now. Try again.";
    case "retry":
      return "Couldn't retry the run right now. Try again.";
  }
}

async function runLifecycleAction(
  id: string,
  action: LifecycleAction,
  request?: Request,
): Promise<Run> {
  try {
    switch (action) {
      case "cancel":
        return await apiData(() => runsApi.cancelRun(id, requestSignalOptions(request)));
      case "approve":
        return await apiData(() => runsApi.approveRun(id, requestSignalOptions(request)));
      case "deny":
        return await apiData(() => runsApi.denyRun(id, undefined, requestSignalOptions(request)));
      case "archive":
        return await apiData(() => runsApi.archiveRun(id, requestSignalOptions(request)));
      case "unarchive":
        return await apiData(() => runsApi.unarchiveRun(id, requestSignalOptions(request)));
      case "retry":
        return await apiData(() => runsApi.retryRun(id, requestSignalOptions(request)));
    }
  } catch (error) {
    throw lifecycleActionErrorFromError(error);
  }
}

async function batchRunLifecycleAction(
  runIds: string[],
  action: "archive" | "unarchive",
  request?: Request,
): Promise<BatchRunLifecycleResponse> {
  try {
    const body: BatchRunLifecycleRequest = { run_ids: runIds };
    switch (action) {
      case "archive":
        return await apiData(() => runsApi.batchArchiveRuns(body, requestSignalOptions(request)));
      case "unarchive":
        return await apiData(() => runsApi.batchUnarchiveRuns(body, requestSignalOptions(request)));
    }
  } catch (error) {
    throw lifecycleActionErrorFromError(error);
  }
}

function lifecycleActionErrorFromError(error: unknown): LifecycleActionError {
  if (!(error instanceof ApiError)) throw error;
  return {
    status: error.status,
    errors: parseLifecycleErrors(error.body),
  };
}

function parseLifecycleErrors(body: unknown): ErrorResponseEntry[] {
  if (!body || typeof body !== "object") return [];
  const errors = (body as { errors?: unknown }).errors;
  if (!Array.isArray(errors)) return [];
  return errors.filter(isErrorResponseEntry);
}

export function isLifecycleActionError(value: unknown): value is LifecycleActionError {
  if (!value || typeof value !== "object") return false;
  const record = value as Record<string, unknown>;
  return typeof record.status === "number" && Array.isArray(record.errors);
}

function isErrorResponseEntry(value: unknown): value is ErrorResponseEntry {
  if (!value || typeof value !== "object") return false;
  const record = value as Record<string, unknown>;
  return (
    typeof record.status === "string"
    && typeof record.title === "string"
    && typeof record.detail === "string"
  );
}
