// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::{EC2Credential, KeystoneError, KeystoneToken, KeystoneVersion, Result};
use reqwest::{Client, StatusCode};
use serde_json::json;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};

/// Keystone client for API interactions
#[derive(Clone)]
pub struct KeystoneClient {
    client: Client,
    auth_url: String,
    version: KeystoneVersion,
    admin_token: Arc<RwLock<Option<AdminToken>>>,
    admin_user: Option<String>,
    admin_password: Option<String>,
    admin_project: Option<String>,
    admin_domain: String,
    #[allow(
        dead_code,
        reason = "TLS verification flag parsed from config; the reqwest client is built before it is consulted, so nothing reads it back (backlog#1823)"
    )]
    verify_ssl: bool,
    /// Request timeout applied to the underlying HTTP client.
    timeout: std::time::Duration,
}

#[derive(Clone)]
struct AdminToken {
    token: String,
    expires_at: OffsetDateTime,
}

impl AdminToken {
    fn is_expired(&self) -> bool {
        OffsetDateTime::now_utc() >= self.expires_at
    }
}

impl KeystoneClient {
    /// Create new Keystone client
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        auth_url: String,
        version: KeystoneVersion,
        admin_user: Option<String>,
        admin_password: Option<String>,
        admin_project: Option<String>,
        admin_domain: String,
        verify_ssl: bool,
        timeout: std::time::Duration,
    ) -> Self {
        if !verify_ssl {
            warn!(
                "Keystone client for '{}' is configured to skip TLS certificate verification. This permits MITM attacks and should not be used in production.",
                auth_url
            );
        }

        let client = Client::builder()
            .danger_accept_invalid_certs(!verify_ssl)
            .timeout(timeout)
            .build()
            .unwrap();

        Self {
            client,
            auth_url,
            version,
            admin_token: Arc::new(RwLock::new(None)),
            admin_user,
            admin_password,
            admin_project,
            admin_domain,
            verify_ssl,
            timeout,
        }
    }

    /// Request timeout applied to the underlying HTTP client.
    pub fn timeout(&self) -> std::time::Duration {
        self.timeout
    }

    /// Validate a Keystone token
    #[hotpath::measure]
    pub async fn validate_token(&self, token: &str) -> Result<KeystoneToken> {
        match self.version {
            KeystoneVersion::V3 => self.validate_token_v3(token).await,
            KeystoneVersion::V2_0 => self.validate_token_v2(token).await,
        }
    }

    /// Validate token using Keystone v3 API
    async fn validate_token_v3(&self, token: &str) -> Result<KeystoneToken> {
        let url = format!("{}/v3/auth/tokens", self.auth_url);

        debug!("Validating token with Keystone v3: {}", url);

        let response = self
            .client
            .get(&url)
            .header("X-Auth-Token", token)
            .header("X-Subject-Token", token)
            .send()
            .await
            .map_err(|e| {
                error!("Failed to send token validation request: {}", e);
                KeystoneError::HttpError(e.to_string())
            })?;

        let status = response.status();
        debug!("Token validation response status: {}", status);

        if status == StatusCode::NOT_FOUND || status == StatusCode::UNAUTHORIZED {
            return Err(KeystoneError::InvalidToken);
        }

        if !status.is_success() {
            return Err(KeystoneError::AuthenticationFailed(format!(
                "Token validation failed with status: {}",
                status
            )));
        }

        let body: serde_json::Value = response.json().await.map_err(|e| KeystoneError::ParseError(e.to_string()))?;

        self.parse_token_v3(&body)
    }

    fn parse_token_v3(&self, body: &serde_json::Value) -> Result<KeystoneToken> {
        let token_data = body
            .get("token")
            .ok_or_else(|| KeystoneError::ParseError("Missing token field".to_string()))?;

        let user = token_data
            .get("user")
            .ok_or_else(|| KeystoneError::ParseError("Missing user field".to_string()))?;

        let user_id = user
            .get("id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| KeystoneError::ParseError("Missing user id".to_string()))?
            .to_string();

        let username = user.get("name").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();

        let project = token_data.get("project");
        let (project_id, project_name) = if let Some(proj) = project {
            (
                proj.get("id").and_then(|v| v.as_str()).map(String::from),
                proj.get("name").and_then(|v| v.as_str()).map(String::from),
            )
        } else {
            (None, None)
        };

        let domain = user.get("domain");
        let (domain_id, domain_name) = if let Some(dom) = domain {
            (
                dom.get("id").and_then(|v| v.as_str()).map(String::from),
                dom.get("name").and_then(|v| v.as_str()).map(String::from),
            )
        } else {
            (None, None)
        };

        let roles = token_data
            .get("roles")
            .and_then(|v| v.as_array())
            .map(|roles| {
                roles
                    .iter()
                    .filter_map(|r| r.get("name").and_then(|n| n.as_str()).map(String::from))
                    .collect()
            })
            .unwrap_or_default();

        let expires_at = token_data
            .get("expires_at")
            .and_then(|v| v.as_str())
            .and_then(|s| OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339).ok())
            .ok_or_else(|| KeystoneError::ParseError("Invalid expires_at".to_string()))?;

        let issued_at = token_data
            .get("issued_at")
            .and_then(|v| v.as_str())
            .and_then(|s| OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339).ok())
            .unwrap_or_else(OffsetDateTime::now_utc);

        Ok(KeystoneToken {
            token: String::new(),
            user_id,
            username,
            project_id,
            project_name,
            domain_id,
            domain_name,
            roles,
            expires_at,
            issued_at,
        })
    }

    async fn validate_token_v2(&self, _token: &str) -> Result<KeystoneToken> {
        warn!("Keystone v2.0 support is deprecated");
        Err(KeystoneError::UnsupportedVersion)
    }

    /// Validate EC2 credentials
    ///
    /// This path is intentionally not implemented and fails closed. A correct
    /// implementation must derive the identity (user, project, roles, expiry)
    /// from the Keystone `/v3/ec2tokens` response, not from client-supplied
    /// input. The previous implementation discarded the response body and
    /// fabricated an identity by splitting the client access key on `:`, which
    /// let a caller shape their own user_id/project_id. Until the response
    /// schema is modeled against a live server, refuse to produce credentials.
    pub async fn validate_ec2_credentials(
        &self,
        _access_key: &str,
        _signature: &str,
        _string_to_sign: &str,
    ) -> Result<EC2Credential> {
        warn!("EC2/SigV4 credential authentication is not supported; refusing to fabricate identity from client input");
        Err(KeystoneError::Ec2AuthUnsupported)
    }

    /// Get EC2 credentials for a user
    #[hotpath::measure]
    pub async fn get_ec2_credentials(&self, user_id: &str, project_id: Option<&str>) -> Result<Vec<EC2Credential>> {
        let admin_token = self.get_admin_token().await?;

        let url = if let Some(proj_id) = project_id {
            format!("{}/v3/users/{}/credentials/OS-EC2?project_id={}", self.auth_url, user_id, proj_id)
        } else {
            format!("{}/v3/users/{}/credentials/OS-EC2", self.auth_url, user_id)
        };

        debug!("Fetching EC2 credentials for user: {}", user_id);

        let response = self
            .client
            .get(&url)
            .header("X-Auth-Token", admin_token)
            .send()
            .await
            .map_err(|e| KeystoneError::HttpError(e.to_string()))?;

        if !response.status().is_success() {
            return Ok(vec![]);
        }

        let body: serde_json::Value = response.json().await.map_err(|e| KeystoneError::ParseError(e.to_string()))?;

        let credentials = body
            .get("credentials")
            .and_then(|v| v.as_array())
            .map(|arr| arr.iter().filter_map(|cred| self.parse_ec2_credential(cred).ok()).collect())
            .unwrap_or_default();

        Ok(credentials)
    }

    fn parse_ec2_credential(&self, cred: &serde_json::Value) -> Result<EC2Credential> {
        let access = cred
            .get("access")
            .and_then(|v| v.as_str())
            .ok_or_else(|| KeystoneError::ParseError("Missing access key".to_string()))?
            .to_string();

        let secret = cred
            .get("secret")
            .and_then(|v| v.as_str())
            .ok_or_else(|| KeystoneError::ParseError("Missing secret key".to_string()))?
            .to_string();

        let user_id = cred
            .get("user_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| KeystoneError::ParseError("Missing user_id".to_string()))?
            .to_string();

        let project_id = cred.get("project_id").and_then(|v| v.as_str()).map(String::from);

        let trust_id = cred.get("trust_id").and_then(|v| v.as_str()).map(String::from);

        Ok(EC2Credential {
            access,
            secret,
            user_id,
            project_id,
            trust_id,
        })
    }

    /// Get admin token for privileged operations
    async fn get_admin_token(&self) -> Result<String> {
        // Check if we have a valid cached token
        {
            let guard = self.admin_token.read().await;
            if let Some(token) = guard.as_ref()
                && !token.is_expired()
            {
                return Ok(token.token.clone());
            }
        }

        // Need to authenticate as admin
        let admin_user = self
            .admin_user
            .as_ref()
            .ok_or_else(|| KeystoneError::ConfigError("Missing admin user".to_string()))?;
        let admin_password = self
            .admin_password
            .as_ref()
            .ok_or_else(|| KeystoneError::ConfigError("Missing admin password".to_string()))?;

        let url = format!("{}/v3/auth/tokens", self.auth_url);

        debug!("Authenticating as admin user: {}", admin_user);

        let mut auth_payload = json!({
            "auth": {
                "identity": {
                    "methods": ["password"],
                    "password": {
                        "user": {
                            "name": admin_user,
                            "password": admin_password,
                            "domain": {"name": self.admin_domain}
                        }
                    }
                }
            }
        });

        if let Some(proj) = &self.admin_project {
            auth_payload["auth"]["scope"] = json!({
                "project": {
                    "name": proj,
                    "domain": {"name": self.admin_domain}
                }
            });
        }

        let response = self
            .client
            .post(&url)
            .json(&auth_payload)
            .send()
            .await
            .map_err(|e| KeystoneError::HttpError(e.to_string()))?;

        if !response.status().is_success() {
            return Err(KeystoneError::AuthenticationFailed("Admin authentication failed".to_string()));
        }

        let token = response
            .headers()
            .get("X-Subject-Token")
            .and_then(|v| v.to_str().ok())
            .ok_or_else(|| KeystoneError::ParseError("Missing X-Subject-Token header".to_string()))?
            .to_string();

        // Parse expiration from response body
        let body: serde_json::Value = response.json().await.map_err(|e| KeystoneError::ParseError(e.to_string()))?;

        let expires_at = body
            .get("token")
            .and_then(|t| t.get("expires_at"))
            .and_then(|v| v.as_str())
            .and_then(|s| OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339).ok())
            .unwrap_or_else(|| OffsetDateTime::now_utc() + time::Duration::hours(1));

        // Cache the token
        let mut guard = self.admin_token.write().await;
        *guard = Some(AdminToken {
            token: token.clone(),
            expires_at,
        });

        info!("Admin token obtained successfully");
        Ok(token)
    }

    /// Clear cached admin token
    pub async fn clear_admin_token(&self) {
        let mut guard = self.admin_token.write().await;
        *guard = None;
    }
}

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

    #[test]
    fn test_client_creation() {
        let client = KeystoneClient::new(
            "http://keystone:5000".to_string(),
            KeystoneVersion::V3,
            Some("admin".to_string()),
            Some("secret".to_string()),
            Some("admin".to_string()),
            "Default".to_string(),
            true,
            std::time::Duration::from_secs(30),
        );

        assert_eq!(client.auth_url, "http://keystone:5000");
        assert_eq!(client.version, KeystoneVersion::V3);
    }
}
