//! IPAM gRPC service implementation for k8shost Service IP allocation

use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::sync::Arc;
use tonic::{Request, Response, Status};

use prismnet_api::{
    ipam_service_server::IpamService,
    AllocateServiceIpRequest, AllocateServiceIpResponse,
    CreateServiceIpPoolRequest, CreateServiceIpPoolResponse,
    DeleteServiceIpPoolRequest, DeleteServiceIpPoolResponse,
    GetIpAllocationRequest, GetIpAllocationResponse,
    GetServiceIpPoolRequest, GetServiceIpPoolResponse,
    IpAllocation as ProtoIPAllocation,
    ListServiceIpPoolsRequest, ListServiceIpPoolsResponse,
    ReleaseServiceIpRequest, ReleaseServiceIpResponse,
    ServiceIpPool as ProtoServiceIPPool,
    ServiceIpPoolStatus as ProtoServiceIPPoolStatus,
    ServiceIpPoolType as ProtoServiceIPPoolType,
    UpdateServiceIpPoolRequest, UpdateServiceIpPoolResponse,
};
use iam_service_auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use prismnet_types::{
    IPAllocation, ServiceIPPool, ServiceIPPoolId, ServiceIPPoolStatus, ServiceIPPoolType,
};

use crate::NetworkMetadataStore;

const ACTION_POOL_CREATE: &str = "network:ip-pools:create";
const ACTION_POOL_READ: &str = "network:ip-pools:read";
const ACTION_POOL_LIST: &str = "network:ip-pools:list";
const ACTION_POOL_UPDATE: &str = "network:ip-pools:update";
const ACTION_POOL_DELETE: &str = "network:ip-pools:delete";
const ACTION_ALLOCATE_IP: &str = "network:ip-allocations:create";
const ACTION_RELEASE_IP: &str = "network:ip-allocations:delete";
const ACTION_ALLOC_READ: &str = "network:ip-allocations:read";

#[derive(Clone)]
pub struct IpamServiceImpl {
    metadata: Arc<NetworkMetadataStore>,
    auth: Arc<AuthService>,
}

impl IpamServiceImpl {
    pub fn new(metadata: Arc<NetworkMetadataStore>, auth: Arc<AuthService>) -> Self {
        Self { metadata, auth }
    }
}

fn parse_ipv4_cidr(cidr: &str) -> Result<(Ipv4Addr, u8), Status> {
    let (ip, prefix) = cidr
        .split_once('/')
        .ok_or_else(|| Status::invalid_argument("cidr_block must be in a.b.c.d/prefix form"))?;
    let ip: Ipv4Addr = ip
        .parse()
        .map_err(|_| Status::invalid_argument("cidr_block must contain a valid IPv4 address"))?;
    let prefix: u8 = prefix
        .parse()
        .map_err(|_| Status::invalid_argument("cidr_block prefix must be an integer"))?;
    if prefix > 32 {
        return Err(Status::invalid_argument(
            "cidr_block prefix must be between 0 and 32",
        ));
    }
    Ok((ip, prefix))
}

fn ensure_unique_pool_name(
    existing: &[ServiceIPPool],
    desired_name: &str,
    exclude: Option<ServiceIPPoolId>,
) -> Result<(), Status> {
    if desired_name.trim().is_empty() {
        return Err(Status::invalid_argument("Service IP Pool name is required"));
    }
    if existing
        .iter()
        .any(|pool| pool.name == desired_name && Some(pool.id) != exclude)
    {
        return Err(Status::already_exists(
            "Service IP Pool name already exists in the tenant",
        ));
    }
    Ok(())
}

// Proto conversion functions

fn pool_to_proto(pool: &ServiceIPPool) -> ProtoServiceIPPool {
    ProtoServiceIPPool {
        id: pool.id.to_string(),
        org_id: pool.org_id.clone(),
        project_id: pool.project_id.clone(),
        name: pool.name.clone(),
        description: pool.description.clone().unwrap_or_default(),
        cidr_block: pool.cidr_block.clone(),
        pool_type: pool_type_to_proto(&pool.pool_type) as i32,
        allocated_ips: pool.allocated_ips.iter().cloned().collect(),
        status: pool_status_to_proto(&pool.status) as i32,
        created_at: pool.created_at,
        updated_at: pool.updated_at,
    }
}

fn pool_type_to_proto(pool_type: &ServiceIPPoolType) -> ProtoServiceIPPoolType {
    match pool_type {
        ServiceIPPoolType::ClusterIp => ProtoServiceIPPoolType::ClusterIp,
        ServiceIPPoolType::LoadBalancer => ProtoServiceIPPoolType::LoadBalancer,
        ServiceIPPoolType::NodePort => ProtoServiceIPPoolType::NodePort,
    }
}

fn pool_type_from_proto(pool_type: i32) -> ServiceIPPoolType {
    match ProtoServiceIPPoolType::try_from(pool_type) {
        Ok(ProtoServiceIPPoolType::ClusterIp) => ServiceIPPoolType::ClusterIp,
        Ok(ProtoServiceIPPoolType::LoadBalancer) => ServiceIPPoolType::LoadBalancer,
        Ok(ProtoServiceIPPoolType::NodePort) => ServiceIPPoolType::NodePort,
        _ => ServiceIPPoolType::ClusterIp,
    }
}

fn pool_status_to_proto(status: &ServiceIPPoolStatus) -> ProtoServiceIPPoolStatus {
    match status {
        ServiceIPPoolStatus::Provisioning => ProtoServiceIPPoolStatus::Provisioning,
        ServiceIPPoolStatus::Active => ProtoServiceIPPoolStatus::Active,
        ServiceIPPoolStatus::Updating => ProtoServiceIPPoolStatus::Updating,
        ServiceIPPoolStatus::Deleting => ProtoServiceIPPoolStatus::Deleting,
        ServiceIPPoolStatus::Error => ProtoServiceIPPoolStatus::Error,
    }
}

fn allocation_to_proto(allocation: &IPAllocation) -> ProtoIPAllocation {
    ProtoIPAllocation {
        ip_address: allocation.ip_address.clone(),
        pool_id: allocation.pool_id.to_string(),
        org_id: allocation.org_id.clone(),
        project_id: allocation.project_id.clone(),
        resource_type: allocation.resource_type.clone(),
        resource_id: allocation.resource_id.clone(),
        allocated_at: allocation.allocated_at,
    }
}

#[tonic::async_trait]
impl IpamService for IpamServiceImpl {
    async fn create_service_ip_pool(
        &self,
        request: Request<CreateServiceIpPoolRequest>,
    ) -> Result<Response<CreateServiceIpPoolResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_POOL_CREATE,
                &resource_for_tenant("service-ip-pool", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        // Validate CIDR
        if req.cidr_block.is_empty() {
            return Err(Status::invalid_argument("cidr_block is required"));
        }
        parse_ipv4_cidr(&req.cidr_block)?;
        let existing = self
            .metadata
            .list_service_ip_pools(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        ensure_unique_pool_name(&existing, &req.name, None)?;

        let pool_type = pool_type_from_proto(req.pool_type);
        let mut pool = ServiceIPPool::new(
            &req.name,
            &org_id,
            &project_id,
            &req.cidr_block,
            pool_type,
        );

        if !req.description.is_empty() {
            pool.description = Some(req.description);
        }

        self.metadata
            .create_service_ip_pool(pool.clone())
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(CreateServiceIpPoolResponse {
            pool: Some(pool_to_proto(&pool)),
        }))
    }

    async fn get_service_ip_pool(
        &self,
        request: Request<GetServiceIpPoolRequest>,
    ) -> Result<Response<GetServiceIpPoolResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let id = uuid::Uuid::parse_str(&req.id)
            .map_err(|_| Status::invalid_argument("Invalid pool ID"))?;
        let pool_id = ServiceIPPoolId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_POOL_READ,
                &resource_for_tenant(
                    "service-ip-pool",
                    pool_id.to_string(),
                    &org_id,
                    &project_id,
                ),
            )
            .await?;

        let pool = self
            .metadata
            .get_service_ip_pool(&org_id, &project_id, &pool_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Service IP Pool not found"))?;

        Ok(Response::new(GetServiceIpPoolResponse {
            pool: Some(pool_to_proto(&pool)),
        }))
    }

    async fn list_service_ip_pools(
        &self,
        request: Request<ListServiceIpPoolsRequest>,
    ) -> Result<Response<ListServiceIpPoolsResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_POOL_LIST,
                &resource_for_tenant("service-ip-pool", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        let mut pools = self
            .metadata
            .list_service_ip_pools(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        // Filter by pool type if specified
        if req.pool_type != 0 {
            let filter_type = pool_type_from_proto(req.pool_type);
            pools.retain(|p| p.pool_type == filter_type);
        }

        let proto_pools: Vec<ProtoServiceIPPool> = pools.iter().map(pool_to_proto).collect();

        Ok(Response::new(ListServiceIpPoolsResponse {
            pools: proto_pools,
            next_page_token: String::new(),
        }))
    }

    async fn update_service_ip_pool(
        &self,
        request: Request<UpdateServiceIpPoolRequest>,
    ) -> Result<Response<UpdateServiceIpPoolResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let id = uuid::Uuid::parse_str(&req.id)
            .map_err(|_| Status::invalid_argument("Invalid pool ID"))?;
        let pool_id = ServiceIPPoolId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_POOL_UPDATE,
                &resource_for_tenant(
                    "service-ip-pool",
                    pool_id.to_string(),
                    &org_id,
                    &project_id,
                ),
            )
            .await?;

        let name = if !req.name.is_empty() {
            Some(req.name)
        } else {
            None
        };
        let description = if !req.description.is_empty() {
            Some(req.description)
        } else {
            None
        };
        if let Some(name) = name.as_deref() {
            let existing = self
                .metadata
                .list_service_ip_pools(&org_id, &project_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
            ensure_unique_pool_name(&existing, name, Some(pool_id))?;
        }

        let pool = self
            .metadata
            .update_service_ip_pool(&org_id, &project_id, &pool_id, name, description)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Service IP Pool not found"))?;

        Ok(Response::new(UpdateServiceIpPoolResponse {
            pool: Some(pool_to_proto(&pool)),
        }))
    }

    async fn delete_service_ip_pool(
        &self,
        request: Request<DeleteServiceIpPoolRequest>,
    ) -> Result<Response<DeleteServiceIpPoolResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let id = uuid::Uuid::parse_str(&req.id)
            .map_err(|_| Status::invalid_argument("Invalid pool ID"))?;
        let pool_id = ServiceIPPoolId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_POOL_DELETE,
                &resource_for_tenant(
                    "service-ip-pool",
                    pool_id.to_string(),
                    &org_id,
                    &project_id,
                ),
            )
            .await?;

        let pool = self
            .metadata
            .get_service_ip_pool(&org_id, &project_id, &pool_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Service IP Pool not found"))?;

        if !pool.allocated_ips.is_empty() {
            return Err(Status::failed_precondition(
                "cannot delete Service IP Pool with allocated IPs",
            ));
        }

        self.metadata
            .delete_service_ip_pool(&org_id, &project_id, &pool_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Service IP Pool not found"))?;

        Ok(Response::new(DeleteServiceIpPoolResponse {}))
    }

    async fn allocate_service_ip(
        &self,
        request: Request<AllocateServiceIpRequest>,
    ) -> Result<Response<AllocateServiceIpResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_ALLOCATE_IP,
                &resource_for_tenant("ip-allocation", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        // Determine which pool to use
        let pool = if !req.pool_id.is_empty() {
            // Specific pool requested
            let id = uuid::Uuid::parse_str(&req.pool_id)
                .map_err(|_| Status::invalid_argument("Invalid pool ID"))?;
            let pool_id = ServiceIPPoolId::from_uuid(id);

            self.metadata
                .get_service_ip_pool(&org_id, &project_id, &pool_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?
                .ok_or_else(|| Status::not_found("Service IP Pool not found"))?
        } else {
            // Find first active pool of the requested type
            let pool_type = pool_type_from_proto(req.pool_type);
            let pools = self
                .metadata
                .list_service_ip_pools(&org_id, &project_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;

            pools
                .into_iter()
                .find(|p| p.pool_type == pool_type && p.status == ServiceIPPoolStatus::Active)
                .ok_or_else(|| {
                    Status::not_found(format!(
                        "No active Service IP Pool found for type {:?}",
                        pool_type
                    ))
                })?
        };

        // Allocate IP from pool
        let ip_address = if !req.requested_ip.is_empty() {
            // Specific IP requested
            if pool.is_allocated(&req.requested_ip) {
                return Err(Status::already_exists(format!(
                    "IP {} is already allocated",
                    req.requested_ip
                )));
            }
            req.requested_ip.clone()
        } else {
            // Allocate next available IP
            self.allocate_next_available_ip(&pool)
                .await
                .ok_or_else(|| Status::resource_exhausted("No available IPs in pool"))?
        };

        // Create allocation record
        let allocation = IPAllocation::new(
            &ip_address,
            pool.id,
            &org_id,
            &project_id,
            "k8s-service",
            &req.service_uid,
        );

        // Save allocation
        self.metadata
            .allocate_service_ip(&pool.id, &ip_address, allocation)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(AllocateServiceIpResponse {
            ip_address,
            pool_id: pool.id.to_string(),
        }))
    }

    async fn release_service_ip(
        &self,
        request: Request<ReleaseServiceIpRequest>,
    ) -> Result<Response<ReleaseServiceIpResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_RELEASE_IP,
                &resource_for_tenant("ip-allocation", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        if req.ip_address.is_empty() {
            return Err(Status::invalid_argument("ip_address is required"));
        }

        self.metadata
            .release_service_ip(&org_id, &project_id, &req.ip_address)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(ReleaseServiceIpResponse {}))
    }

    async fn get_ip_allocation(
        &self,
        request: Request<GetIpAllocationRequest>,
    ) -> Result<Response<GetIpAllocationResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_ALLOC_READ,
                &resource_for_tenant("ip-allocation", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        let allocation = self
            .metadata
            .get_ip_allocation(&org_id, &project_id, &req.ip_address)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("IP allocation not found"))?;

        Ok(Response::new(GetIpAllocationResponse {
            allocation: Some(allocation_to_proto(&allocation)),
        }))
    }
}

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

    #[test]
    fn rejects_invalid_pool_cidr() {
        let err = parse_ipv4_cidr("10.96.0.0/99").unwrap_err();
        assert_eq!(err.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn rejects_duplicate_pool_name() {
        let existing = vec![ServiceIPPool::new(
            "svc",
            "org",
            "proj",
            "10.96.0.0/24",
            ServiceIPPoolType::ClusterIp,
        )];
        let err = ensure_unique_pool_name(&existing, "svc", None).unwrap_err();
        assert_eq!(err.code(), tonic::Code::AlreadyExists);
    }
}

impl IpamServiceImpl {
    /// Allocate next available IP from pool's CIDR
    async fn allocate_next_available_ip(&self, pool: &ServiceIPPool) -> Option<String> {
        // Parse CIDR to get network and prefix length
        let parts: Vec<&str> = pool.cidr_block.split('/').collect();
        if parts.len() != 2 {
            return None;
        }

        let network: IpAddr = parts[0].parse().ok()?;
        let prefix_len: u8 = parts[1].parse().ok()?;

        // For IPv4, enumerate IPs in range
        if let IpAddr::V4(net_v4) = network {
            let net_bits = u32::from(net_v4);
            let host_bits = 32 - prefix_len;
            let max_hosts = (1u32 << host_bits) - 2; // Exclude network and broadcast

            for i in 1..=max_hosts {
                let ip_bits = net_bits + i;
                let candidate = std::net::Ipv4Addr::from(ip_bits).to_string();

                if !pool.is_allocated(&candidate) {
                    return Some(candidate);
                }
            }
        }

        None
    }
}
