//! Hetzner Cloud infrastructure differ
//!
//! This module implements the InfrastructureDiffer port for Hetzner Cloud,
//! comparing desired state with actual state to determine changes needed.

use crate::domain::*;
use crate::ports::{DifferError, InfrastructureDiffer};
use crate::{FirewallManager, HetznerClient, LoadBalancerManager, NetworkManager, ServerManager};
use async_trait::async_trait;
use tracing::{debug, info};

/// Hetzner infrastructure differ adapter
///
/// Compares desired infrastructure specification with actual Hetzner Cloud state
/// to determine what changes need to be made.
pub struct HetznerDiffer {
    server_manager: ServerManager,
    network_manager: NetworkManager,
    firewall_manager: FirewallManager,
    loadbalancer_manager: LoadBalancerManager,
}

impl HetznerDiffer {
    /// Create a new Hetzner differ with the given API token
    pub fn new(api_token: String) -> Result<Self, DifferError> {
        let client = HetznerClient::builder()
            .api_token(api_token)
            .build()
            .map_err(|e| DifferError::ConnectionError(e.to_string()))?;

        Ok(Self {
            server_manager: ServerManager::new(client.clone()),
            network_manager: NetworkManager::new(client.clone()),
            firewall_manager: FirewallManager::new(client.clone()),
            loadbalancer_manager: LoadBalancerManager::new(client),
        })
    }

    async fn diff_servers(&self, desired: &[ServerSpec]) -> Result<Vec<DiffItem>, DifferError> {
        let mut diffs = Vec::new();

        // Get actual servers
        let actual_servers = self
            .server_manager
            .list_servers()
            .await
            .map_err(|e| DifferError::ConnectionError(e.to_string()))?;

        // Check desired servers
        for spec in desired {
            if let Some(existing) = actual_servers.iter().find(|s| s.name == spec.name()) {
                // Server exists - check if configuration matches
                let mut changes = Vec::new();

                // Check server type (size)
                if existing.server_type.name != spec.size() {
                    changes.push(format!(
                        "Server type: {} → {}",
                        existing.server_type.name,
                        spec.size()
                    ));
                }

                // Check location
                if existing.datacenter.location.name != spec.location() {
                    changes.push(format!(
                        "Location: {} → {}",
                        existing.datacenter.location.name,
                        spec.location()
                    ));
                }

                // Check image (if present)
                if let Some(ref img) = existing.image
                    && img.description != spec.image()
                {
                    changes.push(format!("Image: {} → {}", img.description, spec.image()));
                }

                // Check labels
                let desired_role = spec.server_type().to_string();
                if existing.labels.get("role").map(|s| s.as_str()) != Some(desired_role.as_str()) {
                    changes.push(format!(
                        "Role label: {:?} → {}",
                        existing.labels.get("role"),
                        desired_role
                    ));
                }

                if changes.is_empty() {
                    diffs.push(
                        DiffItem::new(
                            "server".to_string(),
                            spec.name().to_string(),
                            DiffType::NoChange,
                        )
                        .with_details(vec!["Server matches desired state".to_string()]),
                    );
                } else {
                    diffs.push(
                        DiffItem::new(
                            "server".to_string(),
                            spec.name().to_string(),
                            DiffType::Update,
                        )
                        .with_details(changes),
                    );
                }
            } else {
                // Server needs to be created
                diffs.push(
                    DiffItem::new(
                        "server".to_string(),
                        spec.name().to_string(),
                        DiffType::Create,
                    )
                    .with_details(vec![format!(
                        "Create {} server at {}",
                        spec.size(),
                        spec.location()
                    )]),
                );
            }
        }

        // Check for servers to delete
        let desired_names: Vec<&str> = desired.iter().map(|s| s.name()).collect();
        for actual in &actual_servers {
            if !desired_names.contains(&actual.name.as_str()) {
                diffs.push(
                    DiffItem::new("server".to_string(), actual.name.clone(), DiffType::Delete)
                        .with_details(vec!["Server not in desired state".to_string()]),
                );
            }
        }

        Ok(diffs)
    }

    async fn diff_network(&self, desired: &NetworkSpec) -> Result<Vec<DiffItem>, DifferError> {
        let mut diffs = Vec::new();

        // Get actual network
        if let Some(existing) = self
            .network_manager
            .get_network_by_name(desired.name())
            .await
            .map_err(|e| DifferError::ConnectionError(e.to_string()))?
        {
            // Network exists - check if configuration matches
            let mut changes = Vec::new();

            // Check IP range
            if existing.ip_range != desired.ip_range() {
                changes.push(format!(
                    "IP range: {} → {} (requires recreation)",
                    existing.ip_range,
                    desired.ip_range()
                ));
            }

            // Check subnets
            let actual_subnet_count = existing.subnets.len();
            let desired_subnet_count = desired.subnets().len();
            if actual_subnet_count != desired_subnet_count {
                changes.push(format!(
                    "Subnets: {} → {}",
                    actual_subnet_count, desired_subnet_count
                ));
            }

            if changes.is_empty() {
                diffs.push(
                    DiffItem::new(
                        "network".to_string(),
                        desired.name().to_string(),
                        DiffType::NoChange,
                    )
                    .with_details(vec!["Network matches desired state".to_string()]),
                );
            } else {
                diffs.push(
                    DiffItem::new(
                        "network".to_string(),
                        desired.name().to_string(),
                        DiffType::Update,
                    )
                    .with_details(changes),
                );
            }
        } else {
            diffs.push(
                DiffItem::new(
                    "network".to_string(),
                    desired.name().to_string(),
                    DiffType::Create,
                )
                .with_details(vec![format!(
                    "Create network with range {}",
                    desired.ip_range()
                )]),
            );
        }

        Ok(diffs)
    }

    async fn diff_firewall(&self, desired: &FirewallSpec) -> Result<Vec<DiffItem>, DifferError> {
        let mut diffs = Vec::new();

        // Get actual firewall
        if let Some(existing) = self
            .firewall_manager
            .get_firewall_by_name(desired.name())
            .await
            .map_err(|e| DifferError::ConnectionError(e.to_string()))?
        {
            // Firewall exists - check if rules match
            let mut changes = Vec::new();

            // Check rule count
            if existing.rules.len() != desired.rules().len() {
                changes.push(format!(
                    "Rules count: {} → {}",
                    existing.rules.len(),
                    desired.rules().len()
                ));
            }

            // Deep comparison of rules (simplified - just check count and protocols)
            for (i, desired_rule) in desired.rules().iter().enumerate() {
                if let Some(actual_rule) = existing.rules.get(i) {
                    if actual_rule.protocol != desired_rule.protocol {
                        changes.push(format!(
                            "Rule {} protocol: {} → {}",
                            i, actual_rule.protocol, desired_rule.protocol
                        ));
                    }
                    if actual_rule.direction != desired_rule.direction {
                        changes.push(format!(
                            "Rule {} direction: {} → {}",
                            i, actual_rule.direction, desired_rule.direction
                        ));
                    }
                }
            }

            if changes.is_empty() {
                diffs.push(
                    DiffItem::new(
                        "firewall".to_string(),
                        desired.name().to_string(),
                        DiffType::NoChange,
                    )
                    .with_details(vec!["Firewall matches desired state".to_string()]),
                );
            } else {
                diffs.push(
                    DiffItem::new(
                        "firewall".to_string(),
                        desired.name().to_string(),
                        DiffType::Update,
                    )
                    .with_details(changes),
                );
            }
        } else {
            diffs.push(
                DiffItem::new(
                    "firewall".to_string(),
                    desired.name().to_string(),
                    DiffType::Create,
                )
                .with_details(vec![format!(
                    "Create firewall with {} rules",
                    desired.rules().len()
                )]),
            );
        }

        Ok(diffs)
    }

    async fn diff_load_balancer(
        &self,
        desired: &LoadBalancerSpec,
    ) -> Result<Vec<DiffItem>, DifferError> {
        let mut diffs = Vec::new();

        // Get actual load balancer
        if let Some(_existing) = self
            .loadbalancer_manager
            .get_load_balancer_by_name(desired.name())
            .await
            .map_err(|e| DifferError::ConnectionError(e.to_string()))?
        {
            diffs.push(
                DiffItem::new(
                    "loadbalancer".to_string(),
                    desired.name().to_string(),
                    DiffType::NoChange,
                )
                .with_details(vec!["Load balancer already exists".to_string()]),
            );
        } else {
            diffs.push(
                DiffItem::new(
                    "loadbalancer".to_string(),
                    desired.name().to_string(),
                    DiffType::Create,
                )
                .with_details(vec![format!(
                    "Create {} load balancer at {}",
                    desired.load_balancer_type(),
                    desired.location()
                )]),
            );
        }

        Ok(diffs)
    }
}

#[async_trait]
impl InfrastructureDiffer for HetznerDiffer {
    async fn diff(&self, spec: &InfrastructureSpec) -> Result<InfrastructureDiff, DifferError> {
        info!("Computing infrastructure diff");

        let mut all_diffs = Vec::new();

        // Diff network
        debug!("Diffing network");
        all_diffs.extend(self.diff_network(&spec.network).await?);

        // Diff firewall
        debug!("Diffing firewall");
        all_diffs.extend(self.diff_firewall(&spec.firewall).await?);

        // Diff servers
        debug!("Diffing servers");
        all_diffs.extend(self.diff_servers(&spec.servers).await?);

        // Diff load balancer
        debug!("Diffing load balancer");
        all_diffs.extend(self.diff_load_balancer(&spec.loadbalancer).await?);

        info!("Diff complete: {} changes detected", all_diffs.len());
        Ok(InfrastructureDiff::new(all_diffs))
    }
}
