/**
 * Notification Watcher Service
 *
 * Polls the agent_runs and phase_timings tables to detect status
 * transitions, dispatching NotificationEvents via the NotificationService
 * (which handles settings filters, bus fan-out, and desktop dispatch).
 *
 * Maintains in-memory tracking of last-seen status per run to avoid
 * duplicate notifications. Polling starts/stops with start()/stop()
 * lifecycle methods.
 *
 * Design decision: Uses DB polling instead of IPC because the worker
 * process is spawned detached with IPC disconnected.
 */
import { AgentRunStatus, NotificationEventType, NotificationSeverity, SdlcLifecycle, } from '../../../domain/generated/output.js';
const DEFAULT_POLL_INTERVAL_MS = 3000;
const ACTIVE_STATUSES = new Set([
    AgentRunStatus.pending,
    AgentRunStatus.running,
    AgentRunStatus.waitingApproval,
]);
const TERMINAL_STATUSES = new Set([
    AgentRunStatus.completed,
    AgentRunStatus.failed,
    AgentRunStatus.cancelled,
    AgentRunStatus.interrupted,
]);
const STATUS_TO_EVENT = {
    [AgentRunStatus.running]: {
        eventType: NotificationEventType.AgentStarted,
        severity: NotificationSeverity.Info,
    },
    [AgentRunStatus.waitingApproval]: {
        eventType: NotificationEventType.WaitingApproval,
        severity: NotificationSeverity.Warning,
    },
    [AgentRunStatus.completed]: {
        eventType: NotificationEventType.AgentCompleted,
        severity: NotificationSeverity.Success,
    },
    [AgentRunStatus.failed]: {
        eventType: NotificationEventType.AgentFailed,
        severity: NotificationSeverity.Error,
    },
};
const EVENT_MESSAGES = {
    [NotificationEventType.AgentStarted]: 'Feature agent started',
    [NotificationEventType.WaitingApproval]: 'Feature agent is waiting for approval',
    [NotificationEventType.AgentCompleted]: 'Feature agent completed successfully',
    [NotificationEventType.AgentFailed]: 'Feature agent failed',
};
export class NotificationWatcherService {
    runRepository;
    phaseTimingRepository;
    featureRepository;
    notificationService;
    pollIntervalMs;
    trackedRuns = new Map();
    trackedFeatureLifecycles = new Map();
    intervalId = null;
    // Suppresses notifications on the first poll to avoid replaying historical state
    isBootstrapped = false;
    constructor(runRepository, phaseTimingRepository, featureRepository, notificationService, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
        this.runRepository = runRepository;
        this.phaseTimingRepository = phaseTimingRepository;
        this.featureRepository = featureRepository;
        this.notificationService = notificationService;
        this.pollIntervalMs = pollIntervalMs;
    }
    isRunning() {
        return this.intervalId !== null;
    }
    start() {
        if (this.intervalId !== null)
            return;
        // Run first poll immediately
        void this.poll();
        this.intervalId = setInterval(() => {
            void this.poll();
        }, this.pollIntervalMs);
    }
    stop() {
        if (this.intervalId !== null) {
            clearInterval(this.intervalId);
            this.intervalId = null;
        }
    }
    async poll() {
        try {
            const runs = await this.runRepository.list();
            await this.processRuns(runs);
        }
        catch {
            // DB not ready or query failed — skip this poll cycle
        }
        try {
            const features = await this.featureRepository.list();
            this.checkFeatureLifecycles(features);
        }
        catch {
            // DB not ready or query failed — skip this poll cycle
        }
    }
    async processRuns(runs) {
        const currentRunIds = new Set();
        for (const run of runs) {
            if (!ACTIVE_STATUSES.has(run.status) && !this.trackedRuns.has(run.id)) {
                // Skip terminal runs we haven't been tracking
                continue;
            }
            currentRunIds.add(run.id);
            const prevState = this.trackedRuns.get(run.id);
            if (!prevState) {
                // New run — track and emit initial status event
                const { featureId, featureName } = await this.resolveFeatureIdentity(run);
                const newState = {
                    status: run.status,
                    completedPhases: new Set(),
                    featureId,
                    featureName,
                };
                this.trackedRuns.set(run.id, newState);
                if (this.isBootstrapped) {
                    this.emitStatusEvent(run, newState);
                }
                // Check for completed phases on first observation
                await this.checkPhaseCompletions(run.id, newState);
            }
            else if (prevState.status !== run.status) {
                // Status changed — emit event
                prevState.status = run.status;
                this.emitStatusEvent(run, prevState);
                if (TERMINAL_STATUSES.has(run.status)) {
                    // Run reached terminal state — clean up after emitting
                    this.trackedRuns.delete(run.id);
                }
                else {
                    // Still active — check for phase completions
                    await this.checkPhaseCompletions(run.id, prevState);
                }
            }
            else {
                // Same status — just check for phase completions
                await this.checkPhaseCompletions(run.id, prevState);
            }
        }
        if (!this.isBootstrapped) {
            this.isBootstrapped = true;
        }
        // Clean up tracking for runs that disappeared from the list
        for (const trackedId of this.trackedRuns.keys()) {
            if (!currentRunIds.has(trackedId)) {
                this.trackedRuns.delete(trackedId);
            }
        }
    }
    emitStatusEvent(run, state) {
        const mapping = STATUS_TO_EVENT[run.status];
        if (!mapping)
            return;
        const event = {
            eventType: mapping.eventType,
            agentRunId: run.id,
            featureId: state.featureId,
            featureName: state.featureName,
            message: EVENT_MESSAGES[mapping.eventType] ?? `Agent status: ${run.status}`,
            severity: mapping.severity,
            timestamp: new Date().toISOString(),
        };
        this.notificationService.notify(event);
    }
    async checkPhaseCompletions(runId, state) {
        const timings = await this.phaseTimingRepository.findByRunId(runId);
        for (const timing of timings) {
            if (timing.completedAt && !state.completedPhases.has(timing.phase)) {
                state.completedPhases.add(timing.phase);
                if (this.isBootstrapped) {
                    const event = {
                        eventType: NotificationEventType.PhaseCompleted,
                        agentRunId: runId,
                        featureId: state.featureId,
                        featureName: state.featureName,
                        phaseName: timing.phase,
                        message: `Completed ${timing.phase} phase`,
                        severity: NotificationSeverity.Info,
                        timestamp: new Date().toISOString(),
                    };
                    this.notificationService.notify(event);
                }
            }
        }
    }
    checkFeatureLifecycles(features) {
        for (const feature of features) {
            const prevLifecycle = this.trackedFeatureLifecycles.get(feature.id);
            this.trackedFeatureLifecycles.set(feature.id, feature.lifecycle);
            // Emit notification when a feature transitions TO Review from a known previous state.
            // Skip when prevLifecycle is undefined (first observation / bootstrap seeding) or
            // when the watcher hasn't completed its first poll yet.
            if (this.isBootstrapped &&
                prevLifecycle !== undefined &&
                feature.lifecycle === SdlcLifecycle.Review &&
                prevLifecycle !== SdlcLifecycle.Review) {
                const prUrl = feature.pr?.url;
                const message = prUrl ? `Ready for merge review — PR: ${prUrl}` : 'Ready for merge review';
                const event = {
                    eventType: NotificationEventType.MergeReviewReady,
                    agentRunId: feature.agentRunId ?? '',
                    featureId: feature.id,
                    featureName: feature.name,
                    message,
                    severity: NotificationSeverity.Info,
                    timestamp: new Date().toISOString(),
                };
                this.notificationService.notify(event);
            }
        }
    }
    async resolveFeatureIdentity(run) {
        if (run.featureId) {
            try {
                const feature = await this.featureRepository.findById(run.featureId);
                if (feature)
                    return { featureId: feature.id, featureName: feature.name };
            }
            catch {
                // Fall through to fallback
            }
        }
        return { featureId: run.featureId ?? run.id, featureName: `Agent ${run.id}` };
    }
}
// --- Singleton accessors (follows getNotificationBus() pattern) ---
let watcherInstance = null;
/**
 * Initialize the notification watcher singleton.
 * Must be called once during web server startup.
 *
 * @throws Error if the watcher is already initialized
 */
export function initializeNotificationWatcher(runRepository, phaseTimingRepository, featureRepository, notificationService, pollIntervalMs) {
    if (watcherInstance !== null) {
        throw new Error('Notification watcher already initialized. Cannot re-initialize.');
    }
    watcherInstance = new NotificationWatcherService(runRepository, phaseTimingRepository, featureRepository, notificationService, pollIntervalMs);
}
/**
 * Get the notification watcher singleton.
 *
 * @returns The notification watcher service
 * @throws Error if the watcher hasn't been initialized yet
 */
export function getNotificationWatcher() {
    if (watcherInstance === null) {
        throw new Error('Notification watcher not initialized. Call initializeNotificationWatcher() during web server startup.');
    }
    return watcherInstance;
}
/**
 * Check if the notification watcher has been initialized.
 */
export function hasNotificationWatcher() {
    return watcherInstance !== null;
}
/**
 * Reset the notification watcher singleton (for testing purposes only).
 * Stops the watcher if running before resetting.
 *
 * @internal
 */
export function resetNotificationWatcher() {
    if (watcherInstance !== null) {
        watcherInstance.stop();
    }
    watcherInstance = null;
}
