//! Port gRPC service implementation

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

use prismnet_api::{
    port_service_server::PortService, AttachDeviceRequest, AttachDeviceResponse, CreatePortRequest,
    CreatePortResponse, DeletePortRequest, DeletePortResponse, DetachDeviceRequest,
    DetachDeviceResponse, DeviceType as ProtoDeviceType, GetPortRequest, GetPortResponse,
    ListPortsRequest, ListPortsResponse, Port as ProtoPort, PortStatus as ProtoPortStatus,
    UpdatePortRequest, UpdatePortResponse,
};
use iam_service_auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use prismnet_types::{DeviceType, Port, PortId, PortStatus, SecurityGroupId, Subnet, SubnetId};

use crate::{NetworkMetadataStore, OvnClient};

const ACTION_PORT_CREATE: &str = "network:ports:create";
const ACTION_PORT_READ: &str = "network:ports:read";
const ACTION_PORT_LIST: &str = "network:ports:list";
const ACTION_PORT_UPDATE: &str = "network:ports:update";
const ACTION_PORT_DELETE: &str = "network:ports:delete";
const ACTION_PORT_ATTACH: &str = "network:ports:attach";
const ACTION_PORT_DETACH: &str = "network:ports:detach";

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

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

    async fn validate_subnet_in_tenant(
        &self,
        org_id: &str,
        project_id: &str,
        subnet_id: &SubnetId,
    ) -> Result<Subnet, Status> {
        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"))?;

        if self
            .metadata
            .get_vpc(org_id, project_id, &subnet.vpc_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .is_none()
        {
            return Err(Status::permission_denied("Subnet not in tenant scope"));
        }

        Ok(subnet)
    }

    async fn resolve_security_groups_in_tenant(
        &self,
        org_id: &str,
        project_id: &str,
        security_group_ids: &[String],
    ) -> Result<Vec<SecurityGroupId>, Status> {
        let mut resolved = Vec::with_capacity(security_group_ids.len());

        for security_group_id in security_group_ids {
            let uuid = uuid::Uuid::parse_str(security_group_id)
                .map_err(|_| Status::invalid_argument("Invalid SecurityGroup ID"))?;
            let security_group_id = SecurityGroupId::from_uuid(uuid);
            self.metadata
                .get_security_group(org_id, project_id, &security_group_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?
                .ok_or_else(|| {
                    Status::not_found(format!(
                        "SecurityGroup {} not found",
                        security_group_id
                    ))
                })?;
            resolved.push(security_group_id);
        }

        Ok(resolved)
    }
}

fn port_to_proto(port: &Port) -> ProtoPort {
    ProtoPort {
        id: port.id.to_string(),
        subnet_id: port.subnet_id.to_string(),
        name: port.name.clone(),
        description: port.description.clone().unwrap_or_default(),
        mac_address: port.mac_address.clone(),
        ip_address: port.ip_address.clone().unwrap_or_default(),
        device_id: port.device_id.clone().unwrap_or_default(),
        device_type: device_type_to_proto(&port.device_type) as i32,
        security_group_ids: port
            .security_groups
            .iter()
            .map(|id| id.to_string())
            .collect(),
        admin_state_up: port.admin_state_up,
        status: port_status_to_proto(&port.status) as i32,
        created_at: port.created_at,
        updated_at: port.updated_at,
    }
}

fn port_status_to_proto(status: &PortStatus) -> ProtoPortStatus {
    match status {
        PortStatus::Build => ProtoPortStatus::Build,
        PortStatus::Active => ProtoPortStatus::Active,
        PortStatus::Down => ProtoPortStatus::Down,
        PortStatus::Error => ProtoPortStatus::Error,
    }
}

fn device_type_to_proto(device_type: &DeviceType) -> ProtoDeviceType {
    match device_type {
        DeviceType::None => ProtoDeviceType::None,
        DeviceType::Vm => ProtoDeviceType::Vm,
        DeviceType::Router => ProtoDeviceType::Router,
        DeviceType::LoadBalancer => ProtoDeviceType::LoadBalancer,
        DeviceType::DhcpServer => ProtoDeviceType::DhcpServer,
        DeviceType::Other => ProtoDeviceType::Other,
    }
}

fn proto_to_device_type(device_type: i32) -> DeviceType {
    match ProtoDeviceType::try_from(device_type) {
        Ok(ProtoDeviceType::None) | Ok(ProtoDeviceType::Unspecified) => DeviceType::None,
        Ok(ProtoDeviceType::Vm) => DeviceType::Vm,
        Ok(ProtoDeviceType::Router) => DeviceType::Router,
        Ok(ProtoDeviceType::LoadBalancer) => DeviceType::LoadBalancer,
        Ok(ProtoDeviceType::DhcpServer) => DeviceType::DhcpServer,
        Ok(ProtoDeviceType::Other) => DeviceType::Other,
        Err(_) => DeviceType::Other,
    }
}

fn parse_ipv4_cidr(cidr: &str) -> Result<(Ipv4Addr, u8), Status> {
    let (ip, prefix) = cidr
        .split_once('/')
        .ok_or_else(|| Status::invalid_argument("subnet cidr_block must be in a.b.c.d/prefix form"))?;
    let ip: Ipv4Addr = ip
        .parse()
        .map_err(|_| Status::invalid_argument("subnet cidr_block must contain a valid IPv4 address"))?;
    let prefix: u8 = prefix
        .parse()
        .map_err(|_| Status::invalid_argument("subnet cidr_block prefix must be an integer"))?;
    if prefix > 32 {
        return Err(Status::invalid_argument(
            "subnet cidr_block 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_contains_ip(cidr: (Ipv4Addr, u8), ip: Ipv4Addr) -> bool {
    let mask = network_mask(cidr.1);
    (u32::from(cidr.0) & mask) == (u32::from(ip) & mask)
}

fn validate_requested_ip(subnet: &Subnet, requested_ip: &str, existing_ports: &[Port]) -> Result<(), Status> {
    let subnet_cidr = parse_ipv4_cidr(&subnet.cidr_block)?;
    let requested_ip = parse_ipv4(requested_ip, "ip_address")?;
    let requested_ip_string = requested_ip.to_string();

    if !cidr_contains_ip(subnet_cidr, requested_ip) {
        return Err(Status::invalid_argument(
            "ip_address must fall within the subnet cidr_block",
        ));
    }

    if subnet
        .gateway_ip
        .as_deref()
        .map(|gateway_ip| gateway_ip == requested_ip_string.as_str())
        .unwrap_or(false)
    {
        return Err(Status::invalid_argument(
            "ip_address cannot reuse the subnet gateway_ip",
        ));
    }

    if existing_ports.iter().any(|port| {
        port.ip_address
            .as_deref()
            .map(|ip_address| ip_address == requested_ip_string.as_str())
            .unwrap_or(false)
    }) {
        return Err(Status::already_exists(
            "ip_address is already allocated in the subnet",
        ));
    }

    Ok(())
}

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

#[tonic::async_trait]
impl PortService for PortServiceImpl {
    async fn create_port(
        &self,
        request: Request<CreatePortRequest>,
    ) -> Result<Response<CreatePortResponse>, 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 subnet_id = uuid::Uuid::parse_str(&req.subnet_id)
            .map_err(|_| Status::invalid_argument("Invalid Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(subnet_id);

        let subnet = self
            .validate_subnet_in_tenant(&org_id, &project_id, &subnet_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_PORT_CREATE,
                &resource_for_tenant("port", "*", &org_id, &project_id),
            )
            .await?;

        let existing_ports = self
            .metadata
            .list_ports(Some(&subnet_id), None)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        ensure_unique_port_name(&existing_ports, &req.name, None)?;

        let port = Port::new(&req.name, subnet_id);
        let mut port = port;
        if !req.description.is_empty() {
            port.description = Some(req.description);
        }

        // IP allocation: use provided IP or auto-allocate
        if !req.ip_address.is_empty() {
            validate_requested_ip(&subnet, &req.ip_address, &existing_ports)?;
            port.ip_address = Some(req.ip_address);
        } else {
            // Auto-allocate IP from subnet CIDR
            port.ip_address = self
                .metadata
                .allocate_ip(&org_id, &project_id, &subnet_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
            if port.ip_address.is_none() {
                return Err(Status::resource_exhausted(
                    "no available IP addresses remain in the subnet",
                ));
            }
        }

        if !req.security_group_ids.is_empty() {
            port.security_groups = self
                .resolve_security_groups_in_tenant(
                    &org_id,
                    &project_id,
                    &req.security_group_ids,
                )
                .await?;
        }

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

        let ip_address = port
            .ip_address
            .as_ref()
            .ok_or_else(|| Status::internal("IP allocation failed"))?;

        if let Err(error) = self
            .ovn
            .create_logical_switch_port(&port, &subnet.vpc_id, ip_address)
            .await
        {
            let _ = self
                .metadata
                .delete_port(&org_id, &project_id, &subnet_id, &port.id)
                .await;
            return Err(Status::internal(error.to_string()));
        }

        Ok(Response::new(CreatePortResponse {
            port: Some(port_to_proto(&port)),
        }))
    }

    async fn get_port(
        &self,
        request: Request<GetPortRequest>,
    ) -> Result<Response<GetPortResponse>, 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 Port ID"))?;
        let port_id = PortId::from_uuid(id);
        let subnet_uuid = uuid::Uuid::parse_str(&req.subnet_id)
            .map_err(|_| Status::invalid_argument("Invalid Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(subnet_uuid);

        self.validate_subnet_in_tenant(&org_id, &project_id, &subnet_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_PORT_READ,
                &resource_for_tenant("port", port_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let port = self
            .metadata
            .get_port(&subnet_id, &port_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Port not found"))?;

        Ok(Response::new(GetPortResponse {
            port: Some(port_to_proto(&port)),
        }))
    }

    async fn list_ports(
        &self,
        request: Request<ListPortsRequest>,
    ) -> Result<Response<ListPortsResponse>, 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_PORT_LIST,
                &resource_for_tenant("port", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();

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

        let device_id = if !req.device_id.is_empty() {
            Some(req.device_id)
        } else {
            None
        };

        self.validate_subnet_in_tenant(&org_id, &project_id, &subnet_id)
            .await?;

        let ports = self
            .metadata
            .list_ports(Some(&subnet_id), device_id.as_deref())
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(ListPortsResponse {
            ports: ports.iter().map(port_to_proto).collect(),
            next_page_token: String::new(),
        }))
    }

    async fn update_port(
        &self,
        request: Request<UpdatePortRequest>,
    ) -> Result<Response<UpdatePortResponse>, 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 Port ID"))?;
        let port_id = PortId::from_uuid(id);
        let subnet_uuid = uuid::Uuid::parse_str(&req.subnet_id)
            .map_err(|_| Status::invalid_argument("Invalid Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(subnet_uuid);

        self.validate_subnet_in_tenant(&org_id, &project_id, &subnet_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_PORT_UPDATE,
                &resource_for_tenant("port", port_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 security_group_ids = if !req.security_group_ids.is_empty() {
            Some(
                self.resolve_security_groups_in_tenant(
                    &org_id,
                    &project_id,
                    &req.security_group_ids,
                )
                .await?,
            )
        } else {
            None
        };
        if let Some(name) = name.as_deref() {
            let existing_ports = self
                .metadata
                .list_ports(Some(&subnet_id), None)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
            ensure_unique_port_name(&existing_ports, name, Some(port_id))?;
        }

        let port = self
            .metadata
            .update_port(
                &org_id,
                &project_id,
                &subnet_id,
                &port_id,
                name,
                description,
                security_group_ids,
                Some(req.admin_state_up),
            )
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Port not found"))?;

        Ok(Response::new(UpdatePortResponse {
            port: Some(port_to_proto(&port)),
        }))
    }

    async fn delete_port(
        &self,
        request: Request<DeletePortRequest>,
    ) -> Result<Response<DeletePortResponse>, 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 Port ID"))?;
        let port_id = PortId::from_uuid(id);
        let subnet_uuid = uuid::Uuid::parse_str(&req.subnet_id)
            .map_err(|_| Status::invalid_argument("Invalid Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(subnet_uuid);

        self.validate_subnet_in_tenant(&org_id, &project_id, &subnet_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_PORT_DELETE,
                &resource_for_tenant("port", port_id.to_string(), &org_id, &project_id),
            )
            .await?;

        self.metadata
            .delete_port(&org_id, &project_id, &subnet_id, &port_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Port not found"))?;

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

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

    async fn attach_device(
        &self,
        request: Request<AttachDeviceRequest>,
    ) -> Result<Response<AttachDeviceResponse>, 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 port_id = uuid::Uuid::parse_str(&req.port_id)
            .map_err(|_| Status::invalid_argument("Invalid Port ID"))?;
        let port_id = PortId::from_uuid(port_id);
        let subnet_uuid = uuid::Uuid::parse_str(&req.subnet_id)
            .map_err(|_| Status::invalid_argument("Invalid Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(subnet_uuid);

        self.validate_subnet_in_tenant(&org_id, &project_id, &subnet_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_PORT_ATTACH,
                &resource_for_tenant("port", port_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let device_type = proto_to_device_type(req.device_type);

        let port = self
            .metadata
            .attach_device(&port_id, &subnet_id, req.device_id, device_type)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Port not found"))?;

        Ok(Response::new(AttachDeviceResponse {
            port: Some(port_to_proto(&port)),
        }))
    }

    async fn detach_device(
        &self,
        request: Request<DetachDeviceRequest>,
    ) -> Result<Response<DetachDeviceResponse>, 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 port_id = uuid::Uuid::parse_str(&req.port_id)
            .map_err(|_| Status::invalid_argument("Invalid Port ID"))?;
        let port_id = PortId::from_uuid(port_id);
        let subnet_uuid = uuid::Uuid::parse_str(&req.subnet_id)
            .map_err(|_| Status::invalid_argument("Invalid Subnet ID"))?;
        let subnet_id = SubnetId::from_uuid(subnet_uuid);

        self.validate_subnet_in_tenant(&org_id, &project_id, &subnet_id)
            .await?;
        self.auth
            .authorize(
                &tenant,
                ACTION_PORT_DETACH,
                &resource_for_tenant("port", port_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let port = self
            .metadata
            .detach_device(&port_id, &subnet_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("Port not found"))?;

        Ok(Response::new(DetachDeviceResponse {
            port: Some(port_to_proto(&port)),
        }))
    }
}

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

    #[test]
    fn rejects_requested_ip_outside_subnet() {
        let mut subnet = Subnet::new("subnet", VpcId::new(), "10.0.1.0/24");
        subnet.gateway_ip = Some("10.0.1.1".to_string());
        let err = validate_requested_ip(&subnet, "10.0.2.10", &[]).unwrap_err();
        assert_eq!(err.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn rejects_requested_ip_matching_gateway() {
        let mut subnet = Subnet::new("subnet", VpcId::new(), "10.0.1.0/24");
        subnet.gateway_ip = Some("10.0.1.1".to_string());
        let err = validate_requested_ip(&subnet, "10.0.1.1", &[]).unwrap_err();
        assert_eq!(err.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn rejects_duplicate_requested_ip() {
        let mut subnet = Subnet::new("subnet", VpcId::new(), "10.0.1.0/24");
        subnet.gateway_ip = Some("10.0.1.1".to_string());

        let mut existing_port = Port::new("existing", subnet.id);
        existing_port.ip_address = Some("10.0.1.10".to_string());

        let err = validate_requested_ip(&subnet, "10.0.1.10", &[existing_port]).unwrap_err();
        assert_eq!(err.code(), tonic::Code::AlreadyExists);
    }

    #[test]
    fn rejects_duplicate_port_name() {
        let existing = vec![Port::new("frontend", SubnetId::new())];
        let err = ensure_unique_port_name(&existing, "frontend", None).unwrap_err();
        assert_eq!(err.code(), tonic::Code::AlreadyExists);
    }
}
