use serde_json::{json, Value};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;

pub struct GcConfig {
    pub interval_hours: f64,
    pub temp_file_max_age_hours: f64,
    pub multipart_max_age_days: u64,
    pub lock_file_max_age_hours: f64,
    pub quarantine_max_age_days: u64,
    pub dry_run: bool,
}

impl Default for GcConfig {
    fn default() -> Self {
        Self {
            interval_hours: 6.0,
            temp_file_max_age_hours: 24.0,
            multipart_max_age_days: 7,
            lock_file_max_age_hours: 1.0,
            quarantine_max_age_days: 7,
            dry_run: false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn dry_run_reports_but_does_not_delete_temp_files() {
        let tmp = tempfile::tempdir().unwrap();
        let tmp_dir = tmp.path().join(".myfsio.sys").join("tmp");
        std::fs::create_dir_all(&tmp_dir).unwrap();
        let file_path = tmp_dir.join("stale.tmp");
        std::fs::write(&file_path, b"temporary").unwrap();
        tokio::time::sleep(std::time::Duration::from_millis(5)).await;

        let service = GcService::new(
            tmp.path().to_path_buf(),
            GcConfig {
                temp_file_max_age_hours: 0.0,
                dry_run: true,
                ..GcConfig::default()
            },
        );

        let result = service.run_now(false).await.unwrap();

        assert_eq!(result["temp_files_deleted"], 1);
        assert!(file_path.exists());
    }
}

pub struct GcService {
    storage_root: PathBuf,
    config: GcConfig,
    running: Arc<RwLock<bool>>,
    started_at: Arc<RwLock<Option<Instant>>>,
    history: Arc<RwLock<Vec<Value>>>,
    history_path: PathBuf,
}

impl GcService {
    pub fn new(storage_root: PathBuf, config: GcConfig) -> Self {
        let history_path = storage_root
            .join(".myfsio.sys")
            .join("config")
            .join("gc_history.json");

        let history = if history_path.exists() {
            std::fs::read_to_string(&history_path)
                .ok()
                .and_then(|s| serde_json::from_str::<Value>(&s).ok())
                .and_then(|v| v.get("executions").and_then(|e| e.as_array().cloned()))
                .unwrap_or_default()
        } else {
            Vec::new()
        };

        Self {
            storage_root,
            config,
            running: Arc::new(RwLock::new(false)),
            started_at: Arc::new(RwLock::new(None)),
            history: Arc::new(RwLock::new(history)),
            history_path,
        }
    }

    pub async fn status(&self) -> Value {
        let running = *self.running.read().await;
        let scan_elapsed_seconds = self
            .started_at
            .read()
            .await
            .as_ref()
            .map(|started| started.elapsed().as_secs_f64());
        json!({
            "enabled": true,
            "running": running,
            "scanning": running,
            "scan_elapsed_seconds": scan_elapsed_seconds,
            "interval_hours": self.config.interval_hours,
            "temp_file_max_age_hours": self.config.temp_file_max_age_hours,
            "multipart_max_age_days": self.config.multipart_max_age_days,
            "lock_file_max_age_hours": self.config.lock_file_max_age_hours,
            "quarantine_max_age_days": self.config.quarantine_max_age_days,
            "dry_run": self.config.dry_run,
        })
    }

    pub async fn history(&self) -> Value {
        let history = self.history.read().await;
        let mut executions: Vec<Value> = history.iter().cloned().collect();
        executions.reverse();
        json!({ "executions": executions })
    }

    pub async fn run_now(&self, dry_run: bool) -> Result<Value, String> {
        {
            let mut running = self.running.write().await;
            if *running {
                return Err("GC already running".to_string());
            }
            *running = true;
        }
        *self.started_at.write().await = Some(Instant::now());

        let start = Instant::now();
        let result = self.execute_gc(dry_run || self.config.dry_run).await;
        let elapsed = start.elapsed().as_secs_f64();

        *self.running.write().await = false;
        *self.started_at.write().await = None;

        let mut result_json = result.clone();
        if let Some(obj) = result_json.as_object_mut() {
            obj.insert("execution_time_seconds".to_string(), json!(elapsed));
        }

        let record = json!({
            "timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
            "dry_run": dry_run || self.config.dry_run,
            "result": result_json,
        });

        {
            let mut history = self.history.write().await;
            history.push(record);
            if history.len() > 50 {
                let excess = history.len() - 50;
                history.drain(..excess);
            }
        }
        self.save_history().await;

        Ok(result)
    }

    async fn execute_gc(&self, dry_run: bool) -> Value {
        let mut temp_files_deleted = 0u64;
        let mut temp_bytes_freed = 0u64;
        let mut multipart_uploads_deleted = 0u64;
        let mut lock_files_deleted = 0u64;
        let mut empty_dirs_removed = 0u64;
        let mut quarantine_entries_deleted = 0u64;
        let mut quarantine_bytes_freed = 0u64;
        let mut errors: Vec<String> = Vec::new();

        let now = std::time::SystemTime::now();
        let temp_max_age =
            std::time::Duration::from_secs_f64(self.config.temp_file_max_age_hours * 3600.0);
        let multipart_max_age =
            std::time::Duration::from_secs(self.config.multipart_max_age_days * 86400);
        let lock_max_age =
            std::time::Duration::from_secs_f64(self.config.lock_file_max_age_hours * 3600.0);
        let quarantine_max_age =
            std::time::Duration::from_secs(self.config.quarantine_max_age_days * 86400);

        let tmp_dir = self.storage_root.join(".myfsio.sys").join("tmp");
        if tmp_dir.exists() {
            match std::fs::read_dir(&tmp_dir) {
                Ok(entries) => {
                    for entry in entries.flatten() {
                        if let Ok(metadata) = entry.metadata() {
                            if let Ok(modified) = metadata.modified() {
                                if let Ok(age) = now.duration_since(modified) {
                                    if age > temp_max_age {
                                        let size = metadata.len();
                                        if !dry_run {
                                            if let Err(e) = std::fs::remove_file(entry.path()) {
                                                errors.push(format!(
                                                    "Failed to remove temp file: {}",
                                                    e
                                                ));
                                                continue;
                                            }
                                        }
                                        temp_files_deleted += 1;
                                        temp_bytes_freed += size;
                                    }
                                }
                            }
                        }
                    }
                }
                Err(e) => errors.push(format!("Failed to read tmp dir: {}", e)),
            }
        }

        let multipart_dir = self.storage_root.join(".myfsio.sys").join("multipart");
        if multipart_dir.exists() {
            if let Ok(bucket_dirs) = std::fs::read_dir(&multipart_dir) {
                for bucket_entry in bucket_dirs.flatten() {
                    if let Ok(uploads) = std::fs::read_dir(bucket_entry.path()) {
                        for upload in uploads.flatten() {
                            if let Ok(metadata) = upload.metadata() {
                                if let Ok(modified) = metadata.modified() {
                                    if let Ok(age) = now.duration_since(modified) {
                                        if age > multipart_max_age {
                                            if !dry_run {
                                                let _ = std::fs::remove_dir_all(upload.path());
                                            }
                                            multipart_uploads_deleted += 1;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        let buckets_dir = self.storage_root.join(".myfsio.sys").join("buckets");
        if buckets_dir.exists() {
            if let Ok(bucket_dirs) = std::fs::read_dir(&buckets_dir) {
                for bucket_entry in bucket_dirs.flatten() {
                    let locks_dir = bucket_entry.path().join("locks");
                    if locks_dir.exists() {
                        if let Ok(locks) = std::fs::read_dir(&locks_dir) {
                            for lock in locks.flatten() {
                                if let Ok(metadata) = lock.metadata() {
                                    if let Ok(modified) = metadata.modified() {
                                        if let Ok(age) = now.duration_since(modified) {
                                            if age > lock_max_age {
                                                if !dry_run {
                                                    let _ = std::fs::remove_file(lock.path());
                                                }
                                                lock_files_deleted += 1;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        let quarantine_dir = self.storage_root.join(".myfsio.sys").join("quarantine");
        if quarantine_dir.exists() {
            if let Ok(bucket_dirs) = std::fs::read_dir(&quarantine_dir) {
                for bucket_entry in bucket_dirs.flatten() {
                    if !bucket_entry.path().is_dir() {
                        continue;
                    }
                    if let Ok(ts_dirs) = std::fs::read_dir(bucket_entry.path()) {
                        for ts_entry in ts_dirs.flatten() {
                            let ts_path = ts_entry.path();
                            if !ts_path.is_dir() {
                                continue;
                            }
                            let modified = ts_entry.metadata().ok().and_then(|m| m.modified().ok());
                            let Some(modified) = modified else {
                                continue;
                            };
                            let Ok(age) = now.duration_since(modified) else {
                                continue;
                            };
                            if age <= quarantine_max_age {
                                continue;
                            }
                            let bytes = dir_total_bytes(&ts_path);
                            if !dry_run {
                                if let Err(e) = std::fs::remove_dir_all(&ts_path) {
                                    errors.push(format!(
                                        "Failed to remove quarantine {}: {}",
                                        ts_path.display(),
                                        e
                                    ));
                                    continue;
                                }
                            }
                            quarantine_entries_deleted += 1;
                            quarantine_bytes_freed += bytes;
                        }
                    }
                    if !dry_run {
                        if let Ok(mut remaining) = std::fs::read_dir(bucket_entry.path()) {
                            if remaining.next().is_none() {
                                let _ = std::fs::remove_dir(bucket_entry.path());
                            }
                        }
                    }
                }
            }
        }

        if !dry_run {
            for dir in [&tmp_dir, &multipart_dir] {
                if dir.exists() {
                    if let Ok(entries) = std::fs::read_dir(dir) {
                        for entry in entries.flatten() {
                            if entry.path().is_dir() {
                                if let Ok(mut contents) = std::fs::read_dir(entry.path()) {
                                    if contents.next().is_none() {
                                        let _ = std::fs::remove_dir(entry.path());
                                        empty_dirs_removed += 1;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        json!({
            "temp_files_deleted": temp_files_deleted,
            "temp_bytes_freed": temp_bytes_freed,
            "multipart_uploads_deleted": multipart_uploads_deleted,
            "lock_files_deleted": lock_files_deleted,
            "empty_dirs_removed": empty_dirs_removed,
            "quarantine_entries_deleted": quarantine_entries_deleted,
            "quarantine_bytes_freed": quarantine_bytes_freed,
            "errors": errors,
        })
    }

    async fn save_history(&self) {
        let history = self.history.read().await;
        let data = json!({ "executions": *history });
        if let Some(parent) = self.history_path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        let _ = std::fs::write(
            &self.history_path,
            serde_json::to_string_pretty(&data).unwrap_or_default(),
        );
    }

    pub fn start_background(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
        let interval = std::time::Duration::from_secs_f64(self.config.interval_hours * 3600.0);
        tokio::spawn(async move {
            let mut timer = tokio::time::interval(interval);
            timer.tick().await;
            loop {
                timer.tick().await;
                tracing::info!("GC cycle starting");
                match self.run_now(false).await {
                    Ok(result) => tracing::info!("GC cycle complete: {:?}", result),
                    Err(e) => tracing::warn!("GC cycle failed: {}", e),
                }
            }
        })
    }
}

fn dir_total_bytes(path: &std::path::Path) -> u64 {
    let mut total: u64 = 0;
    let mut stack: Vec<PathBuf> = vec![path.to_path_buf()];
    while let Some(dir) = stack.pop() {
        let Ok(entries) = std::fs::read_dir(&dir) else {
            continue;
        };
        for entry in entries.flatten() {
            let Ok(ft) = entry.file_type() else { continue };
            if ft.is_dir() {
                stack.push(entry.path());
            } else if ft.is_file() {
                total = total.saturating_add(entry.metadata().map(|m| m.len()).unwrap_or(0));
            }
        }
    }
    total
}
