//! VPC gRPC service implementation

use std::net::Ipv4Addr;
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 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_vpc_name(existing: &[Vpc], desired_name: &str, exclude: Option<VpcId>) -> Result<(), Status> {
    if desired_name.trim().is_empty() {
        return Err(Status::invalid_argument("VPC name is required"));
    }
    if existing
        .iter()
        .any(|vpc| vpc.name == desired_name && Some(vpc.id) != exclude)
    {
        return Err(Status::already_exists("VPC name already exists"));
    }
    Ok(())
}

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 existing = self
            .metadata
            .list_vpcs(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        ensure_unique_vpc_name(&existing, &req.name, None)?;
        parse_ipv4_cidr(&req.cidr_block)?;

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

        if let Err(error) = self
            .ovn
            .create_logical_switch(&vpc.id, &vpc.cidr_block)
            .await
        {
            return Err(Status::internal(error.to_string()));
        }

        vpc.status = VpcStatus::Active;
        if let Err(error) = self.metadata.create_vpc(vpc.clone()).await {
            let _ = self.ovn.delete_logical_switch(&vpc.id).await;
            return Err(Status::internal(error.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)
        };
        if let Some(name) = name.as_deref() {
            let existing = self
                .metadata
                .list_vpcs(&org_id, &project_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
            ensure_unique_vpc_name(&existing, name, Some(vpc_id))?;
        }

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

        let subnets = self
            .metadata
            .list_subnets(&org_id, &project_id, &vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        if !subnets.is_empty() {
            return Err(Status::failed_precondition(
                "cannot delete VPC while subnets still exist",
            ));
        }

        let routers = self
            .metadata
            .list_routers(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        if routers.iter().any(|router| router.vpc_id == vpc_id) {
            return Err(Status::failed_precondition(
                "cannot delete VPC while routers still exist",
            ));
        }

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

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

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

    #[test]
    fn rejects_duplicate_vpc_name() {
        let existing = vec![Vpc::new("prod", "org", "proj", "10.0.0.0/16")];
        let err = ensure_unique_vpc_name(&existing, "prod", None).unwrap_err();
        assert_eq!(err.code(), tonic::Code::AlreadyExists);
    }
}
