use crate::permissions::Perm;
use crate::repository::TreePath;
use crate::{config::AcceptJson, get_user_id_strict, Config, Redirect};
use axum::{
    debug_handler,
    extract::{Form, Path, State},
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::{get, post},
    Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use axum_extra::TypedHeader;
use diesel::dsl::now;
use diesel::expression::AsExpression;
use diesel::upsert::excluded;
use diesel::{BoolExpressionMethods, ExpressionMethods, Insertable, OptionalExtension, QueryDsl};
use diesel_async::RunQueryDsl;
use serde_derive::*;
use tracing::*;

pub fn router() -> Router<Config> {
    Router::new()
        .route("/", get(admin))
        .route("/permission", post(perm))
        .route("/tag", post(tag))
        .fallback(crate::fallback)
}

#[derive(Debug, Serialize)]
struct Admin {
    token: Option<String>,
    owner: String,
    repo: String,
    is_private: bool,
    permissions: Vec<LoginPerm>,
    tags: Vec<Tag>,
    login: String,
}

#[derive(Debug, Serialize)]
struct LoginPerm {
    login: String,
    perm: i64,
}

#[derive(Debug, Serialize)]
struct Tag {
    name: String,
    color: i32,
    id: uuid::Uuid,
}

#[debug_handler]
pub async fn admin(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    tree: crate::repository::RepoPath,
) -> Result<Response, crate::Error> {
    let (uid, login) = crate::get_user_login_strict(&jar, &config).await?;
    let mut db = config.db.get().await?;

    let id = if let Some(id) = r::repositories
        .inner_join(u::users)
        .filter(u::login.eq(&tree.owner))
        .filter(r::name.eq(&tree.repo))
        .filter(u::id.eq(uid))
        .select(r::id)
        .get_result::<uuid::Uuid>(&mut db)
        .await
        .optional()?
    {
        id
    } else {
        return Ok((StatusCode::FORBIDDEN, "{}").into_response());
    };

    use crate::db::permissions::dsl as p;
    use crate::db::repositories::dsl as r;
    use crate::db::tags::dsl as t;
    use crate::db::users::dsl as u;

    let mut is_private = true;
    let permissions = p::permissions
        .inner_join(u::users)
        .inner_join(r::repositories)
        .filter(p::repo_id.eq(id))
        .filter(p::end_date.is_null().or(p::end_date.gt(now)))
        .select((u::id, u::login, p::perm))
        .order_by(p::start_date.desc())
        .get_results::<(uuid::Uuid, String, i64)>(&mut db)
        .await?
        .into_iter()
        .filter_map(|(id, login, perm)| {
            debug!("{:?} {:?} {:?}", id, login, perm);
            if id.is_nil() {
                let perm = Perm::from_bits(perm).unwrap();
                is_private = !perm.contains(Perm::READ);
                None
            } else {
                Some(LoginPerm { login, perm })
            }
        })
        .collect();

    let tags = t::tags
        .filter(t::repository_id.eq(id))
        .select((t::id, t::name, t::color))
        .get_results::<(uuid::Uuid, String, i32)>(&mut db)
        .await?
        .into_iter()
        .map(|(id, name, color)| Tag { id, name, color })
        .collect();

    let token_ = token.authenticity_token().ok();

    Ok((
        token,
        Json(Admin {
            token: token_,
            owner: tree.owner,
            repo: tree.repo,
            is_private,
            permissions,
            tags,
            login,
        }),
    )
        .into_response())
}

#[derive(Debug, Deserialize)]
struct PermForm {
    token: String,
    #[serde(default)]
    login: Option<String>,
}

#[debug_handler]
async fn perm(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
    Path(tree): Path<TreePath>,
    body: bytes::Bytes,
) -> Result<Response, crate::Error> {
    let form: PermForm = serde_urlencoded::from_bytes(&body)?;
    token.verify(&form.token)?;
    let mut perm = Perm::empty();
    for (k, v) in url::form_urlencoded::parse(&body) {
        if k.starts_with("p") && v == "on" {
            let (_, b) = k.split_at(1);
            if let Some(b) = b.parse().ok().and_then(|x: i32| Perm::from_bits(1 << x)) {
                perm |= b
            }
        }
    }

    debug!("new {:?}", perm);
    let uid = get_user_id_strict(&jar)?;
    let mut db = config.db.get().await?;

    use crate::db::permissions::dsl as p;
    use crate::db::users::dsl as u;
    use diesel::sql_types::{BigInt, Uuid};

    let (id, _) = crate::repository::repository_id(
        &mut db,
        &tree.owner,
        &tree.repo,
        Some(uid),
        Perm::EDIT_PERMISSIONS,
    )
    .await?;

    if let Some(l) = form.login {
        if l != tree.owner {
            u::users
                .filter(u::login.eq(&l))
                .select((
                    u::id,
                    &AsExpression::<Uuid>::as_expression(id),
                    &AsExpression::<BigInt>::as_expression(perm.bits()),
                ))
                .insert_into(p::permissions)
                .into_columns((p::user_id, p::repo_id, p::perm))
                .on_conflict((p::user_id, p::repo_id))
                .do_update()
                .set(p::user_id.eq(excluded(p::user_id)))
                .execute(&mut db)
                .await?;
        }
    } else {
        diesel::insert_into(p::permissions)
            .values((
                p::user_id.eq(uuid::Uuid::nil()),
                p::repo_id.eq(id),
                p::perm.eq(perm.bits()),
            ))
            .on_conflict((p::user_id, p::repo_id))
            .do_update()
            .set(p::user_id.eq(excluded(p::user_id)))
            .execute(&mut db)
            .await?;
    }

    if let Some(TypedHeader(AcceptJson(true))) = accept_json {
        Ok(Json(()).into_response())
    } else {
        Ok(Redirect::to(&format!("/{}/{}/admin", tree.owner, tree.repo)).into_response())
    }
}

#[derive(Debug, Deserialize)]
struct TagForm {
    token: String,
    id: Option<uuid::Uuid>,
    tag_name: String,
    colorp: i32,
}

#[debug_handler]
async fn tag(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
    Path(tree): Path<TreePath>,
    Form(form): Form<TagForm>,
) -> Result<Response, crate::Error> {
    token.verify(&form.token)?;
    debug!("new {:?}", tree);
    let uid = get_user_id_strict(&jar)?;
    let mut db = config.db.get().await?;
    let (id, _) = crate::repository::repository_id(
        &mut db,
        &tree.owner,
        &tree.repo,
        Some(uid),
        Perm::EDIT_TAGS,
    )
    .await?;

    use crate::db::tags::dsl as t;
    if let Some(tag_id) = form.id {
        diesel::update(t::tags.find(tag_id).filter(t::repository_id.eq(id)))
            .set((t::name.eq(form.tag_name), t::color.eq(form.colorp)))
            .execute(&mut db)
            .await?;
    } else {
        diesel::insert_into(t::tags)
            .values((
                t::repository_id.eq(id),
                t::name.eq(form.tag_name),
                t::color.eq(form.colorp),
            ))
            .execute(&mut db)
            .await?;
    }

    if let Some(TypedHeader(AcceptJson(true))) = accept_json {
        Ok(Json(()).into_response())
    } else {
        Ok(Redirect::to(&format!("/{}/{}/admin", tree.owner, tree.repo)).into_response())
    }
}