//! Sweep and reactive subscriber for promoting `done` items to `archived`.
//!
//! Items in `Stage::Done` whose `merged_at` timestamp exceeds the configured
//! retention duration are promoted to `Stage::Archived` via the canonical
//! pipeline state machine (story 934, stage 5).
//!
//! `sweep_done_to_archived` is the synchronous one-shot sweep (used in tests
//! and for backwards-compat call sites). `spawn_done_to_archived_subscriber`
//! is the reactive replacement for the tick-loop periodic scan.

use crate::slog;
use crate::slog_warn;
use std::time::Duration;

/// Spawn a subscriber that archives each `Stage::Done` story after the
/// configured retention period expires.
///
/// Subscribes to the pipeline [`TransitionFired`][crate::pipeline_state::TransitionFired]
/// broadcast channel. On each `Stage::Done` transition, spawns a short-lived
/// task that sleeps for the remaining retention time (computed from the story's
/// `merged_at` timestamp) and then calls
/// [`apply_transition`][crate::pipeline_state::apply_transition] with
/// `PipelineEvent::Accepted` to move it to `Stage::Archived`.
///
/// Using `merged_at` rather than a fixed sleep means the subscriber correctly
/// handles stories that have been in `Done` for hours before a server restart:
/// the computed remaining time will be small (or zero), so archival happens
/// promptly rather than waiting another full retention period.
///
/// Replaces the periodic `sweep_done_to_archived` call from the tick loop.
pub(crate) fn spawn_done_to_archived_subscriber(done_retention: Duration) {
    use crate::pipeline_state::{
        PipelineEvent, Stage, Status, apply_transition, subscribe_transitions,
    };

    let mut rx = subscribe_transitions();
    tokio::spawn(async move {
        loop {
            match rx.recv().await {
                Ok(fired) => {
                    // Story 1086: gate on the typed `Status::Done` projection;
                    // the variant pattern is still required to read `merged_at`.
                    if fired.after.status() != Status::Done {
                        continue;
                    }
                    if let Stage::Done { merged_at, .. } = fired.after {
                        let story_id = fired.story_id.0.clone();
                        let retention = done_retention;
                        tokio::spawn(async move {
                            let age = chrono::Utc::now()
                                .signed_duration_since(merged_at)
                                .to_std()
                                .unwrap_or_default();
                            if age < retention {
                                tokio::time::sleep(retention - age).await;
                            }
                            match apply_transition(&story_id, PipelineEvent::Accepted, None) {
                                Ok(_) => {
                                    slog!("[watcher] sweep: promoted {story_id} → archived")
                                }
                                Err(e) => {
                                    slog!("[watcher] sweep: transition error for {story_id}: {e}")
                                }
                            }
                        });
                    }
                }
                Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
                    slog_warn!(
                        "[done-archive-sub] Lagged, skipped {n} event(s); some Done stories \
                         may not auto-archive."
                    );
                }
                Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
            }
        }
    });
}

/// Reconcile: sweep items in `Stage::Done` whose `merged_at` timestamp exceeds the
/// retention duration to `Stage::Archived` via the typed transition table.
///
/// Routes through [`crate::pipeline_state::apply_transition`] so the
/// `Done + Accepted → Archived` transition is validated and a
/// `TransitionFired` event is emitted to subscribers (worktree pruning,
/// matrix notifier, etc.).
///
/// Called at startup and by the periodic reconciler to archive Done stories
/// whose retention has elapsed, even when the `TransitionFired` subscriber
/// lagged and missed their Done event.  Production reactive archiving uses
/// [`spawn_done_to_archived_subscriber`] instead.
///
/// Logs a summary INFO line on every call: candidates evaluated and items
/// archived, or "no items past retention" when nothing was swept.
pub(crate) fn sweep_done_to_archived(done_retention: Duration) {
    use crate::pipeline_state::{PipelineEvent, Stage, apply_transition, read_all_typed};

    let mut candidates: usize = 0;
    let mut archived: usize = 0;

    for item in read_all_typed() {
        if let Stage::Done { merged_at, .. } = &item.stage {
            candidates += 1;
            let age = chrono::Utc::now()
                .signed_duration_since(*merged_at)
                .to_std()
                .unwrap_or_default();
            if age >= done_retention {
                let story_id = item.story_id.0.clone();
                match apply_transition(&story_id, PipelineEvent::Accepted, None) {
                    Ok(_) => {
                        archived += 1;
                        slog!("[watcher] sweep: promoted {story_id} → archived")
                    }
                    Err(e) => {
                        slog!("[watcher] sweep: transition error for {story_id}: {e}")
                    }
                }
            }
        }
    }

    if archived > 0 {
        slog!("[watcher] sweep: {candidates} candidate(s) evaluated, {archived} archived");
    } else {
        slog!("[watcher] sweep: {candidates} candidate(s) evaluated, no items past retention");
    }
}
