use crate::{
    EntityTrait, Identity, IdentityOf, Iterable, QuerySelect, Select, join_tbl_on_condition,
};
use core::marker::PhantomData;
use sea_query::{
    Condition, ConditionType, DynIden, ForeignKeyCreateStatement, IntoIden, JoinType,
    TableForeignKey, TableRef,
};
use std::{fmt::Debug, sync::Arc};

/// Cardinality of a relation.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum RelationType {
    /// A 1-1 relation (also used as half of a `belongs_to` / `has_one` pair).
    HasOne,
    /// A 1-N relation.
    HasMany,
}

/// `ON DELETE` / `ON UPDATE` action attached to a foreign key. Re-exported
/// from [`sea_query`] for convenience.
pub type ForeignKeyAction = sea_query::ForeignKeyAction;

/// Each variant of an entity's `Relation` enum implements this trait,
/// providing the [`RelationDef`] that backs joins, foreign keys, and
/// related-model lookups.
///
/// Usually generated by `#[derive(DeriveRelation)]` (or by `#[sea_orm::model]`
/// in the 2.0 dense format).
pub trait RelationTrait: Iterable + Debug + 'static {
    /// The [`RelationDef`] describing this relation.
    fn def(&self) -> RelationDef;

    /// Name of the relation variant (e.g. `"Fruit"`), used for diagnostics.
    fn name(&self) -> String {
        format!("{self:?}")
    }
}

/// `Self` is related to entity `R`, which lets queries join the two tables
/// without a manual `ON` clause. For many-to-many relations, override
/// [`via`](Self::via) to point at the junction table.
pub trait Related<R>
where
    R: EntityTrait,
{
    /// The relation from `Self` to `R`.
    fn to() -> RelationDef;

    /// Junction table relation, if `Self <-> R` is many-to-many.
    fn via() -> Option<RelationDef> {
        None
    }

    /// Build a [`Select<R>`] pre-joined with `Self` via this relation.
    fn find_related() -> Select<R> {
        Select::<R>::new().join_join_rev(JoinType::InnerJoin, Self::to(), Self::via())
    }
}

/// Self-referential many-to-many relation: `Self <-> Self` through a
/// junction table (e.g. `user_follower`).
pub trait RelatedSelfVia<R>
where
    R: EntityTrait,
{
    /// The relation back to `Self` (via the junction).
    fn to() -> RelationDef;

    /// The relation from `Self` to the junction table.
    fn via() -> RelationDef;

    /// Build a [`Select<R>`] pre-joined with `Self` via the junction table.
    fn find_related() -> Select<R> {
        Select::<R>::new().join_join_rev(JoinType::InnerJoin, Self::to(), Some(Self::via()))
    }
}

/// Concrete description of a relation between two entities: which tables
/// and columns participate, whether `Self` is the FK-owning side, and the
/// optional foreign-key constraint metadata.
///
/// Build one with [`RelationBuilder`] (returned by
/// [`EntityTrait::belongs_to`] / [`has_one`](EntityTrait::has_one) /
/// [`has_many`](EntityTrait::has_many)), or destructure one returned by a
/// [`RelationTrait::def`] impl when you need to customise it.
#[derive(derive_more::Debug, Clone)]
pub struct RelationDef {
    /// Cardinality of the relation.
    pub rel_type: RelationType,
    /// Table at the FK-owning end of the relation.
    pub from_tbl: TableRef,
    /// Table being pointed to by the foreign key.
    pub to_tbl: TableRef,
    /// FK column(s) on [`from_tbl`](Self::from_tbl).
    pub from_col: Identity,
    /// Referenced column(s) on [`to_tbl`](Self::to_tbl) (usually the primary key).
    pub to_col: Identity,
    /// `true` if `Self` is the FK-owning side (i.e. holds `from_col`).
    pub is_owner: bool,
    /// Skip emitting a `FOREIGN KEY` constraint when this relation is used
    /// to generate schema DDL.
    pub skip_fk: bool,
    /// `ON DELETE` action for the foreign key.
    pub on_delete: Option<ForeignKeyAction>,
    /// `ON UPDATE` action for the foreign key.
    pub on_update: Option<ForeignKeyAction>,
    /// Extra predicate to AND/OR into the join's `ON` clause; see
    /// [`RelationDef::on_condition`].
    #[debug("{}", on_condition.is_some())]
    pub on_condition: Option<Arc<dyn Fn(DynIden, DynIden) -> Condition + Send + Sync>>,
    /// Name of the foreign-key constraint to emit in DDL.
    pub fk_name: Option<String>,
    /// How [`on_condition`](Self::on_condition) is combined with the
    /// column equality predicate (`All` = AND, `Any` = OR).
    pub condition_type: ConditionType,
}

/// Idiomatically generate the join condition.
///
/// This allows using a [`RelationDef`] directly anywhere [`sea_query`] expects
/// an [`IntoCondition`](sea_query::IntoCondition).
///
/// ## Examples
///
/// ```
/// use sea_orm::tests_cfg::{cake, fruit};
/// use sea_orm::{entity::*, sea_query::*};
///
/// let query = Query::select()
///     .from(fruit::Entity)
///     .inner_join(cake::Entity, fruit::Relation::Cake.def())
///     .to_owned();
///
/// assert_eq!(
///     query.to_string(MysqlQueryBuilder),
///     r#"SELECT  FROM `fruit` INNER JOIN `cake` ON `fruit`.`cake_id` = `cake`.`id`"#
/// );
/// assert_eq!(
///     query.to_string(PostgresQueryBuilder),
///     r#"SELECT  FROM "fruit" INNER JOIN "cake" ON "fruit"."cake_id" = "cake"."id""#
/// );
/// assert_eq!(
///     query.to_string(SqliteQueryBuilder),
///     r#"SELECT  FROM "fruit" INNER JOIN "cake" ON "fruit"."cake_id" = "cake"."id""#
/// );
/// ```
impl From<RelationDef> for Condition {
    fn from(mut rel: RelationDef) -> Condition {
        // Use table alias (if any) to construct the join condition
        let from_tbl = match rel.from_tbl.sea_orm_table_alias() {
            Some(alias) => alias,
            None => rel.from_tbl.sea_orm_table(),
        };
        let to_tbl = match rel.to_tbl.sea_orm_table_alias() {
            Some(alias) => alias,
            None => rel.to_tbl.sea_orm_table(),
        };
        let owner_keys = rel.from_col;
        let foreign_keys = rel.to_col;

        let mut condition = match rel.condition_type {
            ConditionType::All => Condition::all(),
            ConditionType::Any => Condition::any(),
        };

        condition = condition.add(join_tbl_on_condition(
            from_tbl.clone(),
            to_tbl.clone(),
            owner_keys,
            foreign_keys,
        ));
        if let Some(f) = rel.on_condition.take() {
            condition = condition.add(f(from_tbl.clone(), to_tbl.clone()));
        }

        condition
    }
}

/// Fluent builder for a [`RelationDef`]. Construct one via
/// [`EntityTrait::belongs_to`] / [`has_one`](EntityTrait::has_one) /
/// [`has_many`](EntityTrait::has_many).
#[derive(derive_more::Debug)]
pub struct RelationBuilder<E, R>
where
    E: EntityTrait,
    R: EntityTrait,
{
    entities: PhantomData<(E, R)>,
    rel_type: RelationType,
    from_tbl: TableRef,
    to_tbl: TableRef,
    from_col: Option<Identity>,
    to_col: Option<Identity>,
    is_owner: bool,
    skip_fk: bool,
    on_delete: Option<ForeignKeyAction>,
    on_update: Option<ForeignKeyAction>,
    #[debug("{}", on_condition.is_some())]
    on_condition: Option<Arc<dyn Fn(DynIden, DynIden) -> Condition + Send + Sync>>,
    fk_name: Option<String>,
    condition_type: ConditionType,
}

impl RelationDef {
    /// Swap the `from` and `to` ends of this relation. Useful when joining
    /// from the FK-pointed-at side back to the FK-owning side.
    pub fn rev(self) -> Self {
        Self {
            rel_type: self.rel_type,
            from_tbl: self.to_tbl,
            to_tbl: self.from_tbl,
            from_col: self.to_col,
            to_col: self.from_col,
            is_owner: !self.is_owner,
            skip_fk: self.skip_fk,
            on_delete: self.on_delete,
            on_update: self.on_update,
            on_condition: self.on_condition,
            fk_name: None,
            condition_type: self.condition_type,
        }
    }

    /// Express the relation from a table alias.
    ///
    /// This is a shorter and more discoverable equivalent to modifying `from_tbl` field by hand.
    ///
    /// # Examples
    ///
    /// Here's a short synthetic example.
    /// In real life you'd use aliases when the table name comes up twice and you need to disambiguate,
    /// e.g. <https://github.com/SeaQL/sea-orm/discussions/2133>.
    ///
    /// ```
    /// use sea_orm::{
    ///     DbBackend,
    ///     entity::*,
    ///     query::*,
    ///     tests_cfg::{cake, cake_filling},
    /// };
    /// use sea_query::Alias;
    ///
    /// let cf = "cf";
    ///
    /// assert_eq!(
    ///     cake::Entity::find()
    ///         .join_as(
    ///             JoinType::LeftJoin,
    ///             cake_filling::Relation::Cake.def().rev(),
    ///             cf.clone()
    ///         )
    ///         .join(
    ///             JoinType::LeftJoin,
    ///             cake_filling::Relation::Filling.def().from_alias(cf)
    ///         )
    ///         .build(DbBackend::MySql)
    ///         .to_string(),
    ///     [
    ///         "SELECT `cake`.`id`, `cake`.`name` FROM `cake`",
    ///         "LEFT JOIN `cake_filling` AS `cf` ON `cake`.`id` = `cf`.`cake_id`",
    ///         "LEFT JOIN `filling` ON `cf`.`filling_id` = `filling`.`id`",
    ///     ]
    ///     .join(" ")
    /// );
    /// ```
    pub fn from_alias<A>(mut self, alias: A) -> Self
    where
        A: IntoIden,
    {
        self.from_tbl = self.from_tbl.alias(alias);
        self
    }

    /// Set custom join ON condition.
    ///
    /// This method takes a closure with two parameters
    /// denoting the left-hand side and right-hand side table in the join expression.
    ///
    /// This replaces the current condition if it is already set.
    ///
    /// # Examples
    ///
    /// ```
    /// use sea_orm::{entity::*, query::*, DbBackend, tests_cfg::{cake, cake_filling}};
    /// use sea_query::{Expr, ExprTrait, IntoCondition};
    ///
    /// assert_eq!(
    ///     cake::Entity::find()
    ///         .join(
    ///             JoinType::LeftJoin,
    ///             cake_filling::Relation::Cake
    ///                 .def()
    ///                 .rev()
    ///                 .on_condition(|_left, right| {
    ///                     Expr::col((right, cake_filling::Column::CakeId))
    ///                         .gt(10i32)
    ///                         .into_condition()
    ///                 })
    ///         )
    ///         .build(DbBackend::MySql)
    ///         .to_string(),
    ///     [
    ///         "SELECT `cake`.`id`, `cake`.`name` FROM `cake`",
    ///         "LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id` AND `cake_filling`.`cake_id` > 10",
    ///     ]
    ///     .join(" ")
    /// );
    /// ```
    pub fn on_condition<F>(mut self, f: F) -> Self
    where
        F: Fn(DynIden, DynIden) -> Condition + 'static + Send + Sync,
    {
        self.on_condition = Some(Arc::new(f));
        self
    }

    /// Set the condition type of join on expression
    ///
    /// # Examples
    ///
    /// ```
    /// use sea_orm::{entity::*, query::*, DbBackend, tests_cfg::{cake, cake_filling}};
    /// use sea_query::{Expr, ExprTrait, IntoCondition, ConditionType};
    ///
    /// assert_eq!(
    ///     cake::Entity::find()
    ///         .join(
    ///             JoinType::LeftJoin,
    ///             cake_filling::Relation::Cake
    ///                 .def()
    ///                 .rev()
    ///                 .condition_type(ConditionType::Any)
    ///                 .on_condition(|_left, right| {
    ///                     Expr::col((right, cake_filling::Column::CakeId))
    ///                         .gt(10i32)
    ///                         .into_condition()
    ///                 })
    ///         )
    ///         .build(DbBackend::MySql)
    ///         .to_string(),
    ///     [
    ///         "SELECT `cake`.`id`, `cake`.`name` FROM `cake`",
    ///         "LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id` OR `cake_filling`.`cake_id` > 10",
    ///     ]
    ///     .join(" ")
    /// );
    /// ```
    pub fn condition_type(mut self, condition_type: ConditionType) -> Self {
        self.condition_type = condition_type;
        self
    }
}

impl<E, R> RelationBuilder<E, R>
where
    E: EntityTrait,
    R: EntityTrait,
{
    pub(crate) fn new(rel_type: RelationType, from: E, to: R, is_owner: bool) -> Self {
        Self {
            entities: PhantomData,
            rel_type,
            from_tbl: from.table_ref(),
            to_tbl: to.table_ref(),
            from_col: None,
            to_col: None,
            is_owner,
            skip_fk: false,
            on_delete: None,
            on_update: None,
            on_condition: None,
            fk_name: None,
            condition_type: ConditionType::All,
        }
    }

    pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef, is_owner: bool) -> Self {
        Self {
            entities: PhantomData,
            rel_type,
            from_tbl: rel.from_tbl,
            to_tbl: rel.to_tbl,
            from_col: Some(rel.from_col),
            to_col: Some(rel.to_col),
            is_owner,
            skip_fk: false,
            on_delete: None,
            on_update: None,
            on_condition: None,
            fk_name: None,
            condition_type: ConditionType::All,
        }
    }

    /// Column(s) on the FK-owning side. Accepts a column or a tuple of
    /// columns for composite keys.
    pub fn from<T>(mut self, identifier: T) -> Self
    where
        T: IdentityOf<E>,
    {
        self.from_col = Some(identifier.identity_of());
        self
    }

    /// Column(s) on the referenced side (usually the related entity's
    /// primary key). Accepts a column or a tuple of columns for composite
    /// keys.
    pub fn to<T>(mut self, identifier: T) -> Self
    where
        T: IdentityOf<R>,
    {
        self.to_col = Some(identifier.identity_of());
        self
    }

    /// Don't emit a `FOREIGN KEY` constraint when this relation is used to
    /// generate schema DDL.
    pub fn skip_fk(mut self) -> Self {
        self.skip_fk = true;
        self
    }

    /// Set the `ON DELETE` action for the foreign key (e.g. `Cascade`,
    /// `SetNull`).
    pub fn on_delete(mut self, action: ForeignKeyAction) -> Self {
        self.on_delete = Some(action);
        self
    }

    /// Set the `ON UPDATE` action for the foreign key.
    pub fn on_update(mut self, action: ForeignKeyAction) -> Self {
        self.on_update = Some(action);
        self
    }

    /// Add an extra predicate to the join's `ON` clause. The closure receives
    /// the left-hand-side and right-hand-side table idens.
    pub fn on_condition<F>(mut self, f: F) -> Self
    where
        F: Fn(DynIden, DynIden) -> Condition + 'static + Send + Sync,
    {
        self.on_condition = Some(Arc::new(f));
        self
    }

    /// Override the name of the `FOREIGN KEY` constraint emitted in DDL.
    pub fn fk_name(mut self, fk_name: &str) -> Self {
        self.fk_name = Some(fk_name.to_owned());
        self
    }

    /// Combine the column equality and [`on_condition`](Self::on_condition)
    /// with `AND` (`ConditionType::All`, default) or `OR` (`ConditionType::Any`).
    pub fn condition_type(mut self, condition_type: ConditionType) -> Self {
        self.condition_type = condition_type;
        self
    }
}

impl<E, R> From<RelationBuilder<E, R>> for RelationDef
where
    E: EntityTrait,
    R: EntityTrait,
{
    fn from(b: RelationBuilder<E, R>) -> Self {
        RelationDef {
            rel_type: b.rel_type,
            from_tbl: b.from_tbl,
            to_tbl: b.to_tbl,
            from_col: b.from_col.expect("Reference column is not set"),
            to_col: b.to_col.expect("Owner column is not set"),
            is_owner: b.is_owner,
            skip_fk: b.skip_fk,
            on_delete: b.on_delete,
            on_update: b.on_update,
            on_condition: b.on_condition,
            fk_name: b.fk_name,
            condition_type: b.condition_type,
        }
    }
}

macro_rules! set_foreign_key_stmt {
    ( $relation: ident, $foreign_key: ident ) => {
        let from_cols: Vec<String> = $relation
            .from_col
            .into_iter()
            .map(|col| {
                let col_name = col.to_string();
                $foreign_key.from_col(col);
                col_name
            })
            .collect();
        for col in $relation.to_col.into_iter() {
            $foreign_key.to_col(col);
        }
        if let Some(action) = $relation.on_delete {
            $foreign_key.on_delete(action);
        }
        if let Some(action) = $relation.on_update {
            $foreign_key.on_update(action);
        }
        let name = if let Some(name) = $relation.fk_name {
            name
        } else {
            let from_tbl = &$relation.from_tbl.sea_orm_table().clone();
            format!("fk-{}-{}", from_tbl.to_string(), from_cols.join("-"))
        };
        $foreign_key.name(&name);
    };
}

impl From<RelationDef> for ForeignKeyCreateStatement {
    fn from(relation: RelationDef) -> Self {
        let mut foreign_key_stmt = Self::new();
        set_foreign_key_stmt!(relation, foreign_key_stmt);
        foreign_key_stmt
            .from_tbl(relation.from_tbl)
            .to_tbl(relation.to_tbl)
            .take()
    }
}

/// Creates a column definition for example to update a table.
/// ```
/// use sea_query::{Alias, IntoIden, MysqlQueryBuilder, TableAlterStatement, IntoTableRef, ConditionType};
/// use sea_orm::{EnumIter, Iden, Identity, PrimaryKeyTrait, RelationDef, RelationTrait, RelationType};
///
/// let relation = RelationDef {
///     rel_type: RelationType::HasOne,
///     from_tbl: "foo".into_table_ref(),
///     to_tbl: "bar".into_table_ref(),
///     from_col: Identity::Unary("bar_id".into_iden()),
///     to_col: Identity::Unary("bar_id".into_iden()),
///     is_owner: false,
///     on_delete: None,
///     on_update: None,
///     on_condition: None,
///     fk_name: Some("foo-bar".to_string()),
///     skip_fk: false,
///     condition_type: ConditionType::All,
/// };
///
/// let mut alter_table = TableAlterStatement::new()
///     .table("foo")
///     .add_foreign_key(&mut relation.into()).take();
/// assert_eq!(
///     alter_table.to_string(MysqlQueryBuilder::default()),
///     "ALTER TABLE `foo` ADD CONSTRAINT `foo-bar` FOREIGN KEY (`bar_id`) REFERENCES `bar` (`bar_id`)"
/// );
/// ```
impl From<RelationDef> for TableForeignKey {
    fn from(relation: RelationDef) -> Self {
        let mut foreign_key = Self::new();
        set_foreign_key_stmt!(relation, foreign_key);
        foreign_key
            .from_tbl(relation.from_tbl.sea_orm_table().clone())
            .to_tbl(relation.to_tbl.sea_orm_table().clone())
            .take()
    }
}

impl std::hash::Hash for RelationDef {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.rel_type.hash(state);
        self.from_tbl.sea_orm_table().hash(state);
        self.to_tbl.sea_orm_table().hash(state);
        self.from_col.hash(state);
        self.to_col.hash(state);
        self.is_owner.hash(state);
    }
}

impl PartialEq for RelationDef {
    fn eq(&self, other: &Self) -> bool {
        self.rel_type.eq(&other.rel_type)
            && self.from_tbl.eq(&other.from_tbl)
            && self.to_tbl.eq(&other.to_tbl)
            && itertools::equal(self.from_col.iter(), other.from_col.iter())
            && itertools::equal(self.to_col.iter(), other.to_col.iter())
            && self.is_owner.eq(&other.is_owner)
    }
}

impl Eq for RelationDef {}

#[cfg(test)]
mod tests {
    use crate::{
        RelationBuilder, RelationDef,
        tests_cfg::{cake, fruit},
    };

    #[cfg(not(feature = "sync"))]
    #[test]
    fn assert_relation_traits() {
        fn assert_send_sync<T: Send + Sync>() {}

        assert_send_sync::<RelationDef>();
        assert_send_sync::<RelationBuilder<cake::Entity, fruit::Entity>>();
    }
}
