use crate::http::AppState;
use harness_core::{types::EventFilters, types::Grade};
use harness_observe::health::generate_health_report;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;

pub struct Scheduler {
    pub gc_interval: Duration,
    pub health_interval: Duration,
    pub self_evolution_interval: Duration,
    /// How often the disk workspace GC scan runs. Default: 1 hour.
    pub workspace_gc_interval: Duration,
}

impl Scheduler {
    pub fn from_grade(grade: Grade) -> Self {
        Self {
            gc_interval: grade.recommended_gc_interval(),
            health_interval: Duration::from_secs(24 * 3600),
            self_evolution_interval: Duration::from_secs(24 * 3600),
            workspace_gc_interval: Duration::from_secs(3600),
        }
    }

    pub fn start(self, state: Arc<AppState>) {
        let gc_state = state.clone();
        let gc_interval = self.gc_interval;
        tokio::spawn(async move {
            loop {
                sleep(gc_interval).await;
                tracing::info!("scheduler: triggering periodic GC run");
                crate::handlers::gc::gc_run(&gc_state, None, None).await;
            }
        });

        let health_state = state.clone();
        let health_interval = self.health_interval;
        tokio::spawn(async move {
            loop {
                sleep(health_interval).await;
                if let Err(err) = Self::run_health_tick(&health_state).await {
                    tracing::error!("scheduler: periodic health tick failed: {err}");
                }
            }
        });

        // Periodic disk workspace GC: removes on-disk worktrees for closed issues/PRs.
        let wgc_state = state.clone();
        let wgc_interval = self.workspace_gc_interval;
        tokio::spawn(async move {
            // Brief init delay so the server is fully up before the first scan.
            sleep(Duration::from_secs(30)).await;
            loop {
                if let Some(ref wmgr) = wgc_state.concurrency.workspace_mgr {
                    let max_rate = wgc_state
                        .core
                        .server
                        .config
                        .reconciliation
                        .max_gh_calls_per_minute;
                    let summary = wmgr
                        .reconcile_disk_workspaces(
                            &wgc_state.core.project_root,
                            "gh",
                            max_rate,
                            wgc_state.core.server.config.server.github_token.as_deref(),
                        )
                        .await;
                    tracing::info!(
                        scanned = summary.scanned,
                        removed = summary.removed,
                        skipped_uuid = summary.skipped_uuid,
                        skipped_open = summary.skipped_open,
                        "scheduler: workspace disk GC complete"
                    );
                }
                sleep(wgc_interval).await;
            }
        });

        let review_config = state.core.server.config.review.clone();
        let retry_config = state.core.server.config.retry_scheduler.clone();
        let reconciliation_config = state.core.server.config.reconciliation.clone();
        crate::self_evolution::start(state.clone(), self.self_evolution_interval);
        crate::periodic_reviewer::start(state.clone(), review_config);
        crate::periodic_retry::start(state.clone(), retry_config);
        crate::reconciliation::start(state, reconciliation_config);
    }

    async fn run_health_tick(state: &AppState) -> anyhow::Result<()> {
        // Query historical events before persisting the current scan to avoid the
        // just-persisted rule_check events inflating the quality stability score.
        let events = state
            .observability
            .events
            .query(&EventFilters::default())
            .await
            .map_err(|err| anyhow::anyhow!("failed to query events: {err}"))?;
        let project_root = state.core.project_root.clone();
        let violations = {
            let rules = state.engines.rules.read().await;
            rules.scan(&project_root).await.map_err(|err| {
                anyhow::anyhow!(
                    "failed to scan rules for '{}': {err}",
                    project_root.display()
                )
            })?
        };
        state
            .observability
            .events
            .persist_rule_scan(&project_root, &violations)
            .await;
        let report = generate_health_report(&events, &violations);
        tracing::info!(
            grade = ?report.quality.grade,
            score = report.quality.score,
            violations = report.violation_summary.len(),
            "scheduler: periodic health report"
        );
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use harness_core::{types::EventFilters, types::Grade, types::GuardId, types::Language};
    use harness_rules::engine::Guard;

    async fn make_test_state(
        dir: &std::path::Path,
        project_root: &std::path::Path,
    ) -> anyhow::Result<Arc<AppState>> {
        crate::test_helpers::make_test_state_with_project_root(dir, project_root).await
    }

    async fn register_failing_guard(state: &AppState) {
        let mut rules = state.engines.rules.write().await;
        rules.register_guard(Guard {
            id: GuardId::from_str("FAIL-SCAN-GUARD"),
            script_path: std::path::PathBuf::from("fail\0scan.sh"),
            language: Language::Common,
            rules: vec![],
        });
    }

    #[test]
    fn from_grade_d_returns_1h_gc_interval() {
        let s = Scheduler::from_grade(Grade::D);
        assert_eq!(s.gc_interval, Duration::from_secs(3600));
        assert_eq!(s.health_interval, Duration::from_secs(24 * 3600));
        assert_eq!(s.self_evolution_interval, Duration::from_secs(24 * 3600));
        assert_eq!(s.workspace_gc_interval, Duration::from_secs(3600));
    }

    #[test]
    fn from_grade_a_returns_7d_gc_interval() {
        let s = Scheduler::from_grade(Grade::A);
        assert_eq!(s.gc_interval, Duration::from_secs(7 * 24 * 3600));
        assert_eq!(s.health_interval, Duration::from_secs(24 * 3600));
        assert_eq!(s.self_evolution_interval, Duration::from_secs(24 * 3600));
        assert_eq!(s.workspace_gc_interval, Duration::from_secs(3600));
    }

    #[test]
    fn from_grade_b_returns_3d_gc_interval() {
        let s = Scheduler::from_grade(Grade::B);
        assert_eq!(s.gc_interval, Duration::from_secs(3 * 24 * 3600));
    }

    #[test]
    fn from_grade_c_returns_1d_gc_interval() {
        let s = Scheduler::from_grade(Grade::C);
        assert_eq!(s.gc_interval, Duration::from_secs(24 * 3600));
    }

    #[tokio::test]
    async fn health_tick_uses_configured_project_root_in_multi_cwd_context() -> anyhow::Result<()> {
        let data_dir = tempfile::tempdir()?;
        let project_root = tempfile::tempdir()?;
        let state = make_test_state(data_dir.path(), project_root.path()).await?;

        let cwd = std::env::current_dir()?;
        assert_ne!(cwd, project_root.path());

        Scheduler::run_health_tick(&state).await?;

        let events = state
            .observability
            .events
            .query(&EventFilters::default())
            .await?;
        let scan = events
            .iter()
            .rev()
            .find(|event| event.hook == "rule_scan")
            .ok_or_else(|| anyhow::anyhow!("expected scheduler to persist rule_scan event"))?;
        let expected_root = project_root.path().display().to_string();
        assert_eq!(scan.detail.as_deref(), Some(expected_root.as_str()));

        Ok(())
    }

    #[tokio::test]
    async fn health_tick_returns_error_when_scan_fails() -> anyhow::Result<()> {
        let data_dir = tempfile::tempdir()?;
        let project_root = tempfile::tempdir()?;
        let state = make_test_state(data_dir.path(), project_root.path()).await?;
        register_failing_guard(&state).await;

        let error = Scheduler::run_health_tick(&state)
            .await
            .expect_err("expected scan failure to be surfaced");
        let message = error.to_string();
        assert!(
            message.contains("failed to scan rules"),
            "unexpected scheduler scan failure message: {message}"
        );

        let events = state
            .observability
            .events
            .query(&EventFilters::default())
            .await?;
        assert!(
            events.iter().all(|event| event.hook != "rule_scan"),
            "scan failure should not persist a rule_scan event"
        );
        Ok(())
    }
}
