use std::fmt::{self, Formatter};
use std::time::{SystemTime, UNIX_EPOCH};

use aws_config::BehaviorVersion;
use aws_smithy_types::date_time::{DateTime, Format};
use chacha20poly1305::XNonce;
use serde::{
    Serialize,
    Deserialize,
    Serializer,
    Deserializer,
};
use serde::de::{self, Visitor};
use sqlx::{
    FromRow,
    Sqlite,
    Transaction,
    types::Uuid,
};

use super::{Credential, Crypto, PersistentCredential};

use crate::errors::*;


#[derive(Debug, Clone, FromRow)]
pub struct AwsRow {
    id: Uuid,
    access_key_id: String,
    secret_key_enc: Vec<u8>,
    nonce: Vec<u8>,
}


#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AwsBaseCredential {
    #[serde(default = "default_credentials_version")]
    pub version: usize,
    pub access_key_id: String,
    pub secret_access_key: String,
}


impl AwsBaseCredential {
    pub fn new(access_key_id: String, secret_access_key: String) -> Self {
        Self {version: 1, access_key_id, secret_access_key}
    }
}

impl PersistentCredential for AwsBaseCredential {
    type Row = AwsRow;

    fn type_name() -> &'static str { "aws" }

    fn into_credential(self) -> Credential { Credential::AwsBase(self) }

    fn row_id(row: &AwsRow) -> Uuid { row.id }

    fn from_row(row: AwsRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
        let nonce = XNonce::clone_from_slice(&row.nonce);
        let secret_key_bytes = crypto.decrypt(&nonce, &row.secret_key_enc)?;
        let secret_key = String::from_utf8(secret_key_bytes)
            .map_err(|_| LoadCredentialsError::InvalidData)?;

        Ok(Self::new(row.access_key_id, secret_key))
    }

    async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError> {
        let (nonce, ciphertext) = crypto.encrypt(self.secret_access_key.as_bytes())?;
        let nonce_bytes = &nonce.as_slice();

        sqlx::query!(
            "INSERT OR REPLACE INTO aws_credentials (
                id,
                access_key_id,
                secret_key_enc,
                nonce
            )
            VALUES (?, ?, ?, ?);",
            id, self.access_key_id, ciphertext, nonce_bytes,
        ).execute(&mut **txn).await?;

        Ok(())
    }
}


#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AwsSessionCredential {
    #[serde(default = "default_credentials_version")]
    pub version: usize,
    pub access_key_id: String,
    pub secret_access_key: String,
    pub session_token: String,
    #[serde(serialize_with = "serialize_expiration")]
    #[serde(deserialize_with = "deserialize_expiration")]
    pub expiration: DateTime,
}

impl AwsSessionCredential {
    pub async fn from_base(base: &AwsBaseCredential) -> Result<Self, GetSessionError> {
        let req_creds = aws_sdk_sts::config::Credentials::new(
            &base.access_key_id,
            &base.secret_access_key,
            None, // token
            None, //expiration
            "Creddy", // "provider name" apparently
        );
        let config = aws_config::defaults(BehaviorVersion::latest())
            .credentials_provider(req_creds)
            .load()
            .await;

        let client = aws_sdk_sts::Client::new(&config);
        let resp = client.get_session_token()
            .duration_seconds(43_200)
            .send()
            .await?;

        let aws_session = resp.credentials.ok_or(GetSessionError::EmptyResponse)?;

        let session_creds = AwsSessionCredential {
            version: 1,
            access_key_id: aws_session.access_key_id,
            secret_access_key: aws_session.secret_access_key,
            session_token: aws_session.session_token,
            expiration: aws_session.expiration,
        };

        #[cfg(debug_assertions)]
        println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());

        Ok(session_creds)
    }

    pub fn is_expired(&self) -> bool {
        let current_ts = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap() // doesn't panic because UNIX_EPOCH won't be later than now()
            .as_secs();

        let expire_ts = self.expiration.secs();
        let remaining = expire_ts - (current_ts as i64);
        remaining < 60
    }
}


fn default_credentials_version() -> usize { 1 }


struct DateTimeVisitor;

impl<'de> Visitor<'de> for DateTimeVisitor {
    type Value = DateTime;

    fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
        write!(formatter, "an RFC 3339 UTC string, e.g. \"2014-01-05T10:17:34Z\"")
    }

    fn visit_str<E: de::Error>(self, v: &str) -> Result<DateTime, E> {
        DateTime::from_str(v, Format::DateTime)
            .map_err(|_| E::custom(format!("Invalid date/time: {v}")))
    }
}


fn deserialize_expiration<'de, D>(deserializer: D) -> Result<DateTime, D::Error>
where D: Deserializer<'de>
{
    deserializer.deserialize_str(DateTimeVisitor)
}

fn serialize_expiration<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer
{
    // this only fails if the d/t is out of range, which it can't be for this format
    let time_str = exp.fmt(Format::DateTime).unwrap();
    serializer.serialize_str(&time_str)
}


#[cfg(test)]
mod tests {
    use super::*;
    use aws_sdk_sts::primitives::DateTimeFormat;
    use creddy_cli::proto::{
        AwsBaseCredential as CliBase,
        AwsSessionCredential as CliSession,
    };
    use sqlx::SqlitePool;
    use sqlx::types::uuid::uuid;



    fn creds() -> AwsBaseCredential {
        AwsBaseCredential::new(
            "AKIAIOSFODNN7EXAMPLE".into(),
            "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
        )
    }

    fn creds_2() -> AwsBaseCredential {
        AwsBaseCredential::new(
            "AKIAIOSFODNN7EXAMPL2".into(),
            "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKE2".into(),
        )
    }


    #[sqlx::test(fixtures("aws_credentials"))]
    async fn test_load(pool: SqlitePool) {
        let crypt = Crypto::fixed();
        let id = uuid!("00000000-0000-0000-0000-000000000000");
        let loaded = AwsBaseCredential::load(&id, &crypt, &pool).await.unwrap();
        assert_eq!(creds(), loaded);
    }


    #[sqlx::test(fixtures("aws_credentials"))]
    async fn test_load_by_name(pool: SqlitePool) {
        let crypt = Crypto::fixed();
        let loaded = AwsBaseCredential::load_by_name("test2", &crypt, &pool).await.unwrap();
        assert_eq!(creds_2(), loaded);
    }


    #[sqlx::test(fixtures("aws_credentials"))]
    async fn test_load_default(pool: SqlitePool) {
        let crypt = Crypto::fixed();
        let loaded = AwsBaseCredential::load_default(&crypt, &pool).await.unwrap();
        assert_eq!(creds(), loaded)
    }


    #[sqlx::test(fixtures("aws_credentials"))]
    async fn test_list(pool: SqlitePool) {
        let crypt = Crypto::fixed();
        let list: Vec<_> = AwsBaseCredential::list(&crypt, &pool)
            .await
            .expect("Failed to load credentials")
            .into_iter()
            .map(|(_, cred)| cred)
            .collect();

        assert_eq!(&creds().into_credential(), &list[0]);
        assert_eq!(&creds_2().into_credential(), &list[1]);
    }


    // In order to avoid the CLI depending on the main app (and thus defeating the purpose
    // of having a separate CLI at all) it re-defines the credentials that need to be sent
    // back and forth. To prevent the separate definitions from drifting aprt, we test
    // serializing/deserializing in both directions.

    #[test]
    fn test_cli_to_app_base() {
        let cli_base = CliBase {
            version: 1,
            access_key_id: "AKIAIOSFODNN7EXAMPLE".into(),
            secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
        };

        let json = serde_json::to_string(&cli_base).unwrap();
        let computed: AwsBaseCredential = serde_json::from_str(&json)
            .expect("Failed to deserialize base credentials from CLI -> main app");

        assert_eq!(creds(), computed);
    }

    #[test]
    fn test_app_to_cli_base() {
        let base = creds();
        let json = serde_json::to_string(&base).unwrap();

        let computed: CliBase = serde_json::from_str(&json)
            .expect("Failed to deserialize base credentials from main app -> CLI");

        let expected = CliBase {
            version: 1,
            access_key_id: "AKIAIOSFODNN7EXAMPLE".into(),
            secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
        };

        assert_eq!(expected, computed);
    }

    #[test]
    fn test_cli_to_app_session() {
        let cli_session = CliSession {
            version: 1,
            access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
            secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
            session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
            expiration: "2024-07-21T00:00:00Z".into(),
        };

        let json = serde_json::to_string(&cli_session).unwrap();
        let computed: AwsSessionCredential = serde_json::from_str(&json)
            .expect("Failed to deserialize session credentials from CLI -> main app");

        let expected = AwsSessionCredential {
            version: 1,
            access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
            secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
            session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
            expiration: DateTime::from_str(
                "2024-07-21T00:00:00Z",
                DateTimeFormat::DateTimeWithOffset
            ).unwrap(),
        };

        assert_eq!(expected, computed);
    }

    #[test]
    fn test_app_to_cli_session() {
        let session = AwsSessionCredential {
            version: 1,
            access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
            secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
            session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
            expiration: DateTime::from_str(
                "2024-07-21T00:00:00Z",
                DateTimeFormat::DateTimeWithOffset
            ).unwrap(),
        };

        let json = serde_json::to_string(&session).unwrap();
        let computed: CliSession = serde_json::from_str(&json)
            .expect("Failed to deserialize session credentials from main app -> CLI");

        let expected = CliSession {
            version: 1,
            access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
            secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
            session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
            expiration: "2024-07-21T00:00:00Z".into(),
        };

        assert_eq!(expected, computed);
    }
}
