import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { $replica } from "~/db.server";
import { runStore } from "~/v3/runStore.server";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
import { S2RealtimeStreams } from "~/services/realtime/s2realtimeStreams.server";
import {
  canonicalSessionAddressingKey,
  resolveSessionByIdOrExternalId,
} from "~/services/realtime/sessions.server";
import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server";
import { requireUserId } from "~/services/session.server";
import { EnvironmentParamSchema } from "~/utils/pathBuilder";

const ParamsSchema = z.object({
  runParam: z.string(),
  sessionId: z.string(),
  io: z.enum(["out", "in"]),
});

// GET: SSE stream subscription for a backing Session's `.out` / `.in`
// channel. Dashboard-auth counterpart to the public API's
// `/realtime/v1/sessions/:sessionId/:io` endpoint. Used by the Agent tab
// in the span inspector to observe assistant chunks (`.out`) and
// user-side ChatInputChunk payloads (`.in`) for a chat.agent run.
//
// The `:sessionId` segment accepts either the `session_*` friendlyId or
// the externalId the transport registered for the chat (typically the
// browser's `chatId`). Runs pre-dating the Sessions migration that have
// `chatId` but no `sessionId` in the payload take the externalId path.
//
// Authenticated by the dashboard session — the user must have access to
// the project, environment, and run. The run binds this resource
// hierarchy; the session identity is verified against the environment.
export async function loader({ request, params }: LoaderFunctionArgs) {
  const userId = await requireUserId(request);
  const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
  const { runParam, sessionId, io } = ParamsSchema.parse(params);

  const project = await findProjectBySlug(organizationSlug, projectParam, userId);
  if (!project) {
    return new Response("Project not found", { status: 404 });
  }

  const environment = await findEnvironmentBySlug(project.id, envParam, userId);
  if (!environment) {
    return new Response("Environment not found", { status: 404 });
  }

  // Verify the run lives in this environment — keeps callers from
  // subscribing to arbitrary sessions via `/runs/$runParam/...`.
  const run = await runStore.findRun(
    {
      friendlyId: runParam,
      runtimeEnvironmentId: environment.id,
    },
    {
      select: { id: true, friendlyId: true },
    },
    $replica
  );

  if (!run) {
    return new Response("Run not found", { status: 404 });
  }

  const session = await resolveSessionByIdOrExternalId($replica, environment.id, sessionId);

  if (!session) {
    return new Response("Session not found", { status: 404 });
  }

  // Enforce run ↔ session linkage. Without this, knowledge of a runId in
  // this environment is enough to subscribe to any session in the same
  // environment — defeats the point of scoping subscriptions through the
  // run route. SessionRun.runId is indexed (@unique), so this is cheap.
  const linkedSessionRun = await $replica.sessionRun.findFirst({
    where: { runId: run.id, sessionId: session.id },
    select: { id: true },
  });

  if (!linkedSessionRun) {
    return new Response("Session not found for run", { status: 404 });
  }

  const realtimeStream = getRealtimeStreamInstance(environment, "v2", { session });

  if (!(realtimeStream instanceof S2RealtimeStreams)) {
    return new Response("Session channels require the S2 realtime backend", {
      status: 501,
    });
  }

  const lastEventId = request.headers.get("Last-Event-ID") || undefined;
  const timeoutInSecondsRaw = request.headers.get("Timeout-Seconds");
  let timeoutInSeconds: number | undefined;
  if (timeoutInSecondsRaw !== null) {
    timeoutInSeconds = Number(timeoutInSecondsRaw);
    if (!Number.isInteger(timeoutInSeconds) || timeoutInSeconds < 1 || timeoutInSeconds > 600) {
      return new Response("Invalid timeout", { status: 400 });
    }
  }

  // The agent writes via the canonical addressing key (externalId if
  // set, else friendlyId). Subscribe with the same key so the read
  // hits the same S2 stream the agent is writing into.
  const addressingKey = canonicalSessionAddressingKey(session, sessionId);

  return realtimeStream.streamResponseFromSessionStream(
    request,
    addressingKey,
    io,
    getRequestAbortSignal(),
    { lastEventId, timeoutInSeconds }
  );
}
