//! Configuration backup and rollback functionality
//!
//! This module provides:
//! - Configuration file backup before changes
//! - Rollback to previous configurations
//! - Configuration history tracking
//! - Automatic backup on configuration changes

use crate::config::PostgresConfig;
use crate::error::{Error, Result};
use lmrc_ssh::SshClient;
use tracing::{debug, info, warn};

/// Backup metadata
#[derive(Debug, Clone)]
pub struct ConfigBackup {
    /// Backup timestamp
    pub timestamp: String,
    /// PostgreSQL version
    pub version: String,
    /// Backup directory path
    pub backup_dir: String,
    /// Backed up files
    pub files: Vec<String>,
}

/// Configuration history entry
#[derive(Debug, Clone)]
pub struct ConfigHistoryEntry {
    /// Entry timestamp
    pub timestamp: String,
    /// Configuration changes made
    pub changes: Vec<String>,
    /// Backup ID
    pub backup_id: String,
}

/// Create a backup of PostgreSQL configuration files
///
/// Backs up:
/// - postgresql.conf
/// - pg_hba.conf
/// - pg_ident.conf (if exists)
///
/// Returns the backup directory path
pub async fn backup_config(ssh: &mut SshClient, config: &PostgresConfig) -> Result<ConfigBackup> {
    info!("Creating configuration backup");

    let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
    let backup_dir = format!("/var/backups/postgresql/{}", timestamp);
    let config_dir = config.config_dir();

    // Create backup directory
    debug!("Creating backup directory: {}", backup_dir);
    ssh.execute(&format!("mkdir -p {}", backup_dir))
        .map_err(|e| Error::Configuration(format!("Failed to create backup directory: {}", e)))?;

    let mut backed_up_files = Vec::new();

    // Backup postgresql.conf
    let postgresql_conf = format!("{}/postgresql.conf", config_dir);
    if ssh.execute(&format!("test -f {}", postgresql_conf)).is_ok() {
        ssh.execute(&format!(
            "cp {} {}/postgresql.conf",
            postgresql_conf, backup_dir
        ))
        .map_err(|e| Error::Configuration(format!("Failed to backup postgresql.conf: {}", e)))?;
        backed_up_files.push("postgresql.conf".to_string());
        debug!("✓ Backed up postgresql.conf");
    }

    // Backup pg_hba.conf
    let pg_hba_conf = format!("{}/pg_hba.conf", config_dir);
    if ssh.execute(&format!("test -f {}", pg_hba_conf)).is_ok() {
        ssh.execute(&format!("cp {} {}/pg_hba.conf", pg_hba_conf, backup_dir))
            .map_err(|e| Error::Configuration(format!("Failed to backup pg_hba.conf: {}", e)))?;
        backed_up_files.push("pg_hba.conf".to_string());
        debug!("✓ Backed up pg_hba.conf");
    }

    // Backup pg_ident.conf if it exists
    let pg_ident_conf = format!("{}/pg_ident.conf", config_dir);
    if ssh.execute(&format!("test -f {}", pg_ident_conf)).is_ok() {
        ssh.execute(&format!(
            "cp {} {}/pg_ident.conf",
            pg_ident_conf, backup_dir
        ))
        .ok(); // Don't fail if this doesn't exist
        backed_up_files.push("pg_ident.conf".to_string());
        debug!("✓ Backed up pg_ident.conf");
    }

    // Save backup metadata
    let metadata = format!(
        "timestamp={}\nversion={}\nfiles={}\n",
        timestamp,
        config.version,
        backed_up_files.join(",")
    );
    ssh.execute(&format!("echo '{}' > {}/backup.meta", metadata, backup_dir))
        .map_err(|e| Error::Configuration(format!("Failed to save backup metadata: {}", e)))?;

    info!(
        "✓ Configuration backup created: {} ({} files)",
        backup_dir,
        backed_up_files.len()
    );

    Ok(ConfigBackup {
        timestamp,
        version: config.version.clone(),
        backup_dir,
        files: backed_up_files,
    })
}

/// List available configuration backups
pub async fn list_backups(ssh: &mut SshClient) -> Result<Vec<ConfigBackup>> {
    debug!("Listing configuration backups");

    let backup_base = "/var/backups/postgresql";

    // Check if backup directory exists
    if ssh.execute(&format!("test -d {}", backup_base)).is_err() {
        return Ok(Vec::new());
    }

    // List backup directories
    let output = ssh
        .execute(&format!("ls -1 {}", backup_base))
        .map_err(|e| Error::Configuration(format!("Failed to list backups: {}", e)))?;

    let mut backups = Vec::new();

    for line in output.stdout.lines() {
        let timestamp = line.trim();
        if timestamp.is_empty() {
            continue;
        }

        let backup_dir = format!("{}/{}", backup_base, timestamp);

        // Read metadata if available
        let meta_result = ssh.execute(&format!("cat {}/backup.meta 2>/dev/null", backup_dir));

        let (version, files) = if let Ok(meta_output) = meta_result {
            let mut ver = String::new();
            let mut file_list = Vec::new();

            for meta_line in meta_output.stdout.lines() {
                if let Some(val) = meta_line.strip_prefix("version=") {
                    ver = val.to_string();
                } else if let Some(val) = meta_line.strip_prefix("files=") {
                    file_list = val.split(',').map(|s| s.to_string()).collect();
                }
            }

            (ver, file_list)
        } else {
            (String::new(), Vec::new())
        };

        backups.push(ConfigBackup {
            timestamp: timestamp.to_string(),
            version,
            backup_dir,
            files,
        });
    }

    debug!("Found {} backup(s)", backups.len());
    Ok(backups)
}

/// Restore configuration from a backup
pub async fn restore_backup(
    ssh: &mut SshClient,
    config: &PostgresConfig,
    backup: &ConfigBackup,
) -> Result<()> {
    info!("Restoring configuration from backup: {}", backup.timestamp);

    let config_dir = config.config_dir();

    // Restore postgresql.conf
    if backup.files.contains(&"postgresql.conf".to_string()) {
        ssh.execute(&format!(
            "cp {}/postgresql.conf {}/postgresql.conf",
            backup.backup_dir, config_dir
        ))
        .map_err(|e| Error::Configuration(format!("Failed to restore postgresql.conf: {}", e)))?;
        debug!("✓ Restored postgresql.conf");
    }

    // Restore pg_hba.conf
    if backup.files.contains(&"pg_hba.conf".to_string()) {
        ssh.execute(&format!(
            "cp {}/pg_hba.conf {}/pg_hba.conf",
            backup.backup_dir, config_dir
        ))
        .map_err(|e| Error::Configuration(format!("Failed to restore pg_hba.conf: {}", e)))?;
        debug!("✓ Restored pg_hba.conf");
    }

    // Restore pg_ident.conf if it was backed up
    if backup.files.contains(&"pg_ident.conf".to_string()) {
        ssh.execute(&format!(
            "cp {}/pg_ident.conf {}/pg_ident.conf",
            backup.backup_dir, config_dir
        ))
        .ok(); // Don't fail if this doesn't exist
        debug!("✓ Restored pg_ident.conf");
    }

    info!("✓ Configuration restored successfully");

    // Note: Service needs to be reloaded
    warn!("Configuration restored. Run 'systemctl reload postgresql' to apply changes.");

    Ok(())
}

/// Rollback to the most recent backup
pub async fn rollback_config(ssh: &mut SshClient, config: &PostgresConfig) -> Result<()> {
    info!("Rolling back to most recent configuration backup");

    let backups = list_backups(ssh).await?;

    if backups.is_empty() {
        return Err(Error::Configuration(
            "No configuration backups found".to_string(),
        ));
    }

    // Get the most recent backup
    let latest_backup = &backups[backups.len() - 1];

    restore_backup(ssh, config, latest_backup).await?;

    info!("✓ Rolled back to backup: {}", latest_backup.timestamp);

    Ok(())
}

/// Delete old backups, keeping only the most recent N backups
pub async fn cleanup_old_backups(ssh: &mut SshClient, keep_count: usize) -> Result<usize> {
    debug!("Cleaning up old backups, keeping {}", keep_count);

    let backups = list_backups(ssh).await?;

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

    let to_delete = backups.len() - keep_count;
    let mut deleted = 0;

    // Delete oldest backups
    for backup in backups.iter().take(to_delete) {
        ssh.execute(&format!("rm -rf {}", backup.backup_dir))
            .map_err(|e| Error::Configuration(format!("Failed to delete backup: {}", e)))?;
        deleted += 1;
        debug!("Deleted old backup: {}", backup.timestamp);
    }

    info!("✓ Cleaned up {} old backup(s)", deleted);
    Ok(deleted)
}

/// Read current pg_hba.conf content
pub async fn read_pg_hba(ssh: &mut SshClient, config: &PostgresConfig) -> Result<String> {
    let pg_hba_path = config.pg_hba_conf_path();

    let output = ssh
        .execute(&format!("cat {}", pg_hba_path))
        .map_err(|e| Error::Configuration(format!("Failed to read pg_hba.conf: {}", e)))?;

    Ok(output.stdout)
}

/// Parse pg_hba.conf into structured rules
pub fn parse_pg_hba_rules(content: &str) -> Vec<PgHbaRule> {
    let mut rules = Vec::new();

    for line in content.lines() {
        let trimmed = line.trim();

        // Skip empty lines and comments
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }

        // Parse rule: TYPE DATABASE USER ADDRESS METHOD
        let parts: Vec<&str> = trimmed.split_whitespace().collect();
        if parts.len() >= 4 {
            rules.push(PgHbaRule {
                rule_type: parts[0].to_string(),
                database: parts[1].to_string(),
                user: parts[2].to_string(),
                address: if parts.len() >= 5 {
                    Some(parts[3].to_string())
                } else {
                    None
                },
                method: parts[parts.len() - 1].to_string(),
                raw_line: line.to_string(),
            });
        }
    }

    rules
}

/// pg_hba.conf rule
#[derive(Debug, Clone, PartialEq)]
pub struct PgHbaRule {
    /// Connection type (local, host, hostssl, hostnossl)
    pub rule_type: String,
    /// Database name
    pub database: String,
    /// User name
    pub user: String,
    /// IP address/CIDR (for host connections)
    pub address: Option<String>,
    /// Authentication method
    pub method: String,
    /// Raw line from config
    pub raw_line: String,
}

impl std::fmt::Display for PgHbaRule {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(ref addr) = self.address {
            write!(
                f,
                "{:<10} {:<15} {:<15} {:<18} {}",
                self.rule_type, self.database, self.user, addr, self.method
            )
        } else {
            write!(
                f,
                "{:<10} {:<15} {:<15} {}",
                self.rule_type, self.database, self.user, self.method
            )
        }
    }
}

/// Compare two sets of pg_hba rules and return differences
pub fn diff_pg_hba_rules(current: &[PgHbaRule], desired: &[PgHbaRule]) -> Vec<PgHbaDiff> {
    let mut diffs = Vec::new();

    // Check for removed or modified rules
    for (idx, current_rule) in current.iter().enumerate() {
        if let Some(desired_rule) = desired.get(idx) {
            if current_rule != desired_rule {
                diffs.push(PgHbaDiff::Modified {
                    line_number: idx + 1,
                    old: current_rule.clone(),
                    new: desired_rule.clone(),
                });
            }
        } else {
            diffs.push(PgHbaDiff::Removed {
                line_number: idx + 1,
                rule: current_rule.clone(),
            });
        }
    }

    // Check for added rules
    for (idx, desired_rule) in desired.iter().enumerate() {
        if idx >= current.len() {
            diffs.push(PgHbaDiff::Added {
                line_number: idx + 1,
                rule: desired_rule.clone(),
            });
        }
    }

    diffs
}

/// pg_hba.conf difference
#[derive(Debug, Clone)]
pub enum PgHbaDiff {
    /// Rule added
    Added { line_number: usize, rule: PgHbaRule },
    /// Rule removed
    Removed { line_number: usize, rule: PgHbaRule },
    /// Rule modified
    Modified {
        line_number: usize,
        old: PgHbaRule,
        new: PgHbaRule,
    },
}

impl std::fmt::Display for PgHbaDiff {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PgHbaDiff::Added { line_number, rule } => {
                write!(f, "[+] Line {}: {}", line_number, rule)
            }
            PgHbaDiff::Removed { line_number, rule } => {
                write!(f, "[-] Line {}: {}", line_number, rule)
            }
            PgHbaDiff::Modified {
                line_number,
                old,
                new,
            } => {
                write!(f, "[~] Line {}: {} → {}", line_number, old, new)
            }
        }
    }
}

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

    #[test]
    fn test_parse_pg_hba_rules() {
        let content = r#"
# Comment line
local   all             postgres                                peer

# IPv4 local connections:
host    all             all             127.0.0.1/32            md5
host    all             all             0.0.0.0/0               md5
"#;

        let rules = parse_pg_hba_rules(content);
        assert_eq!(rules.len(), 3);

        assert_eq!(rules[0].rule_type, "local");
        assert_eq!(rules[0].database, "all");
        assert_eq!(rules[0].user, "postgres");
        assert_eq!(rules[0].method, "peer");
        assert_eq!(rules[0].address, None);

        assert_eq!(rules[1].rule_type, "host");
        assert_eq!(rules[1].database, "all");
        assert_eq!(rules[1].user, "all");
        assert_eq!(rules[1].address, Some("127.0.0.1/32".to_string()));
        assert_eq!(rules[1].method, "md5");
    }

    #[test]
    fn test_diff_pg_hba_rules() {
        let current = vec![
            PgHbaRule {
                rule_type: "local".to_string(),
                database: "all".to_string(),
                user: "postgres".to_string(),
                address: None,
                method: "peer".to_string(),
                raw_line: "local   all             postgres                                peer"
                    .to_string(),
            },
            PgHbaRule {
                rule_type: "host".to_string(),
                database: "all".to_string(),
                user: "all".to_string(),
                address: Some("127.0.0.1/32".to_string()),
                method: "md5".to_string(),
                raw_line: "host    all             all             127.0.0.1/32            md5"
                    .to_string(),
            },
        ];

        let desired = vec![
            PgHbaRule {
                rule_type: "local".to_string(),
                database: "all".to_string(),
                user: "postgres".to_string(),
                address: None,
                method: "trust".to_string(), // Changed
                raw_line: "local   all             postgres                                trust"
                    .to_string(),
            },
            PgHbaRule {
                rule_type: "host".to_string(),
                database: "all".to_string(),
                user: "all".to_string(),
                address: Some("127.0.0.1/32".to_string()),
                method: "md5".to_string(),
                raw_line: "host    all             all             127.0.0.1/32            md5"
                    .to_string(),
            },
            PgHbaRule {
                rule_type: "host".to_string(),
                database: "all".to_string(),
                user: "all".to_string(),
                address: Some("0.0.0.0/0".to_string()),
                method: "md5".to_string(),
                raw_line: "host    all             all             0.0.0.0/0               md5"
                    .to_string(),
            },
        ];

        let diffs = diff_pg_hba_rules(&current, &desired);
        assert_eq!(diffs.len(), 2);

        // First should be modified
        assert!(matches!(diffs[0], PgHbaDiff::Modified { .. }));

        // Second should be added
        assert!(matches!(diffs[1], PgHbaDiff::Added { .. }));
    }

    #[test]
    fn test_pg_hba_rule_display() {
        let rule = PgHbaRule {
            rule_type: "host".to_string(),
            database: "mydb".to_string(),
            user: "myuser".to_string(),
            address: Some("192.168.1.0/24".to_string()),
            method: "md5".to_string(),
            raw_line: "host    mydb            myuser          192.168.1.0/24          md5"
                .to_string(),
        };

        let display = format!("{}", rule);
        assert!(display.contains("host"));
        assert!(display.contains("mydb"));
        assert!(display.contains("192.168.1.0/24"));
    }
}
