//! Floating IP management for Hetzner Cloud
//!
//! This module provides functionality for creating, managing, and deleting floating IPs.

use crate::{HetznerClient, Result};
use serde::{Deserialize, Serialize};
use tracing::info;

/// Floating IP manager for Hetzner Cloud API
pub struct FloatingIpManager {
    client: HetznerClient,
}

/// Floating IP configuration
#[derive(Debug, Clone, Serialize)]
pub struct FloatingIpConfig {
    pub name: String,
    pub ip_type: String, // "ipv4" or "ipv6"
    pub home_location: Option<String>,
    pub server: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub labels: Option<std::collections::HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

/// Floating IP resource
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FloatingIp {
    pub id: u64,
    pub name: String,
    #[serde(rename = "type")]
    pub ip_type: String,
    pub ip: String,
    pub blocked: bool,
    pub home_location: FloatingIpLocation,
    pub server: Option<u64>,
    #[serde(default)]
    pub labels: std::collections::HashMap<String, String>,
    pub description: Option<String>,
    pub created: String,
    pub dns_ptr: Vec<DnsPtr>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FloatingIpLocation {
    pub id: u64,
    pub name: String,
    pub description: String,
    pub country: String,
    pub city: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnsPtr {
    pub ip: String,
    pub dns_ptr: String,
}

#[derive(Debug, Deserialize)]
struct CreateFloatingIpResponse {
    floating_ip: FloatingIp,
    action: Option<crate::Action>,
}

#[derive(Debug, Deserialize)]
struct ListFloatingIpsResponse {
    floating_ips: Vec<FloatingIp>,
}

#[derive(Debug, Serialize)]
struct AssignFloatingIpRequest {
    server: u64,
}

#[derive(Debug, Serialize)]
struct UpdateFloatingIpRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    labels: Option<std::collections::HashMap<String, String>>,
}

impl FloatingIpManager {
    /// Create a new floating IP manager
    pub fn new(client: HetznerClient) -> Self {
        Self { client }
    }

    /// List all floating IPs
    pub async fn list_floating_ips(&self) -> Result<Vec<FloatingIp>> {
        let response: ListFloatingIpsResponse = self.client.get("/floating_ips").await?;
        Ok(response.floating_ips)
    }

    /// Get a specific floating IP by ID
    pub async fn get_floating_ip(&self, id: u64) -> Result<FloatingIp> {
        #[derive(Debug, Deserialize)]
        struct GetFloatingIpResponse {
            floating_ip: FloatingIp,
        }

        let response: GetFloatingIpResponse =
            self.client.get(&format!("/floating_ips/{}", id)).await?;
        Ok(response.floating_ip)
    }

    /// Get floating IP by name
    pub async fn get_floating_ip_by_name(&self, name: &str) -> Result<Option<FloatingIp>> {
        let floating_ips = self.list_floating_ips().await?;
        Ok(floating_ips.into_iter().find(|ip| ip.name == name))
    }

    /// Create a new floating IP
    pub async fn create_floating_ip(&self, config: &FloatingIpConfig) -> Result<FloatingIp> {
        info!(
            "Creating floating IP: {} (type: {})",
            config.name, config.ip_type
        );

        #[derive(Debug, Serialize)]
        struct CreateRequest {
            name: String,
            #[serde(rename = "type")]
            ip_type: String,
            #[serde(skip_serializing_if = "Option::is_none")]
            home_location: Option<String>,
            #[serde(skip_serializing_if = "Option::is_none")]
            server: Option<u64>,
            #[serde(skip_serializing_if = "Option::is_none")]
            labels: Option<std::collections::HashMap<String, String>>,
            #[serde(skip_serializing_if = "Option::is_none")]
            description: Option<String>,
        }

        let request = CreateRequest {
            name: config.name.clone(),
            ip_type: config.ip_type.clone(),
            home_location: config.home_location.clone(),
            server: config.server,
            labels: config.labels.clone(),
            description: config.description.clone(),
        };

        let response: CreateFloatingIpResponse =
            self.client.post("/floating_ips", &request).await?;

        // Wait for action if present
        if let Some(action) = response.action {
            self.client.wait_for_action(action.id, 120).await?;
        }

        info!("Floating IP {} created successfully", config.name);
        Ok(response.floating_ip)
    }

    /// Assign floating IP to a server
    pub async fn assign_to_server(&self, floating_ip_id: u64, server_id: u64) -> Result<()> {
        info!(
            "Assigning floating IP {} to server {}",
            floating_ip_id, server_id
        );

        let request = AssignFloatingIpRequest { server: server_id };

        #[derive(Debug, Deserialize)]
        struct AssignResponse {
            action: crate::Action,
        }

        let response: AssignResponse = self
            .client
            .post(
                &format!("/floating_ips/{}/actions/assign", floating_ip_id),
                &request,
            )
            .await?;

        self.client.wait_for_action(response.action.id, 120).await?;

        info!("Floating IP assigned successfully");
        Ok(())
    }

    /// Unassign floating IP from server
    pub async fn unassign_from_server(&self, floating_ip_id: u64) -> Result<()> {
        info!("Unassigning floating IP {}", floating_ip_id);

        #[derive(Debug, Deserialize)]
        struct UnassignResponse {
            action: crate::Action,
        }

        let response: UnassignResponse = self
            .client
            .post(
                &format!("/floating_ips/{}/actions/unassign", floating_ip_id),
                &(),
            )
            .await?;

        self.client.wait_for_action(response.action.id, 120).await?;

        info!("Floating IP unassigned successfully");
        Ok(())
    }

    /// Update floating IP properties
    pub async fn update_floating_ip(
        &self,
        floating_ip_id: u64,
        name: Option<String>,
        description: Option<String>,
        labels: Option<std::collections::HashMap<String, String>>,
    ) -> Result<FloatingIp> {
        info!("Updating floating IP {}", floating_ip_id);

        let request = UpdateFloatingIpRequest {
            name,
            description,
            labels,
        };

        #[derive(Debug, Deserialize)]
        struct UpdateResponse {
            floating_ip: FloatingIp,
        }

        let response: UpdateResponse = self
            .client
            .put(&format!("/floating_ips/{}", floating_ip_id), &request)
            .await?;

        info!("Floating IP updated successfully");
        Ok(response.floating_ip)
    }

    /// Delete a floating IP
    pub async fn delete_floating_ip(&self, id: u64) -> Result<()> {
        info!("Deleting floating IP with ID: {}", id);
        self.client.delete(&format!("/floating_ips/{}", id)).await?;
        info!("Floating IP deleted successfully");
        Ok(())
    }

    /// Ensure floating IP exists
    pub async fn ensure_floating_ip(&self, config: &FloatingIpConfig) -> Result<FloatingIp> {
        if let Some(floating_ip) = self.get_floating_ip_by_name(&config.name).await? {
            info!("Floating IP {} already exists", config.name);
            return Ok(floating_ip);
        }

        self.create_floating_ip(config).await
    }
}

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

    #[test]
    fn test_floating_ip_config_creation() {
        let config = FloatingIpConfig {
            name: "test-ip".to_string(),
            ip_type: "ipv4".to_string(),
            home_location: Some("nbg1".to_string()),
            server: None,
            labels: None,
            description: Some("Test floating IP".to_string()),
        };

        assert_eq!(config.name, "test-ip");
        assert_eq!(config.ip_type, "ipv4");
        assert_eq!(config.home_location, Some("nbg1".to_string()));
        assert_eq!(config.description, Some("Test floating IP".to_string()));
    }

    #[test]
    fn test_floating_ip_config_with_server() {
        let config = FloatingIpConfig {
            name: "server-ip".to_string(),
            ip_type: "ipv4".to_string(),
            home_location: Some("fsn1".to_string()),
            server: Some(12345),
            labels: None,
            description: None,
        };

        assert_eq!(config.server, Some(12345));
    }

    #[test]
    fn test_floating_ip_config_ipv6() {
        let config = FloatingIpConfig {
            name: "ipv6-test".to_string(),
            ip_type: "ipv6".to_string(),
            home_location: Some("hel1".to_string()),
            server: None,
            labels: None,
            description: Some("IPv6 address".to_string()),
        };

        assert_eq!(config.ip_type, "ipv6");
    }
}
