/**
 * Team-Health-Watchdog (THW) — Service Shell (Layer 4)
 *
 * Wires the pure detector + alert router to live data sources, runs the
 * 60s sweep loop, exposes API for the orchestrator skill / health endpoint.
 *
 * Architecture (§A.5): keep sweep+dedup+rate-limit+heartbeat strictly
 * separable from the detector. v0.5 will extract these protected members
 * into a `WatchdogBase`.
 *
 * Layer-4 invariant (§A.1): READS lower-layer state, EMITS alerts.
 * NEVER mutates `agent.status`, claims, or work items.
 *
 * @module services/team-health/team-health-watchdog.service
 */
import { detectTeamHealth, detectOrphanRequests } from './team-health-detector.js';
import { TeamHealthAlertRouter } from './team-health-alert-router.js';
const noopLogger = {
    info: () => { }, warn: () => { }, error: () => { },
};
/**
 * The watchdog service. Owns the sweep loop and exposes the public API.
 */
export class TeamHealthWatchdogService {
    // Hoist candidates for `WatchdogBase` extraction in v0.5 (§A.5).
    sweepIntervalMs;
    bootedAt;
    router;
    // v0-specific state.
    config;
    dataProvider;
    alertSink;
    logger;
    clock;
    timer = null;
    lastSweep = null;
    isRunning = false;
    constructor(opts) {
        this.config = opts.config;
        this.dataProvider = opts.dataProvider;
        this.alertSink = opts.alertSink;
        this.logger = opts.logger ?? noopLogger;
        this.clock = opts.clock ?? (() => new Date());
        this.bootedAt = opts.bootedAt ?? this.clock();
        this.sweepIntervalMs = opts.config.sweepIntervalMs;
        this.router = new TeamHealthAlertRouter(opts.config.alerting, opts.config.offHoursSuppression, opts.config.shadowMode);
    }
    /**
     * Start the periodic sweep loop. Idempotent. Per §11.3 self-instrumentation,
     * we run `runOnce` immediately on start so /api/health has data from boot.
     */
    start() {
        if (!this.config.enabled) {
            this.logger.info('TeamHealthWatchdog disabled by config; not starting.');
            return;
        }
        if (this.isRunning)
            return;
        this.isRunning = true;
        this.logger.info('TeamHealthWatchdog starting', {
            sweepIntervalMs: this.sweepIntervalMs,
            shadowMode: this.config.shadowMode,
            bootedAt: this.bootedAt.toISOString(),
        });
        this.runOnce().catch((err) => {
            this.logger.error('Initial THW sweep failed', {
                error: err instanceof Error ? err.message : String(err),
            });
        });
        this.timer = setInterval(() => {
            void this.runOnce().catch((err) => {
                this.logger.error('THW sweep failed', {
                    error: err instanceof Error ? err.message : String(err),
                });
            });
        }, this.sweepIntervalMs);
    }
    /**
     * Stop the periodic sweep loop. After stop(), `last_sweep_age_ms` grows;
     * /api/health surfaces this so a stuck watchdog itself shows as 'degraded'
     * (§E.8 watchdog-watchdog).
     */
    stop() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
        this.isRunning = false;
        this.logger.info('TeamHealthWatchdog stopped');
    }
    /**
     * Run one sweep on demand. Catches own errors and returns a degraded
     * sweep result rather than throwing.
     */
    async runOnce() {
        const now = this.clock();
        const startedMs = Date.now();
        let detections = [];
        const alerts = [];
        let bootGraceSuppressed = false;
        try {
            const snapshot = await this.dataProvider.collectSnapshot(now);
            const bootGraceUntil = snapshot.bootedAt.getTime() + this.config.bootGraceMs;
            bootGraceSuppressed = now.getTime() < bootGraceUntil;
            detections = detectTeamHealth(snapshot, this.config);
            const orphans = detectOrphanRequests(snapshot.requests, snapshot.workItems, now, this.config.thresholds.ORPHAN_REQUEST_T1_MS);
            const teamSummaryById = new Map(snapshot.teams.map((t) => [t.id, t]));
            for (const detection of detections) {
                const team = teamSummaryById.get(detection.teamId);
                if (!team)
                    continue;
                const decision = this.router.route(detection, team, now, orphans.systemTotal);
                alerts.push(decision);
            }
            for (const decision of alerts) {
                if (decision.channel === 'suppressed')
                    continue;
                try {
                    await this.alertSink.deliver(decision);
                    this.router.commitDecision(decision, now);
                }
                catch (err) {
                    this.logger.warn('Alert delivery failed; will retry on next sweep', {
                        teamId: decision.detection.teamId,
                        verdict: decision.effectiveVerdict,
                        error: err instanceof Error ? err.message : String(err),
                    });
                }
            }
        }
        catch (err) {
            this.logger.error('THW snapshot collection or detection failed', {
                error: err instanceof Error ? err.message : String(err),
            });
        }
        const result = {
            sweptAt: now.toISOString(),
            durationMs: Date.now() - startedMs,
            detections, alerts, bootGraceSuppressed,
            shadowMode: this.config.shadowMode,
        };
        this.lastSweep = result;
        return result;
    }
    /**
     * Get the most recent sweep result; null until `start()`/`runOnce()` runs.
     */
    getLastSweep() {
        return this.lastSweep;
    }
    /**
     * Milliseconds since the last sweep. -1 if no sweep has ever completed.
     */
    getLastSweepAgeMs() {
        if (!this.lastSweep)
            return -1;
        return Math.max(0, this.clock().getTime() - new Date(this.lastSweep.sweptAt).getTime());
    }
    /**
     * Self-health probe — the watchdog-watchdog (§E.8). True when the
     * sweep loop appears stuck (age > 3× sweep interval).
     */
    isDegraded() {
        const age = this.getLastSweepAgeMs();
        if (age < 0)
            return false;
        return age > 3 * this.sweepIntervalMs;
    }
    /**
     * Per-team verdicts from the most recent sweep. Backs the
     * orchestrator `team-health-scan` skill (§6.5).
     */
    getCurrentVerdicts(teamId) {
        if (!this.lastSweep)
            return [];
        if (!teamId)
            return this.lastSweep.detections;
        return this.lastSweep.detections.filter((d) => d.teamId === teamId);
    }
    /**
     * Apply an updated config at runtime (e.g., post-edit reload).
     */
    reconfigure(config) {
        this.config = config;
        this.sweepIntervalMs = config.sweepIntervalMs;
        this.router.setShadowMode(config.shadowMode);
        this.router.reconfigure(config.alerting, config.offHoursSuppression);
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = setInterval(() => {
                void this.runOnce().catch((err) => {
                    this.logger.error('THW sweep failed (post-reconfigure)', {
                        error: err instanceof Error ? err.message : String(err),
                    });
                });
            }, this.sweepIntervalMs);
        }
    }
    /**
     * Apply a manual silence to a team (Slack 🔇 react handler).
     */
    silenceTeam(teamId, durationMs) {
        this.router.silenceTeam(teamId, this.clock(), durationMs);
    }
    /**
     * Read the running flag — used by /api/health.
     */
    isActive() {
        return this.isRunning;
    }
}
/**
 * Module-level singleton for lazy init (per Sam's etiquette nudge:
 * `getInstance()` + first-call init, not module-load side effect).
 *
 * Production wiring calls `setTeamHealthWatchdogSingleton(svc)` once during
 * boot; the controller and /api/health helpers consume `getTeamHealthWatchdogSingleton()`.
 */
let singletonInstance = null;
export function setTeamHealthWatchdogSingleton(svc) {
    singletonInstance = svc;
}
export function getTeamHealthWatchdogSingleton() {
    return singletonInstance;
}
//# sourceMappingURL=team-health-watchdog.service.js.map