"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.syncEvalResultsFromPostgresToClickhouse = syncEvalResultsFromPostgresToClickhouse;
const date_fns_1 = require("date-fns");
const drizzle_orm_1 = require("drizzle-orm");
const database_1 = require("../../database");
const tables_1 = require("../../database/tables");
const logger_1 = __importDefault(require("../../logger"));
const clickhouseService_1 = require("./clickhouseService");
const DEFAULT_BATCH_SIZE = 250;
const DEFAULT_MAX_BATCHES = 50000;
/** Physical column order expected by Drizzle `eval_results` (snake_case); intersected with information_schema at runtime. */
const EVAL_RESULTS_PG_COLUMNS = [
    'id',
    'created_at',
    'updated_at',
    'eval_id',
    'prompt_idx',
    'test_idx',
    'test_case',
    'prompt',
    'prompt_id',
    'application_id',
    'session_id',
    'trace_id',
    'project_id',
    'provider',
    'latency_ms',
    'cost',
    'response',
    'error',
    'failure_reason',
    'success',
    'score',
    'grading_result',
    'named_scores',
    'metadata',
    'is_redteam',
    'workspace_id',
    'parent_prompt_id',
    'dataset_record_id',
    'trace_ids',
];
function toClickhouseTimestamp(value) {
    const d = value instanceof Date ? value : new Date(value);
    return (0, date_fns_1.format)(d, 'yyyy-MM-dd HH:mm:ss');
}
function normalizeExecuteRows(result) {
    if (Array.isArray(result)) {
        return result;
    }
    if (result !== null &&
        typeof result === 'object' &&
        'rows' in result &&
        Array.isArray(result.rows)) {
        return result.rows;
    }
    return [];
}
async function loadExistingEvalResultsColumnNames(db) {
    const raw = await db.execute((0, drizzle_orm_1.sql) `
    SELECT column_name::text AS column_name
    FROM information_schema.columns
    WHERE table_schema = 'public'
      AND table_name = 'eval_results'
  `);
    const rows = normalizeExecuteRows(raw);
    const set = new Set();
    for (const row of rows) {
        const name = row.column_name;
        if (typeof name === 'string' && name.length > 0) {
            set.add(name);
        }
    }
    return set;
}
function buildQuotedSelectList(existing) {
    const selected = EVAL_RESULTS_PG_COLUMNS.filter((c) => existing.has(c));
    if (!selected.includes('id')) {
        throw new Error('Postgres eval_results table has no "id" column; cannot sync.');
    }
    const safe = /^[a-z0-9_]+$/iu;
    for (const c of selected) {
        if (!safe.test(c)) {
            throw new Error(`Invalid column name for eval_results sync: ${c}`);
        }
    }
    return selected.map((c) => `"${c}"`).join(', ');
}
/** Maps a snake_case row (from raw SELECT) to the ClickHouse insert payload. */
function mapSnakeRowToClickHouseRow(row, testRunId) {
    const pick = (k) => row[k];
    const createdAt = pick('created_at');
    const updatedAt = pick('updated_at');
    return {
        id: String(pick('id') ?? ''),
        created_at: toClickhouseTimestamp((createdAt instanceof Date ? createdAt : String(createdAt))),
        updated_at: toClickhouseTimestamp((updatedAt instanceof Date ? updatedAt : String(updatedAt))),
        eval_id: String(pick('eval_id') ?? ''),
        prompt_idx: Number(pick('prompt_idx') ?? 0),
        test_idx: Number(pick('test_idx') ?? 0),
        test_case: pick('test_case') ?? {},
        prompt: pick('prompt') ?? {},
        prompt_id: pick('prompt_id') ?? null,
        application_id: pick('application_id') ?? null,
        project_id: pick('project_id') ?? null,
        workspace_id: pick('workspace_id') ?? null,
        session_id: pick('session_id') ?? null,
        trace_id: pick('trace_id') ?? null,
        latency_ms: pick('latency_ms') == null ? null : Number(pick('latency_ms')),
        cost: pick('cost') == null ? 0 : Number(pick('cost')),
        provider: pick('provider') ?? {},
        response: pick('response') ?? null,
        error: pick('error') ?? null,
        failure_reason: Number(pick('failure_reason') ?? 0),
        success: Number(pick('success') ?? 0),
        score: Number(pick('score') ?? 0),
        grading_result: pick('grading_result') ?? null,
        named_scores: pick('named_scores') ?? null,
        metadata: pick('metadata') ?? {},
        is_redteam: pick('is_redteam') == null ? 0 : Number(pick('is_redteam')),
        trace_ids: pick('trace_ids') ?? null,
        test_run_id: testRunId ?? null,
    };
}
/**
 * Copies all Postgres `eval_results` rows into ClickHouse `eval_results`, setting `test_run_id`
 * from `test_runs_to_evals` (first `test_run_id` per `eval_id` when multiple links exist).
 *
 * Not idempotent: a second run inserts duplicate rows unless ClickHouse deduplicates (e.g. ReplacingMergeTree).
 *
 * Selects only columns that exist in Postgres (`information_schema`) so older DBs without newer
 * columns (e.g. `session_id`, `trace_ids`) still sync; missing fields are sent as null/defaults to ClickHouse.
 */
async function syncEvalResultsFromPostgresToClickhouse(options) {
    const batchSize = Math.min(500, Math.max(1, Math.floor(options?.batchSize ?? DEFAULT_BATCH_SIZE)));
    const maxBatches = Math.max(1, Math.floor(options?.maxBatches ?? DEFAULT_MAX_BATCHES));
    const db = (0, database_1.getDb)();
    const existingCols = await loadExistingEvalResultsColumnNames(db);
    const quotedSelect = buildQuotedSelectList(existingCols);
    logger_1.default.info(`syncEvalResultsFromPostgresToClickhouse: Postgres eval_results columns in use (${EVAL_RESULTS_PG_COLUMNS.filter((c) => existingCols.has(c)).length}): ${EVAL_RESULTS_PG_COLUMNS.filter((c) => existingCols.has(c)).join(', ')}`);
    let inserted = 0;
    let batches = 0;
    let rowsMissingTestRunLink = 0;
    let cursorId = null;
    while (batches < maxBatches) {
        const limit = batchSize;
        const query = cursorId === null
            ? `SELECT ${quotedSelect} FROM "eval_results" ORDER BY "id" ASC LIMIT ${limit}`
            : `SELECT ${quotedSelect} FROM "eval_results" WHERE "id" > '${cursorId.replace(/'/g, "''")}' ORDER BY "id" ASC LIMIT ${limit}`;
        let evalRows;
        try {
            evalRows = normalizeExecuteRows(await db.execute(drizzle_orm_1.sql.raw(query)));
        }
        catch (err) {
            const pg = err;
            const detail = pg?.cause?.detail ?? pg?.cause?.message ?? pg?.message ?? String(err);
            logger_1.default.error(`syncEvalResultsFromPostgresToClickhouse: Postgres query failed: ${detail}`);
            throw err;
        }
        if (evalRows.length === 0) {
            break;
        }
        const evalIdSet = new Set();
        for (const r of evalRows) {
            const eid = r.eval_id;
            if (typeof eid === 'string' && eid.length > 0) {
                evalIdSet.add(eid);
            }
        }
        const evalIds = Array.from(evalIdSet);
        const links = evalIds.length === 0
            ? []
            : await db
                .select({
                evalId: tables_1.testRunsToEvalsTable.evalId,
                testRunId: tables_1.testRunsToEvalsTable.testRunId,
            })
                .from(tables_1.testRunsToEvalsTable)
                .where((0, drizzle_orm_1.inArray)(tables_1.testRunsToEvalsTable.evalId, evalIds))
                .orderBy((0, drizzle_orm_1.asc)(tables_1.testRunsToEvalsTable.evalId), (0, drizzle_orm_1.asc)(tables_1.testRunsToEvalsTable.testRunId));
        const evalToTestRun = new Map();
        for (const link of links) {
            if (!evalToTestRun.has(link.evalId)) {
                evalToTestRun.set(link.evalId, link.testRunId);
            }
        }
        const chRows = [];
        for (const row of evalRows) {
            const evalId = typeof row.eval_id === 'string' ? row.eval_id : '';
            const testRunId = evalToTestRun.get(evalId);
            if (testRunId == null) {
                rowsMissingTestRunLink += 1;
            }
            chRows.push(mapSnakeRowToClickHouseRow(row, testRunId));
        }
        await clickhouseService_1.clickHouseService.insert('eval_results', chRows);
        inserted += chRows.length;
        batches += 1;
        const lastId = evalRows[evalRows.length - 1]?.id;
        cursorId = typeof lastId === 'string' ? lastId : lastId == null ? null : String(lastId);
        logger_1.default.info(`syncEvalResultsFromPostgresToClickhouse: batch ${batches}, inserted ${chRows.length} (total ${inserted})`);
        if (evalRows.length < batchSize) {
            break;
        }
    }
    return { inserted, batches, rowsMissingTestRunLink };
}
//# sourceMappingURL=evalResultsPostgresToClickhouseSync.js.map