//! IPAM Client - Allocates Service IPs from PrismNET IPAM
//!
//! This client wraps gRPC calls to PrismNET's IpamService for allocating
//! and releasing Cluster IPs for Kubernetes Services.

use anyhow::{anyhow, Result};
use prismnet_api::ipam_service_client::IpamServiceClient;
use prismnet_api::{
    AllocateServiceIpRequest, CreateServiceIpPoolRequest, ListServiceIpPoolsRequest,
    ReleaseServiceIpRequest, ServiceIpPoolStatus as ProtoServiceIpPoolStatus,
    ServiceIpPoolType as ProtoServiceIpPoolType,
};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::Request;

/// IPAM client for allocating Service IPs
pub struct IpamClient {
    prismnet_addr: String,
}

impl IpamClient {
    /// Create a new IPAM client
    pub fn new(prismnet_addr: String) -> Self {
        Self { prismnet_addr }
    }

    async fn connect(&self) -> Result<IpamServiceClient<Channel>> {
        let endpoint = normalize_endpoint(&self.prismnet_addr);
        let channel = Channel::from_shared(endpoint.clone())
            .map_err(|e| anyhow!("Invalid PrismNET address {}: {}", endpoint, e))?
            .connect()
            .await
            .map_err(|e| anyhow!("Failed to connect to PrismNET {}: {}", endpoint, e))?;

        Ok(IpamServiceClient::new(channel))
    }

    fn with_auth<T>(message: T, authorization: Option<&str>) -> Result<Request<T>> {
        let mut request = Request::new(message);
        if let Some(header) = authorization.filter(|value| !value.trim().is_empty()) {
            let value = MetadataValue::try_from(header)
                .map_err(|e| anyhow!("Invalid authorization metadata: {}", e))?;
            request.metadata_mut().insert("authorization", value);
        }
        Ok(request)
    }

    async fn ensure_default_cluster_ip_pool(
        &self,
        client: &mut IpamServiceClient<Channel>,
        org_id: &str,
        project_id: &str,
        authorization: Option<&str>,
    ) -> Result<()> {
        let list_request = Self::with_auth(
            ListServiceIpPoolsRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                pool_type: ProtoServiceIpPoolType::ClusterIp as i32,
                page_size: 100,
                page_token: String::new(),
            },
            authorization,
        )?;
        let pools = client
            .list_service_ip_pools(list_request)
            .await
            .map_err(|e| anyhow!("Failed to list Cluster IP pools: {}", e))?
            .into_inner()
            .pools;

        let has_active_pool = pools.iter().any(|pool| {
            pool.pool_type == ProtoServiceIpPoolType::ClusterIp as i32
                && pool.status == ProtoServiceIpPoolStatus::Active as i32
        });
        if has_active_pool {
            return Ok(());
        }

        let cidr_block = default_cluster_ip_cidr(org_id, project_id);
        let create_request = Self::with_auth(
            CreateServiceIpPoolRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                name: "default-cluster-ip-pool".to_string(),
                description: "Auto-provisioned default ClusterIP pool for k8shost".to_string(),
                cidr_block,
                pool_type: ProtoServiceIpPoolType::ClusterIp as i32,
            },
            authorization,
        )?;

        client
            .create_service_ip_pool(create_request)
            .await
            .map_err(|e| anyhow!("Failed to create default Cluster IP pool: {}", e))?;
        Ok(())
    }

    /// Allocate a Cluster IP for a service
    ///
    /// # Arguments
    /// * `org_id` - Organization ID
    /// * `project_id` - Project ID
    /// * `service_uid` - Kubernetes Service UID for tracking
    ///
    /// # Returns
    /// The allocated IP address as a String
    pub async fn allocate_cluster_ip(
        &self,
        org_id: &str,
        project_id: &str,
        service_uid: &str,
        authorization: Option<&str>,
    ) -> Result<String> {
        let mut client = self.connect().await?;
        self.ensure_default_cluster_ip_pool(&mut client, org_id, project_id, authorization)
            .await?;
        let request = Self::with_auth(
            AllocateServiceIpRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                pool_id: String::new(), // Use default pool
                pool_type: ProtoServiceIpPoolType::ClusterIp as i32,
                service_uid: service_uid.to_string(),
                requested_ip: String::new(), // Auto-allocate
            },
            authorization,
        )?;

        let response = client
            .allocate_service_ip(request)
            .await
            .map_err(|e| anyhow!("Failed to allocate Cluster IP: {}", e))?;

        let inner = response.into_inner();
        Ok(inner.ip_address)
    }

    /// Release a Cluster IP back to the pool
    ///
    /// # Arguments
    /// * `org_id` - Organization ID
    /// * `project_id` - Project ID
    /// * `ip_address` - The IP address to release
    pub async fn release_cluster_ip(
        &self,
        org_id: &str,
        project_id: &str,
        ip_address: &str,
        authorization: Option<&str>,
    ) -> Result<()> {
        let mut client = self.connect().await?;
        let request = Self::with_auth(
            ReleaseServiceIpRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                ip_address: ip_address.to_string(),
            },
            authorization,
        )?;

        client
            .release_service_ip(request)
            .await
            .map_err(|e| anyhow!("Failed to release Cluster IP: {}", e))?;

        Ok(())
    }
}

fn normalize_endpoint(endpoint: &str) -> String {
    if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
        endpoint.to_string()
    } else {
        format!("http://{}", endpoint)
    }
}

fn default_cluster_ip_cidr(org_id: &str, project_id: &str) -> String {
    let mut hasher = DefaultHasher::new();
    org_id.hash(&mut hasher);
    project_id.hash(&mut hasher);
    let octet = ((hasher.finish() % 250) + 1) as u8;
    format!("10.96.{}.0/24", octet)
}

#[cfg(test)]
mod tests {
    use super::{default_cluster_ip_cidr, normalize_endpoint};

    #[test]
    fn normalizes_scheme_less_endpoint_to_http() {
        assert_eq!(
            normalize_endpoint("10.100.0.11:50054"),
            "http://10.100.0.11:50054"
        );
    }

    #[test]
    fn preserves_existing_scheme() {
        assert_eq!(
            normalize_endpoint("https://prismnet.internal:50054"),
            "https://prismnet.internal:50054"
        );
        assert_eq!(
            normalize_endpoint("http://prismnet.internal:50054"),
            "http://prismnet.internal:50054"
        );
    }

    #[test]
    fn derives_stable_service_cidr() {
        let cidr = default_cluster_ip_cidr("org-a", "project-a");
        assert!(cidr.starts_with("10.96."));
        assert!(cidr.ends_with(".0/24"));
        assert_eq!(cidr, default_cluster_ip_cidr("org-a", "project-a"));
        assert_ne!(cidr, default_cluster_ip_cidr("org-a", "project-b"));
    }
}
