import { MongoClient } from 'mongodb';
import { Logger } from '../utils/logger.js';
/**
 * MongoDB client wrapper for persistent storage.
 */
export class MongoManager {
    config;
    static instance;
    client = null;
    db = null;
    collection = null;
    constructor(config) {
        this.config = config;
    }
    static async getInstance(config) {
        if (!this.instance) {
            if (!config)
                throw new Error('MongoConfig required for initial connection');
            this.instance = new MongoManager(config);
        }
        return this.instance;
    }
    /**
     * Connects to the database with retry logic.
     */
    async connect() {
        if (!this.config.enabled)
            return;
        if (this.client && this.db)
            return; // Already connected
        try {
            this.client = new MongoClient(this.config.uri);
            await this.client.connect();
            this.db = this.client.db(this.config.dbName);
            this.collection = this.db.collection(this.config.collection);
            // Ensure unique index on doc_hash for deduplication
            await this.collection.createIndex({ doc_hash: 1 }, { unique: true, background: true });
            // Ensure unique indexes for system collections
            await this.db.collection('organizations').createIndex({ organizationId: 1 }, { unique: true, background: true });
            await this.db.collection('users').createIndex({ email: 1 }, { unique: true, background: true });
            Logger.log(`Connected to MongoDB: ${this.config.dbName}.${this.config.collection}`);
        }
        catch (error) {
            Logger.error('Failed to connect to MongoDB', error);
            this.client = null;
            // Pipeline should continue even if DB is down
        }
    }
    /**
     * Saves a document to a specific collection.
     */
    async saveTo(collectionName, document) {
        if (!this.db) {
            Logger.warn('Cannot save to MongoDB: No connection initialized.');
            return;
        }
        try {
            const col = this.db.collection(collectionName);
            await col.insertOne(document);
            Logger.log(`Record persisted to ${collectionName}`);
        }
        catch (error) {
            Logger.error(`Error inserting document into ${collectionName}`, error);
        }
    }
    /**
     * Finds a single document in a collection.
     */
    async findOne(collectionName, query = {}) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return null;
        }
        try {
            const col = this.db.collection(collectionName);
            return await col.findOne(query);
        }
        catch (error) {
            Logger.error(`Error finding document in ${collectionName}`, error);
            return null;
        }
    }
    /**
     * Updates documents in a specific collection.
     */
    async updateTo(collectionName, query, update) {
        if (!this.db)
            return;
        try {
            const col = this.db.collection(collectionName);
            await col.updateMany(query, { $set: update });
            Logger.log(`Documents updated in ${collectionName}`);
        }
        catch (error) {
            Logger.error(`Error updating documents in ${collectionName}`, error);
        }
    }
    async saveScreenshot(runId, batchId, screenshotBase64) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return;
        }
        try {
            const col = this.db.collection('job_screenshots');
            // Ensure TTL index (30 days = 2592000 seconds)
            await col.createIndex({ createdAt: 1 }, { expireAfterSeconds: 2592000 });
            await col.updateOne({ runId }, {
                $set: {
                    batchId,
                    screenshot: screenshotBase64,
                    createdAt: new Date()
                }
            }, { upsert: true });
        }
        catch (error) {
            Logger.error(`Error saving screenshot for run ${runId}`, error);
        }
    }
    /**
     * Saves a log entry for a specific run with TTL support.
     */
    async saveRunLog(runId, batchId, logEntry, organizationId) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return;
        }
        try {
            const col = this.db.collection('job_run_logs');
            // Ensure TTL index (30 days = 2592000 seconds)
            await col.createIndex({ createdAt: 1 }, { expireAfterSeconds: 2592000 });
            await col.createIndex({ runId: 1 });
            await col.insertOne({
                runId,
                batchId,
                organizationId,
                ...logEntry,
                createdAt: new Date()
            });
        }
        catch (error) {
            // Avoid infinite recursion by not using Logger.error here if it triggers another log emission
            console.error(`[MongoManager] Failed to save run log:`, error);
        }
    }
    /**
     * Retrieves logs for a specific run with pagination support.
     */
    async getRunLogs(runId, limit = 50, skip = 0) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return [];
        }
        try {
            const col = this.db.collection('job_run_logs');
            // Search by either runId or batchId to ensure all related logs are captured
            return await col.find({
                $or: [
                    { runId: runId },
                    { batchId: runId }
                ]
            })
                .sort({ createdAt: -1 }) // Latest first for efficient streaming
                .skip(skip)
                .limit(limit)
                .toArray();
        }
        catch (error) {
            console.error(`[MongoManager] Failed to fetch logs for run ${runId}:`, error);
            return [];
        }
    }
    /**
     * Retrieves the screenshot for a specific run.
     */
    async getScreenshot(runId) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return null;
        }
        try {
            const col = this.db.collection('job_screenshots');
            const doc = await col.findOne({ runId });
            return doc ? doc.screenshot : null;
        }
        catch (error) {
            Logger.error(`Error fetching screenshot for run ${runId}`, error);
            return null;
        }
    }
    /**
     * Updates a job run record based on runId.
     */
    async upsertRun(collectionName, runId, data) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return;
        }
        try {
            const col = this.db.collection(collectionName);
            await col.updateOne({ runId }, { $set: { ...data, updatedAt: new Date() } }, { upsert: true });
        }
        catch (error) {
            Logger.error(`Error upserting run into ${collectionName}`, error);
        }
    }
    /**
     * Queries a collection.
     */
    async find(collectionName, query, limit = 100, skip = 0) {
        await this.connect();
        if (!this.db)
            return [];
        try {
            const col = this.db.collection(collectionName);
            return await col.find(query)
                .sort({ createdAt: -1, startTime: -1 })
                .skip(skip)
                .limit(limit)
                .toArray();
        }
        catch (error) {
            Logger.error(`Error querying collection ${collectionName}`, error);
            return [];
        }
    }
    /**
     * Saves a document to the default collection.
     */
    async save(document) {
        if (!this.collection) {
            Logger.warn('Cannot save to MongoDB: No default collection initialized.');
            return;
        }
        await this.saveTo(this.config.collection, document);
    }
    /**
     * Lists all non-system collections in the database.
     */
    async listCollections() {
        if (!this.db)
            return [];
        try {
            const collections = await this.db.listCollections().toArray();
            // Filter out system and history collections
            return collections
                .map(c => c.name)
                .filter(name => !name.startsWith('system.') && name !== 'job_history');
        }
        catch (error) {
            Logger.error('Error listing collections', error);
            return [];
        }
    }
    /**
     * Samples a collection to find groupable fields (including nested ones).
     */
    async getSampleFields(collectionName) {
        if (!this.db)
            return [];
        try {
            const col = this.db.collection(collectionName);
            const sample = await col.findOne({});
            if (!sample)
                return [];
            const fields = [];
            const extract = (obj, prefix = '') => {
                for (const key in obj) {
                    const val = obj[key];
                    const path = prefix ? `${prefix}.${key}` : key;
                    if (key === '_id' || key === 'doc_hash' || key === 'logs')
                        continue;
                    if (val && typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length < 20) {
                        extract(val, path);
                    }
                    else if (typeof val === 'string' || typeof val === 'number') {
                        fields.push(path);
                    }
                }
            };
            extract(sample);
            return fields;
        }
        catch (error) {
            Logger.error(`Error sampling fields for ${collectionName}`, error);
            return [];
        }
    }
    /**
     * Executes an aggregation pipeline.
     */
    async aggregate(collectionName, pipeline) {
        if (!this.db)
            return [];
        try {
            const col = this.db.collection(collectionName);
            return await col.aggregate(pipeline).toArray();
        }
        catch (error) {
            Logger.error(`Aggregation failed for ${collectionName}`, error);
            return [];
        }
    }
    /**
     * General purpose upsert for system records (Organizations, Users, Settings, etc).
     */
    async upsertRecord(collectionName, filterKey, idValue, data) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return;
        }
        try {
            const col = this.db.collection(collectionName);
            // Prevent conflicts by stripping immutable metadata from the $set part
            const { _id, createdAt, ...updateData } = data;
            await col.updateOne({ [filterKey]: idValue }, {
                $set: { ...updateData, updatedAt: new Date() },
                $setOnInsert: { createdAt: new Date() }
            }, { upsert: true });
            Logger.log(`[${collectionName}] Persisted: ${idValue}`);
        }
        catch (error) {
            Logger.error(`Error upserting record into ${collectionName}`, error);
        }
    }
    /**
     * Deletes records matching a query.
     */
    async deleteRecords(collectionName, query) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return;
        }
        try {
            const col = this.db.collection(collectionName);
            await col.deleteMany(query);
            Logger.log(`[${collectionName}] Deleted records matching query`);
        }
        catch (error) {
            Logger.error(`Error deleting records from ${collectionName}`, error);
        }
    }
    /**
     * Deprecated: Use upsertRecord instead.
     * Upserts a test definition (Scenario, Flow, or SelectorSet) using 'name' as default key.
     */
    async upsertTest(collectionName, name, data) {
        return this.upsertRecord(collectionName, 'name', name, data);
    }
    /**
     * Retrieves a specific test definition by name.
     */
    async getTest(collectionName, name) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return null;
        }
        try {
            const col = this.db.collection(collectionName);
            return await col.findOne({ name });
        }
        catch (error) {
            Logger.error(`Error fetching test [${name}] from ${collectionName}`, error);
            return null;
        }
    }
    /**
     * Queries test definitions (e.g. by tag or namespace).
     */
    async findTests(collectionName, query, limit = 100, skip = 0) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return [];
        }
        try {
            const col = this.db.collection(collectionName);
            return await col.find(query)
                .sort({ createdAt: -1, startTime: -1 })
                .skip(skip)
                .limit(limit)
                .toArray();
        }
        catch (error) {
            Logger.error(`Error querying tests in ${collectionName}`, error);
            return [];
        }
    }
    /**
     * Deletes documents from a specific collection.
     */
    async deleteFrom(collectionName, query) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return;
        }
        try {
            const col = this.db.collection(collectionName);
            await col.deleteMany(query);
            Logger.log(`Documents deleted from ${collectionName}`);
        }
        catch (error) {
            Logger.error(`Error deleting documents from ${collectionName}`, error);
        }
    }
    /**
     * Deletes a specific test definition by name.
     */
    async deleteTest(collectionName, name) {
        await this.deleteFrom(collectionName, { name });
    }
    /**
     * Fetches distinct values for a field.
     */
    async distinct(collectionName, fieldName, query = {}) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return [];
        }
        try {
            const col = this.db.collection(collectionName);
            return await col.distinct(fieldName, query);
        }
        catch (error) {
            Logger.error(`Error fetching distinct ${fieldName} from ${collectionName}`, error);
            return [];
        }
    }
    /**
     * Updates multiple documents in a collection.
     */
    async updateMany(collectionName, query, update) {
        if (!this.db) {
            await this.connect();
            if (!this.db)
                return;
        }
        try {
            const col = this.db.collection(collectionName);
            await col.updateMany(query, { $set: update });
        }
        catch (error) {
            Logger.error(`Error updating documents in ${collectionName}`, error);
        }
    }
    /**
     * Closes the connection.
     */
    async close() {
        if (this.client) {
            await this.client.close();
            this.client = null;
            this.db = null;
            this.collection = null;
            Logger.log('MongoDB connection closed.');
        }
    }
}
