import { json } from "@remix-run/server-runtime";
import { tryCatch } from "@trigger.dev/core/utils";
import { z } from "zod";
import { $replica } from "~/db.server";
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
import {
  getInputStreamWaitpoint,
  deleteInputStreamWaitpoint,
} from "~/services/inputStreamWaitpointCache.server";
import {
  anyResource,
  createActionApiRoute,
  createLoaderApiRoute,
} from "~/services/routeBuilders/apiBuilder.server";
import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server";
import { engine } from "~/v3/runEngine.server";
import { ServiceValidationError } from "~/v3/services/common.server";
import { runStore } from "~/v3/runStore.server";

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

const BodySchema = z.object({
  data: z.unknown(),
});

// POST: Send data to an input stream
const { action } = createActionApiRoute(
  {
    params: ParamsSchema,
    maxContentLength: 1024 * 1024, // 1MB max
    allowJWT: true,
    corsStrategy: "all",
    authorization: {
      action: "write",
      resource: (params) => ({ type: "inputStreams", id: params.runId }),
    },
  },
  async ({ request, params, authentication }) => {
    const run = await runStore.findRun(
      {
        friendlyId: params.runId,
        runtimeEnvironmentId: authentication.environment.id,
      },
      {
        select: {
          id: true,
          friendlyId: true,
          completedAt: true,
          realtimeStreamsVersion: true,
          streamBasinName: true,
        },
      },
      $replica
    );

    if (!run) {
      return json({ ok: false, error: "Run not found" }, { status: 404 });
    }

    if (run.completedAt) {
      return json(
        { ok: false, error: "Cannot send to input stream on a completed run" },
        { status: 400 }
      );
    }

    const body = BodySchema.safeParse(await request.json());

    if (!body.success) {
      return json({ ok: false, error: "Invalid request body" }, { status: 400 });
    }

    const realtimeStream = getRealtimeStreamInstance(
      authentication.environment,
      run.realtimeStreamsVersion,
      { run }
    );

    // Build the input stream record (raw user data, no wrapper)
    const recordId = `inp_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
    const record = JSON.stringify(body.data.data);

    // Append the record to the per-stream S2 stream (auto-creates on
    // first write). `appendPart` can throw `S2RecordTooLargeError` (a
    // `ServiceValidationError` with status 413) when the wrapped
    // record exceeds S2's per-record ceiling — surface that as 413
    // rather than letting it propagate to the apiBuilder catch-all
    // as a generic 500.
    const [appendError] = await tryCatch(
      realtimeStream.appendPart(
        record,
        recordId,
        run.friendlyId,
        `$trigger.input:${params.streamId}`
      )
    );
    if (appendError) {
      if (appendError instanceof ServiceValidationError) {
        return json(
          { ok: false, error: appendError.message },
          { status: appendError.status ?? 422 }
        );
      }
      throw appendError;
    }

    // Check Redis cache for a linked .wait() waitpoint (fast, no DB hit if none)
    // Get first, complete, then delete — so the mapping survives if completeWaitpoint throws
    const waitpointId = await getInputStreamWaitpoint(params.runId, params.streamId);
    if (waitpointId) {
      await engine.completeWaitpoint({
        id: waitpointId,
        output: {
          value: JSON.stringify(body.data.data),
          type: "application/json",
          isError: false,
        },
      });
      await deleteInputStreamWaitpoint(params.runId, params.streamId);
    }

    return json({ ok: true });
  }
);

// GET: SSE stream for reading input stream data (used by the in-task SSE tail)
const loader = createLoaderApiRoute(
  {
    params: ParamsSchema,
    allowJWT: true,
    corsStrategy: "all",
    findResource: async (params, auth) => {
      return runStore.findRun(
        {
          friendlyId: params.runId,
          runtimeEnvironmentId: auth.environment.id,
        },
        {
          include: {
            batch: {
              select: {
                friendlyId: true,
              },
            },
          },
        },
        $replica
      );
    },
    authorization: {
      action: "read",
      resource: (run) => {
        const resources = [
          { type: "runs", id: run.friendlyId },
          { type: "tasks", id: run.taskIdentifier },
          ...run.runTags.map((tag) => ({ type: "tags", id: tag })),
        ];
        if (run.batch?.friendlyId) {
          resources.push({ type: "batch", id: run.batch.friendlyId });
        }
        return anyResource(resources);
      },
    },
  },
  async ({ params, request, resource: run, authentication }) => {
    const lastEventId = request.headers.get("Last-Event-ID") || undefined;

    const timeoutInSecondsRaw = request.headers.get("Timeout-Seconds") ?? undefined;
    const timeoutInSeconds =
      timeoutInSecondsRaw !== undefined ? parseInt(timeoutInSecondsRaw, 10) : undefined;

    if (timeoutInSeconds !== undefined && isNaN(timeoutInSeconds)) {
      return new Response("Invalid timeout seconds", { status: 400 });
    }

    if (timeoutInSeconds !== undefined && timeoutInSeconds < 1) {
      return new Response("Timeout seconds must be greater than 0", { status: 400 });
    }

    if (timeoutInSeconds !== undefined && timeoutInSeconds > 600) {
      return new Response("Timeout seconds must be less than 600", { status: 400 });
    }

    const realtimeStream = getRealtimeStreamInstance(
      authentication.environment,
      run.realtimeStreamsVersion,
      { run }
    );

    // Read from the internal S2 stream name (prefixed to avoid user stream collisions)
    return realtimeStream.streamResponse(
      request,
      run.friendlyId,
      `$trigger.input:${params.streamId}`,
      getRequestAbortSignal(),
      {
        lastEventId,
        timeoutInSeconds,
      }
    );
  }
);

export { action, loader };
