import { EngineServiceValidationError } from "@internal/run-engine";
import { json } from "@remix-run/server-runtime";
import {
  generateJWT as internal_generateJWT,
  RunEngineVersionSchema,
  TriggerTaskRequestBody,
} from "@trigger.dev/core/v3";
import type { TaskRun } from "@trigger.dev/database";
import { z } from "zod";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import type { ApiAuthenticationResultSuccess } from "~/services/apiAuth.server";
import { getOneTimeUseToken } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server";
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { resolveIdempotencyKeyTTL } from "~/utils/idempotencyKeys.server";
import {
  handleRequestIdempotency,
  saveRequestIdempotency,
} from "~/utils/requestIdempotency.server";
import { sanitizeTriggerSource } from "~/utils/triggerSource";
import { runStore } from "~/v3/runStore.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { OutOfEntitlementError, TriggerTaskService } from "~/v3/services/triggerTask.server";

const ParamsSchema = z.object({
  taskId: z.string(),
});

export const HeadersSchema = z.object({
  "idempotency-key": z
    .string()
    .max(2048, "idempotency-key must be 2048 characters or less")
    .nullish(),
  "idempotency-key-ttl": z.string().nullish(),
  "trigger-version": z.string().nullish(),
  "x-trigger-span-parent-as-link": z.coerce.number().nullish(),
  "x-trigger-worker": z.string().nullish(),
  "x-trigger-client": z.string().nullish(),
  "x-trigger-engine-version": RunEngineVersionSchema.nullish(),
  "x-trigger-request-idempotency-key": z.string().nullish(),
  "x-trigger-realtime-streams-version": z.string().nullish(),
  "x-trigger-source": z.string().nullish(),
  traceparent: z.string().optional(),
  tracestate: z.string().optional(),
});

const { action, loader } = createActionApiRoute(
  {
    headers: HeadersSchema,
    params: ParamsSchema,
    body: TriggerTaskRequestBody,
    allowJWT: true,
    maxContentLength: env.TASK_PAYLOAD_MAXIMUM_SIZE,
    authorization: {
      action: "trigger",
      resource: (params) => ({ type: "tasks", id: params.taskId }),
    },
    corsStrategy: "all",
  },
  async ({ body, headers, params, authentication }) => {
    const {
      "idempotency-key": idempotencyKey,
      "idempotency-key-ttl": idempotencyKeyTTL,
      "trigger-version": triggerVersion,
      "x-trigger-span-parent-as-link": spanParentAsLink,
      traceparent,
      tracestate,
      "x-trigger-worker": isFromWorker,
      "x-trigger-client": _triggerClient,
      "x-trigger-engine-version": engineVersion,
      "x-trigger-request-idempotency-key": requestIdempotencyKey,
      "x-trigger-realtime-streams-version": realtimeStreamsVersion,
      "x-trigger-source": triggerSourceHeader,
    } = headers;

    const cachedResponse = await handleRequestIdempotency(requestIdempotencyKey, {
      requestType: "trigger",
      findCachedEntity: async (cachedRequestId) => {
        return await runStore.findRun(
          {
            id: cachedRequestId,
          },
          {
            select: {
              friendlyId: true,
            },
          },
          prisma
        );
      },
      buildResponse: (cachedRun) => ({
        id: cachedRun.friendlyId,
        isCached: false,
      }),
      buildResponseHeaders: async (responseBody, cachedEntity) => {
        return await responseHeaders(cachedEntity, authentication);
      },
    });

    if (cachedResponse) {
      return cachedResponse;
    }

    const service = new TriggerTaskService();

    try {
      const traceContext = isFromWorker
        ? { traceparent, tracestate }
        : { external: { traceparent, tracestate } };

      const oneTimeUseToken = await getOneTimeUseToken(authentication);

      const idempotencyKeyExpiresAt = resolveIdempotencyKeyTTL(idempotencyKeyTTL);

      const result = await service.call(
        params.taskId,
        authentication.environment,
        body,
        {
          idempotencyKey: idempotencyKey ?? undefined,
          idempotencyKeyExpiresAt: idempotencyKeyExpiresAt,
          triggerVersion: triggerVersion ?? undefined,
          traceContext,
          spanParentAsLink: spanParentAsLink === 1,
          oneTimeUseToken,
          realtimeStreamsVersion: determineRealtimeStreamsVersion(
            realtimeStreamsVersion ?? undefined
          ),
          triggerSource: isFromWorker
            ? "sdk"
            : (sanitizeTriggerSource(triggerSourceHeader) ?? "api"),
          triggerAction: "trigger",
        },
        engineVersion ?? undefined
      );

      if (!result) {
        return json({ error: "Task not found" }, { status: 404 });
      }

      // Skip request-idempotency caching when the gate diverted to the
      // mollifier buffer. `result.run.id` is a synthesised cuid with no
      // corresponding PG row, so a lost-response SDK retry that reaches
      // `handleRequestIdempotency` would lookup that id, miss in PG, and
      // fall through to a fresh trigger — producing a duplicate buffer
      // entry for triggers without a task-level idempotency key (the
      // task-level path still dedupes via the buffer's SETNX in
      // `findBufferedRunWithIdempotency`). Accepting the retry-as-fresh-
      // trigger semantics here is bounded by the drainer's eventual
      // materialisation: once the run lands in PG, normal request-
      // idempotency from that point forward works as usual.
      if (!result.isMollified) {
        await saveRequestIdempotency(requestIdempotencyKey, "trigger", result.run.id);
      }

      const $responseHeaders = await responseHeaders(result.run, authentication);

      return json(
        {
          id: result.run.friendlyId,
          isCached: result.isCached,
        },
        {
          headers: $responseHeaders,
          status: 200,
        }
      );
    } catch (error) {
      if (error instanceof ServiceValidationError) {
        return json({ error: error.message }, { status: error.status ?? 422 });
      } else if (error instanceof EngineServiceValidationError) {
        return json({ error: error.message }, { status: error.status ?? 422 });
      } else if (error instanceof OutOfEntitlementError) {
        return json({ error: error.message }, { status: 422 });
      }

      logger.error("Trigger task failed", { error });
      return json({ error: "Something went wrong" }, { status: 500 });
    }
  }
);

async function responseHeaders(
  run: Pick<TaskRun, "friendlyId">,
  authentication: ApiAuthenticationResultSuccess
): Promise<Record<string, string>> {
  const { environment, realtime } = authentication;

  const claims = {
    sub: environment.id,
    pub: true,
    scopes: [`read:runs:${run.friendlyId}`, `write:inputStreams:${run.friendlyId}`],
    realtime,
  };

  const jwt = await internal_generateJWT({
    secretKey: extractJwtSigningSecretKey(environment),
    payload: claims,
    expirationTime: "1h",
  });

  return {
    "x-trigger-jwt-claims": JSON.stringify(claims),
    "x-trigger-jwt": jwt,
  };
}

export { action, loader };
