use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;

use super::panel_adapter::PanelAdapter;

/// Metadata for a stored backup.
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct BackupInfo {
    pub id: Uuid,
    pub license_id: Uuid,
    pub wipe_history_id: Option<Uuid>,
    pub storage_path: String,
    pub size_bytes: i64,
    pub created_at: DateTime<Utc>,
}

/// Pre-wipe backup and rollback management.
///
/// Creates snapshots of server state (map, save data, plugin data,
/// config files) before a wipe executes. Backups are used by the
/// wipe engine's rollback mechanism if post-wipe verification fails.
pub struct BackupManager {
    db: sqlx::PgPool,
    storage_base_path: String,
}

impl BackupManager {
    pub fn new(db: sqlx::PgPool, storage_base_path: String) -> Self {
        Self {
            db,
            storage_base_path,
        }
    }

    /// Create a pre-wipe backup of the server's current state.
    ///
    /// Captures: map/save files, plugin data directories, server.cfg,
    /// and any other files configured for backup in the wipe profile.
    /// Returns a backup reference ID stored in wipe_history.
    pub async fn create_backup(
        &self,
        adapter: &dyn PanelAdapter,
        server_id: &str,
        license_id: Uuid,
        wipe_history_id: Uuid,
    ) -> Result<String> {
        // Generate backup ID
        let backup_id = Uuid::new_v4();

        // Define files to backup
        let files_to_backup = vec![
            "/server/rust/server.cfg",
            "/server/rust/server/procedural/",
            "/server/rust/oxide/data/",
            "/server/rust/oxide/config/",
        ];

        // Create temporary directory for collecting files
        let temp_dir = std::env::temp_dir().join(format!("backup_{}", backup_id));
        tokio::fs::create_dir_all(&temp_dir)
            .await
            .context("Failed to create temporary backup directory")?;

        // Download files from server via panel adapter
        for file_path in &files_to_backup {
            match adapter.get_file(server_id, file_path).await {
                Ok(data) => {
                    // Create local path preserving structure
                    let local_path = temp_dir.join(file_path.trim_start_matches('/'));
                    if let Some(parent) = local_path.parent() {
                        tokio::fs::create_dir_all(parent)
                            .await
                            .context("Failed to create backup directory structure")?;
                    }
                    tokio::fs::write(&local_path, data)
                        .await
                        .with_context(|| format!("Failed to write backup file: {}", file_path))?;
                }
                Err(e) => {
                    tracing::warn!("Failed to backup file {}: {}", file_path, e);
                    // Continue with other files
                }
            }
        }

        // Create tar.gz archive
        let archive_path = format!(
            "{}/{}/backup_{}.tar.gz",
            self.storage_base_path, license_id, backup_id
        );

        // Ensure parent directory exists
        if let Some(parent) = std::path::Path::new(&archive_path).parent() {
            tokio::fs::create_dir_all(parent)
                .await
                .context("Failed to create backup storage directory")?;
        }

        // Create archive using tar command
        let output = tokio::process::Command::new("tar")
            .arg("-czf")
            .arg(&archive_path)
            .arg("-C")
            .arg(&temp_dir)
            .arg(".")
            .output()
            .await
            .context("Failed to execute tar command")?;

        if !output.status.success() {
            anyhow::bail!(
                "tar failed: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }

        // Get archive size
        let metadata = tokio::fs::metadata(&archive_path)
            .await
            .context("Failed to read archive metadata")?;
        let size_bytes = metadata.len() as i64;

        // Clean up temp directory
        let _ = tokio::fs::remove_dir_all(&temp_dir).await;

        // Insert backup record in DB
        sqlx::query(
            "INSERT INTO backups (id, license_id, wipe_history_id, storage_path, size_bytes, created_at)
             VALUES ($1, $2, $3, $4, $5, NOW())",
        )
        .bind(backup_id)
        .bind(license_id)
        .bind(wipe_history_id)
        .bind(&archive_path)
        .bind(size_bytes)
        .execute(&self.db)
        .await
        .context("Failed to insert backup record")?;

        tracing::info!(
            "Backup created: {} ({} bytes) for wipe {}",
            backup_id,
            size_bytes,
            wipe_history_id
        );

        Ok(backup_id.to_string())
    }

    /// Restore a previously created backup to the server.
    ///
    /// Used during rollback when post-wipe verification fails.
    pub async fn restore_backup(
        &self,
        adapter: &dyn PanelAdapter,
        server_id: &str,
        backup_reference: &str,
    ) -> Result<()> {
        // Parse backup ID
        let backup_id = Uuid::parse_str(backup_reference)
            .context("Invalid backup reference format")?;

        // Load backup record from DB
        let backup: Option<(String,)> = sqlx::query_as(
            "SELECT storage_path FROM backups WHERE id = $1",
        )
        .bind(backup_id)
        .fetch_optional(&self.db)
        .await
        .context("Failed to query backup")?;

        if let Some((storage_path,)) = backup {
            // Create temporary directory for extraction
            let temp_dir = std::env::temp_dir().join(format!("restore_{}", backup_id));
            tokio::fs::create_dir_all(&temp_dir)
                .await
                .context("Failed to create temporary restore directory")?;

            // Extract archive
            let output = tokio::process::Command::new("tar")
                .arg("-xzf")
                .arg(&storage_path)
                .arg("-C")
                .arg(&temp_dir)
                .output()
                .await
                .context("Failed to execute tar extraction")?;

            if !output.status.success() {
                anyhow::bail!(
                    "tar extraction failed: {}",
                    String::from_utf8_lossy(&output.stderr)
                );
            }

            // Upload extracted files back to server
            self.upload_directory_recursive(adapter, server_id, &temp_dir, "/")
                .await?;

            // Clean up temp directory
            let _ = tokio::fs::remove_dir_all(&temp_dir).await;

            tracing::info!("Backup restored: {}", backup_reference);
        } else {
            anyhow::bail!("Backup not found: {}", backup_reference);
        }

        Ok(())
    }

    /// Recursively upload directory contents to server.
    fn upload_directory_recursive<'a>(
        &'a self,
        adapter: &'a dyn PanelAdapter,
        server_id: &'a str,
        local_dir: &'a std::path::Path,
        remote_base: &'a str,
    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
        Box::pin(async move {
        let mut entries = tokio::fs::read_dir(local_dir)
            .await
            .context("Failed to read local directory")?;

        while let Some(entry) = entries.next_entry().await? {
            let path = entry.path();
            let file_name = entry.file_name();
            let remote_path = format!(
                "{}/{}",
                remote_base.trim_end_matches('/'),
                file_name.to_string_lossy()
            );

            if path.is_dir() {
                // Recurse into subdirectory
                self.upload_directory_recursive(adapter, server_id, &path, &remote_path)
                    .await?;
            } else {
                // Upload file
                let data = tokio::fs::read(&path)
                    .await
                    .with_context(|| format!("Failed to read file: {:?}", path))?;

                adapter
                    .put_file(server_id, &remote_path, &data)
                    .await
                    .with_context(|| format!("Failed to upload file: {}", remote_path))?;

                tracing::debug!("Restored file: {}", remote_path);
            }
        }

        Ok(())
        })
    }

    /// List all backups for a license, ordered by creation date (newest first).
    pub async fn list_backups(&self, license_id: Uuid) -> Result<Vec<BackupInfo>> {
        let backups: Vec<BackupInfo> = sqlx::query_as(
            "SELECT id, license_id, wipe_history_id, storage_path, size_bytes, created_at
             FROM backups
             WHERE license_id = $1
             ORDER BY created_at DESC",
        )
        .bind(license_id)
        .fetch_all(&self.db)
        .await
        .context("Failed to query backups")?;

        Ok(backups)
    }

    /// Clean up old backups beyond the configured retention period/count.
    pub async fn cleanup_old_backups(&self, license_id: Uuid) -> Result<u32> {
        // Load retention config (default: keep last 10 backups)
        let retention_count = 10;

        // Query backups older than retention threshold
        let old_backups: Vec<(Uuid, String)> = sqlx::query_as(
            "SELECT id, storage_path
             FROM backups
             WHERE license_id = $1
             ORDER BY created_at DESC
             OFFSET $2",
        )
        .bind(license_id)
        .bind(retention_count)
        .fetch_all(&self.db)
        .await
        .context("Failed to query old backups")?;

        let mut deleted_count = 0;

        for (backup_id, storage_path) in old_backups {
            // Delete backup file from storage
            if tokio::fs::remove_file(&storage_path).await.is_err() {
                tracing::warn!("Failed to delete backup file: {}", storage_path);
            }

            // Delete backup record from DB
            sqlx::query("DELETE FROM backups WHERE id = $1")
                .bind(backup_id)
                .execute(&self.db)
                .await
                .context("Failed to delete backup record")?;

            deleted_count += 1;
        }

        if deleted_count > 0 {
            tracing::info!(
                "Cleaned up {} old backups for license {}",
                deleted_count,
                license_id
            );
        }

        Ok(deleted_count)
    }
}
