use serde::{Serialize, Deserialize};
use sqlx::{
    FromRow,
    Sqlite,
    SqlitePool,
    sqlite::SqliteRow,
    Transaction,
    types::Uuid,
};
use tokio_stream::StreamExt;

use crate::errors::*;

mod aws;
pub use aws::{AwsBaseCredential, AwsSessionCredential};

mod crypto;
pub use crypto::Crypto;

mod docker;
pub use docker::DockerCredential;

mod record;
pub use record::CredentialRecord;

mod session;
pub use session::AppSession;

mod ssh;
pub use ssh::SshKey;


#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Credential {
    AwsBase(AwsBaseCredential),
    AwsSession(AwsSessionCredential),
    Docker(DockerCredential),
    Ssh(SshKey),
}


pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
    type Row: Send + Unpin + for<'r> FromRow<'r, SqliteRow>;

    fn type_name() -> &'static str;

    fn into_credential(self) -> Credential;

    fn row_id(row: &Self::Row) -> Uuid;

    fn from_row(row: Self::Row, crypto: &Crypto) -> Result<Self, LoadCredentialsError>;

    // save_details needs to be implemented per-type because we don't know the number of parameters in advance
    async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError>;

    fn table_name() -> String {
        format!("{}_credentials", Self::type_name())
    }

    async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
        let q = format!("SELECT * FROM {} WHERE id = ?", Self::table_name());
        let row: Self::Row = sqlx::query_as(&q)
            .bind(id)
            .fetch_optional(pool)
            .await?
            .ok_or(LoadCredentialsError::NoCredentials)?;

        Self::from_row(row, crypto)
    }

    async fn load_by_name(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
        let q = format!(
            "SELECT * FROM {} WHERE id = (SELECT id FROM credentials WHERE name = ?)",
            Self::table_name(),
        );
        let row: Self::Row = sqlx::query_as(&q)
            .bind(name)
            .fetch_optional(pool)
            .await?
            .ok_or(LoadCredentialsError::NoCredentials)?;

        Self::from_row(row, crypto)
    }

    async fn load_by<T>(column: &str, value: T, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError>
    where T: Send + for<'q> sqlx::Encode<'q, Sqlite> + sqlx::Type<Sqlite>
    {
        let query = format!(
            "SELECT * FROM {} where {} = ?",
            Self::table_name(),
            column,
        );
        let row: Self::Row = sqlx::query_as(&query)
            .bind(value)
            .fetch_optional(pool)
            .await?
            .ok_or(LoadCredentialsError::NoCredentials)?;

        Self::from_row(row, crypto)
    }

    async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
        let q = format!(
            "SELECT details.*
            FROM {} details
                JOIN credentials c
                    ON c.id = details.id
                    AND c.is_default = 1",
            Self::table_name(),
        );
        let row: Self::Row = sqlx::query_as(&q)
            .fetch_optional(pool)
            .await?
            .ok_or(LoadCredentialsError::NoCredentials)?;

        Self::from_row(row, crypto)
    }

    async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<(Uuid, Credential)>, LoadCredentialsError> {
        let q = format!(
            "SELECT details.*
            FROM
                {} details
                JOIN credentials c
                    ON c.id = details.id
             ORDER BY c.created_at",
             Self::table_name(),
        );
        let mut rows = sqlx::query_as::<_, Self::Row>(&q).fetch(pool);

        let mut creds = Vec::new();
        while let Some(row) = rows.try_next().await? {
            let id = Self::row_id(&row);
            let cred = Self::from_row(row, crypto)?.into_credential();
            creds.push((id, cred));
        }

        Ok(creds)
    }
}
