"use strict";
// Triggers: lightweight polling that runs scripts on a short interval and
// emits any non-empty stdout as a `trigger.message` event. The model is NOT
// invoked — exit-0 with output means "something happened, deliver this as a
// channel message". Exit non-zero means "nothing happened" and the trigger
// stays silent.
//
// Trigger envelope (`.harness/triggers/triggers.json`):
//
//   {
//     "version": 1,
//     "triggers": [
//       { "id": "email-poll", "command": "node", "args": ["scripts/check-email.js"], "intervalSeconds": 30, "enabled": true }
//     ]
//   }
//
// Built-in safeguards:
//   - Minimum interval clamp (5s) per trigger.
//   - Output cap (4KB) per run.
//   - Per-trigger concurrency guard (skip tick if previous run still in flight).
//   - Startup cooldown (30s) so triggers don't all fire at boot.
//
// Reads support a legacy v1 bare-array form: `[{ ... }]` becomes
// `{ version: 1, triggers: [{ ... }] }` automatically.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TriggerScheduler = exports.DEFAULT_TRIGGERS = void 0;
exports.ensureDefaultTriggers = ensureDefaultTriggers;
exports.loadTriggers = loadTriggers;
exports.saveTriggers = saveTriggers;
exports.normalizeEnvelope = normalizeEnvelope;
const node_child_process_1 = require("node:child_process");
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const logger_1 = require("../core/logger");
const eventStore_1 = require("../persistence/eventStore");
const DEFAULT_TICK_MS = 1000;
const DEFAULT_STARTUP_COOLDOWN_MS = 30_000;
const DEFAULT_OUTPUT_CAP = 4096;
const MIN_INTERVAL_SECONDS = 5;
exports.DEFAULT_TRIGGERS = [
    { id: 'morning-priority', command: 'node', args: ['scripts/morning-priority.js'], intervalSeconds: 900, enabled: true },
];
/**
 * Ensure that built-in default triggers exist in `.harness/triggers/triggers.json`.
 * Matches by `id`: existing entries (including user customizations) are preserved.
 * Only writes if at least one default is missing.
 */
async function ensureDefaultTriggers(projectDir) {
    const existing = await loadTriggers(projectDir);
    const existingIds = new Set(existing.map((t) => t.id));
    const missing = exports.DEFAULT_TRIGGERS.filter((t) => !existingIds.has(t.id));
    if (missing.length === 0)
        return;
    await saveTriggers(projectDir, [...existing, ...missing]);
}
function triggersFilePath(projectDir) {
    return path.join(projectDir, '.harness', 'triggers', 'triggers.json');
}
async function loadTriggers(projectDir) {
    const fp = triggersFilePath(projectDir);
    let raw;
    try {
        raw = await fs.readFile(fp, 'utf-8');
    }
    catch {
        return [];
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        return [];
    }
    return normalizeEnvelope(parsed).triggers;
}
async function saveTriggers(projectDir, triggers) {
    const fp = triggersFilePath(projectDir);
    await fs.mkdir(path.dirname(fp), { recursive: true });
    const envelope = { version: 1, triggers };
    await fs.writeFile(fp, JSON.stringify(envelope, null, 2), 'utf-8');
}
function normalizeEnvelope(value) {
    if (Array.isArray(value))
        return { version: 1, triggers: value.filter(isTriggerLike) };
    if (value && typeof value === 'object') {
        const env = value;
        if (Array.isArray(env.triggers)) {
            return { version: 1, triggers: env.triggers.filter(isTriggerLike) };
        }
    }
    return { version: 1, triggers: [] };
}
function isTriggerLike(value) {
    if (!value || typeof value !== 'object')
        return false;
    const v = value;
    return typeof v.id === 'string' && typeof v.command === 'string' && typeof v.intervalSeconds === 'number';
}
class TriggerScheduler {
    timer = null;
    startedAtMs = 0;
    lastRunMs = new Map();
    inflight = new Set();
    opts;
    constructor(opts) {
        this.opts = {
            projectDir: opts.projectDir,
            tickMs: opts.tickMs ?? DEFAULT_TICK_MS,
            startupCooldownMs: opts.startupCooldownMs ?? DEFAULT_STARTUP_COOLDOWN_MS,
            outputCapBytes: opts.outputCapBytes ?? DEFAULT_OUTPUT_CAP,
            isKillSwitchActive: opts.isKillSwitchActive,
            isEnabled: opts.isEnabled,
            spawn: opts.spawn,
        };
    }
    start() {
        if (this.timer)
            return;
        this.startedAtMs = Date.now();
        ensureDefaultTriggers(this.opts.projectDir).catch((error) => logger_1.logger.warn('Triggers', 'Failed to seed default triggers', { error: error instanceof Error ? error.message : String(error) }));
        this.timer = setInterval(() => {
            this.tick().catch((error) => logger_1.logger.warn('Triggers', 'Tick failed', { error: error instanceof Error ? error.message : String(error) }));
        }, this.opts.tickMs);
        if (typeof this.timer.unref === 'function')
            this.timer.unref();
    }
    stop() {
        if (this.timer)
            clearInterval(this.timer);
        this.timer = null;
    }
    /**
     * Drop cached `lastRunMs` timestamps for triggers that no longer exist or
     * whose interval has changed. Call this whenever the on-disk
     * `triggers.json` is mutated so the running scheduler picks up changes
     * without waiting for the existing interval to elapse.
     */
    async invalidate() {
        const definitions = await loadTriggers(this.opts.projectDir);
        const known = new Set(definitions.map((definition) => definition.id));
        // Drop timestamps for deleted triggers.
        for (const id of Array.from(this.lastRunMs.keys())) {
            if (!known.has(id))
                this.lastRunMs.delete(id);
        }
        // For changed intervals, reset the timestamp so the next tick re-evaluates.
        for (const definition of definitions) {
            const existingLast = this.lastRunMs.get(definition.id);
            if (existingLast !== undefined) {
                // Reset so the next tick uses the new interval as the gate.
                this.lastRunMs.set(definition.id, 0);
            }
        }
    }
    /** Run one tick. Returns the executions that fired. Public for tests. */
    async tick(now = new Date()) {
        const results = [];
        if (!this.opts.isEnabled())
            return results;
        if (this.opts.isKillSwitchActive())
            return results;
        if (now.getTime() - this.startedAtMs < this.opts.startupCooldownMs)
            return results;
        const triggers = await loadTriggers(this.opts.projectDir);
        for (const definition of triggers) {
            if (definition.enabled === false)
                continue;
            if (this.inflight.has(definition.id))
                continue;
            const intervalMs = Math.max(MIN_INTERVAL_SECONDS, definition.intervalSeconds) * 1000;
            const last = this.lastRunMs.get(definition.id) ?? 0;
            if (now.getTime() - last < intervalMs)
                continue;
            this.inflight.add(definition.id);
            try {
                const result = await this.execute(definition);
                this.lastRunMs.set(definition.id, now.getTime());
                results.push(result);
                if (result.exitCode === 0 && result.stdout.trim().length > 0) {
                    await (0, eventStore_1.emitEvent)(this.opts.projectDir, 'notification', 'trigger.message', {
                        triggerId: definition.id,
                        message: result.stdout,
                        durationMs: result.durationMs,
                    }, 'trigger', definition.id).catch(() => { });
                }
            }
            catch (error) {
                logger_1.logger.warn('Triggers', `Execution failed for ${definition.id}`, { error: error instanceof Error ? error.message : String(error) });
            }
            finally {
                this.inflight.delete(definition.id);
            }
        }
        return results;
    }
    async execute(definition) {
        const cwd = definition.cwd ?? this.opts.projectDir;
        const started = Date.now();
        const spawnFn = this.opts.spawn ?? defaultSpawnFn(this.opts.outputCapBytes);
        const { exitCode, stdout, stderr } = await spawnFn(definition, cwd);
        return {
            triggerId: definition.id,
            exitCode,
            stdout: capOutput(stdout, this.opts.outputCapBytes),
            stderr: capOutput(stderr, this.opts.outputCapBytes),
            durationMs: Date.now() - started,
        };
    }
}
exports.TriggerScheduler = TriggerScheduler;
function capOutput(value, capBytes) {
    if (Buffer.byteLength(value, 'utf-8') <= capBytes)
        return value;
    // Slice by characters first; this is approximate but cheap and safe.
    return value.slice(0, capBytes) + `\n[truncated to ${capBytes} bytes]`;
}
function defaultSpawnFn(outputCapBytes) {
    return (definition, cwd) => new Promise((resolve) => {
        const child = (0, node_child_process_1.spawn)(definition.command, definition.args ?? [], { cwd, shell: false });
        let stdout = '';
        let stderr = '';
        child.stdout?.on('data', (chunk) => {
            if (Buffer.byteLength(stdout, 'utf-8') < outputCapBytes)
                stdout += chunk.toString('utf-8');
        });
        child.stderr?.on('data', (chunk) => {
            if (Buffer.byteLength(stderr, 'utf-8') < outputCapBytes)
                stderr += chunk.toString('utf-8');
        });
        child.on('error', (error) => {
            stderr += '\n' + (error instanceof Error ? error.message : String(error));
            resolve({ exitCode: 1, stdout, stderr });
        });
        child.on('close', (code) => resolve({ exitCode: code, stdout, stderr }));
    });
}
//# sourceMappingURL=triggerScheduler.js.map