import { ScheduleEngine } from "@internal/schedule-engine";
import type { TriggerScheduledTaskErrorType } from "@internal/schedule-engine";
import { stringifyIO } from "@trigger.dev/core/v3";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { devPresence } from "~/presenters/v3/DevPresence.server";
import { logger } from "~/services/logger.server";
import { singleton } from "~/utils/singleton";
import { OutOfEntitlementError, TriggerTaskService } from "./services/triggerTask.server";
import { meter, tracer } from "./tracer.server";
import { workerQueue } from "~/services/worker.server";
import { ServiceValidationError } from "./services/common.server";
import { isV3Disabled } from "./engineDeprecation.server";

export const scheduleEngine = singleton("ScheduleEngine", createScheduleEngine);

export type { ScheduleEngine };

async function isDevEnvironmentConnectedHandler(environmentId: string) {
  const environment = await prisma.runtimeEnvironment.findFirst({
    where: {
      id: environmentId,
    },
    select: {
      currentSession: {
        select: {
          disconnectedAt: true,
        },
      },
      project: {
        select: {
          engine: true,
        },
      },
    },
  });

  if (!environment) {
    return false;
  }

  if (environment.project.engine === "V1") {
    const v3Disconnected = !environment.currentSession || environment.currentSession.disconnectedAt;

    return !v3Disconnected;
  }

  const v4Connected = await devPresence.isConnected(environmentId);

  return v4Connected;
}

function createScheduleEngine() {
  const engine = new ScheduleEngine({
    prisma,
    logLevel: env.SCHEDULE_ENGINE_LOG_LEVEL,
    redis: {
      host: env.SCHEDULE_WORKER_REDIS_HOST ?? "localhost",
      port: env.SCHEDULE_WORKER_REDIS_PORT ?? 6379,
      username: env.SCHEDULE_WORKER_REDIS_USERNAME,
      password: env.SCHEDULE_WORKER_REDIS_PASSWORD,
      keyPrefix: "schedule:",
      enableAutoPipelining: true,
      ...(env.SCHEDULE_WORKER_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
    },
    worker: {
      concurrency: env.SCHEDULE_WORKER_CONCURRENCY_LIMIT,
      workers: env.SCHEDULE_WORKER_CONCURRENCY_WORKERS,
      tasksPerWorker: env.SCHEDULE_WORKER_CONCURRENCY_TASKS_PER_WORKER,
      pollIntervalMs: env.SCHEDULE_WORKER_POLL_INTERVAL,
      shutdownTimeoutMs: env.SCHEDULE_WORKER_SHUTDOWN_TIMEOUT_MS,
      disabled: env.SCHEDULE_WORKER_ENABLED === "0",
    },
    distributionWindow: {
      seconds: env.SCHEDULE_WORKER_DISTRIBUTION_WINDOW_SECONDS,
    },
    tracer,
    meter,
    onTriggerScheduledTask: async ({
      taskIdentifier,
      environment,
      payload,
      scheduleInstanceId,
      scheduleId,
      exactScheduleTime,
    }) => {
      try {
        // v3 (engine V1) shutdown: skip firing schedules for V1 projects so the
        // cron doesn't keep doing trigger work just to be rejected. Return success
        // so the schedule engine treats it as handled and doesn't retry. v4 is
        // unaffected.
        if (isV3Disabled() && environment.project.engine === "V1") {
          logger.debug("[ScheduleEngine] Skipping scheduled fire for shut-down v3 project", {
            taskIdentifier,
            scheduleId,
          });
          return { success: true };
        }

        // This will trigger either v1 or v2 depending on the engine of the project
        const triggerService = new TriggerTaskService();

        const payloadPacket = await stringifyIO(payload);

        logger.debug("Triggering scheduled task", {
          taskIdentifier,
          environment,
          payload,
          scheduleInstanceId,
          scheduleId,
          exactScheduleTime,
        });

        const result = await triggerService.call(
          taskIdentifier,
          environment,
          { payload: payloadPacket.data, options: { payloadType: payloadPacket.dataType } },
          {
            customIcon: "scheduled",
            scheduleId,
            scheduleInstanceId,
            queueTimestamp: exactScheduleTime,
            overrideCreatedAt: exactScheduleTime,
            triggerSource: "schedule",
            triggerAction: "trigger",
          }
        );

        return { success: !!result };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        let errorType: TriggerScheduledTaskErrorType = "SYSTEM_ERROR";

        if (
          error instanceof ServiceValidationError &&
          errorMessage.includes("queue size limit for this environment has been reached")
        ) {
          errorType = "QUEUE_LIMIT";
        } else if (error instanceof OutOfEntitlementError) {
          // The org is out of entitlements. This is an expected outcome, not a
          // system error, so the engine logs it as a warning rather than
          // reporting it as an error.
          errorType = "OUT_OF_ENTITLEMENTS";
        }

        return {
          success: false,
          error: errorMessage,
          errorType,
        };
      }
    },
    isDevEnvironmentConnectedHandler: isDevEnvironmentConnectedHandler,
    onRegisterScheduleInstance: removeDeprecatedWorkerQueueItem,
  });

  return engine;
}

async function removeDeprecatedWorkerQueueItem(instanceId: string) {
  // We need to dequeue the instance from the existing workerQueue
  try {
    await workerQueue.dequeue(`scheduled-task-instance:${instanceId}`);

    logger.debug("Removed deprecated worker queue item", {
      instanceId,
    });
  } catch (error) {
    logger.error("Error dequeuing scheduled task instance from deprecated queue", {
      instanceId,
      error: error instanceof Error ? error.message : String(error),
    });
  }
}
