use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use std::path::PathBuf;
use std::process::Command;
use tracing::{error, info, warn};

pub struct DatabaseBackup {
    database_url: String,
    backup_dir: PathBuf,
}

impl DatabaseBackup {
    pub fn new(database_url: String, backup_dir: impl Into<PathBuf>) -> Self {
        Self {
            database_url,
            backup_dir: backup_dir.into(),
        }
    }

    /// Create a full database backup
    pub async fn create_backup(&self) -> Result<PathBuf> {
        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
        let backup_filename = format!("church_db_backup_{}.sql", timestamp);
        let backup_path = self.backup_dir.join(&backup_filename);

        // Ensure backup directory exists
        if !self.backup_dir.exists() {
            std::fs::create_dir_all(&self.backup_dir)
                .context("Failed to create backup directory")?;
        }

        info!("Creating database backup at: {}", backup_path.display());

        // Parse database URL to extract connection details
        let db_url = url::Url::parse(&self.database_url)
            .context("Failed to parse database URL")?;

        let host = db_url.host_str().unwrap_or("localhost");
        let port = db_url.port().unwrap_or(5432);
        let username = db_url.username();
        let password = db_url.password().unwrap_or("");
        let database = db_url.path().trim_start_matches('/');

        // Set PGPASSWORD environment variable for pg_dump
        let mut cmd = Command::new("pg_dump");
        cmd.env("PGPASSWORD", password)
            .arg("--host").arg(host)
            .arg("--port").arg(port.to_string())
            .arg("--username").arg(username)
            .arg("--no-password")
            .arg("--verbose")
            .arg("--clean")
            .arg("--create")
            .arg("--if-exists")
            .arg("--format=custom")
            .arg("--file").arg(&backup_path)
            .arg(database);

        let output = cmd.output()
            .context("Failed to execute pg_dump command")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            error!("pg_dump failed: {}", stderr);
            return Err(anyhow::anyhow!("Backup failed: {}", stderr));
        }

        info!("Database backup completed successfully: {}", backup_path.display());
        Ok(backup_path)
    }

    /// Create a compressed SQL backup (text format)
    pub async fn create_sql_backup(&self) -> Result<PathBuf> {
        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
        let backup_filename = format!("church_db_backup_{}.sql.gz", timestamp);
        let backup_path = self.backup_dir.join(&backup_filename);

        // Ensure backup directory exists
        if !self.backup_dir.exists() {
            std::fs::create_dir_all(&self.backup_dir)
                .context("Failed to create backup directory")?;
        }

        info!("Creating compressed SQL database backup at: {}", backup_path.display());

        // Parse database URL to extract connection details
        let db_url = url::Url::parse(&self.database_url)
            .context("Failed to parse database URL")?;

        let host = db_url.host_str().unwrap_or("localhost");
        let port = db_url.port().unwrap_or(5432);
        let username = db_url.username();
        let password = db_url.password().unwrap_or("");
        let database = db_url.path().trim_start_matches('/');

        // Create pg_dump command piped to gzip
        let mut pg_dump = Command::new("pg_dump");
        pg_dump.env("PGPASSWORD", password)
            .arg("--host").arg(host)
            .arg("--port").arg(port.to_string())
            .arg("--username").arg(username)
            .arg("--no-password")
            .arg("--verbose")
            .arg("--clean")
            .arg("--create")
            .arg("--if-exists")
            .arg(database)
            .stdout(std::process::Stdio::piped());

        let pg_dump_child = pg_dump.spawn()
            .context("Failed to spawn pg_dump process")?;

        let mut gzip = Command::new("gzip");
        gzip.stdin(pg_dump_child.stdout.unwrap())
            .stdout(std::fs::File::create(&backup_path)?)
            .stderr(std::process::Stdio::piped());

        let gzip_output = gzip.output()
            .context("Failed to execute gzip command")?;

        if !gzip_output.status.success() {
            let stderr = String::from_utf8_lossy(&gzip_output.stderr);
            error!("gzip failed: {}", stderr);
            return Err(anyhow::anyhow!("Backup compression failed: {}", stderr));
        }

        info!("Compressed SQL backup completed successfully: {}", backup_path.display());
        Ok(backup_path)
    }

    /// Clean up old backups, keeping only the specified number
    pub async fn cleanup_old_backups(&self, keep_count: usize) -> Result<()> {
        info!("Cleaning up old backups, keeping {} most recent", keep_count);

        let mut backup_files: Vec<_> = std::fs::read_dir(&self.backup_dir)
            .context("Failed to read backup directory")?
            .filter_map(|entry| {
                let entry = entry.ok()?;
                let path = entry.path();
                if path.is_file() {
                    let filename = path.file_name()?.to_str()?;
                    if filename.starts_with("church_db_backup_") && 
                       (filename.ends_with(".sql") || filename.ends_with(".sql.gz")) {
                        let metadata = std::fs::metadata(&path).ok()?;
                        let modified = metadata.modified().ok()?;
                        return Some((path, modified));
                    }
                }
                None
            })
            .collect();

        if backup_files.len() <= keep_count {
            info!("No old backups to clean up");
            return Ok(());
        }

        // Sort by modification time (newest first)
        backup_files.sort_by(|a, b| b.1.cmp(&a.1));

        // Remove old backups
        for (path, _) in backup_files.into_iter().skip(keep_count) {
            match std::fs::remove_file(&path) {
                Ok(()) => info!("Removed old backup: {}", path.display()),
                Err(e) => warn!("Failed to remove old backup {}: {}", path.display(), e),
            }
        }

        Ok(())
    }

    /// List all available backups
    pub async fn list_backups(&self) -> Result<Vec<BackupInfo>> {
        let mut backups = Vec::new();

        if !self.backup_dir.exists() {
            return Ok(backups);
        }

        for entry in std::fs::read_dir(&self.backup_dir)
            .context("Failed to read backup directory")? 
        {
            let entry = entry.context("Failed to read directory entry")?;
            let path = entry.path();
            
            if path.is_file() {
                if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
                    if filename.starts_with("church_db_backup_") && 
                       (filename.ends_with(".sql") || filename.ends_with(".sql.gz")) {
                        
                        let metadata = std::fs::metadata(&path)
                            .context("Failed to get file metadata")?;
                        
                        let size = metadata.len();
                        let created = metadata.created()
                            .or_else(|_| metadata.modified())
                            .context("Failed to get file timestamp")?;
                        
                        let created_dt: DateTime<Utc> = created.into();
                        
                        backups.push(BackupInfo {
                            path: path.clone(),
                            filename: filename.to_string(),
                            size,
                            created: created_dt,
                            is_compressed: filename.ends_with(".gz"),
                        });
                    }
                }
            }
        }

        // Sort by creation time (newest first)
        backups.sort_by(|a, b| b.created.cmp(&a.created));

        Ok(backups)
    }
}

#[derive(Debug, Clone)]
pub struct BackupInfo {
    pub path: PathBuf,
    pub filename: String,
    pub size: u64,
    pub created: DateTime<Utc>,
    pub is_compressed: bool,
}

impl BackupInfo {
    pub fn size_human_readable(&self) -> String {
        const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
        let mut size = self.size as f64;
        let mut unit_index = 0;
        
        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
            size /= 1024.0;
            unit_index += 1;
        }
        
        format!("{:.1} {}", size, UNITS[unit_index])
    }
}