//! Subnet gRPC service implementation

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

use iam_service_auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use prismnet_api::{
    subnet_service_server::SubnetService, CreateSubnetRequest, CreateSubnetResponse,
    DeleteSubnetRequest, DeleteSubnetResponse, GetSubnetRequest, GetSubnetResponse,
    ListSubnetsRequest, ListSubnetsResponse, Subnet as ProtoSubnet,
    SubnetStatus as ProtoSubnetStatus, UpdateSubnetRequest, UpdateSubnetResponse,
};
use prismnet_types::{Subnet, SubnetId, SubnetStatus, Vpc, VpcId};

use crate::NetworkMetadataStore;

const ACTION_SUBNET_CREATE: &str = "network:subnets:create";
const ACTION_SUBNET_READ: &str = "network:subnets:read";
const ACTION_SUBNET_LIST: &str = "network:subnets:list";
const ACTION_SUBNET_UPDATE: &str = "network:subnets:update";
const ACTION_SUBNET_DELETE: &str = "network:subnets:delete";

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

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

    async fn validate_vpc_in_tenant(
        &self,
        org_id: &str,
        project_id: &str,
        vpc_id: &VpcId,
    ) -> Result<Vpc, Status> {
        self.metadata
            .get_vpc(org_id, project_id, vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::permission_denied("VPC not in tenant scope"))
    }
}

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

fn parse_ipv4(ip: &str, field_name: &str) -> Result<Ipv4Addr, Status> {
    ip.parse()
        .map_err(|_| Status::invalid_argument(format!("{field_name} must be a valid IPv4 address")))
}

fn network_mask(prefix: u8) -> u32 {
    if prefix == 0 {
        0
    } else {
        u32::MAX << (32 - prefix)
    }
}

fn cidr_range(cidr: (Ipv4Addr, u8)) -> (u32, u32) {
    let mask = network_mask(cidr.1);
    let start = u32::from(cidr.0) & mask;
    let size = if cidr.1 == 32 {
        1
    } else {
        1u64 << (32 - cidr.1)
    };
    let end = start + (size as u32) - 1;
    (start, end)
}

fn cidr_contains_ip(cidr: (Ipv4Addr, u8), ip: Ipv4Addr) -> bool {
    let mask = network_mask(cidr.1);
    (u32::from(cidr.0) & mask) == (u32::from(ip) & mask)
}

fn cidr_contains_cidr(parent: (Ipv4Addr, u8), child: (Ipv4Addr, u8)) -> bool {
    let (parent_start, parent_end) = cidr_range(parent);
    let (child_start, child_end) = cidr_range(child);
    child_start >= parent_start && child_end <= parent_end
}

fn cidr_overlaps(a: (Ipv4Addr, u8), b: (Ipv4Addr, u8)) -> bool {
    let (a_start, a_end) = cidr_range(a);
    let (b_start, b_end) = cidr_range(b);
    a_start <= b_end && b_start <= a_end
}

fn validate_subnet_inputs(
    vpc: &Vpc,
    cidr_block: &str,
    gateway_ip: Option<&str>,
    sibling_cidrs: impl IntoIterator<Item = String>,
) -> Result<(), Status> {
    let vpc_cidr = parse_ipv4_cidr(&vpc.cidr_block, "vpc cidr_block")?;
    let subnet_cidr = parse_ipv4_cidr(cidr_block, "cidr_block")?;
    if !cidr_contains_cidr(vpc_cidr, subnet_cidr) {
        return Err(Status::invalid_argument(
            "subnet cidr_block must be contained within the VPC cidr_block",
        ));
    }

    if let Some(gateway_ip) = gateway_ip {
        let gateway_ip = parse_ipv4(gateway_ip, "gateway_ip")?;
        if !cidr_contains_ip(subnet_cidr, gateway_ip) {
            return Err(Status::invalid_argument(
                "gateway_ip must fall within the subnet cidr_block",
            ));
        }
    }

    for sibling in sibling_cidrs {
        let sibling_cidr = parse_ipv4_cidr(&sibling, "existing subnet cidr_block")?;
        if cidr_overlaps(subnet_cidr, sibling_cidr) {
            return Err(Status::already_exists(
                "subnet cidr_block overlaps an existing subnet in the VPC",
            ));
        }
    }

    Ok(())
}

fn ensure_unique_subnet_name(
    existing: &[Subnet],
    desired_name: &str,
    exclude: Option<SubnetId>,
) -> Result<(), Status> {
    if desired_name.trim().is_empty() {
        return Err(Status::invalid_argument("Subnet name is required"));
    }
    if existing
        .iter()
        .any(|subnet| subnet.name == desired_name && Some(subnet.id) != exclude)
    {
        return Err(Status::already_exists(
            "Subnet name already exists in the VPC",
        ));
    }
    Ok(())
}

fn subnet_to_proto(subnet: &Subnet) -> ProtoSubnet {
    ProtoSubnet {
        id: subnet.id.to_string(),
        vpc_id: subnet.vpc_id.to_string(),
        name: subnet.name.clone(),
        description: subnet.description.clone().unwrap_or_default(),
        cidr_block: subnet.cidr_block.clone(),
        gateway_ip: subnet.gateway_ip.clone().unwrap_or_default(),
        dhcp_enabled: subnet.dhcp_enabled,
        dns_servers: subnet.dns_servers.clone(),
        status: status_to_proto(&subnet.status) as i32,
        created_at: subnet.created_at,
        updated_at: subnet.updated_at,
    }
}

fn status_to_proto(status: &SubnetStatus) -> ProtoSubnetStatus {
    match status {
        SubnetStatus::Provisioning => ProtoSubnetStatus::Provisioning,
        SubnetStatus::Active => ProtoSubnetStatus::Active,
        SubnetStatus::Updating => ProtoSubnetStatus::Updating,
        SubnetStatus::Deleting => ProtoSubnetStatus::Deleting,
        SubnetStatus::Error => ProtoSubnetStatus::Error,
    }
}

#[tonic::async_trait]
impl SubnetService for SubnetServiceImpl {
    async fn create_subnet(
        &self,
        request: Request<CreateSubnetRequest>,
    ) -> Result<Response<CreateSubnetResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(&tenant, "", "")?;
        let req = request.into_inner();

        let vpc_id = uuid::Uuid::parse_str(&req.vpc_id)
            .map_err(|_| Status::invalid_argument("Invalid VPC ID"))?;
        let vpc_id = VpcId::from_uuid(vpc_id);

        let vpc = self
            .validate_vpc_in_tenant(&org_id, &project_id, &vpc_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_SUBNET_CREATE,
                &resource_for_tenant("subnet", "*", &org_id, &project_id),
            )
            .await?;

        let existing_subnets = self
            .metadata
            .list_subnets(&org_id, &project_id, &vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        ensure_unique_subnet_name(&existing_subnets, &req.name, None)?;
        validate_subnet_inputs(
            &vpc,
            &req.cidr_block,
            (!req.gateway_ip.is_empty()).then_some(req.gateway_ip.as_str()),
            existing_subnets
                .into_iter()
                .map(|subnet| subnet.cidr_block)
                .collect::<Vec<_>>(),
        )?;

        let subnet = Subnet::new(&req.name, vpc_id, &req.cidr_block);
        let mut subnet = subnet;
        if !req.description.is_empty() {
            subnet.description = Some(req.description);
        }
        if !req.gateway_ip.is_empty() {
            subnet.gateway_ip = Some(req.gateway_ip);
        }
        subnet.dhcp_enabled = req.dhcp_enabled;
        subnet.status = SubnetStatus::Active;

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

        Ok(Response::new(CreateSubnetResponse {
            subnet: Some(subnet_to_proto(&subnet)),
        }))
    }

    async fn get_subnet(
        &self,
        request: Request<GetSubnetRequest>,
    ) -> Result<Response<GetSubnetResponse>, 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 Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_SUBNET_READ,
                &resource_for_tenant("subnet", subnet_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let subnet = if req.vpc_id.trim().is_empty() {
            let subnet = self
                .metadata
                .find_subnet_by_id(&subnet_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?
                .ok_or_else(|| Status::not_found("Subnet not found"))?;
            self.validate_vpc_in_tenant(&org_id, &project_id, &subnet.vpc_id)
                .await?;
            subnet
        } else {
            let vpc_uuid = uuid::Uuid::parse_str(&req.vpc_id)
                .map_err(|_| Status::invalid_argument("Invalid VPC ID"))?;
            let vpc_id = VpcId::from_uuid(vpc_uuid);
            self.validate_vpc_in_tenant(&org_id, &project_id, &vpc_id)
                .await?;
            self.metadata
                .get_subnet(&vpc_id, &subnet_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?
                .ok_or_else(|| Status::not_found("Subnet not found"))?
        };

        Ok(Response::new(GetSubnetResponse {
            subnet: Some(subnet_to_proto(&subnet)),
        }))
    }

    async fn list_subnets(
        &self,
        request: Request<ListSubnetsRequest>,
    ) -> Result<Response<ListSubnetsResponse>, 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_SUBNET_LIST,
                &resource_for_tenant("subnet", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        let vpc_id = if !req.vpc_id.is_empty() {
            let id = uuid::Uuid::parse_str(&req.vpc_id)
                .map_err(|_| Status::invalid_argument("Invalid VPC ID"))?;
            VpcId::from_uuid(id)
        } else {
            return Err(Status::invalid_argument("vpc_id is required"));
        };

        self.validate_vpc_in_tenant(&org_id, &project_id, &vpc_id)
            .await?;

        let subnets = self
            .metadata
            .list_subnets(&org_id, &project_id, &vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(ListSubnetsResponse {
            subnets: subnets.iter().map(subnet_to_proto).collect(),
            next_page_token: String::new(),
        }))
    }

    async fn update_subnet(
        &self,
        request: Request<UpdateSubnetRequest>,
    ) -> Result<Response<UpdateSubnetResponse>, 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 Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(id);
        let vpc_uuid = uuid::Uuid::parse_str(&req.vpc_id)
            .map_err(|_| Status::invalid_argument("Invalid VPC ID"))?;
        let vpc_id = VpcId::from_uuid(vpc_uuid);

        self.validate_vpc_in_tenant(&org_id, &project_id, &vpc_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_SUBNET_UPDATE,
                &resource_for_tenant("subnet", subnet_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_subnets = self
                .metadata
                .list_subnets(&org_id, &project_id, &vpc_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
            ensure_unique_subnet_name(&existing_subnets, name, Some(subnet_id))?;
        }

        let subnet = self
            .metadata
            .update_subnet(
                &org_id,
                &project_id,
                &vpc_id,
                &subnet_id,
                name,
                description,
                Some(req.dhcp_enabled),
            )
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Subnet not found"))?;

        Ok(Response::new(UpdateSubnetResponse {
            subnet: Some(subnet_to_proto(&subnet)),
        }))
    }

    async fn delete_subnet(
        &self,
        request: Request<DeleteSubnetRequest>,
    ) -> Result<Response<DeleteSubnetResponse>, 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 Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(id);
        let vpc_uuid = uuid::Uuid::parse_str(&req.vpc_id)
            .map_err(|_| Status::invalid_argument("Invalid VPC ID"))?;
        let vpc_id = VpcId::from_uuid(vpc_uuid);

        self.validate_vpc_in_tenant(&org_id, &project_id, &vpc_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_SUBNET_DELETE,
                &resource_for_tenant("subnet", subnet_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let ports = self
            .metadata
            .list_ports(Some(&subnet_id), None)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        if !ports.is_empty() {
            return Err(Status::failed_precondition(
                "cannot delete subnet while ports still exist",
            ));
        }

        self.metadata
            .delete_subnet(&org_id, &project_id, &vpc_id, &subnet_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

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

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

    #[test]
    fn rejects_subnet_outside_vpc() {
        let vpc = Vpc::new("vpc", "org", "proj", "10.0.0.0/16");
        let err =
            validate_subnet_inputs(&vpc, "10.1.0.0/24", None, Vec::<String>::new()).unwrap_err();
        assert_eq!(err.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn rejects_gateway_outside_subnet() {
        let vpc = Vpc::new("vpc", "org", "proj", "10.0.0.0/16");
        let err =
            validate_subnet_inputs(&vpc, "10.0.1.0/24", Some("10.0.2.1"), Vec::<String>::new())
                .unwrap_err();
        assert_eq!(err.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn rejects_overlapping_subnet() {
        let vpc = Vpc::new("vpc", "org", "proj", "10.0.0.0/16");
        let err = validate_subnet_inputs(
            &vpc,
            "10.0.1.0/24",
            Some("10.0.1.1"),
            vec!["10.0.1.128/25".to_string()],
        )
        .unwrap_err();
        assert_eq!(err.code(), tonic::Code::AlreadyExists);
    }

    #[test]
    fn rejects_duplicate_subnet_name() {
        let existing = vec![Subnet::new("app", VpcId::new(), "10.0.1.0/24")];
        let err = ensure_unique_subnet_name(&existing, "app", None).unwrap_err();
        assert_eq!(err.code(), tonic::Code::AlreadyExists);
    }
}
