/**
 * TriggerEngine Service — Manages trigger lifecycle and execution.
 *
 * Handles three trigger types:
 * - TimeTriggers: cron-based or one-shot delay/fireAt scheduling
 * - SignalTriggers: EventBus event subscriptions with optional filters
 * - CompoundTriggers: AND/OR combinations of time+signal conditions
 *
 * Lifecycle management:
 * - Tracks fireCount, maxFires (auto-exhaust), maxIdleFires (auto-cancel on idle)
 * - Persists triggers to disk as JSON
 * - Supports pause/resume/cancel operations
 *
 * @module services/v3/trigger-engine.service
 */
import { type Trigger, type TriggerStatus, type TriggerAction, type CreateTriggerInput } from '../../types/v2/index.js';
import type { EventBusService } from '../event-bus/event-bus.service.js';
/**
 * Callback invoked when a trigger fires and produces an action.
 *
 * @param trigger - The trigger that fired
 * @param action - The action to execute
 */
export type TriggerActionHandler = (trigger: Trigger, action: TriggerAction) => Promise<void>;
/**
 * Result of a trigger fire attempt — indicates whether the fire was productive.
 */
export interface TriggerFireResult {
    /** The trigger that fired */
    triggerId: string;
    /** Whether the fire produced useful work (false = idle fire) */
    productive: boolean;
    /** ISO timestamp of the fire */
    firedAt: string;
}
/**
 * TriggerEngine manages the lifecycle of Trigger entities.
 *
 * Uses singleton pattern. Time triggers are evaluated on a polling interval.
 * Signal triggers subscribe to EventBus events. Compound triggers combine
 * both with AND/OR logic.
 *
 * @example
 * ```typescript
 * const engine = TriggerEngine.getInstance('/path/to/project');
 * engine.setEventBus(eventBusService);
 * engine.setActionHandler(async (trigger, action) => { ... });
 * await engine.start();
 * ```
 */
export declare class TriggerEngine {
    private readonly projectPath;
    private static instance;
    private readonly logger;
    private readonly triggersDir;
    private readonly triggersFile;
    /** In-memory trigger store, keyed by trigger ID. */
    private triggers;
    /** EventBus reference for signal triggers. */
    private eventBus;
    /**
     * Bound signal-listener reference kept so `stop()` can detach it and we
     * don't accumulate a new copy on every `start()`. Without this, repeated
     * start/stop cycles (dev reloads, tests, escalation restarts) would
     * register additional listeners that each fire independently.
     */
    private signalListener;
    /** Callback for executing trigger actions. */
    private actionHandler;
    /** Interval handle for time trigger polling. */
    private timeCheckInterval;
    /** Timeout handles for one-shot delay/fireAt triggers, keyed by trigger ID. */
    private oneShotTimers;
    /** Whether the engine is currently running. */
    private running;
    /**
     * Last persisted active-trigger count, used as the prior reference for
     * the active-count-collapse warn-log in {@link persistTriggers}. Updated
     * inside persistTriggers **only after a successful write** (i.e. inside the
     * try block, post-`atomicWriteJsonWithGuard`); resets on `loadTriggers()`
     * boot to whatever was on disk so post-restart writes are correctly
     * baselined against the persisted state.
     *
     * Update-after-success ordering is load-bearing for the active-collapse
     * warn-log: if the guard rejects a write, the baseline must remain at the
     * pre-regression value so a subsequent persist call (still seeing the same
     * regression) re-emits the warn-log. Updating before the write would
     * silently suppress the signal after the very first guard rejection.
     */
    private lastKnownActiveCount;
    /**
     * Creates a new TriggerEngine.
     *
     * @param projectPath - Absolute path to the project root
     */
    private constructor();
    /**
     * Gets the singleton instance.
     *
     * @param projectPath - Absolute path to the project root
     * @returns The singleton TriggerEngine instance
     */
    static getInstance(projectPath?: string): TriggerEngine;
    /**
     * Resets the singleton instance. For test isolation only.
     */
    static resetInstance(): void;
    /**
     * Sets the EventBus for signal trigger subscriptions.
     *
     * @param eventBus - The EventBusService instance
     */
    setEventBus(eventBus: EventBusService): void;
    /**
     * Sets the handler that executes trigger actions.
     *
     * @param handler - Callback invoked when a trigger fires
     */
    setActionHandler(handler: TriggerActionHandler): void;
    /**
     * Starts the TriggerEngine: loads persisted triggers from disk,
     * sets up time polling interval, and registers signal listeners.
     */
    start(): Promise<void>;
    /**
     * Stops the TriggerEngine: clears intervals, timers, and signal listeners.
     */
    stop(): void;
    /**
     * Returns whether the engine is currently running.
     *
     * @returns True if the engine is running
     */
    isRunning(): boolean;
    /**
     * Creates a new trigger and persists it.
     *
     * @param input - Trigger creation input
     * @returns The created Trigger
     * @throws Error if input validation fails
     */
    create(input: CreateTriggerInput): Promise<Trigger>;
    /**
     * Gets a trigger by ID.
     *
     * @param id - Trigger UUID
     * @returns The trigger, or undefined if not found
     */
    get(id: string): Trigger | undefined;
    /**
     * Lists all triggers, optionally filtered by status.
     *
     * @param status - Optional status filter
     * @returns Array of matching triggers
     */
    list(status?: TriggerStatus): Trigger[];
    /**
     * Pauses an active trigger.
     *
     * @param id - Trigger UUID
     * @returns True if the trigger was paused
     */
    pause(id: string): Promise<boolean>;
    /**
     * Resumes a paused trigger.
     *
     * @param id - Trigger UUID
     * @returns True if the trigger was resumed
     */
    resume(id: string): Promise<boolean>;
    /**
     * Cancels a trigger permanently.
     *
     * @param id - Trigger UUID
     * @returns True if the trigger was cancelled
     */
    cancel(id: string): Promise<boolean>;
    /**
     * Deletes a trigger entirely (removes from store).
     *
     * @param id - Trigger UUID
     * @returns True if the trigger was deleted
     */
    delete(id: string): Promise<boolean>;
    /**
     * Fires a trigger: increments counters, executes action, manages lifecycle.
     *
     * @param trigger - The trigger to fire
     * @param productive - Whether this fire is considered productive (not idle)
     * @returns The fire result
     */
    fire(trigger: Trigger, productive?: boolean): Promise<TriggerFireResult>;
    /**
     * Sets up the polling interval for cron-based time triggers.
     */
    private setupTimePolling;
    /**
     * Evaluates all active cron-based time triggers against the current time.
     */
    evaluateTimeTriggers(): Promise<TriggerFireResult[]>;
    /**
     * Checks if a cron expression matches the current moment.
     *
     * @param cronExpression - Standard 5-field cron expression
     * @param timezone - IANA timezone string
     * @param now - Current time
     * @returns True if the cron matches
     */
    private cronMatchesNow;
    /**
     * Sets up one-shot timers for delay/fireAt time triggers.
     */
    private setupOneShotTimers;
    /**
     * Schedules a one-shot timer for a delay or fireAt trigger.
     *
     * @param trigger - The time trigger to schedule
     */
    private scheduleOneShot;
    /**
     * Clears a one-shot timer for a trigger.
     *
     * @param triggerId - The trigger ID
     */
    private clearOneShotTimer;
    /**
     * Calculates the next fire time for a time trigger config.
     *
     * @param config - Time trigger configuration
     * @returns ISO string of next fire time, or undefined
     */
    private calculateNextFireAt;
    /**
     * Sets up EventBus listeners for signal triggers.
     * Listens to the 'event_published' event on the EventBus.
     */
    private setupSignalListeners;
    /**
     * Detaches the signal listener registered in {@link setupSignalListeners}.
     * Safe to call when no listener is attached.
     */
    private teardownSignalListeners;
    /**
     * Handles an EventBus event by checking all active signal triggers.
     *
     * @param eventData - The event data from EventBus
     */
    handleSignalEvent(eventData: {
        eventId: string;
        eventType: string;
        sessionName: string;
    }): Promise<TriggerFireResult[]>;
    /**
     * Checks if an event matches a signal trigger's config.
     *
     * @param config - Signal trigger configuration
     * @param eventData - The event data to match against
     * @returns True if the event matches
     */
    private signalMatches;
    /**
     * Checks if an event matches a compound trigger's signal conditions.
     * Only evaluates signal sub-conditions (time conditions are handled separately).
     *
     * @param config - Compound trigger configuration
     * @param eventData - The event data to match against
     * @returns True if the compound condition is satisfied
     */
    private compoundSignalMatches;
    /**
     * Loads triggers from disk and collapses accumulated duplicates.
     *
     * Historical accumulation: pre-fix, every boot called `create()` for
     * fixed system crons (e.g. EscalationService's 5-minute reconciler) with
     * no idempotency key, so the store grew by one duplicate each restart.
     * A single workspace was observed with 39 copies of the every-5-min
     * runReconciler, firing in parallel every interval.
     *
     * Policy:
     * - Two triggers are "duplicates" iff they share `createdBy`, `config`,
     *   and `action` (structural-JSON equality) AND both are `active`.
     * - Of each duplicate group, keep the one with the earliest `createdAt`
     *   (or lowest id as tiebreaker) and cancel the rest. We cancel rather
     *   than delete so any in-flight fire completes cleanly and the audit
     *   trail survives.
     */
    private loadTriggers;
    /**
     * Reads the persisted triggers JSON from disk with corruption-aware
     * fallback semantics.
     *
     * Behavior:
     * - **ENOENT** (no prior file): silently return `[]`. Normal first-boot
     *   or post-clean-install state.
     * - **Parse failure or non-array shape**: back up the corrupt file as
     *   `<path>.corrupt.<ts>`, emit a `system:trigger_persistence_corrupt`
     *   warn-log (per B1 spec — uses logger.warn, NOT a new event-bus type),
     *   and return `[]` so the engine boots clean. The next persistTriggers()
     *   call will overwrite the corrupt file with valid JSON (the
     *   integrity-guarded write tolerates an unreadable prior file per
     *   PR #420 design contract).
     *
     * Why this is custom (not safeReadJson): we need to distinguish
     * "missing file" (silent) from "corrupt file" (loud warn-log) for the
     * B1 acceptance criteria. The shared helper collapses both cases to
     * "return default + back up", which is the right *behavior* but doesn't
     * give us the corruption signal.
     *
     * @returns Parsed Trigger array, or empty array on missing/corrupt file
     */
    private readTriggersFromDisk;
    /**
     * Keep the newest `MAX_RETAINED` `.corrupt.<TS>` backups next to the
     * triggers file; unlink everything older.
     *
     * Bounds the directory size when a flapping bug periodically corrupts
     * the persistence file. The retention window is "newest 10" rather than
     * an age-based TTL because in practice operators want the most recent
     * forensic evidence regardless of when the corruption stopped, and a
     * count-based bound is easier to reason about than a time-based one.
     *
     * Best-effort: failure to readdir or unlink is swallowed — corruption
     * recovery itself must never block on bookkeeping. Issue #456.
     */
    private pruneCorruptBackups;
    /**
     * Test-only accessor for the persistence integration tests.
     *
     * `trigger-engine-persistence.integration.test.ts` needs to drive the
     * private `persistTriggers` method and mutate the internal `triggers`
     * `Map` to simulate write failures + partial state. Without a typed
     * accessor the only path is `as unknown as { ... }` casts, which
     * silently break when the field/method is renamed — typecheck passes,
     * runtime returns `undefined`, test falsely passes. Issue #457.
     *
     * Runtime production guard: throws if `NODE_ENV === 'production'`. The
     * `__testing__` name is also a hard-to-grep marker so a reviewer can
     * spot any accidental production call site.
     *
     * Caveat: returns the live `Map` reference, not a defensive copy —
     * tests need to `.set()` and `.clear()` to stage the integrity-guard
     * scenarios. Don't call this from non-test code.
     *
     * @returns A typed view of internals needed by integration tests
     * @throws Error if called when NODE_ENV is 'production'
     */
    __testing__(): {
        triggers: Map<string, Trigger>;
        persistTriggers: () => Promise<void>;
    };
    /**
     * Detects active triggers that are structurally identical (same owner,
     * same config, same action) and cancels all but the oldest in each group.
     *
     * Returns the number of triggers newly marked `cancelled`.
     */
    private dedupeDuplicateTriggers;
    /**
     * Persists all triggers to disk via the integrity-guarded atomic write.
     *
     * **Integrity guard (B1):** Uses
     * {@link atomicWriteJsonWithGuard} so a regression that accidentally
     * empties or aggressively shrinks the trigger registry between two
     * persist calls is **rejected at write time** rather than silently
     * propagating to disk and surviving a subsequent boot-replay.
     *
     * Guard configuration:
     * - `countOf` = `data.length` (total trigger count, including
     *   `cancelled`/`exhausted` triggers — entries are kept after status
     *   change for audit trail, so length only drops on actual deletion).
     * - `maxDecreasePct` = 50 (PR #420 default). Catches catastrophic
     *   length wipes (e.g. `rm -rf .crewly/triggers/`, `Map.clear()` bug,
     *   accidental empty-array assignment).
     * - `allowExplicitDelete` = false. The CRUD operations that legitimately
     *   shrink the file (delete a single trigger) drop length by at most 1
     *   per call, well under the 50% threshold for any non-trivial registry.
     *
     * **Failure mode:** {@link IntegrityViolationError} surfaces to the
     * caller (CRUD method or fire path). The in-memory map is preserved
     * unchanged — the next legitimate persist call has a chance to re-sync
     * disk to memory once the regression is corrected.
     *
     * **Active-count signal:** A separate warn-log fires if the active
     * count collapses to zero from a known-positive value (status-flip
     * failure mode that the length guard cannot catch — entries stay,
     * statuses all flip to terminal). This is observational only (no
     * throw) because legitimate bulk-cancel paths exist and we do not
     * want to block them; ops can investigate the warn-log.
     */
    private persistTriggers;
    /**
     * Returns a summary of trigger engine state.
     *
     * @returns Object with counts by status and type
     */
    getStatus(): {
        running: boolean;
        total: number;
        byStatus: Record<TriggerStatus, number>;
        byType: Record<string, number>;
    };
}
//# sourceMappingURL=trigger-engine.service.d.ts.map