use crate::permissions::Perm;
use crate::*;
use crate::{db, get_user_id_strict, get_user_login_strict, Config, Repo, StatusCode};
use axum::{
    debug_handler,
    extract::{ConnectInfo, Form, State},
    response::{IntoResponse, Redirect, Response},
    routing::{get, post},
    Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use diesel::dsl::sql;
use diesel::sql_types::Bool;
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::scoped_futures::ScopedFutureExt;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use thrussh_keys::PublicKeyBase64;

pub fn router() -> Router<Config> {
    Router::new()
        .route("/", get(settings))
        .route("/ssh", get(ssh).post(ssh_post))
        .route("/ssh/delete", post(ssh_delete))
        .route("/ssh/add", post(ssh_add))
        .route("/repo/delete", post(delete_repo))
        .route("/repo/add", post(create_repo))
        .route("/delete", post(delete))
        .fallback(crate::fallback)
}

#[derive(Debug, Serialize)]
struct Settings<T> {
    login: String,
    token: Option<String>,
    #[serde(flatten)]
    content: T,
}

#[derive(Debug, Serialize)]
struct UserSettings {
    repos: BTreeMap<String, Repo>,
    is_owner: bool,
}

#[derive(Debug, Serialize)]
struct Ssh {
    ssh_keys: BTreeMap<uuid::Uuid, String>,
}

#[debug_handler]
async fn settings(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
) -> Result<Response, crate::Error> {
    use db::repositories::dsl as r;
    let (uid, login) = crate::get_user_login_strict(&jar, &config).await?;
    let mut db = config.db.get().await?;

    let repos = r::repositories
        .filter(r::owner.eq(uid))
        .order_by(r::name)
        .select(r::name)
        .get_results::<String>(&mut db)
        .await?;

    debug!("repos = {:?}", repos);

    let resp = Settings {
        login,
        token: token.authenticity_token().ok(),
        content: UserSettings {
            repos: repos
                .into_iter()
                .map(|r| (r.clone(), Repo { private: false }))
                .collect(),
            is_owner: true,
        },
    };
    debug!("{:?}", resp);
    Ok((token, Json(resp)).into_response())
}

#[debug_handler]
async fn ssh(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
) -> Result<Response, crate::Error> {
    use db::publickeys::dsl as publickeys;
    use db::users::dsl as u;

    let uid = get_user_id_strict(&jar)?;
    let mut db = config.db.get().await?;
    let user = u::users
        .select(u::login)
        .filter(u::id.eq(uid))
        .filter(u::email_is_invalid.is_null())
        .get_result::<String>(&mut db)
        .await?;

    let ssh_keys = publickeys::publickeys
        .filter(publickeys::user_id.eq(uid))
        .order_by(publickeys::publickey)
        .select((publickeys::id, publickeys::publickey))
        .get_results::<(uuid::Uuid, String)>(&mut db)
        .await?;

    let resp = Settings {
        login: user,
        token: token.authenticity_token().ok(),
        content: Ssh {
            ssh_keys: ssh_keys.into_iter().collect(),
        },
    };
    debug!("{:?}", resp);
    Ok((token, Json(resp)).into_response())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SshDelete {
    delete: uuid::Uuid,
    token: String,
}

#[debug_handler]
async fn ssh_delete(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    del: Form<SshDelete>,
) -> Result<Response, crate::Error> {
    token.verify(&del.token)?;
    use db::publickeys::dsl as publickeys;

    let uid = get_user_id_strict(&jar)?;
    diesel::delete(
        publickeys::publickeys
            .find(del.delete)
            .filter(publickeys::user_id.eq(uid)),
    )
    .execute(&mut config.db.get().await?)
    .await?;

    Ok(Redirect::to("/settings/ssh").into_response())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SshAdd {
    key: String,
    token: String,
}

use lazy_static::*;

lazy_static! {
    static ref SSH_KEY: regex::Regex =
        regex::Regex::new(r#"\s*((ssh-\S+)\s+(?P<key>.*)\s+(\S+))"#).unwrap();
}

#[debug_handler]
async fn ssh_add(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    add: Form<SshAdd>,
) -> Result<Response, crate::Error> {
    token.verify(&add.token)?;
    let uid = get_user_id_strict(&jar)?;
    let bin = if let Some(cap) = SSH_KEY.captures(&add.key) {
        let b = cap.name("key").map(|x| x.as_str()).unwrap_or(&add.key);
        match thrussh_keys::parse_public_key_base64(b.trim()) {
            Ok(thrussh_keys::key::PublicKey::RSA { key, .. }) => {
                thrussh_keys::key::PublicKey::RSA {
                    key,
                    hash: thrussh_keys::key::SignatureHash::SHA2_512,
                }
            }
            Ok(k) => k,
            Err(_) => return Ok(StatusCode::BAD_REQUEST.into_response()),
        }
    } else {
        return Ok(StatusCode::BAD_REQUEST.into_response());
    };
    use db::publickeys::dsl as publickeys;
    diesel::insert_into(publickeys::publickeys)
        .values((
            publickeys::user_id.eq(uid),
            publickeys::publickey.eq(&add.key),
            publickeys::bin.eq(&bin.public_key_bytes()),
        ))
        .on_conflict_do_nothing()
        .execute(&mut config.db.get().await?)
        .await?;

    Ok(Redirect::to("/settings/ssh").into_response())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SshPost {
    id: uuid::Uuid,
    key: String,
    token: String,
}

#[debug_handler]
async fn ssh_post(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    post: Form<SshPost>,
) -> Result<Response, crate::Error> {
    token.verify(&post.token)?;

    let uid = get_user_id_strict(&jar)?;
    let bin = if let Some(cap) = SSH_KEY.captures(&post.key) {
        let b = cap.name("key").map(|x| x.as_str()).unwrap_or(&post.key);
        match thrussh_keys::parse_public_key_base64(b.trim()) {
            Ok(thrussh_keys::key::PublicKey::RSA { key, .. }) => {
                thrussh_keys::key::PublicKey::RSA {
                    key,
                    hash: thrussh_keys::key::SignatureHash::SHA2_512,
                }
            }
            Ok(k) => k,
            Err(_) => return Ok(StatusCode::BAD_REQUEST.into_response()),
        }
    } else {
        return Ok(StatusCode::BAD_REQUEST.into_response());
    };

    use db::publickeys::dsl as publickeys;
    diesel::update(publickeys::publickeys.find(post.id))
        .filter(publickeys::user_id.eq(uid))
        .set((
            publickeys::publickey.eq(&post.key),
            publickeys::bin.eq(&bin.public_key_bytes()),
        ))
        .execute(&mut config.db.get().await?)
        .await?;

    Ok(Redirect::to("/settings/ssh").into_response())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateRepo {
    name: String,
    #[serde(default)]
    private: bool,
    token: String,
}

#[debug_handler]
async fn create_repo(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    Form(create): Form<CreateRepo>,
) -> Result<Response, crate::Error> {
    token.verify(&create.token)?;
    use db::permissions::dsl as p;
    use db::repositories::dsl as r;
    use db::users::dsl as u;
    debug!("create repo {:?}", create);

    let mut db_ = config.db.get().await?;
    let owner = get_user_id_strict(&jar)?;
    db_.transaction(|mut txn| {
        (async move {
            let login = if let Some(login) = u::users
                .find(owner)
                .select(u::login)
                .get_result::<String>(&mut txn)
                .await
                .optional()?
            {
                login
            } else {
                return Ok(StatusCode::FORBIDDEN.into_response());
            };

            let repo_id = diesel::insert_into(r::repositories)
                .values((
                    r::owner.eq(owner),
                    r::name.eq(create.name),
                    r::creation_ip.eq(addr.to_string()),
                ))
                .on_conflict_do_nothing()
                .returning(r::id)
                .get_result::<uuid::Uuid>(&mut txn)
                .await?;

            diesel::insert_into(p::permissions)
                .values((
                    p::user_id.eq(owner),
                    p::repo_id.eq(repo_id),
                    p::start_date.eq(diesel::dsl::now),
                    p::perm.eq(Perm::create_owner().bits()),
                ))
                .execute(&mut txn)
                .await?;

            if !create.private {
                diesel::insert_into(p::permissions)
                    .values((
                        p::user_id.eq(uuid::Uuid::nil()),
                        p::repo_id.eq(repo_id),
                        p::start_date.eq(diesel::dsl::now),
                        p::perm.eq(Perm::create_public().bits()),
                    ))
                    .execute(&mut txn)
                    .await?;
            }

            Ok(Redirect::to(&format!("/{}", login)).into_response())
        })
        .scope_boxed()
    })
    .await
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteRepo {
    name: String,
    token: String,
}

#[debug_handler]
async fn delete_repo(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    Form(delete): Form<DeleteRepo>,
) -> Result<Response, crate::Error> {
    token.verify(&delete.token)?;
    let (owner, login) = get_user_login_strict(&jar, &config).await?;
    use db::repositories::dsl as r;
    if let Some(id) = diesel::delete(r::repositories)
        .filter(r::owner.eq(owner))
        .filter(r::name.eq(delete.name))
        .returning(r::id)
        .get_result(&mut config.db.get().await?)
        .await.optional()?
    {
        config.repo_locks.remove(&id).await?;
    }
    Ok(Redirect::to(&format!("/{}", login)).into_response())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteAccount {
    token: String,
    password: String,
}

#[debug_handler]
async fn delete(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    Form(delete): Form<DeleteAccount>,
) -> Result<Response, crate::Error> {
    use db::users::dsl as u;
    token.verify(&delete.token)?;
    let uid = get_user_id_strict(&jar)?;
    let mut db = config.db.get().await?;
    let n = diesel::delete(u::users.find(uid).filter(check_password!(&delete.password)))
        .execute(&mut db)
        .await
        .unwrap();

    if n == 0 {
        use crate::db::profile_pics::dsl as pp;
        use crate::db::publickeys::dsl as pk;
        use crate::db::signingkeys::dsl as sk;
        // We couldn't delete because of conflicts.
        diesel::update(u::users.find(uid).filter(check_password!(&delete.password)))
            .set(u::login.eq(sql("md5(concat(users.id::text, users.login))")))
            .execute(&mut db)
            .await?;
        diesel::delete(pp::profile_pics.find(uid)).execute(&mut db).await?;
        diesel::delete(pk::publickeys.filter(pk::user_id.eq(uid))).execute(&mut db).await?;
        diesel::delete(sk::signingkeys.filter(sk::user_id.eq(uid))).execute(&mut db).await?;
    }
    Ok(Redirect::to("/").into_response())
}