use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use derive_builder::Builder;
use phono_backends::pg_attribute::PgAttribute;
use phono_pestgros::Datum;
use serde::{Deserialize, Serialize};
use sqlx::Acquire as _;
use sqlx::{
    Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query, query_as,
    types::Json,
};
use thiserror::Error;
use uuid::Uuid;

use crate::{client::AppDbClient, presentation::Presentation};

/// A materialization of a database column, fit for consumption by an end user.
///
/// There may be zero or more fields per column/attribute in a Postgres view.
/// There may in some case also be fields with no underlying column, if it has
/// been removed or altered.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Field {
    /// Internal ID for application use.
    pub id: Uuid,

    /// Name of the database column.
    pub name: String,

    /// Refer to documentation for `Presentation`.
    pub presentation: sqlx::types::Json<Presentation>,

    /// Optional human friendly label.
    pub table_label: Option<String>,

    /// Width of UI table column in pixels.
    pub table_width_px: i32,

    /// Position of the field relative to others. Smaller values appear earlier.
    pub ordinality: f64,
}

impl Field {
    /// Constructs a brand new field to be inserted into the application db.
    pub fn insert() -> InsertableFieldBuilder {
        InsertableFieldBuilder::default()
    }

    /// Construct an update to an existing field.
    pub fn update() -> UpdateBuilder {
        UpdateBuilder::default()
    }

    /// Generate a default field config based on an existing column's name and
    /// type.
    pub fn default_from_attr(attr: &PgAttribute) -> Option<Self> {
        Presentation::default_from_attr(attr).map(|presentation| Self {
            id: Uuid::now_v7(),
            name: attr.attname.clone(),
            table_label: None,
            presentation: sqlx::types::Json(presentation),
            table_width_px: 200,
            ordinality: 1.0,
        })
    }

    pub fn get_datum(&self, row: &PgRow) -> Result<Datum, ParseError> {
        let value_ref = row
            .try_get_raw(self.name.as_str())
            .or(Err(ParseError::FieldNotFound))?;
        let type_info = value_ref.type_info();
        let ty = type_info.name();
        Ok(match ty {
            "NUMERIC" => {
                Datum::Numeric(<Option<BigDecimal> as Decode<Postgres>>::decode(value_ref).unwrap())
            }
            "TEXT" | "VARCHAR" => {
                Datum::Text(<Option<String> as Decode<Postgres>>::decode(value_ref).unwrap())
            }
            "UUID" => Datum::Uuid(<Option<Uuid> as Decode<Postgres>>::decode(value_ref).unwrap()),
            "TIMESTAMPTZ" => Datum::Timestamp(
                <Option<DateTime<Utc>> as Decode<Postgres>>::decode(value_ref).unwrap(),
            ),
            _ => return Err(ParseError::UnknownType),
        })
    }

    pub async fn delete(&self, app_db: &mut AppDbClient) -> sqlx::Result<()> {
        query!("delete from fields where id = $1", self.id)
            .execute(app_db.get_conn())
            .await?;
        Ok(())
    }

    pub fn belonging_to_portal(portal_id: Uuid) -> BelongingToPortalQuery {
        BelongingToPortalQuery { portal_id }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct BelongingToPortalQuery {
    portal_id: Uuid,
}

impl BelongingToPortalQuery {
    pub fn with_id(self, id: Uuid) -> WithIdQuery {
        WithIdQuery {
            id,
            portal_id: self.portal_id,
        }
    }

    pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Field>, sqlx::Error> {
        query_as!(
            Field,
            r#"
select
    id,
    name,
    table_label,
    presentation as "presentation: Json<Presentation>",
    table_width_px,
    ordinality
from fields
where portal_id = $1
order by ordinality
"#,
            self.portal_id
        )
        .fetch_all(&mut *app_db.conn)
        .await
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct WithIdQuery {
    id: Uuid,
    portal_id: Uuid,
}

impl WithIdQuery {
    pub async fn fetch_one(self, app_db: &mut AppDbClient) -> Result<Field, sqlx::Error> {
        query_as!(
            Field,
            r#"
select
    id,
    name,
    table_label,
    presentation as "presentation: Json<Presentation>",
    table_width_px,
    ordinality
from fields
where portal_id = $1 and id = $2
"#,
            self.portal_id,
            self.id,
        )
        .fetch_one(&mut *app_db.conn)
        .await
    }
}

#[derive(Builder, Clone, Debug)]
pub struct InsertableField {
    portal_id: Uuid,
    name: String,
    #[builder(default)]
    table_label: Option<String>,
    presentation: Presentation,
    #[builder(default = 200)]
    table_width_px: i32,
}

impl InsertableField {
    pub async fn insert(self, app_db: &mut AppDbClient) -> Result<Field, sqlx::Error> {
        query_as!(
            Field,
            r#"
insert into fields
(
    portal_id,
    name,
    table_label,
    presentation,
    table_width_px,
    ordinality
) (
    select
        $1 as portal_id,
        $2 as name,
        $3 as table_label,
        $4 as presentation,
        $5 as table_width_px,
        coalesce(max(prev.ordinality), 0) + 1 as ordinality
    from fields as prev
    where prev.portal_id = $1
)
returning
    id,
    name,
    table_label,
    presentation as "presentation: Json<Presentation>",
    table_width_px,
    ordinality
"#,
            self.portal_id,
            self.name,
            self.table_label,
            Json::<_>(self.presentation) as Json<Presentation>,
            self.table_width_px,
        )
        .fetch_one(&mut *app_db.conn)
        .await
    }
}

impl InsertableFieldBuilder {
    pub fn default_from_attr(attr: &PgAttribute) -> Self {
        Self {
            name: Some(attr.attname.clone()),
            presentation: Presentation::default_from_attr(attr),
            ..Self::default()
        }
    }
}

#[derive(Builder, Clone, Debug)]
pub struct Update {
    id: Uuid,
    #[builder(default, setter(strip_option))]
    table_label: Option<Option<String>>,
    #[builder(default, setter(strip_option))]
    presentation: Option<Presentation>,
    #[builder(default, setter(strip_option))]
    table_width_px: Option<i32>,
    #[builder(default, setter(strip_option))]
    ordinality: Option<f64>,
}

impl Update {
    pub async fn execute(self, app_db: &mut AppDbClient) -> Result<(), sqlx::Error> {
        // TODO: consolidate statements instead of using transaction
        let mut tx = app_db.get_conn().begin().await?;

        if let Some(table_label) = self.table_label {
            query!(
                "update fields set table_label = $1 where id = $2",
                table_label,
                self.id
            )
            .execute(&mut *tx)
            .await?;
        }
        if let Some(presentation) = self.presentation {
            query!(
                "update fields set presentation = $1 where id = $2",
                Json::<_>(presentation) as Json<Presentation>,
                self.id
            )
            .execute(&mut *tx)
            .await?;
        }
        if let Some(table_width_px) = self.table_width_px {
            query!(
                "update fields set table_width_px = $1 where id = $2",
                table_width_px,
                self.id
            )
            .execute(&mut *tx)
            .await?;
        }
        if let Some(ordinality) = self.ordinality {
            query!(
                "update fields set ordinality = $1 where id = $2",
                ordinality,
                self.id
            )
            .execute(&mut *tx)
            .await?;
        }

        tx.commit().await?;

        Ok(())
    }
}

/// Error when parsing a sqlx value to JSON
#[derive(Debug, Error)]
pub enum ParseError {
    // TODO: can this be removed?
    #[error("incompatible json type")]
    BadJsonType,

    #[error("field not found in row")]
    FieldNotFound,
    #[error("unknown postgres type")]
    UnknownType,
}
