//! VPC gRPC service implementation

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

use prismnet_api::{
    vpc_service_server::VpcService, CreateVpcRequest, CreateVpcResponse, DeleteVpcRequest,
    DeleteVpcResponse, GetVpcRequest, GetVpcResponse, ListVpcsRequest, ListVpcsResponse,
    UpdateVpcRequest, UpdateVpcResponse, Vpc as ProtoVpc, VpcStatus as ProtoVpcStatus,
};
use iam_service_auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use prismnet_types::{Vpc, VpcId, VpcStatus};

use crate::{NetworkMetadataStore, OvnClient};

const ACTION_VPC_CREATE: &str = "network:vpcs:create";
const ACTION_VPC_READ: &str = "network:vpcs:read";
const ACTION_VPC_LIST: &str = "network:vpcs:list";
const ACTION_VPC_UPDATE: &str = "network:vpcs:update";
const ACTION_VPC_DELETE: &str = "network:vpcs:delete";

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

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

fn vpc_to_proto(vpc: &Vpc) -> ProtoVpc {
    ProtoVpc {
        id: vpc.id.to_string(),
        org_id: vpc.org_id.clone(),
        project_id: vpc.project_id.clone(),
        name: vpc.name.clone(),
        description: vpc.description.clone().unwrap_or_default(),
        cidr_block: vpc.cidr_block.clone(),
        status: status_to_proto(&vpc.status) as i32,
        created_at: vpc.created_at,
        updated_at: vpc.updated_at,
    }
}

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

#[tonic::async_trait]
impl VpcService for VpcServiceImpl {
    async fn create_vpc(
        &self,
        request: Request<CreateVpcRequest>,
    ) -> Result<Response<CreateVpcResponse>, 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_VPC_CREATE,
                &resource_for_tenant("vpc", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        let vpc = Vpc::new(&req.name, &org_id, &project_id, &req.cidr_block);
        let mut vpc = vpc;
        if !req.description.is_empty() {
            vpc.description = Some(req.description);
        }

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

        self.ovn
            .create_logical_switch(&vpc.id, &vpc.cidr_block)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(CreateVpcResponse {
            vpc: Some(vpc_to_proto(&vpc)),
        }))
    }

    async fn get_vpc(
        &self,
        request: Request<GetVpcRequest>,
    ) -> Result<Response<GetVpcResponse>, 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 VPC ID"))?;
        let vpc_id = VpcId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_VPC_READ,
                &resource_for_tenant("vpc", vpc_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let vpc = self
            .metadata
            .get_vpc(&org_id, &project_id, &vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("VPC not found"))?;

        Ok(Response::new(GetVpcResponse {
            vpc: Some(vpc_to_proto(&vpc)),
        }))
    }

    async fn list_vpcs(
        &self,
        request: Request<ListVpcsRequest>,
    ) -> Result<Response<ListVpcsResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org, project) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_VPC_LIST,
                &resource_for_tenant("vpc", "*", &org, &project),
            )
            .await?;
        let _req = request.into_inner();

        let vpcs = self
            .metadata
            .list_vpcs(&org, &project)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        let proto_vpcs: Vec<ProtoVpc> = vpcs.iter().map(vpc_to_proto).collect();

        Ok(Response::new(ListVpcsResponse {
            vpcs: proto_vpcs,
            next_page_token: String::new(),
        }))
    }

    async fn update_vpc(
        &self,
        request: Request<UpdateVpcRequest>,
    ) -> Result<Response<UpdateVpcResponse>, 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 VPC ID"))?;
        let vpc_id = VpcId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_VPC_UPDATE,
                &resource_for_tenant("vpc", vpc_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let name = if req.name.is_empty() {
            None
        } else {
            Some(req.name)
        };
        let description = if req.description.is_empty() {
            None
        } else {
            Some(req.description)
        };

        let vpc = self
            .metadata
            .update_vpc(&org_id, &project_id, &vpc_id, name, description)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("VPC not found"))?;

        Ok(Response::new(UpdateVpcResponse {
            vpc: Some(vpc_to_proto(&vpc)),
        }))
    }

    async fn delete_vpc(
        &self,
        request: Request<DeleteVpcRequest>,
    ) -> Result<Response<DeleteVpcResponse>, 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 VPC ID"))?;
        let vpc_id = VpcId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_VPC_DELETE,
                &resource_for_tenant("vpc", vpc_id.to_string(), &org_id, &project_id),
            )
            .await?;

        self.metadata
            .delete_vpc(&org_id, &project_id, &vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("VPC not found"))?;

        self.ovn
            .delete_logical_switch(&vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

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