#![allow(non_snake_case)]

/*
 * Author: Ruben Fiszel
 * Copyright: Windmill Labs, Inc 2023
 * This file and its contents are licensed under the AGPLv3 License.
 * Please see the included NOTICE for copyright information and
 * LICENSE-AGPL for a copy of the license.
 */

use axum::{
    extract::Query,
    middleware::Next,
    response::{IntoResponse, Response},
    routing::get,
    Extension, Router,
};
use bytes::{BufMut, BytesMut};
use hyper::{header, http::HeaderValue, Request, StatusCode};
use mime_guess::mime;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sql_builder::SqlBuilder;
use windmill_common::error::{Error, Result};

#[cfg(feature = "enterprise")]
use axum::{extract::Path, Json};

#[cfg(feature = "enterprise")]
use windmill_common::utils::not_found_if_none;

use crate::db::DB;

lazy_static::lazy_static! {
    static ref SCIM_TOKEN: Option<String> = std::env::var("SCIM_TOKEN")
        .ok();
}

#[cfg(feature = "enterprise")]
pub fn global_service() -> Router {
    Router::new()
        .route("/Users", get(get_users).post(create_user))
        .route("/Groups", get(get_groups).post(create_group))
        .route(
            "/Groups/:id",
            get(get_group)
                .put(update_group)
                .patch(update_group)
                .delete(delete_group),
        )
}

#[cfg(not(feature = "enterprise"))]
pub fn global_service() -> Router {
    Router::new().route("/Users", get(get_users))
}

#[derive(Debug, Clone, Copy, Default)]
pub struct JsonScim<T>(pub T);

pub async fn has_scim_token<B>(request: Request<B>, next: Next<B>) -> Response {
    let header = request.headers().get("Authorization");
    if let Some(header) = header {
        if let Ok(header) = header.to_str() {
            if header.starts_with("Bearer ") {
                let token = header.trim_start_matches("Bearer ");
                if let Some(scim_token) = SCIM_TOKEN.as_ref() {
                    if token == scim_token {
                        return next.run(request).await;
                    }
                }
            }
        }
    }
    return (
        StatusCode::UNAUTHORIZED,
        [(
            header::CONTENT_TYPE,
            HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
        )],
        "Unauthorized",
    )
        .into_response();
}

impl<T> IntoResponse for JsonScim<T>
where
    T: Serialize,
{
    fn into_response(self) -> Response {
        // Use a small initial capacity of 128 bytes like serde_json::to_vec
        // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189
        let mut buf = BytesMut::with_capacity(128).writer();
        match serde_json::to_writer(&mut buf, &self.0) {
            Ok(()) => (
                [(
                    header::CONTENT_TYPE,
                    HeaderValue::from_static("application/scim+json"),
                )],
                buf.into_inner().freeze(),
            )
                .into_response(),
            Err(err) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                [(
                    header::CONTENT_TYPE,
                    HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
                )],
                err.to_string(),
            )
                .into_response(),
        }
    }
}

#[derive(Serialize, Debug)]
struct User {
    id: String,
    userName: String,
    active: bool,
}
pub fn resource_response<S>(schema: &str, resources: Vec<S>) -> JsonScim<serde_json::Value>
where
    S: Serialize,
{
    return JsonScim(json!({
        "schemas": [schema],
        "totalResults": resources.len(),
        "Resources": resources,
        "startIndex": 1,
        "itemsPerPage": 100,
    }));
}

#[derive(Deserialize)]
pub struct ScimQuery {
    startIndex: Option<u32>,
    count: Option<u32>,
    filter: Option<String>,
}

pub async fn get_users(
    Extension(db): Extension<DB>,
    Query(query): Query<ScimQuery>,
) -> Result<JsonScim<serde_json::Value>> {
    let mut sqlb = SqlBuilder::select_from("usr")
        .fields(&["email"])
        .limit(query.count.unwrap_or(100000))
        .offset(query.startIndex.map(|x| x - 1).unwrap_or(0))
        .clone();

    tracing::info!("SCIM filter: {:?}", query.filter);

    if let Some(filter) = query.filter {
        let filter = filter
            .replace("userName", "email")
            .replace("eq", "=")
            .replace("\"", "'");
        sqlb.and_where(&filter);
    }

    let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
    let users = sqlx::query_scalar(&sql)
        .fetch_all(&db)
        .await?
        .into_iter()
        .map(|x: String| User { id: x.clone(), userName: x, active: true })
        .collect();
    tracing::info!("SCIM users: {:?}", users);
    Ok(resource_response(
        "urn:ietf:params:scim:api:messages:2.0:ListResponse",
        users,
    ))
}

#[cfg(feature = "enterprise")]
#[derive(Deserialize, Debug)]
pub struct CreateUser {
    userName: String,
}
#[cfg(feature = "enterprise")]
pub async fn create_user(
    Extension(db): Extension<DB>,
    Json(body): Json<CreateUser>,
) -> Result<JsonScim<serde_json::Value>> {
    tracing::info!("SCIM creating user: {:?}", body);
    sqlx::query!(
        "INSERT INTO password (email, login_type, verified) VALUES ($1, 'saml', true) ON CONFLICT DO NOTHING",
        body.userName,
    ).execute(&db).await?;
    Ok(JsonScim(json!({
        "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
        "id": body.userName,
        "userName": body.userName,
        "active": true
    })))
}

#[cfg(feature = "enterprise")]
pub async fn get_groups(
    Extension(db): Extension<DB>,
    Query(query): Query<ScimQuery>,
) -> Result<JsonScim<serde_json::Value>> {
    let sqlb = SqlBuilder::select_from("instance_group")
        .fields(&["name"])
        .limit(query.count.unwrap_or(100000))
        .offset(query.startIndex.map(|x| x - 1).unwrap_or(0))
        .clone();

    let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
    let users = sqlx::query_scalar(&sql)
        .fetch_all(&db)
        .await?
        .into_iter()
        .map(|x: String| User { id: x.clone(), userName: x, active: true })
        .collect();
    Ok(resource_response(
        "urn:ietf:params:scim:api:messages:2.0:ListResponse",
        users,
    ))
}

// {
//     "schemas": [],
//     "id": "abf4dd94-a4c0-4f67-89c9-76b03340cb9b",
//     "displayName": "Test SCIMv2",
//     "members": [],
//     "meta": {
//         "resourceType": "Group"
//     }
// }

#[cfg(feature = "enterprise")]
#[derive(Serialize)]
struct Group {
    name: String,
    emails: Option<Vec<String>>,
}
#[cfg(feature = "enterprise")]
fn group_response(group: Group) -> JsonScim<serde_json::Value> {
    let json = json!({
        "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
        "displayName": group.name,
        "id": convert_name(&group.name),
        "members": group.emails.unwrap_or_default().into_iter().map(|x| json!({"value": x, "display": x})).collect::<Vec<serde_json::Value>>(),
        "meta": {
            "resourceType": "Group"
        }
    });
    // tracing::info!("SCIM group: {:?}", json);
    return JsonScim(json);
}

#[cfg(feature = "enterprise")]
pub async fn get_group(
    Extension(db): Extension<DB>,
    Path(id): Path<String>,
) -> Result<JsonScim<serde_json::Value>> {
    let group= sqlx::query_as!(
        Group,
        "SELECT name, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup WHERE name = $1 GROUP BY name",
        id
    )
    .fetch_optional(&db)
    .await?;
    let group = not_found_if_none(group, "Group", id)?;
    Ok(group_response(group))
}

// {
//     "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
//     "displayName": "Test SCIMv2",
//     "members": []
// }

#[cfg(feature = "enterprise")]
#[derive(Deserialize, Debug)]
pub struct CreateGroup {
    pub displayName: String,
    pub members: Vec<Member>,
}

#[cfg(feature = "enterprise")]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Member {
    pub value: String,
    pub display: String,
}

#[cfg(feature = "enterprise")]
pub async fn create_group(
    Extension(db): Extension<DB>,
    Json(body): Json<CreateGroup>,
) -> Result<JsonScim<serde_json::Value>> {
    tracing::info!("SCIM creating group: {:?}", body);
    let mut tx: sqlx::Transaction<'_, sqlx::Postgres> = db.begin().await?;
    sqlx::query!(
        "INSERT INTO instance_group (name) VALUES ($1) ON CONFLICT DO NOTHING",
        convert_name(&body.displayName)
    )
    .execute(&mut *tx)
    .await?;
    for member in &body.members {
        sqlx::query!(
            "INSERT INTO email_to_igroup (email, igroup) VALUES ($1, $2) ON CONFLICT DO NOTHING",
            convert_name(&member.display),
            body.displayName,
        )
        .execute(&mut *tx)
        .await?;
    }
    tx.commit().await?;
    Ok(group_response(Group {
        name: body.displayName.clone(),
        emails: Some(
            body.members
                .clone()
                .into_iter()
                .map(|x| x.display.clone())
                .collect(),
        ),
    }))
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Operation {
    pub op: String,
    pub path: String,
    pub value: Option<serde_json::Value>,
}

#[cfg(feature = "enterprise")]
#[derive(Deserialize, Debug)]
pub struct UpdateGroup {
    pub schemas: Vec<String>,
    pub displayName: Option<String>,
    pub members: Option<Vec<Member>>,
    pub Operations: Option<Vec<Operation>>,
}

#[cfg(feature = "enterprise")]
pub async fn update_group(
    Extension(db): Extension<DB>,
    Path(id): Path<String>,
    Json(body): Json<UpdateGroup>,
) -> Result<JsonScim<serde_json::Value>> {
    tracing::info!("SCIM updating group: {:?}", body);
    let mut tx: sqlx::Transaction<'_, sqlx::Postgres> = db.begin().await?;
    if body.schemas.len() == 1 {
        let schema = body.schemas.get(0).unwrap();
        if schema == "urn:ietf:params:scim:schemas:core:2.0:Group" {
            sqlx::query!("DELETE FROM email_to_igroup WHERE igroup = $1", id)
                .execute(&mut *tx)
                .await?;

            if let Some(members) = body.members.clone() {
                for m in members {
                    sqlx::query!(
                        "INSERT INTO email_to_igroup (email, igroup) VALUES ($1, $2) ON CONFLICT DO NOTHING",
                        m.display,
                        id
                    )
                    .execute(&mut *tx)
                    .await?;
                }
                tx.commit().await?;
                Ok(group_response(Group {
                    name: body.displayName.unwrap_or_default(),
                    emails: Some(
                        body.members
                            .unwrap_or_default()
                            .clone()
                            .into_iter()
                            .map(|x| x.display.clone())
                            .collect(),
                    ),
                }))
            } else {
                Err(Error::BadRequest("expected members".to_string()))
            }
        } else {
            Err(Error::BadRequest("Invalid schemas".to_string()))
        }
    } else {
        Err(Error::BadRequest("Invalid schemas".to_string()))
    }
}

#[cfg(feature = "enterprise")]
pub async fn delete_group(Extension(db): Extension<DB>, Path(id): Path<String>) -> Result<()> {
    tracing::info!("SCIM delete group: {:?}", id);
    sqlx::query!("DELETE FROM email_to_igroup WHERE igroup = $1", id)
        .execute(&db)
        .await?;
    sqlx::query!("DELETE FROM instance_group WHERE name = $1", id)
        .execute(&db)
        .await?;
    Ok(())
}

#[cfg(feature = "enterprise")]
fn convert_name(name: &str) -> String {
    name.replace(" ", "_").to_lowercase()
}
