use chrono::{DateTime, Utc};
use derive_builder::Builder;
use interim_pgtypes::pg_attribute::PgAttribute;
use serde::{Deserialize, Serialize};
use sqlx::{Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query_as};
use thiserror::Error;
use uuid::Uuid;

use crate::client::AppDbClient;
use crate::datum::Datum;
use crate::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,
}

impl Field {
    /// Constructs a brand new field to be inserted into the application db.
    pub fn insert() -> InsertableFieldBuilder {
        InsertableFieldBuilder::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,
        })
    }

    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();
        dbg!(&ty);
        Ok(match ty {
            "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 fn belonging_to_portal(portal_id: Uuid) -> BelongingToPortalQuery {
        BelongingToPortalQuery { portal_id }
    }
}

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

impl BelongingToPortalQuery {
    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: sqlx::types::Json<Presentation>",
    table_width_px
from fields
where portal_id = $1
"#,
            self.portal_id
        )
        .fetch_all(&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)
values ($1, $2, $3, $4, $5)
returning
    id,
    name,
    table_label,
    presentation as "presentation: sqlx::types::Json<Presentation>",
    table_width_px
"#,
            self.portal_id,
            self.name,
            self.table_label,
            sqlx::types::Json::<_>(self.presentation) as sqlx::types::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()
        }
    }
}

/// 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,
}
