import { type Span } from "@opentelemetry/api";
import { type ClickHouse } from "@internal/clickhouse";
import { type PrismaClient, type PrismaClientOrTransaction } from "@trigger.dev/database";
import { type Direction } from "~/components/ListPagination";
import { timeFilters } from "~/components/runs/v3/SharedFilters";
import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
import {
  type SessionStatus,
  SessionsRepository,
  LEGACY_PLAYGROUND_TAG,
} from "~/services/sessionsRepository/sessionsRepository.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
import { runStore } from "~/v3/runStore.server";
import { startActiveSpan } from "~/v3/tracer.server";

export type SessionListOptions = {
  userId?: string;
  projectId: string;
  // filters
  types?: string[];
  taskIdentifiers?: string[];
  externalId?: string;
  tags?: string[];
  statuses?: SessionStatus[];
  period?: string;
  from?: number;
  to?: number;
  // pagination
  direction?: Direction;
  cursor?: string;
  pageSize?: number;
};

const DEFAULT_PAGE_SIZE = 25;

export type SessionList = Awaited<ReturnType<SessionListPresenter["call"]>>;
export type SessionListItem = SessionList["sessions"][0];
export type SessionListAppliedFilters = SessionList["filters"];

export class SessionListPresenter {
  constructor(
    private readonly replica: PrismaClientOrTransaction,
    private readonly clickhouse: ClickHouse
  ) {}

  public async call(organizationId: string, environmentId: string, options: SessionListOptions) {
    return startActiveSpan(
      "SessionListPresenter.call",
      (span) => this.#call(organizationId, environmentId, options, span),
      {
        attributes: {
          organizationId,
          environmentId,
          projectId: options.projectId,
        },
      }
    );
  }

  async #call(
    organizationId: string,
    environmentId: string,
    {
      userId,
      projectId,
      types,
      taskIdentifiers,
      externalId,
      tags,
      statuses,
      period,
      from,
      to,
      direction = "forward",
      cursor,
      pageSize = DEFAULT_PAGE_SIZE,
    }: SessionListOptions,
    rootSpan: Span
  ) {
    const time = timeFilters({ period, from, to });

    const hasFilters =
      (types !== undefined && types.length > 0) ||
      (taskIdentifiers !== undefined && taskIdentifiers.length > 0) ||
      (externalId !== undefined && externalId !== "") ||
      (tags !== undefined && tags.length > 0) ||
      (statuses !== undefined && statuses.length > 0) ||
      !time.isDefault;

    rootSpan.setAttribute("filters.hasFilters", hasFilters);
    rootSpan.setAttribute("page.size", pageSize);
    if (cursor) rootSpan.setAttribute("page.cursor", cursor);

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

    const sessionsRepository = new SessionsRepository({
      clickhouse: this.clickhouse,
      prisma: this.replica as PrismaClient,
    });

    function clampToNow(date: Date): Date {
      const now = new Date();
      return date > now ? now : date;
    }

    // Sessions are produced by chat.agent() tasks. The TaskIdentifier
    // registry has historically misclassified agent slugs as STANDARD (the
    // toTriggerSource helper didn't know about "agent" until recently), so
    // read from BackgroundWorkerTask of the current worker instead — same
    // source AgentListPresenter uses for the Agents page.
    const possibleTasksAsync = startActiveSpan(
      "SessionListPresenter.getPossibleTasks",
      async () => {
        const currentWorker = await findCurrentWorkerFromEnvironment(
          { id: environmentId, type: displayableEnvironment.type },
          this.replica
        );
        if (!currentWorker) return [];
        const agents = await this.replica.backgroundWorkerTask.findMany({
          where: { workerId: currentWorker.id, triggerSource: "AGENT" },
          select: { slug: true },
          orderBy: { slug: "asc" },
        });
        return agents.map((a) => ({ slug: a.slug, isInLatestDeployment: true }));
      }
    );

    const { sessions, pagination } = await sessionsRepository.listSessions({
      organizationId,
      projectId,
      environmentId,
      types,
      taskIdentifiers,
      externalId,
      tags,
      statuses,
      period,
      from: time.from ? time.from.getTime() : undefined,
      to: time.to ? clampToNow(time.to).getTime() : undefined,
      page: {
        size: pageSize,
        cursor,
        direction,
      },
    });

    rootSpan.setAttribute("page.count", sessions.length);

    let hasAnySessions = sessions.length > 0;
    if (!hasAnySessions) {
      const firstSession = await startActiveSpan("SessionListPresenter.hasAnySessions", () =>
        this.replica.session.findFirst({
          where: { runtimeEnvironmentId: environmentId },
          select: { id: true },
        })
      );
      if (firstSession) {
        hasAnySessions = true;
      }
    }

    // Resolve current-run friendlyIds in one query so each row can link to
    // its live run. Status is intentionally not joined yet — that lives in
    // ClickHouse and would mean a second query per page; the link itself
    // is the value most viewers want first.
    const currentRunIds = sessions
      .map((s) => s.currentRunId)
      .filter((id): id is string => Boolean(id));

    const possibleTasks = await possibleTasksAsync;

    const currentRuns = await startActiveSpan(
      "SessionListPresenter.findCurrentRuns",
      async (span) => {
        span.setAttribute("currentRunIds.count", currentRunIds.length);
        // Scope by projectId + runtimeEnvironmentId — Session.currentRunId
        // is a plain string column without an FK, so a stale or corrupted
        // pointer could surface another tenant's run. The list query above
        // is already env-scoped; the run lookup needs the same fence.
        return currentRunIds.length > 0
          ? runStore.findRuns(
              {
                where: {
                  id: { in: currentRunIds },
                  projectId,
                  runtimeEnvironmentId: environmentId,
                },
                select: { id: true, friendlyId: true },
              },
              this.replica
            )
          : [];
      }
    );
    const runById = new Map(currentRuns.map((r) => [r.id, r] as const));

    const now = Date.now();

    return {
      sessions: sessions.map((session) => {
        const status: SessionStatus =
          session.closedAt != null
            ? "CLOSED"
            : session.expiresAt != null && session.expiresAt.getTime() < now
              ? "EXPIRED"
              : "ACTIVE";

        const currentRun = session.currentRunId ? runById.get(session.currentRunId) : undefined;

        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))
            : [],
          status,
          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,
          currentRunFriendlyId: currentRun?.friendlyId,
        };
      }),
      pagination: {
        next: pagination.nextCursor ?? undefined,
        previous: pagination.previousCursor ?? undefined,
      },
      filters: {
        types: types ?? [],
        taskIdentifiers: taskIdentifiers ?? [],
        externalId,
        tags: tags ?? [],
        statuses: statuses ?? [],
        from: time.from,
        to: time.to,
      },
      possibleTasks,
      hasFilters,
      hasAnySessions,
    };
  }
}
