import { type Span } from "@opentelemetry/api";
import { type PrismaClientOrTransaction } from "@trigger.dev/database";
import { env } from "~/env.server";
import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
import { chatSnapshotStorageKey } from "~/services/realtime/chatSnapshot.server";
import { resolveSessionByIdOrExternalId } from "~/services/realtime/sessions.server";
import { LEGACY_PLAYGROUND_TAG } from "~/services/sessionsRepository/sessionsRepository.server";
import { logger } from "~/services/logger.server";
import { generatePresignedUrl } from "~/v3/objectStore.server";
import { runStore } from "~/v3/runStore.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { startActiveSpan } from "~/v3/tracer.server";

export type SessionDetail = NonNullable<Awaited<ReturnType<SessionPresenter["call"]>>>;

export class SessionPresenter {
  constructor(private readonly replica: PrismaClientOrTransaction) {}

  public async call(args: {
    userId: string;
    environmentId: string;
    sessionParam: string;
    projectExternalRef: string;
    environmentSlug: string;
  }) {
    return startActiveSpan("SessionPresenter.call", (span) => this.#call(args, span), {
      attributes: {
        environmentId: args.environmentId,
        sessionParam: args.sessionParam,
      },
    });
  }

  async #call(
    {
      userId,
      environmentId,
      sessionParam,
      projectExternalRef,
      environmentSlug,
    }: {
      userId: string;
      environmentId: string;
      sessionParam: string;
      projectExternalRef: string;
      environmentSlug: string;
    },
    rootSpan: Span
  ) {
    const session = await startActiveSpan("SessionPresenter.resolveSession", () =>
      resolveSessionByIdOrExternalId(this.replica, environmentId, sessionParam)
    );
    if (!session) {
      rootSpan.setAttribute("session.found", false);
      return null;
    }
    rootSpan.setAttribute("session.found", true);
    rootSpan.setAttribute("session.id", session.id);

    const displayableEnvironment = await startActiveSpan(
      "SessionPresenter.findDisplayableEnvironment",
      () => findDisplayableEnvironment(environmentId, userId)
    );
    if (!displayableEnvironment) {
      throw new ServiceValidationError("No environment found");
    }

    // Run history is append-only; latest first matches the runs list.
    // 50 covers the vast majority of sessions; longer histories link out
    // to the runs page via tag filter.
    const sessionRuns = await startActiveSpan("SessionPresenter.findSessionRuns", async (span) => {
      const rows = await this.replica.sessionRun.findMany({
        where: { sessionId: session.id },
        orderBy: { triggeredAt: "desc" },
        take: 50,
        select: {
          id: true,
          runId: true,
          reason: true,
          triggeredAt: true,
        },
      });
      span.setAttribute("sessionRuns.count", rows.length);
      return rows;
    });

    const runIds = sessionRuns.map((r) => r.runId);
    const runs = await startActiveSpan("SessionPresenter.findRuns", async (span) => {
      span.setAttribute("runIds.count", runIds.length);
      return runIds.length > 0
        ? runStore.findRuns(
            {
              where: { id: { in: runIds } },
              select: { id: true, friendlyId: true, status: true },
            },
            this.replica
          )
        : [];
    });
    const runsById = new Map(runs.map((r) => [r.id, r] as const));

    const currentRun = session.currentRunId
      ? (runsById.get(session.currentRunId) ??
        (await startActiveSpan("SessionPresenter.findCurrentRunFallback", () =>
          runStore.findRun(
            { id: session.currentRunId! },
            {
              select: { id: true, friendlyId: true, status: true },
            },
            this.replica
          )
        )))
      : null;

    // The dashboard SSE route is cookie-authed, so `publicAccessToken` is
    // unused — kept here to match the existing `AgentViewAuth` shape.
    const addressingKey = session.externalId ?? session.friendlyId;

    // Presign a GET URL for the agent's S3 snapshot blob. The browser
    // fetches it directly, parses + validates, and seeds the
    // TriggerChatTransport with the full history + lastEventId before
    // opening the SSE. Presign succeeds regardless of whether the blob
    // exists; the frontend handles 404 gracefully.
    //
    // Snapshots are only written when no `hydrateMessages` hook is
    // registered — sessions that use `hydrateMessages` will 404 here
    // and the dashboard falls back to seq=0 SSE (which, post-trim,
    // shows only the most recent turn — accepted, those customers
    // have their own DB-backed dashboards).
    // Resolve the snapshot key via the SAME helper the SDK write + boot read
    // use (`chatSnapshotStorageKey`), so the dashboard GET hits the exact
    // object (and object store) the snapshot was written to. Recomputing a
    // bare key here was the bug: an unqualified key reads the base store while
    // the write applied OBJECT_STORE_DEFAULT_PROTOCOL, so they could diverge.
    let snapshotPresignedUrl: string | undefined;
    try {
      const signed = await startActiveSpan("SessionPresenter.presignSnapshot", async () =>
        generatePresignedUrl(
          projectExternalRef,
          environmentSlug,
          chatSnapshotStorageKey(session),
          "GET"
        )
      );
      if (signed.success) {
        snapshotPresignedUrl = signed.url;
      } else {
        logger.warn("SessionPresenter: snapshot presign failed", {
          sessionId: session.id,
          error: signed.error,
        });
      }
    } catch (error) {
      logger.warn("SessionPresenter: snapshot presign threw", {
        sessionId: session.id,
        error: error instanceof Error ? error.message : String(error),
      });
    }

    return {
      id: session.id,
      friendlyId: session.friendlyId,
      externalId: session.externalId,
      type: session.type,
      taskIdentifier: session.taskIdentifier,
      isTest: session.isTest,
      // Hide the legacy "playground" tag (pre-isTest sessions) from display.
      tags: session.tags
        ? [...session.tags]
            .filter((t) => t !== LEGACY_PLAYGROUND_TAG)
            .sort((a, b) => a.localeCompare(b))
        : [],
      metadata: session.metadata,
      triggerConfig: session.triggerConfig,
      streamBasinName: session.streamBasinName,
      closedAt: session.closedAt ? session.closedAt.toISOString() : undefined,
      closedReason: session.closedReason ?? undefined,
      expiresAt: session.expiresAt ? session.expiresAt.toISOString() : undefined,
      createdAt: session.createdAt.toISOString(),
      updatedAt: session.updatedAt.toISOString(),
      environment: displayableEnvironment,
      currentRun: currentRun
        ? { friendlyId: currentRun.friendlyId, status: currentRun.status }
        : null,
      runs: sessionRuns.map((r) => {
        const run = runsById.get(r.runId);
        return {
          id: r.id,
          reason: r.reason,
          triggeredAt: r.triggeredAt.toISOString(),
          run: run ? { friendlyId: run.friendlyId, status: run.status } : null,
        };
      }),
      agentView: {
        publicAccessToken: "",
        apiOrigin: env.API_ORIGIN || env.LOGIN_ORIGIN,
        sessionId: addressingKey,
        initialMessages: [],
        snapshotPresignedUrl,
      },
    };
  }
}
