import type { ManagementApiClient, paths } from "@prisma/management-api-sdk";
import { Result } from "better-result";
import { ApiError, AuthenticationError, CancelledError } from "./errors.ts";

export type ApiClientError = CancelledError | AuthenticationError | ApiError;

/**
 * Lean internal wrapper around ManagementApiClient.
 *
 * Provides typed, domain-specific methods with structured error handling.
 * Has no knowledge of OAuth, service tokens, or token storage — it simply
 * uses the ManagementApiClient it was given.
 */
export class InternalApiClient {
  readonly #client: ManagementApiClient;

  constructor(client: ManagementApiClient) {
    this.#client = client;
  }

  async listProjects(signal?: AbortSignal) {
    return this.#requestPaginated(
      (cursor) =>
        this.#client.GET("/v1/projects", {
          params: { query: cursor ? { cursor } : {} },
          signal,
        }),
      "/v1/projects",
      signal,
    );
  }

  async createProject(body: CreateProjectBody, signal?: AbortSignal) {
    signal?.throwIfAborted();
    return this.#request(
      this.#client.POST("/v1/projects", {
        body: body as SdkCreateProjectBody,
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      "/v1/projects",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async getProject(id: string, signal?: AbortSignal) {
    return this.#request(
      this.#client.GET("/v1/projects/{id}", {
        params: { path: { id } },
        signal,
      }),
      "/v1/projects/{id}",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async listProjectApps(projectId: string, signal?: AbortSignal) {
    return this.#requestPaginated(
      (cursor) =>
        this.#client.GET("/v1/apps", {
          params: {
            query: cursor ? { projectId, cursor } : { projectId },
          },
          signal,
        }),
      "/v1/apps",
      signal,
    );
  }

  async createProjectApp(
    projectId: string,
    body: CreateProjectAppBody,
    signal?: AbortSignal,
  ) {
    signal?.throwIfAborted();
    return this.#request(
      this.#client.POST("/v1/apps", {
        body: { ...body, projectId } as SdkCreateAppBody,
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      "/v1/apps",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async getApp(appId: string, signal?: AbortSignal) {
    return this.#request(
      this.#client.GET("/v1/apps/{appId}", {
        params: { path: { appId } },
        signal,
      }),
      "/v1/apps/{appId}",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async getBranch(branchId: string, signal?: AbortSignal) {
    return this.#request(
      this.#client.GET("/v1/branches/{branchId}", {
        params: { path: { branchId } },
        signal,
      }),
      "/v1/branches/{branchId}",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async deleteApp(
    appId: string,
    signal?: AbortSignal,
  ): Promise<Result<void, ApiClientError>> {
    signal?.throwIfAborted();
    return this.#unwrapVoid(
      this.#client.DELETE("/v1/apps/{appId}", {
        params: { path: { appId } },
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      signal,
    );
  }

  async listAppDeployments(appId: string, signal?: AbortSignal) {
    return this.#requestPaginated(
      (cursor) =>
        this.#client.GET("/v1/apps/{appId}/deployments", {
          params: {
            path: { appId },
            query: cursor ? { cursor } : {},
          },
          signal,
        }),
      "/v1/apps/{appId}/deployments",
      signal,
    );
  }

  async createAppDeployment(
    appId: string,
    body?: CreateAppDeploymentBody,
    signal?: AbortSignal,
  ) {
    signal?.throwIfAborted();
    return this.#request(
      this.#client.POST("/v1/apps/{appId}/deployments", {
        params: { path: { appId } },
        body,
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      "/v1/apps/{appId}/deployments",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async getDeployment(deploymentId: string, signal?: AbortSignal) {
    return this.#request(
      this.#client.GET("/v1/deployments/{deploymentId}", {
        params: { path: { deploymentId } },
        signal,
      }),
      "/v1/deployments/{deploymentId}",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async startDeployment(deploymentId: string, signal?: AbortSignal) {
    signal?.throwIfAborted();
    return this.#request(
      this.#client.POST("/v1/deployments/{deploymentId}/start", {
        params: { path: { deploymentId } },
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      "/v1/deployments/{deploymentId}/start",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async stopDeployment(
    deploymentId: string,
    signal?: AbortSignal,
  ): Promise<Result<void, ApiClientError>> {
    signal?.throwIfAborted();
    return this.#unwrapVoid(
      this.#client.POST("/v1/deployments/{deploymentId}/stop", {
        params: { path: { deploymentId } },
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      signal,
    );
  }

  async deleteDeployment(
    deploymentId: string,
    signal?: AbortSignal,
  ): Promise<Result<void, ApiClientError>> {
    signal?.throwIfAborted();
    return this.#unwrapVoid(
      this.#client.DELETE("/v1/deployments/{deploymentId}", {
        params: { path: { deploymentId } },
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      signal,
    );
  }

  async promoteApp(appId: string, body: PromoteAppBody, signal?: AbortSignal) {
    signal?.throwIfAborted();
    return this.#request(
      this.#client.POST("/v1/apps/{appId}/promote", {
        params: { path: { appId } },
        body,
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      "/v1/apps/{appId}/promote",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async listEnvironmentVariables(
    query: ListEnvironmentVariablesQuery,
    signal?: AbortSignal,
  ) {
    return this.#requestPaginated(
      (cursor) =>
        this.#client.GET("/v1/environment-variables", {
          params: {
            query: cursor ? { ...query, cursor } : query,
          },
          signal,
        }),
      "/v1/environment-variables",
      signal,
    );
  }

  async createEnvironmentVariable(
    body: CreateEnvironmentVariableBody,
    signal?: AbortSignal,
  ) {
    signal?.throwIfAborted();
    return this.#request(
      this.#client.POST("/v1/environment-variables", {
        body,
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      "/v1/environment-variables",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async updateEnvironmentVariable(
    envVarId: string,
    body: UpdateEnvironmentVariableBody,
    signal?: AbortSignal,
  ) {
    signal?.throwIfAborted();
    return this.#request(
      this.#client.PATCH("/v1/environment-variables/{envVarId}", {
        params: { path: { envVarId } },
        body,
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      "/v1/environment-variables/{envVarId}",
      signal,
    ).then((r) => r.map((body) => body.data));
  }

  async deleteEnvironmentVariable(
    envVarId: string,
    signal?: AbortSignal,
  ): Promise<Result<void, ApiClientError>> {
    signal?.throwIfAborted();
    return this.#unwrapVoid(
      this.#client.DELETE("/v1/environment-variables/{envVarId}", {
        params: { path: { envVarId } },
        // Intentionally do not pass AbortSignal for mutating operations.
        // Aborting transport can leave remote completion ambiguous.
      }),
      signal,
    );
  }

  async #request<TData>(
    request: Promise<RequestResult<TData>>,
    endpoint: string,
    signal?: AbortSignal,
  ): Promise<Result<NonNullable<TData>, ApiClientError>> {
    let result: RequestResult<TData>;
    try {
      result = await request;
    } catch (error) {
      return Result.err(parseRequestError(error, undefined, signal));
    }
    if (result.error) {
      return Result.err(
        parseRequestError(result.error, result.response, signal),
      );
    }
    const body = result.data;
    if (body == null) {
      return Result.err(
        parseRequestError(
          undefined,
          undefined,
          signal,
          `Management API returned an empty response body for ${endpoint}`,
        ),
      );
    }
    return Result.ok(body);
  }

  async #requestPaginated<TItem>(
    makeRequest: (cursor: string | undefined) => Promise<
      RequestResult<{
        data: TItem[];
        pagination: { nextCursor: string | null; hasMore: boolean };
      }>
    >,
    endpoint: string,
    signal?: AbortSignal,
  ): Promise<Result<TItem[], ApiClientError>> {
    const allItems: TItem[] = [];
    let cursor: string | undefined;

    for (;;) {
      const result = await this.#request(makeRequest(cursor), endpoint, signal);
      if (result.isErr()) return result;

      allItems.push(...result.value.data);

      const nextCursor = result.value.pagination?.nextCursor;
      if (!nextCursor) break;
      cursor = nextCursor;
    }

    return Result.ok(allItems);
  }

  async #unwrapVoid(
    request: Promise<RequestResult<unknown>>,
    signal?: AbortSignal,
  ): Promise<Result<void, ApiClientError>> {
    let result: RequestResult<unknown>;
    try {
      result = await request;
    } catch (error) {
      return Result.err(parseRequestError(error, undefined, signal));
    }
    if (result.error) {
      return Result.err(
        parseRequestError(result.error, result.response, signal),
      );
    }
    return Result.ok(undefined);
  }
}

type RequestResult<TData> =
  | { data: TData; error?: never; response: Response }
  | { data?: never; error: unknown; response: Response };

type SdkCreateProjectBody = NonNullable<
  paths["/v1/projects"]["post"]["requestBody"]
>["content"]["application/json"];

type CreateProjectBody = Omit<SdkCreateProjectBody, "region"> & {
  region?: string;
};

type SdkCreateAppBody = NonNullable<
  paths["/v1/apps"]["post"]["requestBody"]
>["content"]["application/json"];

type CreateProjectAppBody = Omit<SdkCreateAppBody, "regionId" | "projectId"> & {
  regionId?: string;
};

export type CreateAppDeploymentBody = NonNullable<
  paths["/v1/apps/{appId}/deployments"]["post"]["requestBody"]
>["content"]["application/json"];

export type PromoteAppBody = NonNullable<
  paths["/v1/apps/{appId}/promote"]["post"]["requestBody"]
>["content"]["application/json"];

type ListEnvironmentVariablesQuery = NonNullable<
  paths["/v1/environment-variables"]["get"]["parameters"]["query"]
>;

type CreateEnvironmentVariableBody = NonNullable<
  paths["/v1/environment-variables"]["post"]["requestBody"]
>["content"]["application/json"];

type UpdateEnvironmentVariableBody = NonNullable<
  paths["/v1/environment-variables/{envVarId}"]["patch"]["requestBody"]
>["content"]["application/json"];

const TRACE_HEADER_NAMES = [
  "x-request-id",
  "x-correlation-id",
  "x-amzn-requestid",
  "x-amz-request-id",
  "traceparent",
  "cf-ray",
  "cf-placement",
] as const;

function extractTraceHeaders(response?: Response): Record<string, string> {
  const headers: Record<string, string> = {};
  if (!response) return headers;

  for (const name of TRACE_HEADER_NAMES) {
    const value = response.headers.get(name);
    if (value) {
      headers[name] = value;
    }
  }

  return headers;
}

function parseApiError(
  error: unknown,
  response?: Response,
  fallbackMessage?: string,
): AuthenticationError | ApiError {
  const traceHeaders = extractTraceHeaders(response);

  if (response?.status === 401) {
    return new AuthenticationError({
      statusCode: 401,
      message: "Authentication failed (HTTP 401)",
    });
  }

  const err = error as
    | {
        error?: { message?: string; code?: string; hint?: string };
        message?: string;
        code?: string;
        hint?: string;
      }
    | undefined;

  const message =
    err?.error?.message ??
    err?.message ??
    fallbackMessage ??
    `Request failed with HTTP ${response?.status ?? "unknown"}`;

  const code = err?.error?.code ?? err?.code;
  const hint = err?.error?.hint ?? err?.hint;

  return new ApiError({
    statusCode: response?.status ?? 0,
    statusText: response?.statusText ?? "",
    code,
    message,
    hint,
    traceHeaders,
  });
}

function parseRequestError(
  error: unknown,
  response?: Response,
  signal?: AbortSignal,
  fallbackMessage?: string,
): ApiClientError {
  if (signal?.aborted) {
    return new CancelledError();
  }
  return parseApiError(error, response, fallbackMessage);
}

/** Extract the upload URL from a deployment creation response. */
export function readUploadUrl(data: {
  uploadUrl?: string | null;
}): string | null {
  if (typeof data.uploadUrl === "string" && data.uploadUrl.length > 0) {
    return data.uploadUrl;
  }
  return null;
}

/** Normalize a preview domain to a clickable https URL. */
export function toDeploymentUrl(value: string): string {
  const normalized = value.trim();
  if (normalized.length === 0) return normalized;
  if (normalized.startsWith("//")) return `https:${normalized}`;
  if (/^https?:\/\//i.test(normalized)) return normalized;
  return `https://${normalized}`;
}
