//! Relationship definitions. `RelationDef` is implemented by zero-sized types
//! generated by `#[has_many]` / `#[belongs_to]` attribute parsing.

use std::marker::PhantomData;

use crate::model::Model;
use crate::Error;

pub trait RelationKind: Send + Sync + 'static {}

pub struct HasMany;
impl RelationKind for HasMany {}

pub struct HasOne;
impl RelationKind for HasOne {}

pub struct BelongsTo;
impl RelationKind for BelongsTo {}

pub struct BelongsToMany;
impl RelationKind for BelongsToMany {}

pub trait RelationDef: Send + Sync {
    type Parent: Model;
    type Child: Model;
    type Kind: RelationKind;
    fn local_key() -> &'static str;
    fn foreign_key() -> &'static str;
}

/// A live relation handle: from a parent row, load the related rows.
pub struct Relation<P: Model, C: Model, K: RelationKind> {
    pub parent_value: serde_json::Value,
    pub local_key: &'static str,
    pub foreign_key: &'static str,
    _marker: PhantomData<(P, C, K)>,
}

impl<P: Model, C: Model> Relation<P, C, HasMany> {
    pub async fn load(self, pool: &sqlx::PgPool) -> Result<Vec<C>, Error> {
        let sql = format!(
            "SELECT {} FROM {} WHERE {} = $1",
            C::COLUMNS.join(", "),
            C::TABLE,
            self.foreign_key,
        );
        let rows = sqlx::query_as::<_, C>(&sql)
            .bind(self.parent_value)
            .fetch_all(pool)
            .await?;
        Ok(rows)
    }
}

impl<P: Model, C: Model> Relation<P, C, BelongsTo> {
    pub async fn load(self, pool: &sqlx::PgPool) -> Result<Option<C>, Error> {
        let sql = format!(
            "SELECT {} FROM {} WHERE {} = $1 LIMIT 1",
            C::COLUMNS.join(", "),
            C::TABLE,
            self.local_key,
        );
        let row = sqlx::query_as::<_, C>(&sql)
            .bind(self.parent_value)
            .fetch_optional(pool)
            .await?;
        Ok(row)
    }
}

impl<P: Model, C: Model> Relation<P, C, HasOne> {
    pub async fn load(self, pool: &sqlx::PgPool) -> Result<Option<C>, Error> {
        let sql = format!(
            "SELECT {} FROM {} WHERE {} = $1 LIMIT 1",
            C::COLUMNS.join(", "),
            C::TABLE,
            self.foreign_key,
        );
        let row = sqlx::query_as::<_, C>(&sql)
            .bind(self.parent_value)
            .fetch_optional(pool)
            .await?;
        Ok(row)
    }
}

impl<P: Model, C: Model, K: RelationKind> Relation<P, C, K> {
    pub fn new(
        parent_value: serde_json::Value,
        local_key: &'static str,
        foreign_key: &'static str,
    ) -> Self {
        Self {
            parent_value,
            local_key,
            foreign_key,
            _marker: PhantomData,
        }
    }
}
