import { type PrismaClient, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
import { singleton } from "~/utils/singleton";
import type { SendFunction } from "~/utils/sse";
import { ABORT_REASON_SEND_ERROR, createSSELoader } from "~/utils/sse";
import { throttle } from "~/utils/throttle";
import { getMollifierBuffer } from "~/v3/mollifier/mollifierBuffer.server";
import { deserialiseMollifierSnapshot } from "~/v3/mollifier/mollifierSnapshot.server";
import { runStore } from "~/v3/runStore.server";
import { tracePubSub } from "~/v3/services/tracePubSub.server";

const PING_INTERVAL = 5_000;
const STREAM_TIMEOUT = 30_000;

export class RunStreamPresenter {
  #prismaClient: PrismaClient;

  constructor(prismaClient: PrismaClient = prisma) {
    this.#prismaClient = prismaClient;
  }

  public createLoader() {
    const prismaClient = this.#prismaClient;

    return createSSELoader({
      timeout: STREAM_TIMEOUT,
      interval: PING_INTERVAL,
      handler: async (context) => {
        const runFriendlyId = context.params.runParam;

        if (!runFriendlyId) {
          throw new Response("Missing runParam", { status: 400 });
        }

        const userId = await requireUserId(context.request);

        // Scope the lookup to organizations the requesting user is a member
        // of, matching RunPresenter's run lookup. Unauthorized and missing
        // runs are indistinguishable (both 404).
        // Run-ops read by friendlyId only (routes to the owning DB); the project/org
        // membership auth is a control-plane concern resolved separately below — joining
        // it here is a cross-DB join that returns nothing once the run lives in run-ops.
        const run = await runStore.findRun(
          {
            friendlyId: runFriendlyId,
          },
          {
            select: {
              traceId: true,
              projectId: true,
            },
          }
        );

        // Authorize on the control-plane DB, keyed by the run's project. A non-member
        // (or unresolvable project) is treated as no-access: traceId stays null.
        let authorized = false;
        if (run) {
          const authorizedProject = await prismaClient.project.findFirst({
            where: {
              id: run.projectId,
              organization: { members: { some: { userId } } },
            },
            select: { id: true },
          });
          authorized = authorizedProject !== null;
        }

        // Fall back to the mollifier buffer when the run isn't in PG yet.
        // The buffered run has no execution events to stream, but we still
        // attach a trace-pubsub subscription using the snapshot's traceId
        // so that the moment the drainer materialises the row and execution
        // begins, those events flow to this open SSE connection. Closing
        // with 404 would force the dashboard to keep retrying.
        let traceId: string | null = run && authorized ? run.traceId : null;
        if (!traceId) {
          const buffer = getMollifierBuffer();
          if (buffer) {
            try {
              const entry = await buffer.getEntry(runFriendlyId);
              // Same membership scoping as the PG lookup above — the buffer
              // entry carries the owning org's id.
              const isMember = entry
                ? (await prismaClient.orgMember.findFirst({
                    where: { organizationId: entry.orgId, userId },
                    select: { id: true },
                  })) !== null
                : false;
              if (entry && isMember) {
                // Go through the webapp wrapper so this read-side module
                // shares a single deserialisation path with readFallback —
                // see the contract comment in syntheticRedirectInfo.server.ts.
                const snapshot = deserialiseMollifierSnapshot(entry.payload);
                if (typeof snapshot.traceId === "string") {
                  traceId = snapshot.traceId;
                }
              }
            } catch (err) {
              logger.warn("RunStreamPresenter buffer fallback failed", {
                runFriendlyId,
                err: err instanceof Error ? err.message : String(err),
              });
            }
          }
        }

        if (!traceId) {
          throw new Response("Not found", { status: 404 });
        }
        const resolvedRun = { traceId };

        logger.info("RunStreamPresenter.start", {
          runFriendlyId,
          traceId: resolvedRun.traceId,
        });

        const { unsubscribe, eventEmitter } = await tracePubSub.subscribeToTrace(
          resolvedRun.traceId
        );

        // Only send max every 1 second
        const throttledSend = throttle(
          (args: { send: SendFunction; event?: string; data: string }) => {
            try {
              args.send({ event: args.event, data: args.data });
            } catch (error) {
              if (error instanceof Error) {
                if (error.name !== "TypeError") {
                  logger.debug("Error sending SSE in RunStreamPresenter", {
                    error: {
                      name: error.name,
                      message: error.message,
                      stack: error.stack,
                    },
                  });
                }
              }
              // Abort the stream on send error. Uses a stackless string sentinel
              // from sse.ts — a no-arg abort() would create a DOMException with a
              // stack trace, which is unnecessary retention on the signal.reason.
              context.controller.abort(ABORT_REASON_SEND_ERROR);
            }
          },
          1000
        );

        let messageListener: ((event: string) => void) | undefined;

        return {
          initStream: ({ send }) => {
            throttledSend({ send, event: "message", data: new Date().toISOString() });

            messageListener = (event: string) => {
              throttledSend({ send, event: "message", data: event });
            };
            eventEmitter.addListener("message", messageListener);

            context.debug("Subscribed to trace pub/sub");
          },

          iterator: ({ send }) => {
            try {
              // Send an actual message so the client refreshes
              throttledSend({ send, event: "message", data: new Date().toISOString() });
            } catch (_error) {
              // If we can't send a ping, the connection is likely dead
              return false;
            }
          },

          cleanup: () => {
            logger.info("RunStreamPresenter.cleanup", {
              runFriendlyId,
              traceId: resolvedRun.traceId,
            });

            if (messageListener) {
              eventEmitter.removeListener("message", messageListener);
            }
            eventEmitter.removeAllListeners();

            unsubscribe()
              .then(() => {
                logger.info("RunStreamPresenter.cleanup.unsubscribe succeeded", {
                  runFriendlyId,
                  traceId: resolvedRun.traceId,
                });
              })
              .catch((error) => {
                logger.error("RunStreamPresenter.cleanup.unsubscribe failed", {
                  runFriendlyId,
                  traceId: resolvedRun.traceId,
                  error: {
                    name: error.name,
                    message: error.message,
                    stack: error.stack,
                  },
                });
              });
          },
        };
      },
    });
  }
}

export const runStreamLoader = singleton("runStreamLoader", () => {
  const presenter = new RunStreamPresenter();
  return presenter.createLoader();
});
