"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.functionalChartsService = exports.FunctionalChartsService = void 0;
const drizzle_orm_1 = require("drizzle-orm");
const lodash_1 = __importDefault(require("lodash"));
const constants_1 = require("../../constants");
const database_1 = require("../../database");
const tables_1 = require("../../database/tables");
// Bucketing thresholds for time-series charts (aligned with observeCharts).
// If the range is > WEEKLY_BUCKET_AFTER_MONTHS → bucket weekly
// If the range is > MONTHLY_BUCKET_AFTER_MONTHS → bucket monthly
const WEEKLY_BUCKET_AFTER_MONTHS = 1;
const MONTHLY_BUCKET_AFTER_MONTHS = 4;
class FunctionalChartsService {
    constructor() { }
    static getInstance() {
        if (FunctionalChartsService.instance === null) {
            FunctionalChartsService.instance = new FunctionalChartsService();
        }
        return FunctionalChartsService.instance;
    }
    isNotEmpty(value) {
        return (lodash_1.default.isEmpty(value) === false &&
            value !== 'undefined' &&
            typeof value !== 'undefined' &&
            value !== null);
    }
    buildTestRunConditions(params) {
        const conditions = [
            (0, drizzle_orm_1.eq)(tables_1.testRunsTable.type, constants_1.TEST_RUN_TYPE.EVAL),
            (0, drizzle_orm_1.eq)(tables_1.testRunsTable.status, constants_1.TEST_RUN_STATUS.COMPLETED),
            (0, drizzle_orm_1.gte)(tables_1.testRunsTable.createdAt, new Date(params.timeLimit.start)),
            (0, drizzle_orm_1.lte)(tables_1.testRunsTable.createdAt, new Date(params.timeLimit.end)),
        ];
        if (this.isNotEmpty(params.projectId)) {
            conditions.push((0, drizzle_orm_1.eq)(tables_1.testRunsTable.projectId, params.projectId));
        }
        if (this.isNotEmpty(params.applicationId)) {
            conditions.push((0, drizzle_orm_1.sql) `${tables_1.testRunsTable.applicationIds} @> ${JSON.stringify([params.applicationId])}::jsonb`);
        }
        return conditions;
    }
    /** Choose bucket unit from time range (aligned with observeCharts). */
    getBucketUnit(start, end) {
        const diffDays = Math.abs(end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
        const diffMonthsApprox = diffDays / 30;
        if (diffMonthsApprox > MONTHLY_BUCKET_AFTER_MONTHS)
            return 'month';
        if (diffMonthsApprox > WEEKLY_BUCKET_AFTER_MONTHS)
            return 'week';
        return 'day';
    }
    floorToDayUtc(d) {
        return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0));
    }
    floorToWeekUtc(d) {
        const day = d.getUTCDay();
        const daysSinceMonday = (day + 6) % 7;
        return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - daysSinceMonday, 0, 0, 0, 0));
    }
    floorToMonthUtc(d) {
        return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1, 0, 0, 0, 0));
    }
    floorToBucketUtc(d, unit) {
        if (unit === 'day')
            return this.floorToDayUtc(d);
        if (unit === 'week')
            return this.floorToWeekUtc(d);
        return this.floorToMonthUtc(d);
    }
    /** Format date as yyyy-MM-dd using UTC (for bucket keys). */
    formatUtcDate(d) {
        const y = d.getUTCFullYear();
        const m = String(d.getUTCMonth() + 1).padStart(2, '0');
        const day = String(d.getUTCDate()).padStart(2, '0');
        return `${y}-${m}-${day}`;
    }
    addBucketUtc(d, unit) {
        if (unit === 'day')
            return new Date(d.getTime() + 24 * 60 * 60 * 1000);
        if (unit === 'week')
            return new Date(d.getTime() + 7 * 24 * 60 * 60 * 1000);
        return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1, 0, 0, 0, 0));
    }
    /** Build ordered bucket keys (yyyy-MM-dd, UTC) for the range. */
    buildBucketKeys(start, end, unit) {
        const startF = this.floorToBucketUtc(start, unit);
        const endF = this.floorToBucketUtc(end, unit);
        const keys = [];
        for (let t = new Date(startF); t.getTime() <= endF.getTime(); t = this.addBucketUtc(t, unit)) {
            keys.push(this.formatUtcDate(t));
        }
        return keys;
    }
    /**
     * Single query: test_runs -> test_runs_to_evals -> eval_results.
     * Returns rows with gradingResult and test run id/createdAt for scoping.
     */
    async getFunctionalEvalResultRows(params, options) {
        const db = await (0, database_1.getDb)();
        const conditions = this.buildTestRunConditions(params);
        let q = db
            .select({
            gradingResult: tables_1.evalResultsTable.gradingResult,
            testRunId: tables_1.testRunsTable.id,
            testRunCreatedAt: tables_1.testRunsTable.createdAt,
            evalResultId: tables_1.evalResultsTable.id,
        })
            .from(tables_1.testRunsTable)
            .innerJoin(tables_1.testRunsToEvalsTable, (0, drizzle_orm_1.eq)(tables_1.testRunsTable.id, tables_1.testRunsToEvalsTable.testRunId))
            .innerJoin(tables_1.evalResultsTable, (0, drizzle_orm_1.eq)(tables_1.testRunsToEvalsTable.evalId, tables_1.evalResultsTable.evalId))
            .where((0, drizzle_orm_1.and)(...conditions));
        if (options?.orderByTestRunCreatedAtDesc) {
            q = q.orderBy((0, drizzle_orm_1.desc)(tables_1.testRunsTable.createdAt));
        }
        if (options?.rowLimit != null) {
            q = q.limit(options.rowLimit);
        }
        return q;
    }
    /**
     * Stats: evaluations_completed = count of completed EVAL test runs (matches getWorkspaceDetails functionEvalCount).
     * total_requests / total_success / total_failures = test case counts from eval_results (one row = one test case, success from eval_results.success).
     * Logic aligned with getTestRunMetricsGraph.
     */
    async getFunctionalTestStats(params) {
        const db = await (0, database_1.getDb)();
        const conditions = this.buildTestRunConditions(params);
        const [countResult, testRuns] = await Promise.all([
            db
                .select({ count: (0, drizzle_orm_1.sql) `count(*)::int` })
                .from(tables_1.testRunsTable)
                .where((0, drizzle_orm_1.and)(...conditions)),
            db
                .select({ id: tables_1.testRunsTable.id })
                .from(tables_1.testRunsTable)
                .where((0, drizzle_orm_1.and)(...conditions)),
        ]);
        const evaluations_completed = Number(countResult[0]?.count ?? 0);
        if (testRuns.length === 0) {
            return {
                evaluations_completed: 0,
                total_requests: 0,
                total_success: 0,
                total_failures: 0,
                total_errors: 0,
            };
        }
        const testRunIds = testRuns.map((tr) => tr.id);
        const testRunToEvals = await db
            .select({ evalId: tables_1.testRunsToEvalsTable.evalId })
            .from(tables_1.testRunsToEvalsTable)
            .where((0, drizzle_orm_1.inArray)(tables_1.testRunsToEvalsTable.testRunId, testRunIds));
        if (testRunToEvals.length === 0) {
            return {
                evaluations_completed,
                total_requests: 0,
                total_success: 0,
                total_failures: 0,
                total_errors: 0,
            };
        }
        const allEvalIds = Array.from(new Set(testRunToEvals.map((r) => r.evalId)));
        const evalResults = await db
            .select({ success: tables_1.evalResultsTable.success })
            .from(tables_1.evalResultsTable)
            .where((0, drizzle_orm_1.inArray)(tables_1.evalResultsTable.evalId, allEvalIds));
        let total_success = 0;
        let total_failures = 0;
        for (const row of evalResults) {
            if (row?.success) {
                total_success += 1;
            }
            else {
                total_failures += 1;
            }
        }
        const total_requests = evalResults.length;
        return {
            evaluations_completed,
            total_requests,
            total_success,
            total_failures,
            total_errors: 0,
        };
    }
    /**
     * Trend = count of completed EVAL test runs per bucket (day/week/month by range). One query on test_runs only.
     */
    async getFunctionalEvaluationsTrend(params) {
        const db = await (0, database_1.getDb)();
        const conditions = this.buildTestRunConditions(params);
        const testRuns = await db
            .select({ createdAt: tables_1.testRunsTable.createdAt })
            .from(tables_1.testRunsTable)
            .where((0, drizzle_orm_1.and)(...conditions));
        const startDate = new Date(params.timeLimit.start);
        const endDate = new Date(params.timeLimit.end);
        const bucketUnit = this.getBucketUnit(startDate, endDate);
        const bucketKeys = this.buildBucketKeys(startDate, endDate, bucketUnit);
        const bucketToCount = new Map();
        for (const key of bucketKeys)
            bucketToCount.set(key, 0);
        for (const tr of testRuns ?? []) {
            const d = new Date(tr.createdAt);
            const bucketDate = this.floorToBucketUtc(d, bucketUnit);
            const key = this.formatUtcDate(bucketDate);
            if (bucketToCount.has(key))
                bucketToCount.set(key, bucketToCount.get(key) + 1);
        }
        return bucketKeys.map((timestamp) => ({
            timestamp,
            count: bucketToCount.get(timestamp) ?? 0,
        }));
    }
    /**
     * Recent completed EVAL test runs with status. One joined query, then dedupe test runs in JS.
     */
    async getRecentFunctionalEvaluations(params) {
        const limit = Math.min(Math.max(1, params.limit ?? 20), 100);
        const rows = await this.getFunctionalEvalResultRows(params, {
            orderByTestRunCreatedAtDesc: true,
            rowLimit: 2000,
        });
        const seenTestRunIds = new Set();
        const testRunsOrdered = [];
        for (const row of rows ?? []) {
            const id = row.testRunId ?? '';
            if (!id || seenTestRunIds.has(id))
                continue;
            seenTestRunIds.add(id);
            testRunsOrdered.push({ id, createdAt: row.testRunCreatedAt ?? null });
            if (testRunsOrdered.length >= limit)
                break;
        }
        const failuresByTestRun = new Map();
        for (const row of rows ?? []) {
            const tid = row.testRunId ?? '';
            if (!tid || !seenTestRunIds.has(tid))
                continue;
            const components = row.gradingResult?.componentResults ?? [];
            for (const c of components ?? []) {
                if (c?.pass !== true) {
                    failuresByTestRun.set(tid, (failuresByTestRun.get(tid) ?? 0) + 1);
                }
            }
        }
        return testRunsOrdered.map((tr) => ({
            id: tr.id,
            createdAt: tr.createdAt,
            status: (failuresByTestRun.get(tr.id) ?? 0) > 0 ? 'fail' : 'pass',
        }));
    }
    /**
     * Test case results over time. Logic aligned with getTestRunMetricsGraph:
     * eval IDs from test runs -> eval_results (success) -> attribute to test run bucket (day/week/month by range).
     * Returns graph data for stacked bar chart: { date, total, pass, fail, error } per bucket.
     */
    async getTestCaseResultsOverTime(params) {
        const db = await (0, database_1.getDb)();
        const conditions = this.buildTestRunConditions(params);
        const startDate = new Date(params.timeLimit.start);
        const endDate = new Date(params.timeLimit.end);
        const bucketUnit = this.getBucketUnit(startDate, endDate);
        const bucketKeys = this.buildBucketKeys(startDate, endDate, bucketUnit);
        const testRuns = await db
            .select({ id: tables_1.testRunsTable.id, createdAt: tables_1.testRunsTable.createdAt })
            .from(tables_1.testRunsTable)
            .where((0, drizzle_orm_1.and)(...conditions));
        const byBucket = new Map();
        for (const key of bucketKeys)
            byBucket.set(key, { pass: 0, fail: 0, error: 0 });
        if (testRuns.length > 0) {
            const testRunIds = testRuns.map((tr) => tr.id);
            const testRunToEvals = await db
                .select({ testRunId: tables_1.testRunsToEvalsTable.testRunId, evalId: tables_1.testRunsToEvalsTable.evalId })
                .from(tables_1.testRunsToEvalsTable)
                .where((0, drizzle_orm_1.inArray)(tables_1.testRunsToEvalsTable.testRunId, testRunIds));
            if (testRunToEvals.length > 0) {
                const testRunBucketMap = new Map();
                for (const tr of testRuns) {
                    const bucketDate = this.floorToBucketUtc(new Date(tr.createdAt), bucketUnit);
                    testRunBucketMap.set(tr.id, this.formatUtcDate(bucketDate));
                }
                const evalToTestRunMap = new Map();
                for (const rel of testRunToEvals) {
                    evalToTestRunMap.set(rel.evalId, rel.testRunId);
                }
                const allEvalIds = Array.from(new Set(testRunToEvals.map((r) => r.evalId)));
                const evalResults = await db
                    .select({ evalId: tables_1.evalResultsTable.evalId, success: tables_1.evalResultsTable.success })
                    .from(tables_1.evalResultsTable)
                    .where((0, drizzle_orm_1.inArray)(tables_1.evalResultsTable.evalId, allEvalIds));
                for (const result of evalResults) {
                    const testRunId = evalToTestRunMap.get(result.evalId);
                    if (!testRunId)
                        continue;
                    const bucketKey = testRunBucketMap.get(testRunId);
                    if (!bucketKey)
                        continue;
                    const cur = byBucket.get(bucketKey);
                    if (!cur)
                        continue;
                    if (result?.success) {
                        cur.pass += 1;
                    }
                    else {
                        cur.fail += 1;
                    }
                }
            }
        }
        return bucketKeys.map((date) => {
            const counts = byBucket.get(date) ?? { pass: 0, fail: 0, error: 0 };
            return {
                date,
                total: counts.pass + counts.fail + counts.error,
                pass: counts.pass,
                fail: counts.fail,
                error: counts.error,
            };
        });
    }
    /**
     * Top metric failures. One joined query, aggregate by type in JS.
     */
    async getTopMetricFailures(params) {
        const rows = await this.getFunctionalEvalResultRows(params);
        const byType = {};
        for (const row of rows ?? []) {
            const grading = row.gradingResult;
            const components = grading?.componentResults ?? [];
            if (!Array.isArray(components))
                continue;
            for (const c of components) {
                const label = c?.assertion?.displayLabel ?? c?.assertion?.type ?? c?.type ?? 'Unknown';
                const typeKey = String(label || 'Unknown').trim() || 'Unknown';
                if (!byType[typeKey]) {
                    byType[typeKey] = {
                        successCount: 0,
                        failureCount: 0,
                        errorCount: 0,
                        scoreSum: 0,
                        scoreN: 0,
                    };
                }
                if (c?.pass === true)
                    byType[typeKey].successCount += 1;
                else
                    byType[typeKey].failureCount += 1;
                const s = c?.score;
                if (typeof s === 'number' && !Number.isNaN(s)) {
                    byType[typeKey].scoreSum += s;
                    byType[typeKey].scoreN += 1;
                }
            }
        }
        return Object.entries(byType)
            .filter(([, v]) => v.failureCount > 0)
            .map(([type, v]) => ({
            type,
            successCount: v.successCount,
            failureCount: v.failureCount,
            errorCount: v.errorCount,
            avgScore: v.scoreN > 0 ? v.scoreSum / v.scoreN : 0,
        }))
            .sort((a, b) => b.failureCount - a.failureCount)
            .slice(0, 50);
    }
}
exports.FunctionalChartsService = FunctionalChartsService;
FunctionalChartsService.instance = null;
exports.functionalChartsService = FunctionalChartsService.getInstance();
//# sourceMappingURL=functionalChartsService.js.map