//! AWS Secrets Manager resolver for `awssm://` references (WOR-1451).
//!
//! Grammar: `awssm://<secret-id>[?key=<field>][&version=<stage-or-id>]`, e.g.
//! `awssm://prod/openai?key=api_key&version=AWSCURRENT`. `<secret-id>` is the
//! secret name or ARN, `key` selects a field from a JSON secret string, and
//! `version` is a version stage (`AWSCURRENT`, ...) or a version id.
//!
//! It calls the `GetSecretValue` JSON API directly over the blocking client and
//! signs the request with the crate's existing SigV4 signer
//! ([`mcptest_core::auth::sigv4_sign_request`], the official `aws-sigv4`), so no
//! AWS SDK is pulled in. Credentials come from `AWS_ACCESS_KEY_ID` /
//! `AWS_SECRET_ACCESS_KEY` (+ optional `AWS_SESSION_TOKEN`); the region from
//! `AWS_REGION` or `AWS_DEFAULT_REGION`. STS web-identity (GitHub OIDC) on CI
//! exports those same env vars, so that path works unchanged.

use serde_json::json;

use mcptest_config::secrets::{ResolverError, SecretRef, SecretResolver};
use mcptest_core::auth::{sigv4_sign_request, SigV4Config, SigV4Credentials};

use super::{env_optional, http_client, json_field, split_query};

/// Resolver for the `awssm://` scheme. Stateless: region, endpoint, and
/// credentials are read from the environment per resolution.
pub(super) struct AwsSecretsManagerResolver;

impl AwsSecretsManagerResolver {
    pub(super) fn new() -> Self {
        Self
    }
}

impl SecretResolver for AwsSecretsManagerResolver {
    fn scheme(&self) -> &str {
        "awssm"
    }

    fn resolve(&self, reference: &SecretRef) -> Result<String, ResolverError> {
        let region = aws_region()?;
        let endpoint = env_optional("AWS_ENDPOINT_URL_SECRETS_MANAGER")
            .unwrap_or_else(|| format!("https://secretsmanager.{region}.amazonaws.com"));
        let url = format!("{}/", endpoint.trim_end_matches('/'));

        let parsed = split_query(&reference.raw, "awssm");
        let body = request_body(&parsed.body, parsed.version.as_deref());

        let config = SigV4Config {
            region,
            service: "secretsmanager".to_string(),
            credentials: SigV4Credentials::FromEnvironment,
            signature_lifetime: None,
        };
        let signed = sigv4_sign_request("POST", &url, body.as_bytes(), &config)
            .map_err(|e| ResolverError(format!("AWS SigV4 signing failed: {e}")))?;

        let client = http_client()?;
        let mut request = client
            .post(&url)
            .header("Content-Type", "application/x-amz-json-1.1")
            .header("X-Amz-Target", "secretsmanager.GetSecretValue")
            .body(body);
        for header in &signed {
            request = request.header(header.name.as_str(), header.value.as_str());
        }
        let response = request
            .send()
            .map_err(|e| ResolverError(format!("AWS Secrets Manager request failed: {e}")))?;
        let status = response.status();
        let text = response
            .text()
            .map_err(|e| ResolverError(format!("AWS Secrets Manager response read failed: {e}")))?;
        if !status.is_success() {
            return Err(ResolverError(format!(
                "AWS Secrets Manager returned HTTP {} for `{}`",
                status.as_u16(),
                reference.raw
            )));
        }
        extract_secret(&text, parsed.key.as_deref())
    }
}

/// Resolve the signing region, failing closed when neither var is set.
fn aws_region() -> Result<String, ResolverError> {
    env_optional("AWS_REGION")
        .or_else(|| env_optional("AWS_DEFAULT_REGION"))
        .ok_or_else(|| {
            ResolverError("no AWS region: set AWS_REGION or AWS_DEFAULT_REGION".to_string())
        })
}

/// Build the `GetSecretValue` JSON body, mapping a requested version onto the
/// stage or id field. A label beginning with `AWS` is a version stage (the
/// rotation aliases); anything else is treated as a version id.
fn request_body(secret_id: &str, version: Option<&str>) -> String {
    let mut map = serde_json::Map::new();
    map.insert("SecretId".to_string(), json!(secret_id));
    if let Some(version) = version {
        if version.to_ascii_uppercase().starts_with("AWS") {
            map.insert("VersionStage".to_string(), json!(version));
        } else {
            map.insert("VersionId".to_string(), json!(version));
        }
    }
    serde_json::Value::Object(map).to_string()
}

/// Pull the plaintext from a `GetSecretValue` response, optionally selecting a
/// JSON field. A binary-only secret (`SecretBinary`, no `SecretString`) is
/// rejected rather than guessed at.
fn extract_secret(text: &str, key: Option<&str>) -> Result<String, ResolverError> {
    let json: serde_json::Value = serde_json::from_str(text)
        .map_err(|_| ResolverError("AWS Secrets Manager response is not JSON".to_string()))?;
    let secret_string = json
        .get("SecretString")
        .and_then(serde_json::Value::as_str)
        .ok_or_else(|| {
            ResolverError(
                "AWS Secrets Manager returned no SecretString (binary secrets are unsupported)"
                    .to_string(),
            )
        })?;
    match key {
        Some(key) => json_field(secret_string, key),
        None => Ok(secret_string.to_string()),
    }
}

#[cfg(test)]
mod tests {
    use super::super::testutil::{rt, EnvScope};
    use super::*;
    use mcptest_config::secrets::detect_reference;
    use wiremock::matchers::{header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    const AWS_VARS: &[&str] = &[
        "AWS_REGION",
        "AWS_DEFAULT_REGION",
        "AWS_ACCESS_KEY_ID",
        "AWS_SECRET_ACCESS_KEY",
        "AWS_SESSION_TOKEN",
        "AWS_ENDPOINT_URL_SECRETS_MANAGER",
    ];

    fn reference(raw: &str) -> SecretRef {
        detect_reference(raw).expect("an awssm:// value is a reference")
    }

    fn with_creds(scope: &EnvScope, uri: &str) {
        scope.set("AWS_REGION", "us-east-1");
        scope.set("AWS_ACCESS_KEY_ID", "AKIAEXAMPLE");
        scope.set("AWS_SECRET_ACCESS_KEY", "secretkeyexample");
        scope.set("AWS_ENDPOINT_URL_SECRETS_MANAGER", uri);
    }

    #[test]
    fn get_secret_value_returns_the_secret_string() {
        let scope = EnvScope::new(AWS_VARS);
        let runtime = rt();
        let (uri, _server) = runtime.block_on(async {
            let server = MockServer::start().await;
            Mock::given(method("POST"))
                .and(path("/"))
                .and(header("x-amz-target", "secretsmanager.GetSecretValue"))
                .respond_with(
                    ResponseTemplate::new(200)
                        .set_body_json(serde_json::json!({ "SecretString": "sk-aws-secret" })),
                )
                .mount(&server)
                .await;
            let uri = server.uri();
            (uri, server)
        });
        with_creds(&scope, &uri);

        let value = AwsSecretsManagerResolver::new()
            .resolve(&reference("awssm://prod/openai"))
            .expect("signed GetSecretValue resolves");
        assert_eq!(value, "sk-aws-secret");
    }

    #[test]
    fn key_extracts_a_field_from_a_json_secret_string() {
        let scope = EnvScope::new(AWS_VARS);
        let runtime = rt();
        let (uri, _server) = runtime.block_on(async {
            let server = MockServer::start().await;
            Mock::given(method("POST"))
                .and(path("/"))
                .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                    "SecretString": "{\"api_key\":\"sk-json\",\"user\":\"u\"}"
                })))
                .mount(&server)
                .await;
            let uri = server.uri();
            (uri, server)
        });
        with_creds(&scope, &uri);

        let value = AwsSecretsManagerResolver::new()
            .resolve(&reference("awssm://prod/db?key=api_key"))
            .expect("json key extraction resolves");
        assert_eq!(value, "sk-json");
    }

    #[test]
    fn fails_closed_without_a_region() {
        let _scope = EnvScope::new(AWS_VARS);
        let err = AwsSecretsManagerResolver::new()
            .resolve(&reference("awssm://prod/x"))
            .expect_err("no region must fail closed");
        assert!(err.0.contains("no AWS region"), "got: {}", err.0);
    }

    #[test]
    fn version_maps_stage_versus_id() {
        assert!(request_body("s", Some("AWSCURRENT")).contains("\"VersionStage\":\"AWSCURRENT\""));
        assert!(request_body("s", Some("uuid-1234")).contains("\"VersionId\":\"uuid-1234\""));
        assert!(!request_body("s", None).contains("Version"));
    }
}
