//! Router 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::{
    router_service_server::RouterService, CreateRouterRequest, CreateRouterResponse,
    DeleteRouterRequest, DeleteRouterResponse, GetRouterRequest, GetRouterResponse,
    ListRoutersRequest, ListRoutersResponse, Router as ProtoRouter,
    RouterStatus as ProtoRouterStatus, UpdateRouterRequest, UpdateRouterResponse,
};
use prismnet_types::{Router, RouterId, RouterStatus, Vpc, VpcId};

use crate::{NetworkMetadataStore, OvnClient};

const ACTION_ROUTER_CREATE: &str = "network:routers:create";
const ACTION_ROUTER_READ: &str = "network:routers:read";
const ACTION_ROUTER_LIST: &str = "network:routers:list";
const ACTION_ROUTER_UPDATE: &str = "network:routers:update";
const ACTION_ROUTER_DELETE: &str = "network:routers:delete";

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

impl RouterServiceImpl {
    pub fn new(
        metadata: Arc<NetworkMetadataStore>,
        ovn: Arc<OvnClient>,
        auth: Arc<AuthService>,
    ) -> Self {
        Self {
            metadata,
            ovn,
            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 router_to_proto(router: &Router) -> ProtoRouter {
    ProtoRouter {
        id: router.id.to_string(),
        org_id: router.org_id.clone(),
        project_id: router.project_id.clone(),
        vpc_id: router.vpc_id.to_string(),
        name: router.name.clone(),
        description: router.description.clone().unwrap_or_default(),
        gateway_cidr: router.gateway_cidr.clone(),
        mac_address: router.mac_address.clone(),
        external_ip: router.external_ip.clone(),
        status: status_to_proto(&router.status) as i32,
        created_at: router.created_at,
        updated_at: router.updated_at,
    }
}

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

fn parse_ipv4_cidr(cidr: &str) -> Result<(Ipv4Addr, u8), Status> {
    let (ip, prefix) = cidr
        .split_once('/')
        .ok_or_else(|| Status::invalid_argument("CIDR must be in a.b.c.d/prefix form"))?;
    let ip: Ipv4Addr = ip
        .parse()
        .map_err(|_| Status::invalid_argument("CIDR must contain a valid IPv4 address"))?;
    let prefix: u8 = prefix
        .parse()
        .map_err(|_| Status::invalid_argument("CIDR prefix must be an integer"))?;
    if prefix > 32 {
        return Err(Status::invalid_argument(
            "CIDR 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 validate_mac_address(mac_address: &str) -> Result<(), Status> {
    let octets: Vec<_> = mac_address.split(':').collect();
    if octets.len() != 6
        || octets
            .iter()
            .any(|octet| octet.len() != 2 || u8::from_str_radix(octet, 16).is_err())
    {
        return Err(Status::invalid_argument(
            "mac_address must be a valid MAC address",
        ));
    }
    Ok(())
}

fn ipv4_to_u32(ip: Ipv4Addr) -> u32 {
    u32::from(ip)
}

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

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

fn validate_router_inputs(vpc: &Vpc, gateway_cidr: &str, mac_address: &str, external_ip: &str) -> Result<(), Status> {
    let vpc_cidr = parse_ipv4_cidr(&vpc.cidr_block)?;
    let (gateway_ip, _) = parse_ipv4_cidr(gateway_cidr)?;
    let _ = parse_ipv4(external_ip, "external_ip")?;
    validate_mac_address(mac_address)?;

    if !cidr_contains(vpc_cidr, gateway_ip) {
        return Err(Status::invalid_argument(
            "gateway_cidr must use an address within the VPC cidr_block",
        ));
    }

    Ok(())
}

#[tonic::async_trait]
impl RouterService for RouterServiceImpl {
    async fn create_router(
        &self,
        request: Request<CreateRouterRequest>,
    ) -> Result<Response<CreateRouterResponse>, 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 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);
        let vpc = self.validate_vpc_in_tenant(&org_id, &project_id, &vpc_id).await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_ROUTER_CREATE,
                &resource_for_tenant("router", "*", &org_id, &project_id),
            )
            .await?;

        if req.name.trim().is_empty() {
            return Err(Status::invalid_argument("router name is required"));
        }
        validate_router_inputs(&vpc, &req.gateway_cidr, &req.mac_address, &req.external_ip)?;

        let existing = self
            .metadata
            .list_routers(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        if existing.iter().any(|router| router.vpc_id == vpc_id) {
            return Err(Status::already_exists("VPC already has a router"));
        }

        let mut router = Router::new(
            &req.name,
            &org_id,
            &project_id,
            vpc_id,
            &req.gateway_cidr,
            &req.mac_address,
            &req.external_ip,
        );
        if !req.description.is_empty() {
            router.description = Some(req.description);
        }

        let ovn_router_id = self
            .ovn
            .create_logical_router(&format!("router-{}", router.id))
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        let ovn_router_port_id = match self
            .ovn
            .add_router_port(&ovn_router_id, &vpc_id, &router.gateway_cidr, &router.mac_address)
            .await
        {
            Ok(port_id) => port_id,
            Err(error) => {
                let _ = self.ovn.delete_logical_router(&ovn_router_id).await;
                return Err(Status::internal(error.to_string()));
            }
        };

        if let Err(error) = self
            .ovn
            .configure_snat(&ovn_router_id, &router.external_ip, &vpc.cidr_block)
            .await
        {
            let _ = self.ovn.delete_logical_router(&ovn_router_id).await;
            return Err(Status::internal(error.to_string()));
        }

        router.ovn_router_id = ovn_router_id;
        router.ovn_router_port_id = ovn_router_port_id;

        if let Err(error) = self.metadata.create_router(router.clone()).await {
            let _ = self.ovn.delete_logical_router(&router.ovn_router_id).await;
            return Err(Status::internal(error.to_string()));
        }

        Ok(Response::new(CreateRouterResponse {
            router: Some(router_to_proto(&router)),
        }))
    }

    async fn get_router(
        &self,
        request: Request<GetRouterRequest>,
    ) -> Result<Response<GetRouterResponse>, 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 router ID"))?;
        let router_id = RouterId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_ROUTER_READ,
                &resource_for_tenant("router", router_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let router = self
            .metadata
            .get_router(&org_id, &project_id, &router_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Router not found"))?;

        Ok(Response::new(GetRouterResponse {
            router: Some(router_to_proto(&router)),
        }))
    }

    async fn list_routers(
        &self,
        request: Request<ListRoutersRequest>,
    ) -> Result<Response<ListRoutersResponse>, 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_ROUTER_LIST,
                &resource_for_tenant("router", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

        let vpc_id = if req.vpc_id.is_empty() {
            None
        } else {
            let vpc_uuid = uuid::Uuid::parse_str(&req.vpc_id)
                .map_err(|_| Status::invalid_argument("Invalid VPC ID"))?;
            Some(VpcId::from_uuid(vpc_uuid))
        };

        if let Some(vpc_id) = vpc_id.as_ref() {
            self.validate_vpc_in_tenant(&org_id, &project_id, vpc_id).await?;
        }

        let mut routers = self
            .metadata
            .list_routers(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        if let Some(vpc_id) = vpc_id {
            routers.retain(|router| router.vpc_id == vpc_id);
        }

        Ok(Response::new(ListRoutersResponse {
            routers: routers.iter().map(router_to_proto).collect(),
            next_page_token: String::new(),
        }))
    }

    async fn update_router(
        &self,
        request: Request<UpdateRouterRequest>,
    ) -> Result<Response<UpdateRouterResponse>, 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 router ID"))?;
        let router_id = RouterId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_ROUTER_UPDATE,
                &resource_for_tenant("router", router_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 router = self
            .metadata
            .update_router(&org_id, &project_id, &router_id, name, description)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Router not found"))?;

        Ok(Response::new(UpdateRouterResponse {
            router: Some(router_to_proto(&router)),
        }))
    }

    async fn delete_router(
        &self,
        request: Request<DeleteRouterRequest>,
    ) -> Result<Response<DeleteRouterResponse>, 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 router ID"))?;
        let router_id = RouterId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_ROUTER_DELETE,
                &resource_for_tenant("router", router_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let router = self
            .metadata
            .get_router(&org_id, &project_id, &router_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Router not found"))?;

        self.ovn
            .delete_logical_router(&router.ovn_router_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

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

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

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

    #[test]
    fn rejects_invalid_mac_address() {
        let err = validate_mac_address("02:00:00:00:00").unwrap_err();
        assert_eq!(err.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn rejects_gateway_outside_vpc_cidr() {
        let vpc = Vpc::new("test", "org", "proj", "10.0.0.0/16");
        let err = validate_router_inputs(
            &vpc,
            "10.1.0.1/24",
            "02:00:00:00:00:01",
            "203.0.113.10",
        )
        .unwrap_err();
        assert_eq!(err.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn accepts_router_inputs_inside_vpc_cidr() {
        let vpc = Vpc::new("test", "org", "proj", "10.0.0.0/16");
        validate_router_inputs(
            &vpc,
            "10.0.0.1/24",
            "02:00:00:00:00:01",
            "203.0.113.10",
        )
        .unwrap();
    }
}
