import { randomUUID } from 'crypto';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { loadUapConfig } from '../utils/config-loader.js';
import { CoordinationDatabase, getDefaultCoordinationDbPath } from './database.js';
import { execFile as execFileCb } from 'child_process';
import { isParallelEnabled, getMaxParallel } from '../utils/system-resources.js';
import { concurrentMapSettled } from '../utils/concurrency-pool.js';
const DEFAULT_DYNAMIC_WINDOWS = {
    commit: 30000, // 30s - allows squashing multiple commits
    push: 5000, // 5s - fast for PR creation
    merge: 10000, // 10s - moderate safety
    workflow: 5000, // 5s - fast workflow triggers
    deploy: 60000, // 60s - safety buffer for deployments
};
export class DeployBatcher {
    db;
    dynamicWindows;
    maxBatchSize;
    dryRun;
    parallelExecution;
    maxParallelActions;
    execTimeoutMs;
    constructor(config = {}) {
        const dbPath = config.dbPath || getDefaultCoordinationDbPath();
        this.db = CoordinationDatabase.getInstance(dbPath).getDatabase();
        // Support both legacy single window and new dynamic windows
        // T7: Also check .uap.json timeOptimization.batchWindows for project-level config
        if (config.dynamicWindows) {
            this.dynamicWindows = { ...DEFAULT_DYNAMIC_WINDOWS, ...config.dynamicWindows };
        }
        else if (config.batchWindowMs) {
            // Legacy mode: use single window for all types
            this.dynamicWindows = {
                commit: config.batchWindowMs,
                push: config.batchWindowMs,
                merge: config.batchWindowMs,
                workflow: config.batchWindowMs,
                deploy: config.batchWindowMs,
            };
        }
        else {
            // Try loading from .uap.json timeOptimization config
            let loaded = false;
            try {
                const cfg = loadUapConfig();
                const bw = cfg?.timeOptimization?.batchWindows;
                if (bw && typeof bw === 'object') {
                    this.dynamicWindows = { ...DEFAULT_DYNAMIC_WINDOWS, ...bw };
                    loaded = true;
                }
            }
            catch {
                // Config load failure is non-fatal
            }
            if (!loaded) {
                this.dynamicWindows = DEFAULT_DYNAMIC_WINDOWS;
            }
        }
        this.maxBatchSize = config.maxBatchSize || 20;
        this.dryRun = config.dryRun || false;
        this.parallelExecution = config.parallelExecution ?? isParallelEnabled();
        this.maxParallelActions = config.maxParallelActions || getMaxParallel('io');
        this.execTimeoutMs = config.execTimeoutMs ?? 300000; // Default: 300s (5 minutes)
        // Validate window values
        const windowEntries = Object.entries(this.dynamicWindows);
        for (const [key, value] of windowEntries) {
            if (value < 1000) {
                // Validation warning - below minimum recommended window
                void `DeployBatcher: window '${key}' is ${value}ms, below minimum 1000ms`;
            }
            if (value > 300000) {
                // Validation warning - exceeds maximum recommended window
                void `DeployBatcher: window '${key}' is ${value}ms, exceeds maximum 300000ms`;
            }
        }
    }
    /**
     * Get the batch window for a specific action type.
     */
    getBatchWindow(actionType) {
        return this.dynamicWindows[actionType] || DEFAULT_DYNAMIC_WINDOWS.commit;
    }
    /**
     * Update batch windows dynamically (useful for urgent operations).
     */
    setUrgentMode(urgent) {
        if (urgent) {
            // Reduce all windows to minimum for urgent operations
            this.dynamicWindows = {
                commit: 2000,
                push: 1000,
                merge: 2000,
                workflow: 1000,
                deploy: 5000,
            };
        }
        else {
            // Restore defaults
            this.dynamicWindows = { ...DEFAULT_DYNAMIC_WINDOWS };
        }
    }
    /**
     * Queue a deploy action with type-specific batching delay.
     */
    async queue(agentId, actionType, target, payload, options = {}) {
        const now = new Date().toISOString();
        // Use type-specific window, or immediate for urgent
        const windowMs = options.urgent ? 1000 : this.getBatchWindow(actionType);
        const executeAfter = new Date(Date.now() + windowMs).toISOString();
        // Check for similar pending action to merge
        const existing = this.findSimilarAction(actionType, target);
        if (existing && this.canMerge(existing, { actionType, target, payload })) {
            await this.mergeActions(existing.id, payload);
            return existing.id;
        }
        const stmt = this.db.prepare(`
      INSERT INTO deploy_queue (agent_id, action_type, target, payload, status, queued_at, execute_after, priority, dependencies)
      VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
    `);
        const result = stmt.run(agentId, actionType, target, payload ? JSON.stringify(payload) : null, now, executeAfter, options.priority || 5, options.dependencies ? JSON.stringify(options.dependencies) : null);
        return result.lastInsertRowid;
    }
    /**
     * Queue multiple actions atomically with optimized windows.
     */
    async queueBulk(agentId, actions) {
        const ids = [];
        const transaction = this.db.transaction(() => {
            for (const action of actions) {
                const id = this.queueSync(agentId, action.actionType, action.target, action.payload, {
                    priority: action.priority,
                });
                ids.push(id);
            }
        });
        transaction();
        return ids;
    }
    /**
     * Synchronous queue for use in transactions.
     */
    queueSync(agentId, actionType, target, payload, options = {}) {
        const now = new Date().toISOString();
        const windowMs = this.getBatchWindow(actionType);
        const executeAfter = new Date(Date.now() + windowMs).toISOString();
        const stmt = this.db.prepare(`
      INSERT INTO deploy_queue (agent_id, action_type, target, payload, status, queued_at, execute_after, priority, dependencies)
      VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
    `);
        const result = stmt.run(agentId, actionType, target, payload ? JSON.stringify(payload) : null, now, executeAfter, options.priority || 5, options.dependencies ? JSON.stringify(options.dependencies) : null);
        return result.lastInsertRowid;
    }
    findSimilarAction(actionType, target) {
        const stmt = this.db.prepare(`
      SELECT id, agent_id as agentId, action_type as actionType, target, payload,
             status, batch_id as batchId, queued_at as queuedAt, 
             execute_after as executeAfter, priority, dependencies
      FROM deploy_queue
      WHERE action_type = ? AND target = ? AND status = 'pending'
      ORDER BY queued_at DESC
      LIMIT 1
    `);
        const row = stmt.get(actionType, target);
        if (!row)
            return null;
        return {
            ...row,
            payload: row.payload ? JSON.parse(row.payload) : undefined,
            dependencies: row.dependencies ? JSON.parse(row.dependencies) : undefined,
        };
    }
    canMerge(existing, incoming) {
        if (existing.actionType !== incoming.actionType || existing.target !== incoming.target) {
            return false;
        }
        // Commits can be squashed
        if (existing.actionType === 'commit') {
            return true;
        }
        // Pushes to same branch can be merged
        if (existing.actionType === 'push') {
            return true;
        }
        // Workflow triggers can be deduplicated
        if (existing.actionType === 'workflow') {
            return true;
        }
        return false;
    }
    async mergeActions(existingId, newPayload) {
        if (!newPayload)
            return;
        const stmt = this.db.prepare(`
      SELECT payload FROM deploy_queue WHERE id = ?
    `);
        const row = stmt.get(existingId);
        if (!row)
            return;
        const parsed = row.payload ? JSON.parse(row.payload) : {};
        const existingPayload = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
        const merged = this.mergePayloads(existingPayload, newPayload);
        const updateStmt = this.db.prepare(`
      UPDATE deploy_queue SET payload = ? WHERE id = ?
    `);
        updateStmt.run(JSON.stringify(merged), existingId);
    }
    mergePayloads(existing, incoming) {
        const result = { ...existing };
        for (const [key, value] of Object.entries(incoming)) {
            if (Array.isArray(value) && Array.isArray(result[key])) {
                result[key] = [...result[key], ...value];
            }
            else if (key === 'messages' && Array.isArray(value)) {
                result[key] = [...(result[key] || []), ...value];
            }
            else {
                result[key] = value;
            }
        }
        return result;
    }
    /**
     * Create a batch from ready actions.
     */
    async createBatch() {
        const now = new Date().toISOString();
        const stmt = this.db.prepare(`
      SELECT id, agent_id as agentId, action_type as actionType, target, payload,
             status, batch_id as batchId, queued_at as queuedAt, 
             execute_after as executeAfter, priority, dependencies
      FROM deploy_queue
      WHERE status = 'pending' AND execute_after <= ?
      ORDER BY priority DESC, queued_at ASC
      LIMIT ?
    `);
        const rows = stmt.all(now, this.maxBatchSize);
        if (rows.length === 0)
            return null;
        const actions = rows.map((row) => ({
            ...row,
            payload: row.payload ? JSON.parse(row.payload) : undefined,
            dependencies: row.dependencies ? JSON.parse(row.dependencies) : undefined,
        }));
        const grouped = this.groupByTarget(actions);
        const squashed = this.squashActions(grouped);
        const batchId = randomUUID();
        const updateStmt = this.db.prepare(`
      UPDATE deploy_queue SET status = 'batched', batch_id = ? WHERE id = ?
    `);
        const transaction = this.db.transaction(() => {
            for (const action of actions) {
                updateStmt.run(batchId, action.id);
            }
            this.db
                .prepare(`
        INSERT INTO deploy_batches (id, created_at, status)
        VALUES (?, ?, 'pending')
      `)
                .run(batchId, now);
        });
        transaction();
        return {
            id: batchId,
            actions: squashed,
            createdAt: now,
            status: 'pending',
        };
    }
    groupByTarget(actions) {
        const groups = new Map();
        for (const action of actions) {
            const key = `${action.actionType}:${action.target}`;
            const existing = groups.get(key) || [];
            existing.push(action);
            groups.set(key, existing);
        }
        return groups;
    }
    squashActions(grouped) {
        const result = [];
        for (const [, actions] of grouped) {
            if (actions.length === 0)
                continue;
            if (actions.length === 1) {
                result.push(actions[0]);
                continue;
            }
            const first = actions[0];
            if (first.actionType === 'commit') {
                const squashed = this.squashCommits(actions);
                result.push(squashed);
                continue;
            }
            if (first.actionType === 'push') {
                result.push(first);
                continue;
            }
            if (first.actionType === 'workflow') {
                result.push(first);
                continue;
            }
            result.push(...actions);
        }
        return result;
    }
    squashCommits(commits) {
        const messages = [];
        const allFiles = [];
        for (const commit of commits) {
            const payload = commit.payload;
            if (payload?.message) {
                messages.push(payload.message);
            }
            if (payload?.files) {
                allFiles.push(...payload.files);
            }
        }
        const squashedMessage = messages.length === 1
            ? messages[0]
            : `Squashed ${messages.length} commits:\n\n${messages.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
        return {
            ...commits[0],
            payload: {
                message: squashedMessage,
                files: [...new Set(allFiles)],
                squashedFrom: commits.map((c) => c.id),
            },
        };
    }
    /**
     * Execute a batch with optional parallel execution for independent actions.
     */
    async executeBatch(batchId) {
        const startTime = Date.now();
        const errors = [];
        let executed = 0;
        let failed = 0;
        const stmt = this.db.prepare(`
      SELECT id, agent_id as agentId, action_type as actionType, target, payload,
             status, batch_id as batchId, queued_at as queuedAt, 
             execute_after as executeAfter, priority, dependencies
      FROM deploy_queue
      WHERE batch_id = ? AND status = 'batched'
      ORDER BY priority DESC, queued_at ASC
    `);
        const rows = stmt.all(batchId);
        const actions = rows.map((row) => ({
            ...row,
            payload: row.payload ? JSON.parse(row.payload) : undefined,
            dependencies: row.dependencies ? JSON.parse(row.dependencies) : undefined,
        }));
        this.db
            .prepare(`
      UPDATE deploy_batches SET status = 'executing', executed_at = ? WHERE id = ?
    `)
            .run(new Date().toISOString(), batchId);
        if (this.parallelExecution) {
            // Group independent actions for parallel execution
            const { sequential, parallel } = this.categorizeActions(actions);
            // Execute parallel-safe actions concurrently
            if (parallel.length > 0) {
                const parallelResults = await this.executeParallel(parallel);
                for (const result of parallelResults) {
                    if (result.success) {
                        executed++;
                    }
                    else {
                        failed++;
                        if (result.error)
                            errors.push(result.error);
                    }
                }
            }
            // Execute sequential actions in order
            for (const action of sequential) {
                try {
                    await this.executeAction(action);
                    this.updateActionStatus(action.id, 'completed');
                    executed++;
                }
                catch (error) {
                    const errorMsg = error instanceof Error ? error.message : String(error);
                    errors.push(`Action ${action.id} (${action.actionType}): ${errorMsg}`);
                    this.updateActionStatus(action.id, 'failed');
                    failed++;
                }
            }
        }
        else {
            // Original sequential execution
            for (const action of actions) {
                try {
                    await this.executeAction(action);
                    this.updateActionStatus(action.id, 'completed');
                    executed++;
                }
                catch (error) {
                    const errorMsg = error instanceof Error ? error.message : String(error);
                    errors.push(`Action ${action.id} (${action.actionType}): ${errorMsg}`);
                    this.updateActionStatus(action.id, 'failed');
                    failed++;
                }
            }
        }
        const batchStatus = failed === 0 ? 'completed' : executed > 0 ? 'completed' : 'failed';
        this.db
            .prepare(`
      UPDATE deploy_batches SET status = ?, result = ? WHERE id = ?
    `)
            .run(batchStatus, JSON.stringify({ executed, failed, errors }), batchId);
        return {
            batchId,
            success: failed === 0,
            executedActions: executed,
            failedActions: failed,
            errors: errors.length > 0 ? errors : undefined,
            duration: Date.now() - startTime,
        };
    }
    /**
     * Categorize actions into parallel-safe and sequential groups.
     */
    categorizeActions(actions) {
        const sequential = [];
        const parallel = [];
        // Actions that can run in parallel (no state dependencies)
        const parallelSafeTypes = ['workflow'];
        // Actions that must be sequential (state-dependent)
        const sequentialTypes = ['commit', 'push', 'merge', 'deploy'];
        for (const action of actions) {
            if (parallelSafeTypes.includes(action.actionType)) {
                parallel.push(action);
            }
            else if (sequentialTypes.includes(action.actionType)) {
                sequential.push(action);
            }
            else {
                // Unknown types go sequential for safety
                sequential.push(action);
            }
        }
        return { sequential, parallel };
    }
    /**
     * Execute actions in parallel with concurrency limit.
     * Uses shared concurrentMapSettled utility.
     */
    async executeParallel(actions) {
        const results = [];
        const settled = await concurrentMapSettled(actions, async (action) => {
            await this.executeAction(action);
            return action;
        }, { maxConcurrent: this.maxParallelActions });
        for (let j = 0; j < settled.length; j++) {
            const result = settled[j];
            const action = actions[j];
            if (result.status === 'fulfilled') {
                this.updateActionStatus(action.id, 'completed');
                results.push({ action, success: true });
            }
            else {
                const errorMsg = result.reason instanceof Error ? result.reason.message : String(result.reason);
                this.updateActionStatus(action.id, 'failed');
                results.push({
                    action,
                    success: false,
                    error: `Action ${action.id} (${action.actionType}): ${errorMsg}`,
                });
            }
        }
        return results;
    }
    async executeAction(action) {
        if (this.dryRun) {
            console.log(`[DRY RUN] Would execute: ${action.actionType} on ${action.target}`);
            return;
        }
        const payload = action.payload || {};
        switch (action.actionType) {
            case 'commit':
                await this.executeCommit(action.target, payload);
                break;
            case 'push':
                await this.executePush(action.target, payload);
                break;
            case 'merge':
                await this.executeMerge(action.target, payload);
                break;
            case 'workflow':
                await this.executeWorkflow(action.target, payload);
                break;
            case 'deploy':
                await this.executeDeploy(action.target, payload);
                break;
            default:
                throw new Error(`Unknown action type: ${action.actionType}`);
        }
    }
    async executeCommit(_target, payload) {
        const message = payload.message || 'Automated commit';
        const files = Array.isArray(payload.files) ? payload.files : [];
        // Queued actions may execute from a different cwd than the queuer's
        // (e.g. `uap deliver --deploy --project-root ../service` then a flush
        // from elsewhere) — honor an explicit working directory when present.
        const cwd = typeof payload.cwd === 'string' ? payload.cwd : undefined;
        if (files.length > 0) {
            // `--` stops option parsing: a queued path starting with '-' (e.g. a
            // model-authored file name) must never be interpreted as a git flag.
            await this.execFileWithTimeout('git', ['add', '--', ...files], { cwd });
        }
        else {
            await this.execFileWithTimeout('git', ['add', '-A'], { cwd });
        }
        await this.execFileWithTimeout('git', ['commit', '-m', message], { cwd });
    }
    async executePush(target, payload) {
        const remote = payload.remote || 'origin';
        const force = payload.force || false;
        const args = ['push'];
        if (force)
            args.push('--force-with-lease');
        args.push(remote, target);
        await this.execFileWithTimeout('git', args);
    }
    async executeMerge(_target, payload) {
        const source = payload.source || 'HEAD';
        const squash = payload.squash || false;
        const args = ['merge'];
        if (squash)
            args.push('--squash');
        args.push(source);
        await this.execFileWithTimeout('git', args);
    }
    async executeWorkflow(target, payload) {
        const workflow = target;
        const ref = payload.ref || 'main';
        const inputs = payload.inputs;
        const args = ['workflow', 'run', workflow, '--ref', ref];
        if (inputs) {
            for (const [key, value] of Object.entries(inputs)) {
                args.push('-f', `${key}=${value}`);
            }
        }
        await this.execFileWithTimeout('gh', args);
    }
    async executeDeploy(target, payload) {
        const environment = target;
        const command = payload.command || `deploy-${environment}`;
        // Split command into executable and args to avoid shell injection.
        // Only simple single-command strings are supported; complex shell
        // pipelines should be wrapped in a script file.
        const parts = command.split(/\s+/);
        const [executable, ...args] = parts;
        await this.execFileWithTimeout(executable, args);
    }
    updateActionStatus(actionId, status) {
        const stmt = this.db.prepare(`
      UPDATE deploy_queue SET status = ? WHERE id = ?
    `);
        stmt.run(status, actionId);
    }
    getBatch(batchId) {
        const batchStmt = this.db.prepare(`
      SELECT id, created_at as createdAt, executed_at as executedAt, status, result
      FROM deploy_batches
      WHERE id = ?
    `);
        const batchRow = batchStmt.get(batchId);
        if (!batchRow)
            return null;
        const actionsStmt = this.db.prepare(`
      SELECT id, agent_id as agentId, action_type as actionType, target, payload,
             status, batch_id as batchId, queued_at as queuedAt, 
             execute_after as executeAfter, priority, dependencies
      FROM deploy_queue
      WHERE batch_id = ?
    `);
        const actionRows = actionsStmt.all(batchId);
        const actions = actionRows.map((row) => ({
            ...row,
            payload: row.payload ? JSON.parse(row.payload) : undefined,
            dependencies: row.dependencies ? JSON.parse(row.dependencies) : undefined,
        }));
        return {
            id: batchId,
            actions,
            createdAt: batchRow.createdAt,
            status: batchRow.status,
        };
    }
    getPendingBatches() {
        const stmt = this.db.prepare(`
      SELECT id FROM deploy_batches WHERE status = 'pending'
    `);
        const rows = stmt.all();
        return rows.map((row) => this.getBatch(row.id)).filter((b) => b !== null);
    }
    /**
     * Force flush all pending deploys immediately.
     */
    async flushAll() {
        const results = [];
        let batch = await this.createBatch();
        while (batch) {
            const result = await this.executeBatch(batch.id);
            results.push(result);
            batch = await this.createBatch();
        }
        return results;
    }
    /**
     * Get current batch window configuration.
     */
    getWindowConfig() {
        return { ...this.dynamicWindows };
    }
    /**
     * Load configuration from `.uap.json` in the project root,
     * falling back to environment variables for each window.
     */
    static loadFromConfig(projectRoot) {
        const configPath = join(projectRoot, '.uap.json');
        let fileWindows = {};
        if (existsSync(configPath)) {
            try {
                const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
                // Note: 'deploy.batchWindows' is a non-schema field (not in AgentContextConfigSchema).
                // Using raw JSON.parse intentionally to read this legacy config path.
                if (raw?.deploy?.batchWindows) {
                    const bw = raw.deploy.batchWindows;
                    if (typeof bw.commit === 'number')
                        fileWindows.commit = bw.commit;
                    if (typeof bw.push === 'number')
                        fileWindows.push = bw.push;
                    if (typeof bw.merge === 'number')
                        fileWindows.merge = bw.merge;
                    if (typeof bw.workflow === 'number')
                        fileWindows.workflow = bw.workflow;
                    if (typeof bw.deploy === 'number')
                        fileWindows.deploy = bw.deploy;
                }
            }
            catch {
                // Ignore malformed config; fall through to env vars
            }
        }
        // Environment variable fallbacks for any windows not set by the file
        const envMap = {
            commit: 'UAP_DEPLOY_COMMIT_WINDOW',
            push: 'UAP_DEPLOY_PUSH_WINDOW',
            merge: 'UAP_DEPLOY_MERGE_WINDOW',
            workflow: 'UAP_DEPLOY_WORKFLOW_WINDOW',
            deploy: 'UAP_DEPLOY_DEPLOY_WINDOW',
        };
        for (const [key, envVar] of Object.entries(envMap)) {
            if (fileWindows[key] === undefined) {
                const envVal = process.env[envVar];
                if (envVal !== undefined) {
                    const parsed = parseInt(envVal, 10);
                    if (!isNaN(parsed)) {
                        fileWindows[key] = parsed;
                    }
                }
            }
        }
        const dynamicWindows = Object.keys(fileWindows).length > 0 ? fileWindows : undefined;
        return { dynamicWindows };
    }
    /**
     * Save the current dynamic window configuration into `.uap.json`
     * under the `deploy.batchWindows` section, preserving existing content.
     */
    saveConfig(projectRoot) {
        const configPath = join(projectRoot, '.uap.json');
        let config = {};
        if (existsSync(configPath)) {
            try {
                config = JSON.parse(readFileSync(configPath, 'utf-8'));
            }
            catch {
                // Start fresh if the file is malformed
                config = {};
            }
        }
        if (!config.deploy || typeof config.deploy !== 'object') {
            config.deploy = {};
        }
        config.deploy.batchWindows = { ...this.dynamicWindows };
        writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
    }
    /**
     * Return a DeployBatcherConfig for a named profile.
     */
    static fromProfile(profile) {
        switch (profile) {
            case 'fast':
                return {
                    dynamicWindows: {
                        commit: 5000,
                        push: 1000,
                        merge: 2000,
                        workflow: 1000,
                        deploy: 10000,
                    },
                };
            case 'safe':
                return {
                    dynamicWindows: {
                        commit: 60000,
                        push: 10000,
                        merge: 30000,
                        workflow: 10000,
                        deploy: 120000,
                    },
                };
            case 'default':
                return {
                    dynamicWindows: { ...DEFAULT_DYNAMIC_WINDOWS },
                };
        }
    }
    /**
     * Execute a command with timeout protection.
     * Prevents hung processes from blocking the batch pipeline.
     */
    execFileWithTimeout(command, args, options = {}) {
        const timeoutMs = options.timeout ?? this.execTimeoutMs;
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new Error(`Command '${command}' timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            execFileCb(command, args, { cwd: options.cwd, maxBuffer: 10 * 1024 * 1024 }, (error, _stdout, stderr) => {
                clearTimeout(timer);
                if (error) {
                    reject(new Error(`${command} ${args.join(' ')} failed: ${stderr || error.message}`));
                }
                else {
                    resolve();
                }
            });
        });
    }
}
//# sourceMappingURL=deploy-batcher.js.map