"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.scheduleJobsService = exports.ScheduleJobsService = void 0;
const drizzle_orm_1 = require("drizzle-orm");
const uuid_1 = require("uuid");
const dayjs_1 = __importDefault(require("dayjs"));
const utc_1 = __importDefault(require("dayjs/plugin/utc"));
const timezone_1 = __importDefault(require("dayjs/plugin/timezone"));
const constants_1 = require("../../constants");
const database_1 = require("../../database");
const tables_1 = require("../../database/tables");
const logger_1 = __importDefault(require("../../logger"));
const scheduleJobRepeatOnDays_1 = require("../../util/scheduleJobRepeatOnDays");
dayjs_1.default.extend(utc_1.default);
dayjs_1.default.extend(timezone_1.default);
const TIMEZONE_ABBR_TO_IANA = [
    { abbr: 'PST', iana: 'America/Los_Angeles' },
    { abbr: 'MST', iana: 'America/Denver' },
    { abbr: 'CST', iana: 'America/Chicago' },
    { abbr: 'EST', iana: 'America/New_York' },
    { abbr: 'BRT', iana: 'America/Sao_Paulo' },
    { abbr: 'GMT/UTC', iana: 'UTC' },
    { abbr: 'CET', iana: 'Europe/Paris' },
    { abbr: 'EET', iana: 'Europe/Athens' },
    { abbr: 'MSK', iana: 'Europe/Moscow' },
    { abbr: 'IST', iana: 'Asia/Kolkata' },
    { abbr: 'ICT', iana: 'Asia/Bangkok' },
    { abbr: 'CST', iana: 'Asia/Shanghai' },
    { abbr: 'SGT', iana: 'Asia/Singapore' },
    { abbr: 'JST', iana: 'Asia/Tokyo' },
    { abbr: 'AEST', iana: 'Australia/Sydney' },
    { abbr: 'NZST', iana: 'Pacific/Auckland' },
];
function resolveIanaTimeZone(tz) {
    if (!tz) {
        return 'UTC';
    }
    const mapped = TIMEZONE_ABBR_TO_IANA.find((t) => t.abbr === tz)?.iana;
    if (mapped) {
        return mapped;
    }
    // Backwards compatibility: if someone already stored an IANA tz
    if (tz.includes('/')) {
        return tz;
    }
    return 'UTC';
}
function parseHHmm(value) {
    if (!value) {
        return null;
    }
    const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value);
    if (!match) {
        return null;
    }
    return { hour: Number(match[1]), minute: Number(match[2]) };
}
function normalizeRepeatOnDays(v) {
    try {
        return (0, scheduleJobRepeatOnDays_1.sanitizeRepeatOnDaysForStorage)(v) ?? [];
    }
    catch {
        return [];
    }
}
/**
 * Calculate the next run timestamp (UTC) using runTime + timeZone (+ repeatOnDays for weekly).
 * Falls back to legacy calculation when runTime/timeZone are not provided.
 */
function calculateNextRunAtUtcFromSchedule(args) {
    const repeatEvery = Number(args.repeatEvery) > 0 ? Number(args.repeatEvery) : 1;
    const repeatUnit = args.repeatUnit;
    const iana = resolveIanaTimeZone(args.timeZone);
    const time = parseHHmm(args.runTime) ?? { hour: 9, minute: 0 };
    const repeatOnDays = normalizeRepeatOnDays(args.repeatOnDays);
    const fromLocal = (0, dayjs_1.default)(args.fromUtc).tz(iana);
    // Use the date part of startDate in the job's timezone as the anchor "start day".
    const startLocal = (0, dayjs_1.default)(args.startDate).tz(iana).startOf('day');
    const makeCandidate = (d) => d
        .tz(iana)
        .set('hour', time.hour)
        .set('minute', time.minute)
        .set('second', 0)
        .set('millisecond', 0);
    const isAfterStart = (d) => d.isSame(startLocal, 'day') || d.isAfter(startLocal, 'day');
    // Helper: check endsDate (when endsOn === 'on') in local terms
    const isPastEnd = (candidateLocal) => {
        if (args.endsOn !== 'on' || !args.endsDate) {
            return false;
        }
        const endsLocal = (0, dayjs_1.default)(args.endsDate).tz(iana);
        return candidateLocal.isAfter(endsLocal);
    };
    // DAY: every N days on runTime
    if (repeatUnit === 'day') {
        // First occurrence on/after start day
        let candidate = makeCandidate(isAfterStart(fromLocal) ? fromLocal.startOf('day') : startLocal);
        if (candidate.isBefore(fromLocal)) {
            // advance by intervals until >= fromLocal
            const daysDiff = fromLocal.startOf('day').diff(candidate.startOf('day'), 'day');
            const intervalsPassed = Math.floor(daysDiff / repeatEvery) + 1;
            candidate = candidate.add(intervalsPassed * repeatEvery, 'day');
        }
        if (isPastEnd(candidate)) {
            return null;
        }
        return candidate.utc().toDate();
    }
    // MONTH: every N months on the startDate's day-of-month (clamped by dayjs)
    if (repeatUnit === 'month') {
        const dayOfMonth = startLocal.date();
        let candidate = makeCandidate(fromLocal.startOf('day')).set('date', dayOfMonth);
        if (candidate.isBefore(startLocal)) {
            candidate = makeCandidate(startLocal).set('date', dayOfMonth);
        }
        if (candidate.isBefore(fromLocal)) {
            const monthsDiff = candidate.startOf('month').diff(fromLocal.startOf('month'), 'month');
            // monthsDiff is negative or zero; compute forward steps:
            const absDiff = Math.abs(monthsDiff);
            const intervalsPassed = Math.floor(absDiff / repeatEvery) + 1;
            candidate = candidate.add(intervalsPassed * repeatEvery, 'month').set('date', dayOfMonth);
        }
        if (isPastEnd(candidate)) {
            return null;
        }
        return candidate.utc().toDate();
    }
    // WEEK: every N weeks, optionally only on selected weekdays
    if (repeatUnit === 'week') {
        const startWeek = startLocal.startOf('week'); // dayjs week starts Sunday by default
        let cursor = makeCandidate(fromLocal.startOf('day'));
        if (cursor.isBefore(startLocal)) {
            cursor = makeCandidate(startLocal);
        }
        const isAllowedWeek = (d) => {
            const weekDiff = d.startOf('week').diff(startWeek, 'week');
            return weekDiff >= 0 && weekDiff % repeatEvery === 0;
        };
        const isAllowedDay = (d) => {
            if (repeatOnDays.length === 0) {
                return true;
            }
            return repeatOnDays.includes(d.day()); // 0..6 Sunday..Saturday
        };
        // Search forward up to 3 * repeatEvery weeks worth of days to find next match.
        // This is cheap since cron runs once per minute/day and schedules are limited.
        const maxDays = Math.max(21, repeatEvery * 7 * 3);
        for (let i = 0; i <= maxDays; i++) {
            const candidate = cursor.add(i, 'day');
            if (!isAllowedWeek(candidate) || !isAllowedDay(candidate)) {
                continue;
            }
            const candidateAtTime = makeCandidate(candidate);
            if (candidateAtTime.isBefore(fromLocal)) {
                continue;
            }
            if (isPastEnd(candidateAtTime)) {
                return null;
            }
            return candidateAtTime.utc().toDate();
        }
        // If we failed to find in window, pick the next allowed week and day as a fallback
        let candidate = cursor.startOf('week');
        while (!isAllowedWeek(candidate)) {
            candidate = candidate.add(1, 'week');
        }
        candidate = makeCandidate(candidate);
        if (isPastEnd(candidate)) {
            return null;
        }
        return candidate.utc().toDate();
    }
    // Unknown unit: no schedule
    return null;
}
class ScheduleJobsService {
    /**
     * Compute nextRunAt for an existing DB row.
     * This is used by cron fallback paths (no testRunId) and internal recalculations.
     */
    computeNextRunAtForJob(job, fromUtc) {
        if (!job.startDate || !job.repeatEvery || !job.repeatUnit) {
            return null;
        }
        return calculateNextRunAtUtcFromSchedule({
            startDate: job.startDate,
            repeatEvery: job.repeatEvery,
            repeatUnit: job.repeatUnit,
            repeatOnDays: job.repeatOnDays,
            runTime: job.runTime ?? null,
            timeZone: job.timeZone ?? null,
            fromUtc,
            endsOn: job.endsOn ?? null,
            endsDate: job.endsDate ?? null,
        });
    }
    /**
     * Mark active schedule jobs as completed when their endsDate is in the past.
     */
    async markEndedScheduleJobsAsCompleted() {
        try {
            const db = await (0, database_1.getDb)();
            const now = new Date();
            const updatedJobs = await db
                .update(tables_1.scheduleJobsTable)
                .set({
                status: constants_1.SCHEDULE_JOB_STATUS.COMPLETED,
                nextRunAt: null,
                updatedAt: now,
            })
                .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.status, constants_1.SCHEDULE_JOB_STATUS.ACTIVE), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.is_archived, false), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.endsOn, 'on'), (0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.endsDate} IS NOT NULL`, (0, drizzle_orm_1.lt)(tables_1.scheduleJobsTable.endsDate, now)))
                .returning({ id: tables_1.scheduleJobsTable.id });
            if (updatedJobs.length > 0) {
                logger_1.default.info(`Marked ${updatedJobs.length} schedule job(s) as COMPLETED because endsDate is in the past`);
            }
            return updatedJobs.length;
        }
        catch (error) {
            logger_1.default.error(`Error marking ended schedule jobs as completed: ${error}`);
            throw error;
        }
    }
    /**
     * Calculate next run time based on schedule
     */
    calculateNextRun(startDate, repeatEvery, repeatUnit, lastRun) {
        const now = new Date();
        // Ensure baseDate is a proper Date object
        const baseDate = lastRun ? new Date(lastRun) : new Date(startDate);
        // Create a new date from baseDate to avoid mutating the original
        let nextRun = new Date(baseDate.getTime());
        // Calculate the next run by adding the repeat interval
        switch (repeatUnit) {
            case 'day':
                // Use setTime to add milliseconds instead of setDate to avoid month overflow issues
                nextRun.setTime(nextRun.getTime() + repeatEvery * 24 * 60 * 60 * 1000);
                break;
            case 'week':
                nextRun.setTime(nextRun.getTime() + repeatEvery * 7 * 24 * 60 * 60 * 1000);
                break;
            case 'month':
                // For months, use setMonth which handles month boundaries correctly
                const currentMonth = nextRun.getMonth();
                nextRun.setMonth(currentMonth + repeatEvery);
                // If the month overflowed (e.g., Jan 31 + 1 month = March 3), adjust
                if (nextRun.getMonth() !== (currentMonth + repeatEvery) % 12) {
                    nextRun.setDate(0); // Set to last day of previous month
                }
                break;
            default:
                return null;
        }
        // If next run is in the past, calculate the next valid run time
        if (nextRun <= now) {
            // Calculate how many intervals have passed since baseDate
            const timeDiff = now.getTime() - baseDate.getTime();
            let intervalsPassed = 0;
            switch (repeatUnit) {
                case 'day':
                    intervalsPassed = Math.floor(timeDiff / (repeatEvery * 24 * 60 * 60 * 1000));
                    break;
                case 'week':
                    intervalsPassed = Math.floor(timeDiff / (repeatEvery * 7 * 24 * 60 * 60 * 1000));
                    break;
                case 'month':
                    // For months, calculate based on the actual month difference
                    const baseYear = baseDate.getFullYear();
                    const baseMonth = baseDate.getMonth();
                    const nowYear = now.getFullYear();
                    const nowMonth = now.getMonth();
                    const monthDiff = (nowYear - baseYear) * 12 + (nowMonth - baseMonth);
                    intervalsPassed = Math.floor(monthDiff / repeatEvery);
                    break;
            }
            // Calculate next run by adding (intervalsPassed + 1) intervals to baseDate
            nextRun = new Date(baseDate.getTime());
            switch (repeatUnit) {
                case 'day':
                    nextRun.setTime(nextRun.getTime() + (intervalsPassed + 1) * repeatEvery * 24 * 60 * 60 * 1000);
                    break;
                case 'week':
                    nextRun.setTime(nextRun.getTime() + (intervalsPassed + 1) * repeatEvery * 7 * 24 * 60 * 60 * 1000);
                    break;
                case 'month':
                    const baseMonth = nextRun.getMonth();
                    nextRun.setMonth(baseMonth + (intervalsPassed + 1) * repeatEvery);
                    // Handle month overflow
                    if (nextRun.getMonth() !== (baseMonth + (intervalsPassed + 1) * repeatEvery) % 12) {
                        nextRun.setDate(0);
                    }
                    break;
            }
        }
        return nextRun;
    }
    /**
     * Create a new schedule job
     */
    async createScheduleJob(params) {
        try {
            const db = await (0, database_1.getDb)();
            const id = (0, uuid_1.v4)();
            const startDate = new Date(params.startDate);
            const now = new Date();
            // Calculate nextRunAt using time + timezone when provided; otherwise fall back to legacy behavior.
            let nextRunAt = calculateNextRunAtUtcFromSchedule({
                startDate,
                repeatEvery: params.repeatEvery,
                repeatUnit: params.repeatUnit,
                repeatOnDays: params.repeatOnDays,
                runTime: params.runTime ?? null,
                timeZone: params.timeZone ?? null,
                fromUtc: now,
                endsOn: params.endsOn,
                endsDate: params.endsDate ? new Date(params.endsDate) : null,
            });
            if (!nextRunAt) {
                // Legacy fallback: run at startDate instant
                nextRunAt = startDate;
            }
            // Then, adjust based on end conditions
            if (params.endsOn === 'on' && params.endsDate) {
                const endsDate = new Date(params.endsDate);
                // If the calculated next run exceeds the end date, set it to null
                if (nextRunAt && nextRunAt > endsDate) {
                    nextRunAt = null;
                }
            }
            if (params.endsOn === 'after' && params.endsAfter !== undefined && params.endsAfter <= 1) {
                // Only one run allowed (at startDate), so no next run
                nextRunAt = null;
            }
            const insertValues = {
                id,
                type: params.type,
                status: constants_1.SCHEDULE_JOB_STATUS.ACTIVE,
                projectId: params.projectId,
                startDate,
                runTime: params.runTime ?? null,
                timeZone: params.timeZone ?? null,
                repeatEvery: params.repeatEvery,
                repeatUnit: params.repeatUnit,
                repeatOnDays: (0, scheduleJobRepeatOnDays_1.sanitizeRepeatOnDaysForStorage)(params.repeatOnDays),
                endsOn: params.endsOn,
                nextRunAt,
                is_archived: false,
            };
            // Only include optional fields if they have values
            if (params.name !== undefined && params.name !== null && params.name !== '') {
                insertValues.name = params.name;
            }
            if (params.workspaceId !== undefined && params.workspaceId !== null) {
                insertValues.workspaceId = params.workspaceId;
            }
            if (params.applicationId !== undefined && params.applicationId !== null) {
                insertValues.applicationId = params.applicationId;
            }
            if (params.libraryId !== undefined && params.libraryId !== null) {
                insertValues.libraryId = params.libraryId;
            }
            if (params.securityConfigId !== undefined && params.securityConfigId !== null) {
                insertValues.securityConfigId = params.securityConfigId;
            }
            if (params.endsDate !== undefined && params.endsDate !== null) {
                insertValues.endsDate = new Date(params.endsDate);
            }
            if (params.endsAfter !== undefined && params.endsAfter !== null) {
                insertValues.endsAfter = params.endsAfter;
            }
            // Security eval: application name lives on security config `config.target.label`; resolve to `application_id`.
            if (params.type === constants_1.SCHEDULE_JOB_EVAL_TYPE.SECURITY_EVAL &&
                params.securityConfigId) {
                const [securityRow] = await db
                    .select({ config: tables_1.securityConfigTable.config })
                    .from(tables_1.securityConfigTable)
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.securityConfigTable.id, params.securityConfigId), (0, drizzle_orm_1.eq)(tables_1.securityConfigTable.is_archived, false)))
                    .limit(1);
                const config = securityRow?.config;
                const applicationName = typeof config?.target?.label === 'string' ? config.target.label : '';
                if (applicationName) {
                    const applicationConditions = [
                        (0, drizzle_orm_1.eq)(tables_1.applicationsTable.name, applicationName),
                        (0, drizzle_orm_1.eq)(tables_1.applicationsTable.projectId, params.projectId),
                        (0, drizzle_orm_1.eq)(tables_1.applicationsTable.is_archived, false),
                    ];
                    const [application] = await db
                        .select({ id: tables_1.applicationsTable.id })
                        .from(tables_1.applicationsTable)
                        .where((0, drizzle_orm_1.and)(...applicationConditions))
                        .limit(1);
                    if (application) {
                        insertValues.applicationId = application.id;
                    }
                    else {
                        logger_1.default.warn(`createScheduleJob: security config ${params.securityConfigId} target.label "${applicationName}" did not match a non-archived application in project ${params.projectId}`);
                    }
                }
            }
            const [scheduleJob] = await db.insert(tables_1.scheduleJobsTable).values(insertValues).returning();
            logger_1.default.info(`Created schedule job ${id} for project ${params.projectId}`);
            return scheduleJob;
        }
        catch (error) {
            logger_1.default.error(`Error creating schedule job: ${error}`);
            throw error;
        }
    }
    /**
     * Get schedule job by ID
     */
    async getScheduleJobById(scheduleJobId, projectId) {
        try {
            const db = await (0, database_1.getDb)();
            const [scheduleJob] = await db
                .select({
                id: tables_1.scheduleJobsTable.id,
                name: tables_1.scheduleJobsTable.name,
                type: tables_1.scheduleJobsTable.type,
                status: tables_1.scheduleJobsTable.status,
                projectId: tables_1.scheduleJobsTable.projectId,
                workspaceId: tables_1.scheduleJobsTable.workspaceId,
                applicationId: tables_1.scheduleJobsTable.applicationId,
                libraryId: tables_1.scheduleJobsTable.libraryId,
                securityConfigId: tables_1.scheduleJobsTable.securityConfigId,
                startDate: tables_1.scheduleJobsTable.startDate,
                runTime: tables_1.scheduleJobsTable.runTime,
                timeZone: tables_1.scheduleJobsTable.timeZone,
                repeatEvery: tables_1.scheduleJobsTable.repeatEvery,
                repeatUnit: tables_1.scheduleJobsTable.repeatUnit,
                repeatOnDays: tables_1.scheduleJobsTable.repeatOnDays,
                endsOn: tables_1.scheduleJobsTable.endsOn,
                endsDate: tables_1.scheduleJobsTable.endsDate,
                endsAfter: tables_1.scheduleJobsTable.endsAfter,
                lastScheduleJobRun: tables_1.scheduleJobsTable.lastScheduleJobRun,
                nextRunAt: tables_1.scheduleJobsTable.nextRunAt,
                createdAt: tables_1.scheduleJobsTable.createdAt,
                updatedAt: tables_1.scheduleJobsTable.updatedAt,
                is_archived: tables_1.scheduleJobsTable.is_archived,
                application: {
                    id: tables_1.applicationsTable.id,
                    name: tables_1.applicationsTable.name,
                },
                library: {
                    id: tables_1.promptsLibraryTable.id,
                    name: tables_1.promptsLibraryTable.name,
                },
                securityConfig: {
                    id: tables_1.securityConfigTable.id,
                    name: tables_1.securityConfigTable.name,
                },
            })
                .from(tables_1.scheduleJobsTable)
                .leftJoin(tables_1.applicationsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.applicationId, tables_1.applicationsTable.id))
                .leftJoin(tables_1.promptsLibraryTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.libraryId, tables_1.promptsLibraryTable.id))
                .leftJoin(tables_1.securityConfigTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.securityConfigId, tables_1.securityConfigTable.id))
                .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.projectId, projectId)))
                .limit(1);
            if (!scheduleJob) {
                return null;
            }
            // Get related test runs
            const testRuns = await db
                .select({
                id: tables_1.testRunsTable.id,
                createdAt: tables_1.testRunsTable.createdAt,
                status: tables_1.testRunsTable.status,
                totalTestCase: tables_1.testRunsTable.totalTestCase,
            })
                .from(tables_1.testRunsToScheduleJobsTable)
                .innerJoin(tables_1.testRunsTable, (0, drizzle_orm_1.eq)(tables_1.testRunsToScheduleJobsTable.testRunId, tables_1.testRunsTable.id))
                .where((0, drizzle_orm_1.eq)(tables_1.testRunsToScheduleJobsTable.scheduleJobId, scheduleJobId))
                .orderBy((0, drizzle_orm_1.desc)(tables_1.testRunsTable.createdAt));
            return {
                ...scheduleJob,
                testRuns,
            };
        }
        catch (error) {
            logger_1.default.error(`Error fetching schedule job: ${error}`);
            throw error;
        }
    }
    /**
     * Get list of schedule jobs with pagination
     */
    async getScheduleJobsList(params) {
        try {
            const db = await (0, database_1.getDb)();
            const page = params.page || 1;
            const limit = params.limit || 10;
            const offset = (page - 1) * limit;
            //const includeArchived = params.archived_data_required !== false;
            const includeArchived = true;
            const whereConditions = [(0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.projectId, params.projectId)];
            if (params.workspaceId) {
                whereConditions.push((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.workspaceId, params.workspaceId));
            }
            if (params.status) {
                whereConditions.push((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.status, params.status));
            }
            if (params.type) {
                whereConditions.push((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.type, params.type));
            }
            if (params.search && params.search.trim() !== '') {
                whereConditions.push((0, drizzle_orm_1.or)((0, drizzle_orm_1.ilike)(tables_1.scheduleJobsTable.name, `%${params.search}%`), (0, drizzle_orm_1.ilike)(tables_1.applicationsTable.name, `%${params.search}%`)));
            }
            if (!includeArchived) {
                whereConditions.push((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.is_archived, false));
            }
            // Get schedule jobs with related data
            const scheduleJobs = await db
                .select({
                id: tables_1.scheduleJobsTable.id,
                name: tables_1.scheduleJobsTable.name,
                type: tables_1.scheduleJobsTable.type,
                status: tables_1.scheduleJobsTable.status,
                applicationId: tables_1.scheduleJobsTable.applicationId,
                securityConfig: tables_1.securityConfigTable.name,
                startDate: tables_1.scheduleJobsTable.startDate,
                repeatEvery: tables_1.scheduleJobsTable.repeatEvery,
                repeatUnit: tables_1.scheduleJobsTable.repeatUnit,
                lastScheduleJobRun: tables_1.scheduleJobsTable.lastScheduleJobRun,
                nextRunAt: tables_1.scheduleJobsTable.nextRunAt,
                createdAt: tables_1.scheduleJobsTable.createdAt,
                updatedAt: tables_1.scheduleJobsTable.updatedAt,
                is_archived: tables_1.scheduleJobsTable.is_archived,
                application: tables_1.applicationsTable.name,
                promptLibrary: tables_1.promptsLibraryTable.name,
            })
                .from(tables_1.scheduleJobsTable)
                .leftJoin(tables_1.applicationsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.applicationId, tables_1.applicationsTable.id))
                .leftJoin(tables_1.promptsLibraryTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.libraryId, tables_1.promptsLibraryTable.id))
                .leftJoin(tables_1.securityConfigTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.securityConfigId, tables_1.securityConfigTable.id))
                .where((0, drizzle_orm_1.and)(...whereConditions))
                .orderBy(
            // Non-archived first, archived last
            (0, drizzle_orm_1.asc)((0, drizzle_orm_1.sql) `CASE WHEN ${tables_1.scheduleJobsTable.is_archived} THEN 1 ELSE 0 END`), (0, drizzle_orm_1.desc)(tables_1.scheduleJobsTable.createdAt))
                .limit(limit)
                .offset(offset);
            // Get total count
            const totalCountResult = await db
                .select({ count: (0, drizzle_orm_1.sql) `count(*)` })
                .from(tables_1.scheduleJobsTable)
                .leftJoin(tables_1.applicationsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.applicationId, tables_1.applicationsTable.id))
                .where((0, drizzle_orm_1.and)(...whereConditions));
            const total = Number(totalCountResult[0]?.count || 0);
            // Get test run counts for each schedule job
            const scheduleJobIds = scheduleJobs.map((job) => job.id);
            const testRunCounts = await db
                .select({
                scheduleJobId: tables_1.testRunsToScheduleJobsTable.scheduleJobId,
                count: (0, drizzle_orm_1.sql) `count(*)`,
            })
                .from(tables_1.testRunsToScheduleJobsTable)
                .where(scheduleJobIds.length > 0
                ? (0, drizzle_orm_1.sql) `${tables_1.testRunsToScheduleJobsTable.scheduleJobId} = ANY(${drizzle_orm_1.sql.param(scheduleJobIds)})`
                : (0, drizzle_orm_1.sql) `FALSE`)
                .groupBy(tables_1.testRunsToScheduleJobsTable.scheduleJobId);
            const testRunCountMap = new Map(testRunCounts.map((item) => [item.scheduleJobId, Number(item.count)]));
            const enrichedJobs = scheduleJobs.map((job) => ({
                ...job,
                testsRun: testRunCountMap.get(job.id) || 0,
            }));
            return {
                data: enrichedJobs,
                pagination: {
                    page,
                    limit,
                    total,
                    totalPages: Math.ceil(total / limit),
                },
            };
        }
        catch (error) {
            logger_1.default.error(`Error fetching schedule jobs list: ${error}`);
            throw error;
        }
    }
    /**
     * Update schedule job
     */
    async updateScheduleJob(scheduleJobId, projectId, params) {
        try {
            const db = await (0, database_1.getDb)();
            const updateData = {};
            if (params.status !== undefined) {
                updateData.status = params.status;
            }
            if (params.startDate !== undefined) {
                updateData.startDate = new Date(params.startDate);
            }
            if (params.runTime !== undefined) {
                updateData.runTime = params.runTime;
            }
            if (params.timeZone !== undefined) {
                updateData.timeZone = params.timeZone;
            }
            if (params.repeatEvery !== undefined) {
                updateData.repeatEvery = params.repeatEvery;
            }
            if (params.repeatUnit !== undefined) {
                updateData.repeatUnit = params.repeatUnit;
            }
            if (params.repeatOnDays !== undefined) {
                updateData.repeatOnDays = (0, scheduleJobRepeatOnDays_1.sanitizeRepeatOnDaysForStorage)(params.repeatOnDays);
            }
            if (params.endsOn !== undefined) {
                updateData.endsOn = params.endsOn;
            }
            if (params.endsDate !== undefined) {
                updateData.endsDate = params.endsDate ? new Date(params.endsDate) : null;
            }
            if (params.endsAfter !== undefined) {
                updateData.endsAfter = params.endsAfter;
            }
            // Recalculate nextRunAt if schedule parameters changed
            if (params.startDate !== undefined ||
                params.runTime !== undefined ||
                params.timeZone !== undefined ||
                params.repeatEvery !== undefined ||
                params.repeatUnit !== undefined ||
                params.repeatOnDays !== undefined ||
                params.endsOn !== undefined ||
                params.endsDate !== undefined) {
                const [currentJob] = await db
                    .select()
                    .from(tables_1.scheduleJobsTable)
                    .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.projectId, projectId)))
                    .limit(1);
                if (currentJob) {
                    const startDate = params.startDate === undefined
                        ? currentJob.startDate || new Date()
                        : new Date(params.startDate);
                    const repeatEvery = params.repeatEvery ?? (currentJob.repeatEvery || 1);
                    const repeatUnit = params.repeatUnit ?? (currentJob.repeatUnit || 'day');
                    const runTime = params.runTime ?? currentJob.runTime;
                    const timeZone = params.timeZone ?? currentJob.timeZone;
                    const repeatOnDays = params.repeatOnDays ?? currentJob.repeatOnDays;
                    const endsOn = params.endsOn ?? currentJob.endsOn;
                    const endsDate = params.endsDate === undefined
                        ? (currentJob.endsDate ?? null)
                        : (params.endsDate ? new Date(params.endsDate) : null);
                    updateData.nextRunAt = calculateNextRunAtUtcFromSchedule({
                        startDate,
                        repeatEvery,
                        repeatUnit,
                        repeatOnDays,
                        runTime,
                        timeZone,
                        fromUtc: new Date(),
                        endsOn,
                        endsDate,
                    });
                }
            }
            updateData.updatedAt = new Date();
            const [updatedJob] = await db
                .update(tables_1.scheduleJobsTable)
                .set(updateData)
                .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.projectId, projectId)))
                .returning();
            if (!updatedJob) {
                return null;
            }
            logger_1.default.info(`Updated schedule job ${scheduleJobId}`);
            return updatedJob;
        }
        catch (error) {
            logger_1.default.error(`Error updating schedule job: ${error}`);
            throw error;
        }
    }
    /**
     * Archive schedule job
     */
    async archiveScheduleJob(scheduleJobId, projectId) {
        try {
            const db = await (0, database_1.getDb)();
            const [archivedJob] = await db
                .update(tables_1.scheduleJobsTable)
                .set({
                status: constants_1.SCHEDULE_JOB_STATUS.ARCHIVED,
                is_archived: true,
                updatedAt: new Date(),
            })
                .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.projectId, projectId)))
                .returning();
            logger_1.default.info(`Archived schedule job ${scheduleJobId}`);
            return archivedJob;
        }
        catch (error) {
            logger_1.default.error(`Error archiving schedule job: ${error}`);
            throw error;
        }
    }
    /**
     * Delete schedule job
     */
    async deleteScheduleJob(scheduleJobId, projectId) {
        try {
            const db = await (0, database_1.getDb)();
            await db
                .delete(tables_1.scheduleJobsTable)
                .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.projectId, projectId)));
            logger_1.default.info(`Deleted schedule job ${scheduleJobId}`);
            return { success: true };
        }
        catch (error) {
            logger_1.default.error(`Error deleting schedule job: ${error}`);
            throw error;
        }
    }
    /**
     * Get schedule jobs that are due to run
     * A job is due if:
     * 1. startDate is today AND nextRunAt is not today (or null)
     * 2. It's been repeatEvery units since last run (or startDate if no last run) AND nextRunAt is not set or is in the past
     */
    async getDueScheduleJobs() {
        try {
            const db = await (0, database_1.getDb)();
            const now = new Date();
            const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
            const todayEnd = new Date(todayStart);
            todayEnd.setDate(todayEnd.getDate() + 1);
            // Small tolerance to avoid missing jobs due to ms-level clock skew / rounding between DB + app time.
            const dueSkewMs = 1500;
            // Get all active, non-archived jobs that are due based on nextRunAt (UTC).
            // Exclude jobs whose linked application / prompt library / security config is archived.
            const allJobs = await db
                .select({
                id: tables_1.scheduleJobsTable.id,
                name: tables_1.scheduleJobsTable.name,
                type: tables_1.scheduleJobsTable.type,
                status: tables_1.scheduleJobsTable.status,
                projectId: tables_1.scheduleJobsTable.projectId,
                workspaceId: tables_1.scheduleJobsTable.workspaceId,
                applicationId: tables_1.scheduleJobsTable.applicationId,
                libraryId: tables_1.scheduleJobsTable.libraryId,
                securityConfigId: tables_1.scheduleJobsTable.securityConfigId,
                startDate: tables_1.scheduleJobsTable.startDate,
                runTime: tables_1.scheduleJobsTable.runTime,
                timeZone: tables_1.scheduleJobsTable.timeZone,
                repeatEvery: tables_1.scheduleJobsTable.repeatEvery,
                repeatUnit: tables_1.scheduleJobsTable.repeatUnit,
                repeatOnDays: tables_1.scheduleJobsTable.repeatOnDays,
                endsOn: tables_1.scheduleJobsTable.endsOn,
                endsDate: tables_1.scheduleJobsTable.endsDate,
                endsAfter: tables_1.scheduleJobsTable.endsAfter,
                lastScheduleJobRun: tables_1.scheduleJobsTable.lastScheduleJobRun,
                nextRunAt: tables_1.scheduleJobsTable.nextRunAt,
                createdAt: tables_1.scheduleJobsTable.createdAt,
                updatedAt: tables_1.scheduleJobsTable.updatedAt,
                is_archived: tables_1.scheduleJobsTable.is_archived,
                testRunCount: (0, drizzle_orm_1.sql) `COALESCE(COUNT(${tables_1.testRunsToScheduleJobsTable.testRunId}), 0)`,
            })
                .from(tables_1.scheduleJobsTable)
                .leftJoin(tables_1.testRunsToScheduleJobsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, tables_1.testRunsToScheduleJobsTable.scheduleJobId))
                .leftJoin(tables_1.applicationsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.applicationId, tables_1.applicationsTable.id))
                .leftJoin(tables_1.promptsLibraryTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.libraryId, tables_1.promptsLibraryTable.id))
                .leftJoin(tables_1.securityConfigTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.securityConfigId, tables_1.securityConfigTable.id))
                .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.status, constants_1.SCHEDULE_JOB_STATUS.ACTIVE), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.is_archived, false), (0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.nextRunAt} IS NOT NULL`, (0, drizzle_orm_1.lte)(tables_1.scheduleJobsTable.nextRunAt, now), 
            // Exclude jobs where linked application is archived (require join hit when FK is set)
            (0, drizzle_orm_1.or)((0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.applicationId} IS NULL`, (0, drizzle_orm_1.and)((0, drizzle_orm_1.isNotNull)(tables_1.applicationsTable.id), (0, drizzle_orm_1.eq)(tables_1.applicationsTable.is_archived, false))), 
            // Exclude jobs where linked prompt library is archived
            (0, drizzle_orm_1.or)((0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.libraryId} IS NULL`, (0, drizzle_orm_1.and)((0, drizzle_orm_1.isNotNull)(tables_1.promptsLibraryTable.id), (0, drizzle_orm_1.eq)(tables_1.promptsLibraryTable.is_archived, false))), (0, drizzle_orm_1.or)((0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.securityConfigId} IS NULL`, (0, drizzle_orm_1.and)((0, drizzle_orm_1.isNotNull)(tables_1.securityConfigTable.id), (0, drizzle_orm_1.eq)(tables_1.securityConfigTable.is_archived, false))), (0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.endsOn, 'never'), (0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.endsDate} IS NULL`, (0, drizzle_orm_1.gte)(tables_1.scheduleJobsTable.endsDate, now))))
                .groupBy(tables_1.scheduleJobsTable.id);
            // Convert testRunCount from string to number
            const jobsWithNumericCounts = allJobs.map((job) => ({
                ...job,
                testRunCount: typeof job.testRunCount === 'string'
                    ? Number.parseInt(job.testRunCount, 10)
                    : Number(job.testRunCount) || 0,
            }));
            const dueJobs = jobsWithNumericCounts.filter((job) => {
                if (job.endsOn === 'after' && job.endsAfter && job.testRunCount >= job.endsAfter) {
                    return false;
                }
                return true;
            });
            /**
             * Backfill legacy jobs that don't have nextRunAt yet (or were created before timezone scheduling).
             * We compute + persist nextRunAt, and if it's already due, include it in this run.
             */
            const legacyJobs = await db
                .select({
                id: tables_1.scheduleJobsTable.id,
                name: tables_1.scheduleJobsTable.name,
                type: tables_1.scheduleJobsTable.type,
                status: tables_1.scheduleJobsTable.status,
                projectId: tables_1.scheduleJobsTable.projectId,
                workspaceId: tables_1.scheduleJobsTable.workspaceId,
                applicationId: tables_1.scheduleJobsTable.applicationId,
                libraryId: tables_1.scheduleJobsTable.libraryId,
                securityConfigId: tables_1.scheduleJobsTable.securityConfigId,
                startDate: tables_1.scheduleJobsTable.startDate,
                runTime: tables_1.scheduleJobsTable.runTime,
                timeZone: tables_1.scheduleJobsTable.timeZone,
                repeatEvery: tables_1.scheduleJobsTable.repeatEvery,
                repeatUnit: tables_1.scheduleJobsTable.repeatUnit,
                repeatOnDays: tables_1.scheduleJobsTable.repeatOnDays,
                endsOn: tables_1.scheduleJobsTable.endsOn,
                endsDate: tables_1.scheduleJobsTable.endsDate,
                endsAfter: tables_1.scheduleJobsTable.endsAfter,
                lastScheduleJobRun: tables_1.scheduleJobsTable.lastScheduleJobRun,
                nextRunAt: tables_1.scheduleJobsTable.nextRunAt,
                is_archived: tables_1.scheduleJobsTable.is_archived,
                testRunCount: (0, drizzle_orm_1.sql) `COALESCE(COUNT(${tables_1.testRunsToScheduleJobsTable.testRunId}), 0)`,
            })
                .from(tables_1.scheduleJobsTable)
                .leftJoin(tables_1.testRunsToScheduleJobsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, tables_1.testRunsToScheduleJobsTable.scheduleJobId))
                .leftJoin(tables_1.applicationsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.applicationId, tables_1.applicationsTable.id))
                .leftJoin(tables_1.promptsLibraryTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.libraryId, tables_1.promptsLibraryTable.id))
                .leftJoin(tables_1.securityConfigTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.securityConfigId, tables_1.securityConfigTable.id))
                .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.status, constants_1.SCHEDULE_JOB_STATUS.ACTIVE), (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.is_archived, false), (0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.nextRunAt} IS NULL`, 
            // Exclude jobs where linked application is archived (require join hit when FK is set)
            (0, drizzle_orm_1.or)((0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.applicationId} IS NULL`, (0, drizzle_orm_1.and)((0, drizzle_orm_1.isNotNull)(tables_1.applicationsTable.id), (0, drizzle_orm_1.eq)(tables_1.applicationsTable.is_archived, false))), 
            // Exclude jobs where linked prompt library is archived
            (0, drizzle_orm_1.or)((0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.libraryId} IS NULL`, (0, drizzle_orm_1.and)((0, drizzle_orm_1.isNotNull)(tables_1.promptsLibraryTable.id), (0, drizzle_orm_1.eq)(tables_1.promptsLibraryTable.is_archived, false))), (0, drizzle_orm_1.or)((0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.securityConfigId} IS NULL`, (0, drizzle_orm_1.and)((0, drizzle_orm_1.isNotNull)(tables_1.securityConfigTable.id), (0, drizzle_orm_1.eq)(tables_1.securityConfigTable.is_archived, false))), (0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.endsOn, 'never'), (0, drizzle_orm_1.sql) `${tables_1.scheduleJobsTable.endsDate} IS NULL`, (0, drizzle_orm_1.gte)(tables_1.scheduleJobsTable.endsDate, now))))
                .groupBy(tables_1.scheduleJobsTable.id)
                .limit(500);
            const normalizedLegacyJobs = legacyJobs.map((job) => ({
                ...job,
                testRunCount: typeof job.testRunCount === 'string'
                    ? Number.parseInt(job.testRunCount, 10)
                    : Number(job.testRunCount) || 0,
            }));
            for (const job of normalizedLegacyJobs) {
                if (job.endsOn === 'after' && job.endsAfter && job.testRunCount >= job.endsAfter) {
                    continue;
                }
                const computedNext = this.computeNextRunAtForJob(job, now);
                if (computedNext) {
                    await db
                        .update(tables_1.scheduleJobsTable)
                        .set({ nextRunAt: computedNext, updatedAt: now })
                        .where((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, job.id));
                    if (computedNext.getTime() <= now.getTime() + dueSkewMs) {
                        dueJobs.push({ ...job, nextRunAt: computedNext });
                    }
                }
            }
            return dueJobs;
        }
        catch (error) {
            logger_1.default.error(`Error fetching due schedule jobs: ${error}`);
            throw error;
        }
    }
    /**
     * Update last run time and calculate next run
     */
    async updateLastRun(scheduleJobId, testRunId) {
        try {
            const db = await (0, database_1.getDb)();
            // Get current schedule job
            const [scheduleJob] = await db
                .select()
                .from(tables_1.scheduleJobsTable)
                .where((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId))
                .limit(1);
            if (!scheduleJob) {
                throw new Error(`Schedule job ${scheduleJobId} not found`);
            }
            // Link test run to schedule job (ignore if already exists)
            await db
                .insert(tables_1.testRunsToScheduleJobsTable)
                .values({
                testRunId,
                scheduleJobId,
            })
                .onConflictDoNothing();
            const now = new Date();
            const nextRunAt = calculateNextRunAtUtcFromSchedule({
                startDate: scheduleJob.startDate,
                repeatEvery: scheduleJob.repeatEvery,
                repeatUnit: scheduleJob.repeatUnit,
                repeatOnDays: scheduleJob.repeatOnDays,
                runTime: scheduleJob.runTime ?? null,
                timeZone: scheduleJob.timeZone ?? null,
                fromUtc: now,
                endsOn: scheduleJob.endsOn ?? null,
                endsDate: scheduleJob.endsDate ?? null,
            });
            // Check if job should be marked as completed
            const updateData = {
                lastScheduleJobRun: now,
                nextRunAt,
                updatedAt: now,
            };
            // Check if nextRunAt exceeds endDate (when endsOn === 'on')
            if (scheduleJob.endsOn === 'on' && scheduleJob.endsDate) {
                const endsDate = new Date(scheduleJob.endsDate);
                if (nextRunAt && nextRunAt > endsDate) {
                    updateData.status = constants_1.SCHEDULE_JOB_STATUS.COMPLETED;
                    updateData.nextRunAt = null; // Clear next run since job is completed
                    logger_1.default.info(`Schedule job ${scheduleJobId} completed - next run ${nextRunAt} exceeds end date ${endsDate}`);
                }
            }
            // Check if job should be completed based on endsAfter (when endsOn === 'after')
            if (scheduleJob.endsOn === 'after' && scheduleJob.endsAfter) {
                // Get current test run count for this schedule job
                const testRunCountResult = await db
                    .select({ count: (0, drizzle_orm_1.sql) `count(*)` })
                    .from(tables_1.testRunsToScheduleJobsTable)
                    .where((0, drizzle_orm_1.eq)(tables_1.testRunsToScheduleJobsTable.scheduleJobId, scheduleJobId));
                const testRunCount = Number(testRunCountResult[0]?.count || 0);
                // If we've reached or exceeded the limit, mark as completed
                if (testRunCount >= scheduleJob.endsAfter) {
                    updateData.status = constants_1.SCHEDULE_JOB_STATUS.COMPLETED;
                    updateData.nextRunAt = null; // Clear next run since job is completed
                    logger_1.default.info(`Schedule job ${scheduleJobId} completed - reached endsAfter limit of ${scheduleJob.endsAfter} runs`);
                }
            }
            // Update last run and next run (and status if completed)
            await db
                .update(tables_1.scheduleJobsTable)
                .set(updateData)
                .where((0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId));
            logger_1.default.info(`Updated last run for schedule job ${scheduleJobId}, next run: ${nextRunAt}, status: ${updateData.status || scheduleJob.status}`);
            return { success: true, nextRunAt, status: updateData.status || scheduleJob.status };
        }
        catch (error) {
            logger_1.default.error(`Error updating last run: ${error}`);
            throw error;
        }
    }
    /**
     * Calculate test run stats from eval results
     */
    calculateTestRunStats(evalResults) {
        const stats = evalResults?.reduce((acc, result) => {
            acc.total += 1;
            if (result?.gradingResult?.pass) {
                acc.successes += 1;
            }
            else {
                acc.failures += 1;
            }
            return acc;
        }, { total: 0, successes: 0, failures: 0 });
        stats.errors = (evalResults.length ?? 0) - stats?.total;
        return stats;
    }
    /**
     * Get evaluations for a schedule job
     */
    async getScheduleJobEvaluations(scheduleJobId, page = 1, limit = 10) {
        try {
            const db = await (0, database_1.getDb)();
            const offset = (page - 1) * limit;
            // Get test runs with library and security config info
            const evaluations = await db
                .select({
                id: tables_1.testRunsTable.id,
                createdAt: tables_1.testRunsTable.createdAt,
                status: tables_1.testRunsTable.status,
                totalTestCase: tables_1.testRunsTable.totalTestCase,
                type: tables_1.testRunsTable.type,
                libraryId: tables_1.testRunsTable.libraryId,
                securityConfigId: tables_1.scheduleJobsTable.securityConfigId,
                promptLibrary: {
                    id: tables_1.promptsLibraryTable.id,
                    name: tables_1.promptsLibraryTable.name,
                },
                securityConfig: {
                    id: tables_1.securityConfigTable.id,
                    name: tables_1.securityConfigTable.name,
                },
            })
                .from(tables_1.testRunsToScheduleJobsTable)
                .leftJoin(tables_1.scheduleJobsTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.id, scheduleJobId))
                .leftJoin(tables_1.securityConfigTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.securityConfigId, tables_1.securityConfigTable.id))
                .innerJoin(tables_1.testRunsTable, (0, drizzle_orm_1.eq)(tables_1.testRunsToScheduleJobsTable.testRunId, tables_1.testRunsTable.id))
                .leftJoin(tables_1.promptsLibraryTable, (0, drizzle_orm_1.eq)(tables_1.scheduleJobsTable.libraryId, tables_1.promptsLibraryTable.id))
                .where((0, drizzle_orm_1.eq)(tables_1.testRunsToScheduleJobsTable.scheduleJobId, scheduleJobId))
                .orderBy((0, drizzle_orm_1.desc)(tables_1.testRunsTable.createdAt))
                .limit(limit)
                .offset(offset);
            const totalCountResult = await db
                .select({ count: (0, drizzle_orm_1.sql) `count(*)` })
                .from(tables_1.testRunsToScheduleJobsTable)
                .where((0, drizzle_orm_1.eq)(tables_1.testRunsToScheduleJobsTable.scheduleJobId, scheduleJobId));
            const total = Number(totalCountResult[0]?.count || 0);
            // Calculate stats for each test run
            const testRunIds = evaluations.map((evaluation) => evaluation.id);
            const enrichedEvaluations = evaluations.map((evaluation) => ({ ...evaluation, stats: null }));
            if (testRunIds.length > 0) {
                // Fetch eval results for all test runs
                const evalResults = await db
                    .select({
                    testRunId: tables_1.testRunsToEvalsTable.testRunId,
                    gradingResult: tables_1.evalResultsTable.gradingResult,
                })
                    .from(tables_1.testRunsToEvalsTable)
                    .innerJoin(tables_1.evalResultsTable, (0, drizzle_orm_1.eq)(tables_1.testRunsToEvalsTable.evalId, tables_1.evalResultsTable.evalId))
                    .where((0, drizzle_orm_1.inArray)(tables_1.testRunsToEvalsTable.testRunId, testRunIds));
                // Group eval results by test run ID
                const evalResultsByTestRun = new Map();
                for (const result of evalResults) {
                    if (!evalResultsByTestRun.has(result.testRunId)) {
                        evalResultsByTestRun.set(result.testRunId, []);
                    }
                    evalResultsByTestRun.get(result.testRunId).push(result);
                }
                // Calculate stats for each test run
                for (let i = 0; i < enrichedEvaluations.length; i++) {
                    const evaluation = enrichedEvaluations[i];
                    const results = evalResultsByTestRun.get(evaluation.id) || [];
                    enrichedEvaluations[i].stats = this.calculateTestRunStats(results);
                }
            }
            return {
                data: enrichedEvaluations,
                pagination: {
                    page,
                    limit,
                    total,
                    totalPages: Math.ceil(total / limit),
                },
            };
        }
        catch (error) {
            logger_1.default.error(`Error fetching schedule job evaluations: ${error}`);
            throw error;
        }
    }
}
exports.ScheduleJobsService = ScheduleJobsService;
// Export singleton instance
exports.scheduleJobsService = new ScheduleJobsService();
//# sourceMappingURL=scheduleJobsService.js.map