use std::collections::BTreeMap;

use base64::{Engine as _, engine::general_purpose::STANDARD};
use crank_core::{AuthConfig, AuthProfile, SecretId};
use serde_json::Value;

use crate::{PreparedRequest, RuntimeError};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ResolvedAuth {
    Bearer { header_name: String, token: String },
    Basic { username: String, password: String },
    ApiKeyHeader { header_name: String, value: String },
    ApiKeyQuery { param_name: String, value: String },
}

impl ResolvedAuth {
    pub fn from_profile(
        profile: &AuthProfile,
        secrets: &BTreeMap<SecretId, Value>,
    ) -> Result<Self, RuntimeError> {
        match &profile.config {
            AuthConfig::Bearer(config) => Ok(Self::Bearer {
                header_name: config.header_name.clone(),
                token: extract_secret_string(secrets, &config.secret_id, &["token", "value"])?,
            }),
            AuthConfig::Basic(config) => Ok(Self::Basic {
                username: extract_secret_string(
                    secrets,
                    &config.username_secret_id,
                    &["username", "value"],
                )?,
                password: extract_secret_string(
                    secrets,
                    &config.password_secret_id,
                    &["password", "value"],
                )?,
            }),
            AuthConfig::ApiKeyHeader(config) => Ok(Self::ApiKeyHeader {
                header_name: config.header_name.clone(),
                value: extract_secret_string(secrets, &config.secret_id, &["token", "value"])?,
            }),
            AuthConfig::ApiKeyQuery(config) => Ok(Self::ApiKeyQuery {
                param_name: config.param_name.clone(),
                value: extract_secret_string(secrets, &config.secret_id, &["token", "value"])?,
            }),
        }
    }

    pub fn apply(&self, prepared_request: &mut PreparedRequest) {
        match self {
            Self::Bearer { header_name, token } => {
                prepared_request
                    .headers
                    .insert(header_name.clone(), format!("Bearer {token}"));
            }
            Self::Basic { username, password } => {
                let credentials = STANDARD.encode(format!("{username}:{password}"));
                prepared_request
                    .headers
                    .insert("Authorization".to_owned(), format!("Basic {credentials}"));
            }
            Self::ApiKeyHeader { header_name, value } => {
                prepared_request
                    .headers
                    .insert(header_name.clone(), value.clone());
            }
            Self::ApiKeyQuery { param_name, value } => {
                prepared_request
                    .query_params
                    .insert(param_name.clone(), value.clone());
            }
        }
    }
}

fn extract_secret_string(
    secrets: &BTreeMap<SecretId, Value>,
    secret_id: &SecretId,
    preferred_fields: &[&str],
) -> Result<String, RuntimeError> {
    let Some(value) = secrets.get(secret_id) else {
        return Err(RuntimeError::MissingSecret {
            secret_id: secret_id.as_str().to_owned(),
        });
    };

    if let Some(text) = value.as_str() {
        return Ok(text.to_owned());
    }

    let Some(object) = value.as_object() else {
        return Err(RuntimeError::InvalidAuthSecretValue {
            secret_id: secret_id.as_str().to_owned(),
            reason: "secret payload must be a string or object".to_owned(),
        });
    };

    for field in preferred_fields {
        if let Some(text) = object.get(*field).and_then(Value::as_str) {
            return Ok(text.to_owned());
        }
    }

    Err(RuntimeError::InvalidAuthSecretValue {
        secret_id: secret_id.as_str().to_owned(),
        reason: format!(
            "secret payload must contain one of: {}",
            preferred_fields.join(", ")
        ),
    })
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use crank_core::{
        ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthKind, AuthProfile,
        BasicAuthConfig, BearerAuthConfig, SecretId, WorkspaceId,
    };
    use serde_json::json;
    use time::{OffsetDateTime, format_description::well_known::Rfc3339};

    use crate::{PreparedRequest, auth::ResolvedAuth};

    fn timestamp(value: &str) -> OffsetDateTime {
        OffsetDateTime::parse(value, &Rfc3339).unwrap()
    }

    #[test]
    fn applies_bearer_auth_to_headers() {
        let auth = ResolvedAuth::from_profile(
            &AuthProfile {
                id: "auth_01".into(),
                workspace_id: WorkspaceId::new("ws_default"),
                name: "bearer".to_owned(),
                kind: AuthKind::Bearer,
                config: AuthConfig::Bearer(BearerAuthConfig {
                    header_name: "Authorization".to_owned(),
                    secret_id: SecretId::new("secret_token"),
                }),
                created_at: timestamp("2026-04-07T00:00:00Z"),
                updated_at: timestamp("2026-04-07T00:00:00Z"),
            },
            &BTreeMap::from([(SecretId::new("secret_token"), json!({"token":"abc"}))]),
        )
        .unwrap();
        let mut request = PreparedRequest::default();

        auth.apply(&mut request);

        assert_eq!(
            request.headers.get("Authorization").map(String::as_str),
            Some("Bearer abc")
        );
    }

    #[test]
    fn applies_basic_auth_to_headers() {
        let auth = ResolvedAuth::from_profile(
            &AuthProfile {
                id: "auth_01".into(),
                workspace_id: WorkspaceId::new("ws_default"),
                name: "basic".to_owned(),
                kind: AuthKind::Basic,
                config: AuthConfig::Basic(BasicAuthConfig {
                    username_secret_id: SecretId::new("secret_user"),
                    password_secret_id: SecretId::new("secret_pass"),
                }),
                created_at: timestamp("2026-04-07T00:00:00Z"),
                updated_at: timestamp("2026-04-07T00:00:00Z"),
            },
            &BTreeMap::from([
                (SecretId::new("secret_user"), json!({"username":"demo"})),
                (SecretId::new("secret_pass"), json!({"password":"s3cr3t"})),
            ]),
        )
        .unwrap();
        let mut request = PreparedRequest::default();

        auth.apply(&mut request);

        assert_eq!(
            request.headers.get("Authorization").map(String::as_str),
            Some("Basic ZGVtbzpzM2NyM3Q=")
        );
    }

    #[test]
    fn applies_api_key_header_auth() {
        let auth = ResolvedAuth::from_profile(
            &AuthProfile {
                id: "auth_01".into(),
                workspace_id: WorkspaceId::new("ws_default"),
                name: "api-header".to_owned(),
                kind: AuthKind::ApiKeyHeader,
                config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig {
                    header_name: "X-Api-Key".to_owned(),
                    secret_id: SecretId::new("secret_api_key"),
                }),
                created_at: timestamp("2026-04-07T00:00:00Z"),
                updated_at: timestamp("2026-04-07T00:00:00Z"),
            },
            &BTreeMap::from([(SecretId::new("secret_api_key"), json!("key-123"))]),
        )
        .unwrap();
        let mut request = PreparedRequest::default();

        auth.apply(&mut request);

        assert_eq!(
            request.headers.get("X-Api-Key").map(String::as_str),
            Some("key-123")
        );
    }

    #[test]
    fn applies_api_key_query_auth() {
        let auth = ResolvedAuth::from_profile(
            &AuthProfile {
                id: "auth_01".into(),
                workspace_id: WorkspaceId::new("ws_default"),
                name: "api-query".to_owned(),
                kind: AuthKind::ApiKeyQuery,
                config: AuthConfig::ApiKeyQuery(ApiKeyQueryAuthConfig {
                    param_name: "api_key".to_owned(),
                    secret_id: SecretId::new("secret_api_key"),
                }),
                created_at: timestamp("2026-04-07T00:00:00Z"),
                updated_at: timestamp("2026-04-07T00:00:00Z"),
            },
            &BTreeMap::from([(SecretId::new("secret_api_key"), json!({"value":"key-123"}))]),
        )
        .unwrap();
        let mut request = PreparedRequest::default();

        auth.apply(&mut request);

        assert_eq!(
            request.query_params.get("api_key").map(String::as_str),
            Some("key-123")
        );
    }
}
