use std::sync::LazyLock;

use derive_builder::Builder;
use regex::Regex;
use serde::Serialize;
use sqlx::{postgres::types::Oid, query, query_as};
use uuid::Uuid;
use validator::Validate;

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

pub static RE_PORTAL_NAME: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9][()a-zA-Z0-9 _-]*[a-zA-Z0-9()_-]$").unwrap());

/// A portal is a derivative representation of a Postgres relation.
#[derive(Clone, Debug, Serialize)]
pub struct Portal {
    /// Primary key (defaults to UUIDv7).
    pub id: Uuid,

    /// Human friendly name for portal.
    pub name: String,

    /// Workspace to which this portal belongs.
    pub workspace_id: Uuid,

    /// OID of the underlying Postgres relation. Currently, this is expected
    /// to be a normal table, not a view, etc.
    pub class_oid: Oid,

    /// Whether the portal's form may be viewed or submitted by users without
    /// direct access to the portal itself.
    pub form_public: bool,

    /// JSONB-encoded expression to use for filtering rows in the web-based
    /// table view.
    pub filter: String,
}

impl Portal {
    /// Build an insert statement to create a new portal.
    pub fn insert() -> InsertBuilder {
        InsertBuilder::default()
    }

    /// Build an update statement to alter an existing portal.
    pub fn update() -> UpdateBuilder {
        UpdateBuilder::default()
    }

    /// Build a query by workspace ID and relation OID.
    pub fn belonging_to_workspace(workspace_id: Uuid) -> BelongingToWorkspaceQuery {
        BelongingToWorkspaceQuery { workspace_id }
    }
}

with_id_query!(
    Portal,
    sql = r#"
select
    id,
    name,
    workspace_id,
    class_oid,
    form_public,
    filter
from portals
where id = $1
"#,
);

#[derive(Clone, Debug)]
pub struct BelongingToWorkspaceQuery {
    workspace_id: Uuid,
}

impl BelongingToWorkspaceQuery {
    pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Portal>, sqlx::Error> {
        query_as!(
            Portal,
            r#"
select
    id,
    name,
    workspace_id,
    class_oid,
    form_public,
    filter
from portals
where workspace_id = $1
"#,
            self.workspace_id,
        )
        .fetch_all(&mut *app_db.conn)
        .await
    }

    pub fn belonging_to_rel(self, rel_oid: Oid) -> BelongingToRelQuery {
        BelongingToRelQuery {
            workspace_id: self.workspace_id,
            rel_oid,
        }
    }
}

#[derive(Clone, Debug)]
pub struct BelongingToRelQuery {
    workspace_id: Uuid,
    rel_oid: Oid,
}

impl BelongingToRelQuery {
    pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Portal>, sqlx::Error> {
        query_as!(
            Portal,
            r#"
select
    id,
    name,
    workspace_id,
    class_oid,
    form_public,
    filter
from portals
where workspace_id = $1 and class_oid = $2
"#,
            self.workspace_id,
            self.rel_oid
        )
        .fetch_all(&mut *app_db.conn)
        .await
    }
}

#[derive(Clone, Debug, Serialize, sqlx::Type)]
#[sqlx(type_name = "lens_display_type", rename_all = "lowercase")]
pub enum LensDisplayType {
    Table,
}

#[derive(Builder, Clone, Debug)]
pub struct Insert {
    name: String,
    workspace_id: Uuid,
    class_oid: Oid,
}

impl Insert {
    pub async fn execute(self, app_db: &mut AppDbClient) -> Result<Portal, sqlx::Error> {
        query_as!(
            Portal,
            r#"
insert into portals
(workspace_id, class_oid, name)
values ($1, $2, $3)
returning
    id,
    name,
    workspace_id,
    class_oid,
    form_public,
    filter
"#,
            self.workspace_id,
            self.class_oid,
            self.name,
        )
        .fetch_one(&mut *app_db.conn)
        .await
    }
}

#[derive(Builder, Clone, Debug, Validate)]
pub struct Update {
    id: Uuid,

    #[builder(default, setter(strip_option = true))]
    form_public: Option<bool>,

    #[builder(default, setter(strip_option = true))]
    filter: Option<String>,

    #[builder(default, setter(strip_option = true))]
    #[validate(regex(path = *RE_PORTAL_NAME))]
    name: Option<String>,
}

impl Update {
    pub async fn execute(self, app_db: &mut AppDbClient) -> Result<(), QueryError> {
        self.validate()?;

        // TODO: consolidate queries
        if let Some(form_public) = self.form_public {
            query!(
                "update portals set form_public = $1 where id = $2",
                form_public,
                self.id,
            )
            .execute(app_db.get_conn())
            .await?;
        }
        if let Some(filter) = self.filter {
            query!(
                "update portals set filter = $1 where id = $2",
                filter,
                self.id,
            )
            .execute(app_db.get_conn())
            .await?;
        }
        if let Some(name) = self.name {
            query!("update portals set name = $1 where id = $2", name, self.id)
                .execute(app_db.get_conn())
                .await?;
        }
        Ok(())
    }
}
