//! PrismNET client for port management

use prismnet_api::proto::{
    port_service_client::PortServiceClient, subnet_service_client::SubnetServiceClient,
    AttachDeviceRequest, CreatePortRequest, DeletePortRequest, DetachDeviceRequest, GetPortRequest,
    GetSubnetRequest,
};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;

/// PrismNET client wrapper
pub struct PrismNETClient {
    auth_token: String,
    port_client: PortServiceClient<Channel>,
    subnet_client: SubnetServiceClient<Channel>,
}

impl PrismNETClient {
    /// Create a new PrismNET client
    pub async fn new(
        endpoint: String,
        auth_token: String,
    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
        let channel = Channel::from_shared(endpoint)?.connect().await?;
        let port_client = PortServiceClient::new(channel.clone());
        let subnet_client = SubnetServiceClient::new(channel);
        Ok(Self {
            auth_token,
            port_client,
            subnet_client,
        })
    }

    fn request_with_auth<T>(
        auth_token: &str,
        payload: T,
    ) -> Result<tonic::Request<T>, Box<dyn std::error::Error + Send + Sync>> {
        let mut request = tonic::Request::new(payload);
        let token_value = MetadataValue::try_from(auth_token)?;
        request
            .metadata_mut()
            .insert("x-photon-auth-token", token_value);
        Ok(request)
    }

    /// Get port details
    pub async fn get_port(
        &mut self,
        org_id: &str,
        project_id: &str,
        subnet_id: &str,
        port_id: &str,
    ) -> Result<prismnet_api::proto::Port, Box<dyn std::error::Error + Send + Sync>> {
        let request = Self::request_with_auth(
            &self.auth_token,
            GetPortRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                subnet_id: subnet_id.to_string(),
                id: port_id.to_string(),
            },
        )?;
        let response = self.port_client.get_port(request).await?;
        Ok(response
            .into_inner()
            .port
            .ok_or("Port not found in response")?)
    }

    /// Get subnet details, resolving by subnet ID when VPC ID is not known locally.
    pub async fn get_subnet(
        &mut self,
        org_id: &str,
        project_id: &str,
        subnet_id: &str,
    ) -> Result<prismnet_api::proto::Subnet, Box<dyn std::error::Error + Send + Sync>> {
        let request = Self::request_with_auth(
            &self.auth_token,
            GetSubnetRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                vpc_id: String::new(),
                id: subnet_id.to_string(),
            },
        )?;
        let response = self.subnet_client.get_subnet(request).await?;
        Ok(response
            .into_inner()
            .subnet
            .ok_or("Subnet not found in response")?)
    }

    /// Create a port for a VM-managed NIC
    pub async fn create_port(
        &mut self,
        org_id: &str,
        project_id: &str,
        subnet_id: &str,
        name: &str,
        description: Option<&str>,
        ip_address: Option<&str>,
        security_group_ids: Vec<String>,
    ) -> Result<prismnet_api::proto::Port, Box<dyn std::error::Error + Send + Sync>> {
        let request = Self::request_with_auth(
            &self.auth_token,
            CreatePortRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                subnet_id: subnet_id.to_string(),
                name: name.to_string(),
                description: description.unwrap_or_default().to_string(),
                ip_address: ip_address.unwrap_or_default().to_string(),
                security_group_ids,
            },
        )?;
        let response = self.port_client.create_port(request).await?;
        Ok(response
            .into_inner()
            .port
            .ok_or("Port not found in response")?)
    }

    /// Attach a device to a port
    pub async fn attach_device(
        &mut self,
        org_id: &str,
        project_id: &str,
        subnet_id: &str,
        port_id: &str,
        device_id: &str,
        device_type: i32,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let request = Self::request_with_auth(
            &self.auth_token,
            AttachDeviceRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                subnet_id: subnet_id.to_string(),
                port_id: port_id.to_string(),
                device_id: device_id.to_string(),
                device_type,
            },
        )?;
        self.port_client.attach_device(request).await?;
        Ok(())
    }

    /// Detach a device from a port
    pub async fn detach_device(
        &mut self,
        org_id: &str,
        project_id: &str,
        subnet_id: &str,
        port_id: &str,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let request = Self::request_with_auth(
            &self.auth_token,
            DetachDeviceRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                subnet_id: subnet_id.to_string(),
                port_id: port_id.to_string(),
            },
        )?;
        self.port_client.detach_device(request).await?;
        Ok(())
    }

    /// Delete a port
    pub async fn delete_port(
        &mut self,
        org_id: &str,
        project_id: &str,
        subnet_id: &str,
        port_id: &str,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let request = Self::request_with_auth(
            &self.auth_token,
            DeletePortRequest {
                org_id: org_id.to_string(),
                project_id: project_id.to_string(),
                subnet_id: subnet_id.to_string(),
                id: port_id.to_string(),
            },
        )?;
        self.port_client.delete_port(request).await?;
        Ok(())
    }
}

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

    #[test]
    fn request_with_auth_adds_internal_token_header() {
        let request = PrismNETClient::request_with_auth(
            "test-token",
            GetPortRequest {
                org_id: "org".to_string(),
                project_id: "project".to_string(),
                subnet_id: "subnet".to_string(),
                id: "port".to_string(),
            },
        )
        .expect("request metadata should be constructible");

        assert_eq!(
            request
                .metadata()
                .get("x-photon-auth-token")
                .and_then(|value| value.to_str().ok()),
            Some("test-token")
        );
    }
}
