import { MachinePresetName, tryCatch } from "@trigger.dev/core/v3";
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
import {
  BillingClient,
  defaultMachine as defaultMachineFromPlatform,
  machines as machinesFromPlatform,
  type BillingAlertsResult,
  type CreatePrivateLinkConnectionBody,
  type Limits,
  type MachineCode,
  type PrivateLinkConnection,
  type PrivateLinkConnectionList,
  type PrivateLinkRegionsResult,
  type SetPlanBody,
  type UpdateBillingAlertsRequest,
  type UsageResult,
  type UsageSeriesParams,
  type CurrentPlan,
} from "@trigger.dev/platform";
import {
  BillingLimitResultSchema,
  BillingLimitsActiveResultSchema,
  BillingLimitsPendingResolvesResultSchema,
  EntitlementResultSchema,
  asPlatformSchema,
  type BillingLimitResult,
  type BillingLimitsActiveResult,
  type BillingLimitsPendingResolvesResult,
  type EntitlementResult,
  type ResolveBillingLimitRequest,
  type UpdateBillingLimitRequest,
} from "~/services/billingLimit.schemas";
import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache";
import { createLRUMemoryStore } from "@internal/cache";
import { existsSync, readFileSync } from "node:fs";
import { redirect } from "remix-typedjson";
import { z } from "zod";
import { env } from "~/env.server";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { logger } from "~/services/logger.server";
import { newProjectPath, organizationBillingPath } from "~/utils/pathBuilder";
import { singleton } from "~/utils/singleton";
import { RedisCacheStore } from "./unkey/redisCacheStore.server";
import { $replica } from "~/db.server";
import { metrics } from "@opentelemetry/api";

function initializeClient() {
  if (isCloud() && process.env.BILLING_API_URL && process.env.BILLING_API_KEY) {
    const client = new BillingClient({
      url: process.env.BILLING_API_URL,
      apiKey: process.env.BILLING_API_KEY,
    });
    return client;
  }
}

const client = singleton("billingClient", initializeClient);

/**
 * `true` when the billing client was instantiated — i.e. we're running
 * in a cloud-style install with `BILLING_API_URL` + `BILLING_API_KEY`
 * configured. OSS / self-hosted installs return `false` here, which
 * lets callers distinguish "no billing wired up, fall back to
 * defaults" from "billing wired up but the call failed, retry."
 */
export function isBillingConfigured(): boolean {
  return client !== undefined;
}
// Failures from @trigger.dev/platform billing client calls are tracked via
// this metric (with low-cardinality {function, kind} labels) rather than
// logged. Every task invocation hits these paths, so per-call logs were too
// noisy; dashboard the counter for visibility instead.
const platformClientMeter = metrics.getMeter("trigger.dev/platform-client");
const platformClientFailuresCounter = platformClientMeter.createCounter(
  "platform_client.failures_total",
  {
    description: "Failures returned or thrown by @trigger.dev/platform billing client calls",
  }
);

function recordPlatformFailure(fn: string, kind: "caught" | "no_success") {
  platformClientFailuresCounter.add(1, { function: fn, kind });
}

export type ValidatedPromoCode = {
  valid: boolean;
  amountInCents?: number;
  expiresAt?: string | null;
};

/**
 * Validate a promo code (no org context). Returns `undefined` when billing
 * isn't configured or the call fails, so callers fall back to treating the
 * code as not-yet-validated rather than crashing the page.
 */
export async function validatePromoCode(code: string): Promise<ValidatedPromoCode | undefined> {
  if (!client) {
    return undefined;
  }

  const [error, result] = await tryCatch(client.validatePromoCode(code));
  if (error) {
    recordPlatformFailure("validatePromoCode", "caught");
    logger.error("validatePromoCode threw", { error });
    return undefined;
  }
  if (!result.success) {
    recordPlatformFailure("validatePromoCode", "no_success");
    return undefined;
  }

  return {
    valid: result.valid,
    amountInCents: result.amountInCents,
    expiresAt: result.expiresAt,
  };
}

export type AppliedPromoCode = {
  applied: boolean;
  amountInCents?: number;
  reason?: string;
};

/**
 * Apply a promo code to a newly created org. Returns `undefined` when billing
 * isn't configured or the call fails — callers treat that as "not applied" and
 * must never block org creation on it.
 */
export async function applyPromoCode(
  orgId: string,
  userId: string,
  code: string
): Promise<AppliedPromoCode | undefined> {
  if (!client) {
    return undefined;
  }

  const [error, result] = await tryCatch(client.applyPromoCode(orgId, { code, userId }));
  if (error) {
    recordPlatformFailure("applyPromoCode", "caught");
    logger.error("applyPromoCode threw", { error });
    return undefined;
  }
  if (!result.success) {
    recordPlatformFailure("applyPromoCode", "no_success");
    return undefined;
  }

  return {
    applied: result.applied,
    amountInCents: result.amountInCents,
    reason: result.reason,
  };
}

function initializePlatformCache() {
  const ctx = new DefaultStatefulContext();
  const memory = createLRUMemoryStore(1000);
  const redisCacheStore = new RedisCacheStore({
    connection: {
      keyPrefix: "tr:cache:platform:v3",
      port: env.CACHE_REDIS_PORT,
      host: env.CACHE_REDIS_HOST,
      username: env.CACHE_REDIS_USERNAME,
      password: env.CACHE_REDIS_PASSWORD,
      tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true",
      clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1",
    },
  });

  // This cache holds the limits fetched from the platform service
  const cache = createCache({
    limits: new Namespace<number>(ctx, {
      stores: [memory, redisCacheStore],
      fresh: 60_000 * 5, // 5 minutes
      stale: 60_000 * 10, // 10 minutes
    }),
    usage: new Namespace<UsageResult>(ctx, {
      stores: [memory, redisCacheStore],
      fresh: 60_000 * 5, // 5 minutes
      stale: 60_000 * 10, // 10 minutes
    }),
    entitlement: new Namespace<EntitlementResult>(ctx, {
      stores: [memory, redisCacheStore],
      fresh: 60_000, // serve without revalidation for 60s
      stale: 120_000, // total TTL — fresh 0-60s, stale-revalidate 60-120s
    }),
    billingLimit: new Namespace<BillingLimitResult>(ctx, {
      stores: [memory, redisCacheStore],
      fresh: 60_000,
      stale: 120_000,
    }),
    promoCredits: new Namespace<PromoCreditsData | null>(ctx, {
      stores: [memory, redisCacheStore],
      fresh: 60_000,
      stale: 120_000,
    }),
  });

  return cache;
}

const platformCache = singleton("platformCache", initializePlatformCache);

function invalidateBillingLimitCaches(organizationId: string) {
  platformCache.billingLimit.remove(organizationId).catch(() => {});
  platformCache.entitlement.remove(organizationId).catch(() => {});
}

export function bustBillingLimitCaches(organizationId: string) {
  invalidateBillingLimitCaches(organizationId);
}

// Clear the cached promo-credits read so a just-granted code shows on the usage
// page immediately rather than after the stale TTL.
export function bustPromoCreditsCache(organizationId: string) {
  platformCache.promoCredits.remove(organizationId).catch(() => {});
}

type Machines = typeof machinesFromPlatform;

const MachineOverrideValues = z.object({
  cpu: z.number(),
  memory: z.number(),
});
type MachineOverrideValues = z.infer<typeof MachineOverrideValues>;

const MachineOverrides = z.record(MachinePresetName, MachineOverrideValues.partial());
type MachineOverrides = z.infer<typeof MachineOverrides>;

const MachinePresetOverrides = z.object({
  defaultMachine: MachinePresetName.optional(),
  machines: MachineOverrides.optional(),
});

function initializeMachinePresets(): {
  defaultMachine: MachineCode;
  machines: Machines;
} {
  const overrides = getMachinePresetOverrides();

  if (!overrides) {
    return {
      defaultMachine: defaultMachineFromPlatform,
      machines: machinesFromPlatform,
    };
  }

  logger.info("🎛️ Overriding machine presets", { overrides });

  return {
    defaultMachine: overrideDefaultMachine(defaultMachineFromPlatform, overrides.defaultMachine),
    machines: overrideMachines(machinesFromPlatform, overrides.machines),
  };
}

export const { defaultMachine, machines } = singleton("machinePresets", initializeMachinePresets);

function overrideDefaultMachine(defaultMachine: MachineCode, override?: MachineCode): MachineCode {
  if (!override) {
    return defaultMachine;
  }

  return override;
}

function overrideMachines(machines: Machines, overrides?: MachineOverrides): Machines {
  if (!overrides) {
    return machines;
  }

  const mergedMachines = {
    ...machines,
  };

  for (const machine of Object.keys(overrides) as MachinePresetName[]) {
    mergedMachines[machine] = {
      ...mergedMachines[machine],
      ...overrides[machine],
    };
  }

  return mergedMachines;
}

function getMachinePresetOverrides() {
  const path = env.MACHINE_PRESETS_OVERRIDE_PATH;
  if (!path) {
    return;
  }

  const overrides = safeReadMachinePresetOverrides(path);
  if (!overrides) {
    return;
  }

  const parsed = MachinePresetOverrides.safeParse(overrides);

  if (!parsed.success) {
    logger.error("Error parsing machine preset overrides", { path, error: parsed.error });
    return;
  }

  return parsed.data;
}

function safeReadMachinePresetOverrides(path: string) {
  try {
    const fileExists = existsSync(path);
    if (!fileExists) {
      logger.error("Machine preset overrides file does not exist", { path });
      return;
    }

    const fileContents = readFileSync(path, "utf8");

    return JSON.parse(fileContents);
  } catch (error) {
    logger.error("Error reading machine preset overrides", {
      path,
      error: error instanceof Error ? error.message : String(error),
    });
    return;
  }
}

export async function getCurrentPlan(orgId: string) {
  if (!client) return undefined;

  try {
    const result = await client.currentPlan(orgId);

    const firstDayOfMonth = new Date();
    firstDayOfMonth.setUTCDate(1);
    firstDayOfMonth.setUTCHours(0, 0, 0, 0);

    const firstDayOfNextMonth = new Date();
    firstDayOfNextMonth.setUTCDate(1);
    firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1);
    firstDayOfNextMonth.setUTCHours(0, 0, 0, 0);

    if (!result.success) {
      recordPlatformFailure("getCurrentPlan", "no_success");
      return undefined;
    }

    const periodStart = firstDayOfMonth;
    const periodEnd = firstDayOfNextMonth;
    const periodRemainingDuration = periodEnd.getTime() - new Date().getTime();

    const usage = {
      periodStart,
      periodEnd,
      periodRemainingDuration,
    };

    return { ...result, usage };
  } catch (_e) {
    recordPlatformFailure("getCurrentPlan", "caught");
    return undefined;
  }
}

export type SelfServePurchaseBlockReason = "plan_unavailable" | "managed_billing";

/**
 * When cloud billing is configured, self-serve purchase endpoints must fail closed
 * if the current plan can't be loaded or the org is on managed billing.
 */
export function getSelfServePurchaseBlockReason(
  currentPlan: Awaited<ReturnType<typeof getCurrentPlan>>
): SelfServePurchaseBlockReason | undefined {
  if (!isBillingConfigured()) {
    return undefined;
  }

  if (!currentPlan) {
    return "plan_unavailable";
  }

  if (currentPlan.v3Subscription?.showSelfServe === false) {
    return "managed_billing";
  }

  return undefined;
}

export async function getLimits(orgId: string) {
  if (!client) return undefined;

  try {
    const result = await client.currentPlan(orgId);
    if (!result.success) {
      recordPlatformFailure("getLimits", "no_success");
      return undefined;
    }

    return result.v3Subscription?.plan?.limits;
  } catch (_e) {
    recordPlatformFailure("getLimits", "caught");
    return undefined;
  }
}

export async function getLimit(orgId: string, limit: keyof Limits, fallback: number) {
  const limits = await getLimits(orgId);
  if (!limits) return fallback;
  const result = limits[limit];

  if (!result) return fallback;
  if (typeof result === "number") return result;
  if (typeof result === "object" && "number" in result) return result.number;
  return fallback;
}

export async function getDefaultEnvironmentConcurrencyLimit(
  organizationId: string,
  environmentType: RuntimeEnvironmentType
): Promise<number> {
  if (!client) {
    const org = await $replica.organization.findFirst({
      where: {
        id: organizationId,
      },
      select: {
        maximumConcurrencyLimit: true,
      },
    });
    if (!org) throw new Error("Organization not found");
    return org.maximumConcurrencyLimit;
  }

  const result = await client.currentPlan(organizationId);
  if (!result.success) throw new Error("Error getting current plan");

  const limit = getDefaultEnvironmentLimitFromPlan(environmentType, result);
  if (!limit) throw new Error("No plan found");

  return limit;
}

export function getDefaultEnvironmentLimitFromPlan(
  environmentType: RuntimeEnvironmentType,
  plan: CurrentPlan
): number | undefined {
  if (!plan.v3Subscription?.plan) return undefined;

  switch (environmentType) {
    case "DEVELOPMENT":
      return plan.v3Subscription.plan.limits.concurrentRuns.development;
    case "STAGING":
      return plan.v3Subscription.plan.limits.concurrentRuns.staging;
    case "PREVIEW":
      return plan.v3Subscription.plan.limits.concurrentRuns.preview;
    case "PRODUCTION":
      return plan.v3Subscription.plan.limits.concurrentRuns.production;
    default:
      return plan.v3Subscription.plan.limits.concurrentRuns.number;
  }
}

export async function getCachedLimit(orgId: string, limit: keyof Limits, fallback: number) {
  return platformCache.limits.swr(`${orgId}:${limit}`, async () => {
    return getLimit(orgId, limit, fallback);
  });
}

export async function customerPortalUrl(orgId: string, orgSlug: string) {
  if (!client) return undefined;

  try {
    return client.createPortalSession(orgId, {
      returnUrl: `${env.APP_ORIGIN}${organizationBillingPath({ slug: orgSlug })}`,
    });
  } catch (_e) {
    recordPlatformFailure("customerPortalUrl", "caught");
    return undefined;
  }
}

export async function getPlans() {
  if (!client) return undefined;

  try {
    const result = await client.plans();
    if (!result.success) {
      recordPlatformFailure("getPlans", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("getPlans", "caught");
    return undefined;
  }
}

export async function setPlan(
  organization: { id: string; slug: string },
  request: Request,
  callerPath: string,
  plan: SetPlanBody,
  opts?: {
    invalidateBillingCache?: (orgId: string) => void;
    // Runs only after the Free plan has actually been provisioned, with the
    // redirect it will return — the single success path where side effects that
    // depend on a working free-plan entitlement (e.g. redeeming a promo code)
    // are safe. It never fires on an error path, so callers can't act on a
    // plan change that didn't happen.
    onFreePlanProvisioned?: (response: Response) => void | Promise<void>;
  }
) {
  if (!client) {
    return redirectWithErrorMessage(callerPath, request, "Error setting plan", {
      ephemeral: false,
    });
  }

  const [error, result] = await tryCatch(client.setPlan(organization.id, plan));

  if (error) {
    return redirectWithErrorMessage(callerPath, request, error.message, { ephemeral: false });
  }

  if (!result) {
    return redirectWithErrorMessage(callerPath, request, "Error setting plan", {
      ephemeral: false,
    });
  }

  if (!result.success) {
    return redirectWithErrorMessage(callerPath, request, result.error, { ephemeral: false });
  }

  switch (result.action) {
    case "free_connect_required":
    case "free_connected": {
      // Selecting Free provisions the plan directly, so any free result is a success.
      opts?.invalidateBillingCache?.(organization.id);
      platformCache.entitlement.remove(organization.id).catch(() => {});
      const response = redirect(newProjectPath(organization, "You're on the Free plan."));
      await opts?.onFreePlanProvisioned?.(response);
      return response;
    }
    case "create_subscription_flow_start": {
      return redirect(result.checkoutUrl);
    }
    case "updated_subscription": {
      // Invalidate billing cache since subscription changed
      opts?.invalidateBillingCache?.(organization.id);
      platformCache.entitlement.remove(organization.id).catch(() => {});
      return redirectWithSuccessMessage(callerPath, request, "Subscription updated successfully.");
    }
    case "canceled_subscription": {
      // Invalidate billing cache since subscription was canceled
      opts?.invalidateBillingCache?.(organization.id);
      platformCache.entitlement.remove(organization.id).catch(() => {});
      return redirectWithSuccessMessage(callerPath, request, "Subscription canceled.");
    }
  }

  // Unrecognised action shape — surface an error rather than falling through to
  // an implicit undefined return, so callers always get a Response back.
  return redirectWithErrorMessage(callerPath, request, "Error setting plan", {
    ephemeral: false,
  });
}

export async function setConcurrencyAddOn(organizationId: string, amount: number) {
  if (!client) return undefined;

  try {
    const result = await client.setAddOn(organizationId, { type: "concurrency", amount });
    if (!result.success) {
      recordPlatformFailure("setConcurrencyAddOn", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("setConcurrencyAddOn", "caught");
    return undefined;
  }
}

export async function setSeatsAddOn(organizationId: string, amount: number) {
  if (!client) return undefined;

  try {
    const result = await client.setAddOn(organizationId, { type: "seats", amount });
    if (!result.success) {
      recordPlatformFailure("setSeatsAddOn", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("setSeatsAddOn", "caught");
    return undefined;
  }
}

export async function setBranchesAddOn(organizationId: string, amount: number) {
  if (!client) return undefined;

  try {
    const result = await client.setAddOn(organizationId, { type: "branches", amount });
    if (!result.success) {
      recordPlatformFailure("setBranchesAddOn", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("setBranchesAddOn", "caught");
    return undefined;
  }
}

export async function setSchedulesAddOn(organizationId: string, amount: number) {
  if (!client) return undefined;

  try {
    const result = await client.setAddOn(organizationId, { type: "schedules", amount });
    if (!result.success) {
      recordPlatformFailure("setSchedulesAddOn", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("setSchedulesAddOn", "caught");
    return undefined;
  }
}

export async function getUsage(organizationId: string, { from, to }: { from: Date; to: Date }) {
  if (!client) return undefined;

  try {
    const result = await client.usage(organizationId, { from, to });
    if (!result.success) {
      recordPlatformFailure("getUsage", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("getUsage", "caught");
    return undefined;
  }
}

export async function getCachedUsage(
  organizationId: string,
  { from, to }: { from: Date; to: Date }
) {
  if (!client) return undefined;

  try {
    const result = await platformCache.usage.swr(
      `${organizationId}:${from.toISOString()}:${to.toISOString()}`,
      async () => {
        const usageResponse = await getUsage(organizationId, { from, to });

        return usageResponse;
      }
    );

    return result.val;
  } catch (_e) {
    recordPlatformFailure("getCachedUsage", "caught");
    return undefined;
  }
}

export async function getUsageSeries(organizationId: string, params: UsageSeriesParams) {
  if (!client) return undefined;

  try {
    const result = await client.usageSeries(organizationId, params);
    if (!result.success) {
      recordPlatformFailure("getUsageSeries", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("getUsageSeries", "caught");
    return undefined;
  }
}

export async function reportInvocationUsage(
  organizationId: string,
  costInCents: number,
  additionalData?: Record<string, any>
) {
  if (!client) return undefined;

  try {
    const result = await client.reportInvocationUsage({
      organizationId,
      costInCents,
      additionalData,
    });
    if (!result.success) {
      recordPlatformFailure("reportInvocationUsage", "no_success");
      return undefined;
    }
    return result;
  } catch (_e) {
    recordPlatformFailure("reportInvocationUsage", "caught");
    return undefined;
  }
}

export async function reportComputeUsage(request: Request) {
  if (!client) return undefined;

  return fetch(`${process.env.BILLING_API_URL}/api/v1/usage/ingest/compute`, {
    method: "POST",
    headers: request.headers,
    body: await request.text(),
  });
}

export async function getEntitlement(
  organizationId: string
): Promise<EntitlementResult | undefined> {
  if (!client) return undefined;

  // Errors must be caught inside the loader — @unkey/cache passes the loader
  // promise to waitUntil() with no .catch(), so an unhandled rejection during
  // background SWR revalidation would crash the process. Returning undefined
  // on error tells SWR not to commit a fail-open value to the cache, which
  // prevents transient billing errors from overwriting a legitimate
  // hasAccess: false entry. The fail-open default is applied *outside* the
  // SWR call so it never becomes a cached access decision.
  const result = await platformCache.entitlement.swr(organizationId, async () => {
    try {
      const response = await client.fetch(
        `/api/v1/orgs/${organizationId}/usage/entitlement`,
        asPlatformSchema(EntitlementResultSchema)
      );
      if (!response.success) {
        recordPlatformFailure("getEntitlement", "no_success");
        return undefined;
      }
      return response;
    } catch (_e) {
      recordPlatformFailure("getEntitlement", "caught");
      return undefined;
    }
  });

  if (result.err || result.val === undefined) {
    return {
      hasAccess: true as const,
    };
  }

  return result.val;
}

export type PromoCreditsData = {
  grantedCents: number;
  remainingCents: number;
  expiresAt: string | null;
};

/**
 * Remaining promo/credit-grant balance for an org, or null when it has none.
 * Billing-side gating keeps this cheap for orgs without credits; the SWR cache
 * keeps repeated dashboard loads off the network. Fails closed to null so the
 * display is simply hidden on any error — never blocks the page.
 */
export async function getPromoCredits(organizationId: string): Promise<PromoCreditsData | null> {
  if (!client) return null;

  const result = await platformCache.promoCredits.swr(organizationId, async () => {
    try {
      const response = await client.promoCredits(organizationId);
      if (!response.success) {
        recordPlatformFailure("promoCredits", "no_success");
        // Return undefined (not null) so SWR doesn't cache a transient failure
        // as "no credits" and hide the display for the stale TTL. null is
        // reserved for a successful "org has no promo credits" response.
        return undefined;
      }
      return response.promoCredits;
    } catch (_e) {
      recordPlatformFailure("promoCredits", "caught");
      logger.error("promoCredits threw", { error: _e });
      return undefined;
    }
  });

  if (result.err || result.val === undefined) {
    return null;
  }
  return result.val;
}

export async function getBillingLimit(
  organizationId: string
): Promise<BillingLimitResult | undefined> {
  if (!client) return undefined;

  // Loader callback errors are caught below; also guard the SWR read itself so
  // Redis/cache infra failures cannot reject org-layout Promise.all callers.
  try {
    const result = await platformCache.billingLimit.swr(organizationId, async () => {
      try {
        const response = await client.fetch(
          `/api/v1/orgs/${organizationId}/billing-limit`,
          asPlatformSchema(BillingLimitResultSchema)
        );
        if (!response.success) {
          recordPlatformFailure("getBillingLimit", "no_success");
          return undefined;
        }
        return response;
      } catch (_e) {
        recordPlatformFailure("getBillingLimit", "caught");
        return undefined;
      }
    });

    if (result.err || result.val === undefined) {
      return undefined;
    }

    return result.val;
  } catch (_e) {
    recordPlatformFailure("getBillingLimit", "caught");
    return undefined;
  }
}

export async function setBillingLimit(
  organizationId: string,
  config: UpdateBillingLimitRequest
): Promise<BillingLimitResult | undefined> {
  if (!client) return undefined;

  const response = await client.fetch(
    `/api/v1/orgs/${organizationId}/billing-limit`,
    asPlatformSchema(BillingLimitResultSchema),
    {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(config),
    }
  );

  if (!response.success) {
    recordPlatformFailure("setBillingLimit", "no_success");
    throw new Error(response.error ?? "Error setting billing limit");
  }

  invalidateBillingLimitCaches(organizationId);
  return response;
}

export async function resolveBillingLimit(
  organizationId: string,
  payload: ResolveBillingLimitRequest
): Promise<BillingLimitResult | undefined> {
  if (!client) return undefined;

  const response = await client.fetch(
    `/api/v1/orgs/${organizationId}/billing-limit/resolve`,
    asPlatformSchema(BillingLimitResultSchema),
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    }
  );

  if (!response.success) {
    recordPlatformFailure("resolveBillingLimit", "no_success");
    throw new Error(response.error ?? "Error resolving billing limit");
  }

  invalidateBillingLimitCaches(organizationId);
  return response;
}

/** Admin: orgs currently in grace or rejected — used by reconciliation worker (Phase 2). */
export async function getActiveBillingLimits(): Promise<BillingLimitsActiveResult | undefined> {
  if (!client) return undefined;

  try {
    const response = await client.fetch(
      `/api/v1/billing-limits/active`,
      asPlatformSchema(BillingLimitsActiveResultSchema)
    );
    if (!response.success) {
      recordPlatformFailure("getActiveBillingLimits", "no_success");
      return undefined;
    }
    return response;
  } catch (_e) {
    recordPlatformFailure("getActiveBillingLimits", "caught");
    return undefined;
  }
}

/** Admin: orgs with pending resolve side effects — used by reconciliation worker. */
export async function getPendingBillingLimitResolves(): Promise<
  BillingLimitsPendingResolvesResult | undefined
> {
  if (!client) return undefined;

  try {
    const response = await client.fetch(
      `/api/v1/billing-limits/pending-resolves`,
      asPlatformSchema(BillingLimitsPendingResolvesResultSchema)
    );
    if (!response.success) {
      recordPlatformFailure("getPendingBillingLimitResolves", "no_success");
      return undefined;
    }
    return response;
  } catch (_e) {
    recordPlatformFailure("getPendingBillingLimitResolves", "caught");
    return undefined;
  }
}

/** Admin: mark billing limit resolve side effects as completed after webapp convergence. */
export async function completeBillingLimitResolve(
  organizationId: string
): Promise<{ completed: boolean } | undefined> {
  if (!client) return undefined;

  const response = await client.fetch(
    `/api/v1/orgs/${organizationId}/billing-limit/resolve-complete`,
    asPlatformSchema(z.object({ completed: z.boolean() })),
    {
      method: "POST",
    }
  );

  if (!response.success) {
    recordPlatformFailure("completeBillingLimitResolve", "no_success");
    throw new Error(response.error ?? "Error completing billing limit resolve");
  }

  return response;
}

export async function getBillingAlerts(
  organizationId: string
): Promise<BillingAlertsResult | undefined> {
  if (!client) return undefined;
  const result = await client.getBillingAlerts(organizationId);
  if (!result.success) {
    recordPlatformFailure("getBillingAlert", "no_success");
    throw new Error("Error getting billing alert");
  }
  return result;
}

export async function setBillingAlert(
  organizationId: string,
  alert: UpdateBillingAlertsRequest
): Promise<BillingAlertsResult | undefined> {
  if (!client) return undefined;
  const result = await client.updateBillingAlerts(organizationId, alert);
  if (!result.success) {
    recordPlatformFailure("setBillingAlert", "no_success");
    throw new Error("Error setting billing alert");
  }
  return result;
}

export async function generateRegistryCredentials(
  projectId: string,
  region: "us-east-1" | "eu-central-1"
) {
  if (!client) return undefined;
  const result = await client.generateRegistryCredentials(projectId, region);
  if (!result.success) {
    recordPlatformFailure("generateRegistryCredentials", "no_success");
    throw new Error("Failed to generate registry credentials");
  }

  return result;
}

export async function enqueueBuild(
  projectId: string,
  deploymentId: string,
  artifactKey: string,
  options: {
    skipPromotion?: boolean;
    configFilePath?: string;
  }
) {
  if (!client) return undefined;
  const result = await client.enqueueBuild(projectId, { deploymentId, artifactKey, options });
  if (!result.success) {
    recordPlatformFailure("enqueueBuild", "no_success");
    throw new Error("Failed to enqueue build");
  }

  return result;
}

export async function getPrivateLinks(
  organizationId: string
): Promise<PrivateLinkConnectionList | undefined> {
  if (!client) return undefined;

  const [error, result] = await tryCatch(client.getPrivateLinks(organizationId));

  if (error) {
    recordPlatformFailure("getPrivateLinks", "caught");
    return undefined;
  }

  if (!result.success) {
    recordPlatformFailure("getPrivateLinks", "no_success");
    return undefined;
  }

  return result;
}

export async function createPrivateLink(
  organizationId: string,
  body: CreatePrivateLinkConnectionBody
): Promise<PrivateLinkConnection | undefined> {
  if (!client) throw new Error("Platform client not configured");

  const [error, result] = await tryCatch(client.createPrivateLink(organizationId, body));

  if (error) {
    recordPlatformFailure("createPrivateLink", "caught");
    throw error;
  }

  if (!result.success) {
    recordPlatformFailure("createPrivateLink", "no_success");
    throw new Error(result.error ?? "Failed to create private link");
  }

  return result;
}

export async function deletePrivateLink(
  organizationId: string,
  connectionId: string
): Promise<void> {
  if (!client) throw new Error("Platform client not configured");

  const [error, result] = await tryCatch(client.deletePrivateLink(organizationId, connectionId));

  if (error) {
    recordPlatformFailure("deletePrivateLink", "caught");
    throw error;
  }

  if (!result.success) {
    recordPlatformFailure("deletePrivateLink", "no_success");
    throw new Error(result.error ?? "Failed to delete private link");
  }
}

export async function getPrivateLinkRegions(
  organizationId: string
): Promise<PrivateLinkRegionsResult | undefined> {
  if (!client) return undefined;

  const [error, result] = await tryCatch(client.getPrivateLinkRegions(organizationId));

  if (error) {
    recordPlatformFailure("getPrivateLinkRegions", "caught");
    return undefined;
  }

  if (!result.success) {
    recordPlatformFailure("getPrivateLinkRegions", "no_success");
    return undefined;
  }

  return result;
}

export async function triggerInitialDeployment(
  projectId: string,
  options: { environment: "preview" | "prod" | "staging" }
): Promise<void> {
  if (!client) return;

  const [error, result] = await tryCatch(client.triggerInitialDeployment(projectId, options));

  if (error) {
    logger.warn("Error triggering initial deployment", {
      projectId,
      environment: options.environment,
      error,
    });
    return;
  }

  if (!result.success) {
    logger.warn("Failed to trigger initial deployment", {
      projectId,
      environment: options.environment,
      error: result.error,
    });
  }
}

export type {
  BillingLimitConfig,
  BillingLimitPageData,
  BillingLimitResult,
  BillingLimitState,
  BillingLimitsActiveResult,
  EntitlementResult,
  ResolveBillingLimitRequest,
  UpdateBillingLimitRequest,
} from "~/services/billingLimit.schemas";

export function isCloud(): boolean {
  const acceptableHosts = [
    "https://cloud.trigger.dev",
    "https://test-cloud.trigger.dev",
    "https://internal.trigger.dev",
  ];

  if (acceptableHosts.includes(env.LOGIN_ORIGIN)) {
    return true;
  }

  if (process.env.CLOUD_ENV === "development" && process.env.NODE_ENV === "development") {
    return true;
  }

  return false;
}
