//! IAM client implementation
//!
//! Provides a thin gRPC client for interacting with the IAM service.

use std::future::Future;
use std::time::Duration;

use iam_api::proto::{
    iam_admin_client::IamAdminClient, iam_authz_client::IamAuthzClient,
    iam_token_client::IamTokenClient, AuthorizeRequest, AuthzContext, CreateBindingRequest,
    CreateOrganizationRequest, CreatePrincipalRequest, CreateProjectRequest, CreateRoleRequest,
    DeleteBindingRequest, DeleteOrganizationRequest, DeleteProjectRequest, GetOrganizationRequest,
    GetPrincipalRequest, GetProjectRequest, GetRoleRequest, IssueTokenRequest, ListBindingsRequest,
    ListOrganizationsRequest, ListPrincipalsRequest, ListProjectsRequest, ListRolesRequest,
    Organization as ProtoOrganization, Principal as ProtoPrincipal,
    PrincipalKind as ProtoPrincipalKind, PrincipalRef as ProtoPrincipalRef,
    Project as ProtoProject, ResourceRef as ProtoResourceRef, RevokeTokenRequest,
    Scope as ProtoScope, UpdateOrganizationRequest, UpdateProjectRequest, ValidateTokenRequest,
};
use iam_types::{
    AuthMethod, Error, IamError, InternalTokenClaims, Organization, PolicyBinding, Principal,
    PrincipalKind as TypesPrincipalKind, PrincipalRef, Project, Resource, Result, Role, Scope,
};
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};
use tonic::{metadata::MetadataValue, Request};

const TRANSIENT_RPC_RETRY_ATTEMPTS: usize = 3;
const TRANSIENT_RPC_INITIAL_BACKOFF: Duration = Duration::from_millis(200);
const TRANSIENT_RPC_MAX_BACKOFF: Duration = Duration::from_millis(1_000);

/// Configuration for the IAM client
#[derive(Debug, Clone)]
pub struct IamClientConfig {
    /// IAM server endpoint
    pub endpoint: String,
    /// Connection timeout
    pub timeout_ms: u64,
    /// Enable TLS
    pub tls: bool,
    /// Optional admin token for privileged APIs
    pub admin_token: Option<String>,
}

impl IamClientConfig {
    /// Create a new config
    pub fn new(endpoint: impl Into<String>) -> Self {
        Self {
            endpoint: endpoint.into(),
            timeout_ms: 5000,
            tls: true,
            admin_token: load_admin_token_from_env(),
        }
    }

    /// Set timeout
    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
        self.timeout_ms = timeout_ms;
        self
    }

    /// Disable TLS
    pub fn without_tls(mut self) -> Self {
        self.tls = false;
        self
    }

    /// Set admin token for privileged APIs.
    pub fn with_admin_token(mut self, admin_token: impl Into<String>) -> Self {
        let token = admin_token.into();
        self.admin_token = if token.trim().is_empty() {
            None
        } else {
            Some(token)
        };
        self
    }
}

/// IAM client
pub struct IamClient {
    channel: Channel,
    admin_token: Option<String>,
}

impl IamClient {
    /// Create a new IAM client
    pub async fn connect(config: IamClientConfig) -> Result<Self> {
        let endpoint_uri =
            if config.endpoint.starts_with("http://") || config.endpoint.starts_with("https://") {
                config.endpoint.clone()
            } else if config.tls {
                format!("https://{}", config.endpoint)
            } else {
                format!("http://{}", config.endpoint)
            };

        let mut endpoint = Endpoint::from_shared(endpoint_uri.clone())
            .map_err(|e| Error::Internal(e.to_string()))?
            .timeout(Duration::from_millis(config.timeout_ms));

        if config.tls {
            endpoint = endpoint
                .tls_config(ClientTlsConfig::new())
                .map_err(|e| Error::Internal(e.to_string()))?;
        }

        let channel = endpoint
            .connect()
            .await
            .map_err(|e| Error::Internal(e.to_string()))?;

        Ok(Self {
            channel,
            admin_token: config.admin_token,
        })
    }

    fn authz_client(&self) -> IamAuthzClient<Channel> {
        IamAuthzClient::new(self.channel.clone())
    }

    fn admin_client(&self) -> IamAdminClient<Channel> {
        IamAdminClient::new(self.channel.clone())
    }

    fn token_client(&self) -> IamTokenClient<Channel> {
        IamTokenClient::new(self.channel.clone())
    }

    fn inject_admin_token<T>(&self, request: &mut Request<T>) {
        let Some(token) = self.admin_token.as_ref() else {
            return;
        };

        if let Ok(value) = MetadataValue::try_from(token.as_str()) {
            request.metadata_mut().insert("x-iam-admin-token", value);
        }
    }

    fn admin_request<T>(&self, message: T) -> Request<T> {
        let mut request = Request::new(message);
        self.inject_admin_token(&mut request);
        request
    }

    async fn call_with_retry<T, F, Fut>(operation: &'static str, mut op: F) -> Result<T>
    where
        F: FnMut() -> Fut,
        Fut: Future<Output = std::result::Result<T, tonic::Status>>,
    {
        let mut last_status = None;
        for attempt in 0..TRANSIENT_RPC_RETRY_ATTEMPTS {
            match op().await {
                Ok(value) => return Ok(value),
                Err(status)
                    if attempt + 1 < TRANSIENT_RPC_RETRY_ATTEMPTS
                        && is_retryable_status(&status) =>
                {
                    let delay = retry_delay(attempt);
                    tracing::warn!(
                        operation,
                        attempt = attempt + 1,
                        retry_after_ms = delay.as_millis() as u64,
                        code = ?status.code(),
                        message = status.message(),
                        "retrying transient IAM RPC"
                    );
                    last_status = Some(status);
                    tokio::time::sleep(delay).await;
                }
                Err(status) => return Err(map_status(status)),
            }
        }

        Err(map_status(last_status.unwrap_or_else(|| {
            tonic::Status::internal(format!("IAM RPC {operation} failed without a status"))
        })))
    }

    // ========================================================================
    // Authorization APIs
    // ========================================================================

    /// Check if an action is allowed
    pub async fn authorize(
        &self,
        principal: &Principal,
        action: &str,
        resource: &Resource,
    ) -> Result<bool> {
        self.authorize_with_context(
            principal,
            action,
            resource,
            std::collections::HashMap::new(),
        )
        .await
    }

    /// Check authorization with additional context
    pub async fn authorize_with_context(
        &self,
        principal: &Principal,
        action: &str,
        resource: &Resource,
        context: std::collections::HashMap<String, String>,
    ) -> Result<bool> {
        let request = AuthorizeRequest {
            principal: Some(to_proto_principal_ref(&principal.to_ref())),
            action: action.to_string(),
            resource: Some(ProtoResourceRef {
                kind: resource.kind.clone(),
                id: resource.id.clone(),
                org_id: resource.org_id.clone(),
                project_id: resource.project_id.clone(),
                owner_id: resource.owner_id.clone(),
                node_id: resource.node_id.clone(),
                region: resource.region.clone(),
                tags: resource.tags.clone(),
            }),
            context: Some(AuthzContext {
                source_ip: context.get("source_ip").cloned().unwrap_or_default(),
                timestamp: 0,
                http_method: context.get("http_method").cloned().unwrap_or_default(),
                request_path: context.get("request_path").cloned().unwrap_or_default(),
                metadata: context,
            }),
        };

        let resp = Self::call_with_retry("authorize", || {
            let mut client = self.authz_client();
            let request = request.clone();
            async move { client.authorize(request).await }
        })
        .await?
        .into_inner();

        Ok(resp.allowed)
    }

    // ========================================================================
    // Principal Management APIs
    // ========================================================================

    /// Create a new user
    pub async fn create_user(&self, id: &str, name: &str) -> Result<Principal> {
        let req = CreatePrincipalRequest {
            id: id.into(),
            kind: ProtoPrincipalKind::User as i32,
            name: name.into(),
            org_id: None,
            project_id: None,
            email: None,
            metadata: Default::default(),
        };

        let resp = Self::call_with_retry("create_principal", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.create_principal(request).await }
        })
        .await?
        .into_inner();
        Ok(ProtoPrincipal::into(resp))
    }

    /// Get a principal
    pub async fn get_principal(&self, principal_ref: &PrincipalRef) -> Result<Option<Principal>> {
        let req = GetPrincipalRequest {
            principal: Some(to_proto_principal_ref(principal_ref)),
        };
        let resp = Self::call_with_retry("get_principal", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.get_principal(request).await }
        })
        .await;
        match resp {
            Ok(r) => Ok(Some(ProtoPrincipal::into(r.into_inner()))),
            Err(Error::Internal(message)) if tonic_not_found(&message) => Ok(None),
            Err(err) => Err(err),
        }
    }

    /// Create a service account
    pub async fn create_service_account(
        &self,
        id: &str,
        name: &str,
        org_id: &str,
        project_id: &str,
    ) -> Result<Principal> {
        let req = CreatePrincipalRequest {
            id: id.into(),
            kind: ProtoPrincipalKind::ServiceAccount as i32,
            name: name.into(),
            org_id: Some(org_id.into()),
            project_id: Some(project_id.into()),
            email: None,
            metadata: Default::default(),
        };
        let resp = Self::call_with_retry("create_service_account", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.create_principal(request).await }
        })
        .await?
        .into_inner();
        Ok(ProtoPrincipal::into(resp))
    }

    /// List users
    pub async fn list_users(&self) -> Result<Vec<Principal>> {
        let req = ListPrincipalsRequest {
            kind: Some(ProtoPrincipalKind::User as i32),
            org_id: None,
            project_id: None,
            page_size: 0,
            page_token: String::new(),
        };

        let resp = Self::call_with_retry("list_principals", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.list_principals(request).await }
        })
        .await?
        .into_inner();

        Ok(resp
            .principals
            .into_iter()
            .map(ProtoPrincipal::into)
            .collect())
    }

    // ========================================================================
    // Tenant Management APIs
    // ========================================================================

    /// Create an organization.
    pub async fn create_organization(
        &self,
        id: &str,
        name: &str,
        description: &str,
    ) -> Result<Organization> {
        let req = CreateOrganizationRequest {
            id: id.into(),
            name: name.into(),
            description: description.into(),
            metadata: Default::default(),
        };
        let resp = Self::call_with_retry("create_organization", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.create_organization(request).await }
        })
        .await?
        .into_inner();
        Ok(ProtoOrganization::into(resp))
    }

    /// Get an organization by id.
    pub async fn get_organization(&self, id: &str) -> Result<Option<Organization>> {
        let req = GetOrganizationRequest { id: id.into() };
        let resp = Self::call_with_retry("get_organization", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.get_organization(request).await }
        })
        .await;
        match resp {
            Ok(r) => Ok(Some(r.into_inner().into())),
            Err(Error::Internal(message)) if tonic_not_found(&message) => Ok(None),
            Err(err) => Err(err),
        }
    }

    /// List organizations.
    pub async fn list_organizations(&self) -> Result<Vec<Organization>> {
        let req = ListOrganizationsRequest {
            include_disabled: false,
            page_size: 0,
            page_token: String::new(),
        };
        let resp = Self::call_with_retry("list_organizations", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.list_organizations(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.organizations.into_iter().map(Into::into).collect())
    }

    /// Update an organization.
    pub async fn update_organization(
        &self,
        id: &str,
        name: Option<&str>,
        description: Option<&str>,
        enabled: Option<bool>,
    ) -> Result<Organization> {
        let req = UpdateOrganizationRequest {
            id: id.into(),
            name: name.map(str::to_string),
            description: description.map(str::to_string),
            metadata: Default::default(),
            enabled,
        };
        let resp = Self::call_with_retry("update_organization", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.update_organization(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.into())
    }

    /// Delete an organization.
    pub async fn delete_organization(&self, id: &str) -> Result<bool> {
        let req = DeleteOrganizationRequest { id: id.into() };
        let resp = Self::call_with_retry("delete_organization", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.delete_organization(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.deleted)
    }

    /// Create a project.
    pub async fn create_project(
        &self,
        org_id: &str,
        id: &str,
        name: &str,
        description: &str,
    ) -> Result<Project> {
        let req = CreateProjectRequest {
            id: id.into(),
            org_id: org_id.into(),
            name: name.into(),
            description: description.into(),
            metadata: Default::default(),
        };
        let resp = Self::call_with_retry("create_project", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.create_project(request).await }
        })
        .await?
        .into_inner();
        Ok(ProtoProject::into(resp))
    }

    /// Get a project by org + id.
    pub async fn get_project(&self, org_id: &str, id: &str) -> Result<Option<Project>> {
        let req = GetProjectRequest {
            org_id: org_id.into(),
            id: id.into(),
        };
        let resp = Self::call_with_retry("get_project", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.get_project(request).await }
        })
        .await;
        match resp {
            Ok(r) => Ok(Some(r.into_inner().into())),
            Err(Error::Internal(message)) if tonic_not_found(&message) => Ok(None),
            Err(err) => Err(err),
        }
    }

    /// List projects, optionally filtered by organization.
    pub async fn list_projects(&self, org_id: Option<&str>) -> Result<Vec<Project>> {
        let req = ListProjectsRequest {
            org_id: org_id.map(str::to_string),
            include_disabled: false,
            page_size: 0,
            page_token: String::new(),
        };
        let resp = Self::call_with_retry("list_projects", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.list_projects(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.projects.into_iter().map(Into::into).collect())
    }

    /// Update a project.
    pub async fn update_project(
        &self,
        org_id: &str,
        id: &str,
        name: Option<&str>,
        description: Option<&str>,
        enabled: Option<bool>,
    ) -> Result<Project> {
        let req = UpdateProjectRequest {
            org_id: org_id.into(),
            id: id.into(),
            name: name.map(str::to_string),
            description: description.map(str::to_string),
            metadata: Default::default(),
            enabled,
        };
        let resp = Self::call_with_retry("update_project", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.update_project(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.into())
    }

    /// Delete a project.
    pub async fn delete_project(&self, org_id: &str, id: &str) -> Result<bool> {
        let req = DeleteProjectRequest {
            org_id: org_id.into(),
            id: id.into(),
        };
        let resp = Self::call_with_retry("delete_project", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.delete_project(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.deleted)
    }

    // ========================================================================
    // Role Management APIs
    // ========================================================================

    /// Get a role by name
    pub async fn get_role(&self, name: &str) -> Result<Option<Role>> {
        let req = GetRoleRequest { name: name.into() };
        let resp = Self::call_with_retry("get_role", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.get_role(request).await }
        })
        .await;
        match resp {
            Ok(r) => Ok(Some(r.into_inner().into())),
            Err(Error::Internal(message)) if tonic_not_found(&message) => Ok(None),
            Err(err) => Err(err),
        }
    }

    /// List all roles
    pub async fn list_roles(&self) -> Result<Vec<Role>> {
        let req = ListRolesRequest {
            scope: None,
            include_builtin: true,
            page_size: 0,
            page_token: String::new(),
        };
        let resp = Self::call_with_retry("list_roles", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.list_roles(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.roles.into_iter().map(Into::into).collect())
    }

    /// Create a custom role
    pub async fn create_role(&self, role: &Role) -> Result<Role> {
        let req = CreateRoleRequest {
            name: role.name.clone(),
            display_name: role.display_name.clone(),
            description: role.description.clone(),
            scope: Some(ProtoScope::from(role.scope.clone())),
            permissions: role
                .permissions
                .clone()
                .into_iter()
                .map(Into::into)
                .collect(),
        };

        let resp = Self::call_with_retry("create_role", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.create_role(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.into())
    }

    // ========================================================================
    // Binding Management APIs
    // ========================================================================

    /// Create a policy binding
    pub async fn create_binding(&self, binding: &PolicyBinding) -> Result<PolicyBinding> {
        let req = CreateBindingRequest {
            principal: Some(to_proto_principal_ref(&binding.principal_ref)),
            role: binding.role_ref.clone(),
            scope: Some(ProtoScope::from(binding.scope.clone())),
            condition: binding.condition.clone().map(Into::into),
            expires_at: binding.expires_at,
        };

        let resp = Self::call_with_retry("create_binding", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.create_binding(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.into())
    }

    /// Delete a policy binding
    pub async fn delete_binding(&self, binding_id: &str) -> Result<bool> {
        let req = DeleteBindingRequest {
            id: binding_id.into(),
        };
        let resp = Self::call_with_retry("delete_binding", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.delete_binding(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.deleted)
    }

    /// List bindings for a principal
    pub async fn list_bindings_for_principal(
        &self,
        principal: &PrincipalRef,
    ) -> Result<Vec<PolicyBinding>> {
        let req = ListBindingsRequest {
            principal: Some(to_proto_principal_ref(principal)),
            role: None,
            scope: None,
            include_disabled: false,
            page_size: 0,
            page_token: String::new(),
        };

        let resp = Self::call_with_retry("list_bindings_for_principal", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.list_bindings(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.bindings.into_iter().map(Into::into).collect())
    }

    /// List bindings for a scope
    pub async fn list_bindings_for_scope(&self, scope: &Scope) -> Result<Vec<PolicyBinding>> {
        let req = ListBindingsRequest {
            principal: None,
            role: None,
            scope: Some(ProtoScope::from(scope.clone())),
            include_disabled: false,
            page_size: 0,
            page_token: String::new(),
        };

        let resp = Self::call_with_retry("list_bindings_for_scope", || {
            let mut client = self.admin_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.list_bindings(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.bindings.into_iter().map(Into::into).collect())
    }

    // ========================================================================
    // Token APIs
    // ========================================================================

    /// Issue an internal token
    pub async fn issue_token(
        &self,
        principal: &Principal,
        roles: Vec<String>,
        scope: Scope,
        ttl_seconds: u64,
    ) -> Result<String> {
        let req = IssueTokenRequest {
            principal_id: principal.id.clone(),
            principal_kind: match principal.kind {
                TypesPrincipalKind::User => ProtoPrincipalKind::User as i32,
                TypesPrincipalKind::ServiceAccount => ProtoPrincipalKind::ServiceAccount as i32,
                TypesPrincipalKind::Group => ProtoPrincipalKind::Group as i32,
            },
            roles,
            scope: Some(ProtoScope::from(scope)),
            ttl_seconds,
        };

        let resp = Self::call_with_retry("issue_token", || {
            let mut client = self.token_client();
            let req = req.clone();
            let request = self.admin_request(req);
            async move { client.issue_token(request).await }
        })
        .await?
        .into_inner();
        Ok(resp.token)
    }

    /// Validate a token
    pub async fn validate_token(&self, token: &str) -> Result<InternalTokenClaims> {
        let req = ValidateTokenRequest {
            token: token.to_string(),
        };
        let resp = Self::call_with_retry("validate_token", || {
            let mut client = self.token_client();
            let req = req.clone();
            async move { client.validate_token(req).await }
        })
        .await?
        .into_inner();

        if !resp.valid {
            return Err(Error::Iam(IamError::InvalidToken(resp.reason.clone())));
        }

        let claims = resp
            .claims
            .ok_or_else(|| Error::Iam(IamError::InvalidToken("missing claims".into())))?;

        let principal_kind = match ProtoPrincipalKind::try_from(claims.principal_kind) {
            Ok(ProtoPrincipalKind::User) => TypesPrincipalKind::User,
            Ok(ProtoPrincipalKind::ServiceAccount) => TypesPrincipalKind::ServiceAccount,
            Ok(ProtoPrincipalKind::Group) => TypesPrincipalKind::Group,
            _ => TypesPrincipalKind::User,
        };

        let auth_method = match claims.auth_method.as_str() {
            "jwt" => AuthMethod::Jwt,
            "mtls" => AuthMethod::Mtls,
            "api_key" => AuthMethod::ApiKey,
            "internal" => AuthMethod::Internal,
            _ => AuthMethod::Unknown,
        };

        let mut internal_claims = InternalTokenClaims::new(
            claims.principal_id,
            principal_kind,
            claims.principal_name,
            claims.scope.map(ProtoScope::into).unwrap_or(Scope::System),
            claims.session_id,
        )
        .with_roles(claims.roles)
        .with_timestamps(claims.iat, claims.exp)
        .with_auth_method(auth_method);

        if let Some(org_id) = claims.org_id {
            internal_claims = internal_claims.with_org_id(&org_id);
        }
        if let Some(project_id) = claims.project_id {
            internal_claims = internal_claims.with_project_id(&project_id);
        }
        if let Some(node_id) = claims.node_id {
            internal_claims = internal_claims.with_node_id(&node_id);
        }

        Ok(internal_claims)
    }

    /// Revoke a token
    pub async fn revoke_token(&self, token: &str) -> Result<()> {
        let req = RevokeTokenRequest {
            token: token.to_string(),
            reason: "client revoke".into(),
        };
        Self::call_with_retry("revoke_token", || {
            let mut client = self.token_client();
            let req = req.clone();
            async move { client.revoke_token(req).await }
        })
        .await?
        .into_inner();
        Ok(())
    }
}

fn load_admin_token_from_env() -> Option<String> {
    std::env::var("IAM_ADMIN_TOKEN")
        .or_else(|_| std::env::var("PHOTON_IAM_ADMIN_TOKEN"))
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn retry_delay(attempt: usize) -> Duration {
    TRANSIENT_RPC_INITIAL_BACKOFF
        .saturating_mul(1u32 << attempt.min(3))
        .min(TRANSIENT_RPC_MAX_BACKOFF)
}

fn is_retryable_status(status: &tonic::Status) -> bool {
    matches!(
        status.code(),
        tonic::Code::Unavailable
            | tonic::Code::Cancelled
            | tonic::Code::DeadlineExceeded
            | tonic::Code::Unknown
    ) || retryable_message(status.message())
}

fn retryable_message(message: &str) -> bool {
    let lower = message.to_ascii_lowercase();
    [
        "transport error",
        "connection was not ready",
        "h2 protocol error",
        "broken pipe",
        "connection refused",
        "connection reset",
    ]
    .iter()
    .any(|needle| lower.contains(needle))
}

fn tonic_not_found(message: &str) -> bool {
    message.contains("status: NotFound") || message.contains("code: NotFound")
}

fn map_status(status: tonic::Status) -> Error {
    Error::Internal(status.to_string())
}

fn to_proto_principal_ref(principal_ref: &PrincipalRef) -> ProtoPrincipalRef {
    ProtoPrincipalRef {
        kind: match principal_ref.kind {
            TypesPrincipalKind::User => ProtoPrincipalKind::User as i32,
            TypesPrincipalKind::ServiceAccount => ProtoPrincipalKind::ServiceAccount as i32,
            TypesPrincipalKind::Group => ProtoPrincipalKind::Group as i32,
        },
        id: principal_ref.id.clone(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    };

    #[test]
    fn retryable_message_covers_connection_readiness() {
        assert!(retryable_message("transport error"));
        assert!(retryable_message("connection was not ready"));
        assert!(retryable_message("h2 protocol error"));
        assert!(!retryable_message("permission denied"));
    }

    #[test]
    fn retry_delay_is_capped() {
        assert_eq!(retry_delay(0), Duration::from_millis(200));
        assert_eq!(retry_delay(1), Duration::from_millis(400));
        assert_eq!(retry_delay(2), Duration::from_millis(800));
        assert_eq!(retry_delay(3), Duration::from_millis(1000));
        assert_eq!(retry_delay(7), Duration::from_millis(1000));
    }

    #[tokio::test(start_paused = true)]
    async fn call_with_retry_retries_transient_statuses() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let attempts_for_task = attempts.clone();
        let task = tokio::spawn(async move {
            IamClient::call_with_retry("test", || {
                let attempts = attempts_for_task.clone();
                async move {
                    let attempt = attempts.fetch_add(1, Ordering::SeqCst);
                    if attempt < 2 {
                        Err(tonic::Status::unavailable("connection was not ready"))
                    } else {
                        Ok("ok")
                    }
                }
            })
            .await
        });

        tokio::time::advance(Duration::from_secs(3)).await;
        assert_eq!(task.await.unwrap().unwrap(), "ok");
        assert_eq!(attempts.load(Ordering::SeqCst), 3);
    }

    #[tokio::test(start_paused = true)]
    async fn call_with_retry_stops_on_non_retryable_status() {
        let attempts = Arc::new(AtomicUsize::new(0));
        let attempts_for_task = attempts.clone();

        let err = IamClient::call_with_retry("test", || {
            let attempts = attempts_for_task.clone();
            async move {
                attempts.fetch_add(1, Ordering::SeqCst);
                Err::<(), _>(tonic::Status::permission_denied("nope"))
            }
        })
        .await
        .unwrap_err();

        assert_eq!(attempts.load(Ordering::SeqCst), 1);
        match err {
            Error::Internal(message) => assert!(message.contains("PermissionDenied")),
            other => panic!("unexpected error: {other:?}"),
        }
    }
}
