//! Subnet gRPC service implementation

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

use prismnet_api::{
    subnet_service_server::SubnetService, CreateSubnetRequest, CreateSubnetResponse,
    DeleteSubnetRequest, DeleteSubnetResponse, GetSubnetRequest, GetSubnetResponse,
    ListSubnetsRequest, ListSubnetsResponse, Subnet as ProtoSubnet,
    SubnetStatus as ProtoSubnetStatus, UpdateSubnetRequest, UpdateSubnetResponse,
};
use iam_service_auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use prismnet_types::{Subnet, SubnetId, SubnetStatus, 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<(), Status> {
        if self
            .metadata
            .get_vpc(org_id, project_id, vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .is_none()
        {
            return Err(Status::permission_denied("VPC not in tenant scope"));
        }

        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);

        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 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;

        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);
        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_READ,
                &resource_for_tenant("subnet", subnet_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let subnet = 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"));
        };

        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
        };

        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?;

        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 {}))
    }
}
