import { type RuntimeEnvironmentType, type ScheduleType } from "@trigger.dev/database";
import { type ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
import { getTaskIdentifiers } from "~/models/task.server";
import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { CheckScheduleService } from "~/v3/services/checkSchedule.server";
import {
  calculateNextScheduledTimestampFromNow,
  previousScheduledTimestamp,
} from "~/v3/utils/calculateNextSchedule.server";
import { BasePresenter } from "./basePresenter.server";

type ScheduleListOptions = {
  projectId: string;
  environmentId: string;
  userId?: string;
  pageSize?: number;
} & ScheduleListFilters;

const DEFAULT_PAGE_SIZE = 20;

export type ScheduleListItem = {
  id: string;
  type: ScheduleType;
  friendlyId: string;
  taskIdentifier: string;
  deduplicationKey: string | null;
  userProvidedDeduplicationKey: boolean;
  cron: string;
  cronDescription: string;
  timezone: string;
  externalId: string | null;
  nextRun: Date;
  lastRun: Date | undefined;
  active: boolean;
  environments: {
    id: string;
    type: RuntimeEnvironmentType;
    userName?: string;
    branchName?: string;
  }[];
};
export type ScheduleList = Awaited<ReturnType<ScheduleListPresenter["call"]>>;
export type ScheduleListAppliedFilters = ScheduleList["filters"];

export class ScheduleListPresenter extends BasePresenter {
  public async call({
    userId,
    projectId,
    environmentId,
    tasks,
    search,
    page,
    type,
    pageSize = DEFAULT_PAGE_SIZE,
  }: ScheduleListOptions) {
    const hasFilters =
      type !== undefined || tasks !== undefined || (search !== undefined && search !== "");

    const filterType =
      type === "declarative" ? "DECLARATIVE" : type === "imperative" ? "IMPERATIVE" : undefined;

    // Find the project scoped to the organization
    const project = await this._replica.project.findFirstOrThrow({
      select: {
        id: true,
        organizationId: true,
        environments: {
          select: {
            id: true,
            type: true,
            slug: true,
            branchName: true,
            archivedAt: true,
            orgMember: {
              select: {
                user: {
                  select: {
                    id: true,
                    name: true,
                    displayName: true,
                  },
                },
              },
            },
          },
        },
      },
      where: {
        id: projectId,
      },
    });

    const environment = project.environments.find((env) => env.id === environmentId);
    if (!environment) {
      throw new ServiceValidationError("No matching environment for project", 404);
    }

    const schedulesCount = await CheckScheduleService.getUsedSchedulesCount({
      prisma: this._replica,
      projectId,
    });

    // Two platform RPCs in parallel. We derive `limit` from `currentPlan`
    // rather than calling `getLimit` separately (which would re-issue
    // `client.currentPlan` upstream — same data fetched twice). The
    // scheduled-task route awaits this presenter synchronously, so every ms
    // here is TTFB.
    const [currentPlan, plans] = await Promise.all([
      getCurrentPlan(project.organizationId),
      getPlans(),
    ]);

    const planLimit = currentPlan?.v3Subscription?.plan?.limits.schedules?.number;
    const limit = typeof planLimit === "number" ? planLimit : 100_000_000;
    const extraSchedules = currentPlan?.v3Subscription?.addOns?.schedules?.purchased ?? 0;
    const canPurchaseSchedules =
      currentPlan?.v3Subscription?.plan?.limits.schedules.canExceed === true;
    const maxScheduleQuota = currentPlan?.v3Subscription?.addOns?.schedules?.quota ?? 0;
    const planScheduleLimit = limit - extraSchedules;
    const schedulePricing = plans?.addOnPricing.schedules ?? null;

    const purchaseInfo = {
      canPurchaseSchedules,
      extraSchedules,
      maxScheduleQuota,
      planScheduleLimit,
      schedulePricing,
    };

    //get the latest BackgroundWorker
    const latestWorker = await findCurrentWorkerFromEnvironment(environment, this._replica);
    if (!latestWorker) {
      return {
        currentPage: 1,
        totalPages: 1,
        totalCount: 0,
        schedules: [],
        possibleTasks: [],
        hasFilters,
        limits: {
          used: schedulesCount,
          limit,
        },
        ...purchaseInfo,
        filters: {
          tasks,
          search,
        },
      };
    }

    //get all possible scheduled tasks
    const allIdentifiers = await getTaskIdentifiers(environmentId);
    const possibleTasks = allIdentifiers
      .filter((t) => t.triggerSource === "SCHEDULED" && t.isInLatestDeployment)
      .map((t) => ({ slug: t.slug }));

    //do this here to protect against SQL injection
    search = search && search !== "" ? `%${search}%` : undefined;

    const totalCount = await this._replica.taskSchedule.count({
      where: {
        projectId: project.id,
        taskIdentifier: tasks ? { in: tasks } : undefined,
        instances: {
          some: {
            environmentId,
          },
        },
        type: filterType,
        AND: search
          ? {
              OR: [
                {
                  externalId: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
                {
                  friendlyId: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
                {
                  deduplicationKey: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
                {
                  generatorExpression: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
              ],
            }
          : undefined,
      },
    });

    const rawSchedules = await this._replica.taskSchedule.findMany({
      select: {
        id: true,
        type: true,
        friendlyId: true,
        taskIdentifier: true,
        deduplicationKey: true,
        userProvidedDeduplicationKey: true,
        generatorExpression: true,
        generatorDescription: true,
        timezone: true,
        externalId: true,
        instances: {
          select: {
            environmentId: true,
          },
        },
        active: true,
        createdAt: true,
        updatedAt: true,
      },
      where: {
        projectId: project.id,
        taskIdentifier: tasks ? { in: tasks } : undefined,
        instances: {
          some: {
            environmentId,
          },
        },
        type: filterType,
        AND: search
          ? {
              OR: [
                {
                  externalId: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
                {
                  friendlyId: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
                {
                  deduplicationKey: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
                {
                  generatorExpression: {
                    contains: search,
                    mode: "insensitive",
                  },
                },
              ],
            }
          : undefined,
      },
      orderBy: {
        createdAt: "desc",
      },
      take: pageSize,
      skip: (page - 1) * pageSize,
    });

    const schedules: ScheduleListItem[] = rawSchedules.map((schedule) => {
      // Approximate "last run" from the cron's previous slot. Skip inactive
      // schedules — the cron's previous slot reflects what *would* have
      // fired, but a deactivated schedule didn't actually fire there. Skip
      // when the cron's previous slot predates `updatedAt`: any config
      // change (cron edited, timezone changed, deactivate/reactivate)
      // bumps updatedAt, and a slot from before the most recent change
      // didn't fire under the current configuration. cron-parser throws
      // on malformed expressions, so degrade to undefined per-row rather
      // than failing the whole list. UI is best-effort; the runs page is
      // the source of truth.
      let lastRun: Date | undefined;
      if (schedule.active) {
        try {
          const cronPrev = previousScheduledTimestamp(
            schedule.generatorExpression,
            schedule.timezone
          );
          lastRun = cronPrev.getTime() > schedule.updatedAt.getTime() ? cronPrev : undefined;
        } catch {
          lastRun = undefined;
        }
      }

      return {
        id: schedule.id,
        type: schedule.type,
        friendlyId: schedule.friendlyId,
        taskIdentifier: schedule.taskIdentifier,
        deduplicationKey: schedule.deduplicationKey,
        userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
        cron: schedule.generatorExpression,
        cronDescription: schedule.generatorDescription,
        timezone: schedule.timezone,
        active: schedule.active,
        externalId: schedule.externalId,
        lastRun,
        nextRun: calculateNextScheduledTimestampFromNow(
          schedule.generatorExpression,
          schedule.timezone
        ),
        environments: schedule.instances.map((instance) => {
          const environment = project.environments.find((env) => env.id === instance.environmentId);
          if (!environment) {
            throw new Error(
              `Environment not found for TaskScheduleInstance env: ${instance.environmentId}`
            );
          }

          return {
            ...displayableEnvironment(environment, userId),
            branchName: environment.branchName ?? undefined,
          };
        }),
      };
    });

    return {
      currentPage: page,
      totalPages: Math.ceil(totalCount / pageSize),
      totalCount: totalCount,
      schedules,
      possibleTasks: possibleTasks.map((task) => task.slug),
      hasFilters,
      limits: {
        used: schedulesCount,
        limit,
      },
      ...purchaseInfo,
      filters: {
        tasks,
        search,
      },
    };
  }
}
