use crate::rate_limiter::RateLimiter;
use crate::web::manage::{StatelessVerifyToken, forms, templates};
use crate::web::{MyResponse, RequestOrigin, manage};
use crate::{mail, tokens};
use anyhow::anyhow;
use gettext_macros::i18n;
use hagrid_database::types::{Email, Fingerprint};
use hagrid_database::{Database, Query, Sqlite};
use rocket::form::Form;
use rocket::uri;
use rocket_codegen::{get, post};
use rocket_i18n::I18n;

#[get("/manage")]
pub fn vks_manage(origin: RequestOrigin, i18n: I18n) -> MyResponse {
    MyResponse::ok_bare("manage/manage", i18n, origin)
}

#[get("/manage/<token>")]
pub fn vks_manage_key(
    origin: RequestOrigin,
    db: &rocket::State<Sqlite>,
    i18n: I18n,
    token: String,
    token_service: &rocket::State<tokens::Service>,
) -> MyResponse {
    if let Ok(StatelessVerifyToken { fpr }) = token_service.check(&token) {
        match db.lookup(&Query::ByFingerprint(fpr)) {
            Ok(Some(tpk)) => {
                let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap();
                let mut emails: Vec<Email> = tpk
                    .userids()
                    .flat_map(|u| u.userid().to_string().parse::<Email>())
                    .collect();
                emails.sort_unstable();
                emails.dedup();
                let uid_status = emails
                    .into_iter()
                    .map(|email| templates::ManageKeyUidStatus {
                        address: email.to_string(),
                        published: true,
                    })
                    .collect();
                let key_link = uri!(crate::routes::vks::search(q = fp.to_string())).to_string();
                let context = templates::ManageKey {
                    key_fpr: fp.to_string(),
                    key_link,
                    uid_status,
                    token,
                    base_uri: origin.get_base_uri().to_owned(),
                };
                MyResponse::ok("manage/manage_key", context, i18n, origin)
            }
            Ok(None) => MyResponse::not_found(
                Some("manage/manage"),
                Some(i18n!(i18n.catalog, "This link is invalid or expired")),
                i18n,
                origin,
            ),
            Err(e) => MyResponse::ise(e),
        }
    } else {
        MyResponse::not_found(
            Some("manage/manage"),
            Some(i18n!(i18n.catalog, "This link is invalid or expired")),
            i18n,
            origin,
        )
    }
}

#[post("/manage", data = "<request>")]
pub fn vks_manage_post(
    db: &rocket::State<Sqlite>,
    origin: RequestOrigin,
    mail_service: &rocket::State<mail::Service>,
    rate_limiter: &rocket::State<RateLimiter>,
    i18n: I18n,
    request: Form<forms::ManageRequest>,
    token_service: &rocket::State<tokens::Service>,
) -> MyResponse {
    let email = match request.search_term.parse::<Email>() {
        Ok(email) => email,
        Err(_) => {
            return MyResponse::not_found(
                Some("manage/manage"),
                Some(i18n!(i18n.catalog, "Malformed address: {}"; request.search_term.as_str())),
                i18n,
                origin,
            );
        }
    };

    let tpk = match db.lookup(&Query::ByEmail(email.clone())) {
        Ok(Some(tpk)) => tpk,
        Ok(None) => {
            return MyResponse::not_found(
                Some("manage/manage"),
                Some(i18n!(i18n.catalog, "No key for address: {}"; request.search_term.as_str())),
                i18n,
                origin,
            );
        }
        Err(e) => return MyResponse::ise(e),
    };

    let email_exists = tpk
        .userids()
        .flat_map(|binding| binding.userid().to_string().parse::<Email>())
        .any(|candidate| candidate == email);

    if !email_exists {
        return MyResponse::ise(anyhow!("Internal error: address check failed!"));
    }

    if !rate_limiter.action_perform(format!("manage-{}", &email)) {
        return MyResponse::not_found(
            Some("manage/manage"),
            Some(i18n!(
                i18n.catalog,
                "A request has already been sent for this address recently."
            )),
            i18n,
            origin,
        );
    }

    let fpr: Fingerprint = tpk.fingerprint().try_into().unwrap();
    let fpr_text = fpr.to_string();
    let token = token_service.create(&StatelessVerifyToken { fpr });
    let link_path = uri!(vks_manage_key(token)).to_string();

    let base_uri = origin.get_base_uri();
    if let Err(e) = mail_service.send_manage_token(&i18n, base_uri, fpr_text, &email, &link_path) {
        return MyResponse::ise(e);
    }

    let ctx = templates::ManageLinkSent {
        address: email.to_string(),
    };
    MyResponse::ok("manage/manage_link_sent", ctx, i18n, origin)
}

#[post("/manage/unpublish", data = "<request>")]
pub fn vks_manage_unpublish(
    origin: RequestOrigin,
    db: &rocket::State<Sqlite>,
    i18n: I18n,
    token_service: &rocket::State<tokens::Service>,
    request: Form<forms::ManageDelete>,
) -> MyResponse {
    manage::vks_manage_unpublish_or_fail(origin, db, token_service, i18n, request)
        .unwrap_or_else(MyResponse::ise)
}

#[cfg(test)]
mod tests {
    use crate::routes::tests::common::*;
    use ::rocket::http::{ContentType, Status};
    use ::rocket::local::blocking::Client;
    use rstest::rstest;
    use tempfile::TempDir;

    mod get_manage {
        use super::*;

        const URI: &str = "/manage";

        #[rstest]
        fn delete_form_is_visible(#[from(client)] (_tmpdir, client): (TempDir, Client)) {
            assert::response(
                client.get(URI).dispatch(),
                Status::Ok,
                ContentType::HTML,
                "any verified email address",
            );
        }

        #[rstest]
        fn maintenance(
            #[from(client)] (tmpdir, client): (TempDir, Client),
            maintenance_text: &str,
        ) {
            test::maintenance(
                || client.get(URI).dispatch(),
                tmpdir,
                ContentType::HTML,
                maintenance_text,
            );
        }
    }
}
