use derive_builder::Builder;
use serde::Serialize;
use sqlx::{Row as _, postgres::PgRow, query, query_as, types::Json};
use uuid::Uuid;

use crate::{client::AppDbClient, expression::PgExpressionAny};

/// A form transition directionally connects two portals within the same
/// workspace, representing a potential navigation of a user between two forms.
/// If the user submits a form, form transitions with `source_id` corresponding
/// to that portal will be evaluated one by one (in order by ID---that is, by
/// creation time), and the first with a condition evaluating to true will be
/// used to direct the user to the form corresponding to portal `dest_id`.
#[derive(Clone, Debug, Serialize)]
pub struct FormTransition {
    /// Primary key (defaults to UUIDv7).
    pub id: Uuid,

    /// When a user is filling out a sequence of forms, this is the ID of the
    /// portal for which they have just submitted a form for.
    ///
    /// **Source portal is expected to belong to the same workspace as the
    /// destination portal.**
    pub source_id: Uuid,

    /// When a user is filling out a sequence of forms, this is the ID of the
    /// portal for which they will be directed to if the condition evaluates to
    /// true.
    ///
    /// **Destination portal is expected to belong to the same workspace as the
    /// source portal.**
    pub dest_id: Uuid,

    /// Represents a semi-arbitrary Postgres expression which will permit this
    /// transition to be followed, only if the expression evaluates to true at
    /// the time of the source form's submission.
    pub condition: Json<Option<PgExpressionAny>>,
}

impl FormTransition {
    /// Build a multi-row update statement to replace all transtitions for a
    /// given source portal.
    pub fn replace_for_portal(portal_id: Uuid) -> ReplaceBuilder {
        ReplaceBuilder {
            portal_id: Some(portal_id),
            ..ReplaceBuilder::default()
        }
    }

    /// Build a single-field query by source portal ID.
    pub fn with_source(id: Uuid) -> WithSourceQuery {
        WithSourceQuery { id }
    }
}

#[derive(Clone, Copy, Debug)]
pub struct WithSourceQuery {
    id: Uuid,
}

impl WithSourceQuery {
    pub async fn fetch_all(
        self,
        app_db: &mut AppDbClient,
    ) -> Result<Vec<FormTransition>, sqlx::Error> {
        query_as!(
            FormTransition,
            r#"
select
    id,
    source_id,
    dest_id,
    condition as "condition: Json<Option<PgExpressionAny>>"
from form_transitions
where source_id = $1
"#,
            self.id,
        )
        .fetch_all(app_db.get_conn())
        .await
    }
}

#[derive(Builder, Clone, Debug)]
pub struct Replacement {
    pub dest_id: Uuid,
    pub condition: Option<PgExpressionAny>,
}

#[derive(Builder, Clone, Debug)]
pub struct Replace {
    #[builder(setter(custom))]
    portal_id: Uuid,

    replacements: Vec<Replacement>,
}

impl Replace {
    /// Insert zero or more form transitions, and then remove all others
    /// associated with the same portal. When they are being used, form
    /// transitions are evaluated from first to last by ID (that is, by
    /// creation timestamp, because IDs are UUIDv7s), so none of the newly added
    /// transitions will supersede their predecessors until the latter are
    /// deleted in one fell swoop. However, there will be a (hopefully brief)
    /// period during which both the old and the new transitions will be
    /// evaluated together in order, so this is not quite equivalent to an
    /// atomic update.
    ///
    /// FIXME: There is a race condition in which executing [`Replace::execute`]
    /// for the same portal two or more times simultaneously may remove *all*
    /// form transitions for that portal, new and old. This would require
    /// impeccable timing, but it should absolutely be fixed... at some point.
    pub async fn execute(self, app_db: &mut AppDbClient) -> Result<(), sqlx::Error> {
        let ids: Vec<Uuid> = if self.replacements.is_empty() {
            vec![]
        } else {
            // Nice to do this in one query to avoid generating even more
            // intermediate database state purgatory. Credit to [@greglearns](
            // https://github.com/launchbadge/sqlx/issues/294#issuecomment-716149160
            // ) for the clever syntax. Too bad it doesn't seem plausible to
            // dovetail this with the [`query!`] macro, but that makes sense
            // given the circuitous query structure.
            query(
                r#"
insert into form_transitions (source_id, dest_id, condition)
    select * from unnest($1, $2, $3)
returning id
"#,
            )
            .bind(
                self.replacements
                    .iter()
                    .map(|_| self.portal_id)
                    .collect::<Vec<_>>(),
            )
            .bind(
                self.replacements
                    .iter()
                    .map(|value| value.dest_id)
                    .collect::<Vec<_>>(),
            )
            .bind(
                self.replacements
                    .iter()
                    .map(|value| Json(value.condition.clone()))
                    .collect::<Vec<_>>() as Vec<Json<Option<PgExpressionAny>>>,
            )
            .map(|row: PgRow| -> Uuid { row.get(0) })
            .fetch_all(app_db.get_conn())
            .await?
        };

        query!(
            "delete from form_transitions where id <> any($1)",
            ids.as_slice(),
        )
        .execute(app_db.get_conn())
        .await?;

        Ok(())
    }
}
