use redact::Secret;
use sqlx::query_as;
use url::Url;
use uuid::Uuid;

use crate::{client::AppDbClient, macros::with_id_query};

/// Represents a Postgres cluster to be used as the backing database for zero
/// or more workspaces. At this time, rows in the `clusters` table are created
/// manually by an administrator rather than through the web application.
#[derive(Clone, Debug)]
pub struct Cluster {
    /// Primary key (defaults to UUIDv7).
    pub id: Uuid,

    /// Host address, including port specifier if not ":5432".
    pub host: String,

    /// Username of the root Phonograph role for this cluster. Defaults to
    /// "phono".
    pub username: String,

    // TODO: encrypt passwords
    /// Password of the root Phonograph role for this cluster.
    pub password: Secret<String>,
}

with_id_query!(Cluster, sql = "select * from clusters where id = $1");

impl Cluster {
    /// Construct an authenticated postgresql:// connection URL for a specific
    /// database in this cluster. URL is wrapped in a [`redact::Secret`] because
    /// it contains a plaintext password.
    pub fn conn_str_for_db(
        &self,
        db_name: &str,
        auth_override: Option<(&str, Secret<&str>)>,
    ) -> Result<Secret<Url>, ConnStrError> {
        let (username, password) = auth_override.unwrap_or((
            self.username.as_str(),
            Secret::new(self.password.expose_secret().as_str()),
        ));
        let mut url = Url::parse(&format!("postgresql://{host}", host = self.host))?;
        url.set_path(db_name);
        url.set_username(username)
            .map_err(|_| ConnStrError::Build {
                context: "username",
            })?;
        url.set_password(Some(password.expose_secret()))
            .map_err(|_| ConnStrError::Build {
                context: "password",
            })?;
        Ok(Secret::new(url))
    }

    /// For use only in single-cluster setups. If exactly one row is present, it
    /// is fetched and returned. Otherwise, returns an error.
    pub async fn fetch_only(app_db: &mut AppDbClient) -> sqlx::Result<Self> {
        let mut rows = query_as!(Self, "select * from clusters limit 2")
            .fetch_all(app_db.get_conn())
            .await?;
        if rows.len() == 1 {
            Ok(rows.pop().expect("just checked that vec len == 1"))
        } else {
            Err(sqlx::Error::RowNotFound)
        }
    }
}

#[derive(Clone, Copy, Debug, thiserror::Error)]
#[error("error building postgresql:// connection string")]
pub enum ConnStrError {
    Parse(url::ParseError),
    #[error("error building postgresql:// connection string: {context}")]
    Build {
        context: &'static str,
    },
}

impl From<url::ParseError> for ConnStrError {
    fn from(value: url::ParseError) -> Self {
        Self::Parse(value)
    }
}
