import { Result } from "better-result";
import type { ApiClientError, InternalApiClient } from "./api-client.ts";
import {
  ApiError,
  CancelledError,
  DeploymentFailedError,
  TimeoutError,
} from "./errors.ts";

export type PollOptions = {
  targetStatus: "running" | "stopped";
  timeoutSeconds: number;
  pollIntervalMs: number;
  signal?: AbortSignal;
  onStatusChange?(status: string): void;
};

export type PollResult = {
  previewDomain: string;
  lastStatus: string;
};

export type PollError =
  | CancelledError
  | TimeoutError
  | DeploymentFailedError
  | ApiClientError;

/**
 * Polls a deployment until it reaches the target status, fails, or times out.
 *
 * Returns a Result with the preview domain on success or a typed error.
 */
export async function pollDeploymentStatus(
  api: InternalApiClient,
  deploymentId: string,
  options: PollOptions,
): Promise<Result<PollResult, PollError>> {
  return Result.gen(async function* () {
    const deadline = Date.now() + options.timeoutSeconds * 1_000;
    let lastStatus = "";

    while (Date.now() <= deadline) {
      if (options.signal?.aborted) {
        return Result.err(new CancelledError());
      }

      const deployment = yield* Result.await(
        api.getDeployment(deploymentId, options.signal),
      );

      if (deployment.status !== lastStatus) {
        lastStatus = deployment.status;
        options.onStatusChange?.(deployment.status);
      }

      if (deployment.status === options.targetStatus) {
        if (options.targetStatus === "running" && !deployment.previewDomain) {
          return Result.err(
            new ApiError({
              statusCode: 0,
              statusText: "",
              message:
                "Deployment reached running state without a previewDomain",
              traceHeaders: {},
            }),
          );
        }
        return Result.ok({
          previewDomain: deployment.previewDomain ?? "",
          lastStatus: deployment.status,
        });
      }

      if (deployment.status === "failed") {
        return Result.err(new DeploymentFailedError({ deploymentId }));
      }

      await sleep(options.pollIntervalMs, options.signal);
    }

    return Result.err(
      new TimeoutError({
        deploymentId,
        elapsedMs: Date.now() - (deadline - options.timeoutSeconds * 1_000),
        lastStatus,
      }),
    );
  });
}

function sleep(ms: number, signal?: AbortSignal): Promise<void> {
  return new Promise((resolve) => {
    if (signal?.aborted) {
      resolve();
      return;
    }

    const onAbort = () => {
      clearTimeout(timer);
      resolve();
    };

    const timer = setTimeout(() => {
      signal?.removeEventListener("abort", onAbort);
      resolve();
    }, ms);

    signal?.addEventListener("abort", onAbort, { once: true });
  });
}
