use diesel::{
    backend::Backend,
    deserialize::{self, FromSql, FromSqlRow},
    dsl::{auto_type, AsSelect},
    expression::AsExpression,
    pg::Pg,
    prelude::*,
    serialize::{Output, ToSql},
    sql_types::Jsonb,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use uuid::Uuid;

use crate::{schema::channels, teams::Team};

pub const CHANNEL_BACKEND_EMAIL: &str = "email";
pub const CHANNEL_BACKEND_SLACK: &str = "slack";

/// Represents a target/destination for messages, with the sender configuration
/// defined in the backend_config field. A single channel may be attached to
/// (in other words, "enabled" or "selected" for) any number of projects within
/// the same team.
#[derive(Associations, Clone, Debug, Identifiable, Queryable, Selectable)]
#[diesel(belongs_to(Team))]
#[diesel(check_for_backend(Pg))]
pub struct Channel {
    pub id: Uuid,
    pub team_id: Uuid,
    pub name: String,
    pub enable_by_default: bool,
    pub backend_config: BackendConfig,
}

impl Channel {
    #[auto_type(no_type_alias)]
    pub fn all() -> _ {
        let select: AsSelect<Channel, Pg> = Channel::as_select();
        channels::table.select(select)
    }

    #[auto_type(no_type_alias)]
    pub fn with_id<'a>(channel_id: &'a Uuid) -> _ {
        channels::id.eq(channel_id)
    }

    #[auto_type(no_type_alias)]
    pub fn with_team<'a>(team_id: &'a Uuid) -> _ {
        channels::team_id.eq(team_id)
    }

    #[auto_type(no_type_alias)]
    pub fn where_enabled_by_default() -> _ {
        channels::enable_by_default.eq(true)
    }
}

// Note: In a previous implementation, channel configuration was handled by
// creating a dedicated table for each channel type and joining them to the
// `channels` table in order to access configuration fields. The jsonb approach
// simplifies database management and lends itself to a cleaner Rust
// implementation in which this enum can be treated as a column type with
// enforcement of data structure invariants handled entirely in the to_sql()
// and from_sql() serialization/deserialization logic.

/// Encapsulates any information that needs to be persisted for setting up or
/// using a channel's backend (that is, email sender, Slack app, etc.). This
/// configuration is encoded to a jsonb column in the database, which determines
/// the channel type along with configuration details.
#[derive(AsExpression, Clone, Debug, FromSqlRow, Deserialize, Serialize)]
#[diesel(sql_type = Jsonb)]
pub enum BackendConfig {
    Email(EmailBackendConfig),
    Slack(SlackBackendConfig),
}

impl ToSql<Jsonb, Pg> for BackendConfig {
    fn to_sql(&self, out: &mut Output<'_, '_, Pg>) -> diesel::serialize::Result {
        match self.clone() {
            BackendConfig::Email(config) => ToSql::<Jsonb, Pg>::to_sql(
                &json!({
                    "t": CHANNEL_BACKEND_EMAIL,
                    "config": config,
                }),
                &mut out.reborrow(),
            ),
            BackendConfig::Slack(config) => ToSql::<Jsonb, Pg>::to_sql(
                &json!({
                    "t": CHANNEL_BACKEND_SLACK,
                    "config": config,
                }),
                &mut out.reborrow(),
            ),
        }
    }
}

impl FromSql<Jsonb, Pg> for BackendConfig {
    fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let value = <serde_json::Value as FromSql<Jsonb, Pg>>::from_sql(bytes)?;
        #[derive(Deserialize)]
        struct IntermediateBackendConfig {
            pub t: String,
            pub config: serde_json::Value,
        }
        let intermediate_value = IntermediateBackendConfig::deserialize(value)?;
        match intermediate_value.t.as_str() {
            CHANNEL_BACKEND_EMAIL => Ok(BackendConfig::Email(EmailBackendConfig::deserialize(
                intermediate_value.config,
            )?)),
            CHANNEL_BACKEND_SLACK => Ok(BackendConfig::Slack(SlackBackendConfig::deserialize(
                intermediate_value.config,
            )?)),
            t => Err(anyhow::anyhow!("backend type not recognized: {}", t).into()),
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct EmailBackendConfig {
    pub recipient: String,
    pub verification_code: String,
    pub verification_code_guesses: u32,
    pub verified: bool,
}

impl TryFrom<BackendConfig> for EmailBackendConfig {
    type Error = anyhow::Error;

    fn try_from(value: BackendConfig) -> Result<Self, Self::Error> {
        if let BackendConfig::Email(config) = value {
            Ok(config)
        } else {
            Err(anyhow::anyhow!(
                "could not load backend config as email channel"
            ))
        }
    }
}

impl From<EmailBackendConfig> for BackendConfig {
    fn from(value: EmailBackendConfig) -> Self {
        Self::Email(value)
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SlackBackendConfig {
    pub oauth_state: String,
    pub access_token: String,
    pub refresh_token: String,
    pub conversation_id: String,
}

impl TryFrom<BackendConfig> for SlackBackendConfig {
    type Error = anyhow::Error;

    fn try_from(value: BackendConfig) -> Result<Self, Self::Error> {
        if let BackendConfig::Slack(config) = value {
            Ok(config)
        } else {
            Err(anyhow::anyhow!(
                "could not load backend config as slack channel"
            ))
        }
    }
}

impl From<SlackBackendConfig> for BackendConfig {
    fn from(value: SlackBackendConfig) -> Self {
        Self::Slack(value)
    }
}
