use std::collections::HashMap;

use axum::{
    Json,
    extract::{Path, State},
    response::{IntoResponse as _, Response},
};
use phono_backends::{pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass};
use phono_models::{
    accessors::{Accessor, Actor, portal::PortalAccessor},
    field::Field,
};
use phono_pestgros::{Datum, Expr, FnArgs, InfixOp, QueryFragment, escape_identifier};
use serde::{Deserialize, Serialize};
use sqlx::{
    Postgres, QueryBuilder,
    postgres::{PgRow, types::Oid},
};
use uuid::Uuid;
use validator::Validate;

use crate::{
    app::AppDbConn,
    errors::AppError,
    extractors::ValidatedForm,
    field_info::TableFieldInfo,
    user::CurrentUser,
    workspace_pooler::{RoleAssignment, WorkspacePooler},
};

#[derive(Clone, Debug, Deserialize)]
pub(super) struct PathParams {
    portal_id: Uuid,
    rel_oid: u32,
    workspace_id: Uuid,
}

#[derive(Debug, Deserialize, Validate)]
pub(super) struct FormBody {
    #[serde(default)]
    subfilter: String,
}

const FRONTEND_ROW_LIMIT: i64 = 1000;

/// HTTP GET handler for an API endpoint returning a JSON encoding of portal
/// data to display in a table or similar form. If the `subfilter` URL parameter
/// is specified, it is `&&`-ed with the portal's stored filter.
///
/// Only queries up to the first [`FRONTEND_ROW_LIMIT`] rows.
pub(super) async fn get(
    State(mut workspace_pooler): State<WorkspacePooler>,
    AppDbConn(mut app_db): AppDbConn,
    CurrentUser(user): CurrentUser,
    Path(PathParams {
        portal_id,
        rel_oid,
        workspace_id,
    }): Path<PathParams>,
    ValidatedForm(form): ValidatedForm<FormBody>,
) -> Result<Response, AppError> {
    let mut workspace_client = workspace_pooler
        .acquire_for(workspace_id, RoleAssignment::User(user.id))
        .await?;

    let rel = PgClass::with_oid(Oid(rel_oid))
        .fetch_one(&mut workspace_client)
        .await?;

    let portal = PortalAccessor::default()
        .id(portal_id)
        .as_actor(Actor::User(user.id))
        .verify_workspace_id(workspace_id)
        .verify_rel_oid(Oid(rel_oid))
        .verify_rel_permissions([PgPrivilegeType::Select])
        .using_rel(&rel)
        .using_workspace_client(&mut workspace_client)
        .using_app_db(&mut app_db)
        .fetch_one()
        .await?;

    let attrs = PgAttribute::all_for_rel(portal.class_oid)
        .fetch_all(&mut workspace_client)
        .await?;
    let pkey_attrs = PgAttribute::pkeys_for_rel(portal.class_oid)
        .fetch_all(&mut workspace_client)
        .await?;

    let fields: Vec<TableFieldInfo> = {
        let fields: Vec<Field> = Field::belonging_to_portal(portal.id)
            .fetch_all(&mut app_db)
            .await?;
        let mut field_info: Vec<TableFieldInfo> = Vec::with_capacity(fields.len());
        for field in fields {
            if let Some(attr) = attrs.iter().find(|attr| attr.attname == field.name) {
                field_info.push(TableFieldInfo {
                    field,
                    column_present: true,
                    has_default: attr.atthasdef,
                    not_null: attr.attnotnull.unwrap_or_default(),
                });
            }
        }
        field_info
    };

    let main_data_query = SelectQuery {
        selection: QueryFragment::from_sql(
            &pkey_attrs
                .iter()
                .chain(attrs.iter())
                .map(|attr| {
                    format!(
                        "main.{ident_esc}",
                        ident_esc = escape_identifier(&attr.attname)
                    )
                })
                .collect::<Vec<_>>()
                .join(", "),
        ),
        source: QueryFragment::from_sql(&format!(
            "{ident_esc} as main",
            ident_esc = rel.get_identifier()
        )),
        filters: QueryFragment::join(
            [
                into_safe_filter_sql(&portal.filter),
                into_safe_filter_sql(&form.subfilter),
            ]
            .into_iter()
            .flatten(),
            QueryFragment::from_sql(" and "),
        ),
        order: QueryFragment::from_sql("_id"),
        limit: QueryFragment::from_param(Datum::Numeric(Some(FRONTEND_ROW_LIMIT.into()))),
    };

    let count_query = SelectQuery {
        selection: QueryFragment::from_sql("count(*)"),
        order: QueryFragment::empty(),
        limit: QueryFragment::empty(),
        ..main_data_query.clone()
    };

    // TODO: Consider running queries in a transaction to improve consistency.

    let rows: Vec<PgRow> = QueryBuilder::from(main_data_query)
        .build()
        .fetch_all(workspace_client.get_conn())
        .await?;
    let count: i64 = QueryBuilder::from(count_query)
        .build_query_scalar()
        .fetch_one(workspace_client.get_conn())
        .await?;

    #[derive(Serialize)]
    struct DataRow {
        pkey: String,
        data: Vec<Datum>,
    }

    let mut data_rows: Vec<DataRow> = vec![];
    let mut pkeys: Vec<String> = vec![];
    for row in rows.iter() {
        let mut pkey_values: HashMap<String, Datum> = HashMap::new();
        for attr in pkey_attrs.clone() {
            let field = Field::default_from_attr(&attr)
                .ok_or(anyhow::anyhow!("unsupported primary key column type"))?;
            pkey_values.insert(field.name.clone(), field.get_datum(row)?);
        }
        let pkey = serde_json::to_string(&pkey_values)?;
        pkeys.push(pkey.clone());
        let mut row_data: Vec<Datum> = vec![];
        for field in fields.iter() {
            row_data.push(field.field.get_datum(row)?);
        }
        data_rows.push(DataRow {
            pkey,
            data: row_data,
        });
    }

    #[derive(Serialize)]
    struct ResponseBody {
        fields: Vec<TableFieldInfo>,
        pkeys: Vec<String>,
        rows: Vec<DataRow>,
        count: i64,
    }
    Ok(Json(ResponseBody {
        count,
        rows: data_rows,
        fields,
        pkeys,
    })
    .into_response())
}

/// Helper type to make it easier to build and reason about multiple related SQL
/// queries.
#[derive(Clone, Debug)]
pub struct SelectQuery {
    /// Query fragment following (not including) "select ".
    pub selection: QueryFragment,

    /// Query fragment following (not including) "from ".
    pub source: QueryFragment,

    /// Query fragment following (not including) "where ", or empty if not
    /// applicable.
    pub filters: QueryFragment,

    /// Query fragment following (not including) "order by ", or empty if not
    /// applicable.
    pub order: QueryFragment,

    /// Query fragment following (not including) "limit ", or empty if not
    /// applicable.
    pub limit: QueryFragment,
}

impl From<SelectQuery> for QueryFragment {
    fn from(value: SelectQuery) -> Self {
        let mut result = QueryFragment::from_sql("select ");
        result.push(value.selection);
        result.push(QueryFragment::from_sql(" from "));
        result.push(value.source);
        if !value.filters.is_empty() {
            result.push(QueryFragment::from_sql(" where "));
            result.push(value.filters);
        }
        if !value.order.is_empty() {
            result.push(QueryFragment::from_sql(" order by "));
            result.push(value.order);
        }
        if !value.limit.is_empty() {
            result.push(QueryFragment::from_sql(" limit "));
            result.push(value.limit);
        }
        result
    }
}

impl From<SelectQuery> for QueryBuilder<'_, Postgres> {
    fn from(value: SelectQuery) -> Self {
        QueryFragment::from(value).into()
    }
}

/// Users are allowed to put any text they want in the `Portal.filter` field.
/// This needs to be either transformed into a SQL expression that we trust to
/// be injected into a `WHERE` clause or disregarded if no such expression can
/// be generated.
///
/// Given the known (not to mention unknown) limitations of [`phono_pestgros`]'s
/// homegrown PostgreSQL grammar, trying to positively establish the correctness
/// and trustworthiness of a filter expression exactly as written would be
/// impractical and dangerous. Instead, we validate the syntax tree as parsed by
/// [`phono_pestgros`] (even if the parsing logic isn't spec-compliant), and use
/// the SQL expression only after it has been converted back from parsed form.
fn into_safe_filter_sql(expr_text: &str) -> Option<QueryFragment> {
    if let Ok(expr) = Expr::try_from(expr_text)
        && is_safe_filter_expr(&expr)
    {
        Some(expr.into())
    } else {
        None
    }
}

fn is_safe_filter_expr(expr: &Expr) -> bool {
    match expr {
        Expr::Literal(_) | &Expr::ObjName(_) => true,
        Expr::Infix { lhs, op, rhs } => {
            is_safe_filter_infix_op(op) && is_safe_filter_expr(lhs) && is_safe_filter_expr(rhs)
        }
        Expr::FnCall { name, args } => match name
            .iter()
            .map(|s| s.as_str())
            .collect::<Vec<_>>()
            .as_slice()
        {
            // Math:
            &["abs"]
            | &["ceil"]
            | &["floor"]
            | &["ln"]
            | &["log"]
            | &["mod"]
            | &["power"]
            | &["pi"]
            | &["round"]
            | &["sqrt"]
            | &["trunc"]
            // Timestamp:
            | &["now"]
            | &["to_timestamp"]
            // Strings:
            | &["upper"]
            | &["lower"]
            | &["replace"]
            | &["btrim"]
            | &["length"]
            | &["concat_ws"]
            | &["lpad"]
            | &["rpad"]
            | &["regexp_replace"]
            | &["regexp_matches"]
            | &["to_char"]
            // Misc:
            | &["any"] => match args {
                FnArgs::Exprs {
                    distinct_flag,
                    exprs,
                } => !distinct_flag && exprs.iter().all(is_safe_filter_expr),
                _ => false,
            },
            _ => false,
        },
        Expr::Not(inner) => is_safe_filter_expr(inner),
        Expr::Nullness {
            is_null: _,
            expr: inner,
        } => is_safe_filter_expr(inner),
        Expr::Array(arr_items) => arr_items.iter().all(is_safe_filter_expr),
        _ => false,
    }
}

fn is_safe_filter_infix_op(op: &InfixOp) -> bool {
    match op {
        InfixOp::Add
            | InfixOp::Concat
            | InfixOp::Div
            | InfixOp::Mult
            | InfixOp::Sub
            // Boolean:
            | InfixOp::And
            | InfixOp::Or
            | InfixOp::Eq
            | InfixOp::Gt
            | InfixOp::Gte
            | InfixOp::Lt
            | InfixOp::Lte
            | InfixOp::Neq
            // Miscellaneous:
            | InfixOp::Cast => true,
        InfixOp::WithCmpModifierAny(inner) | InfixOp::WithCmpModifierAll(inner) => is_safe_filter_infix_op(inner),
        _ => false
    }
}
