use actix_web::http::header;
use actix_web::{web, HttpRequest, HttpResponse};

use crate::pages::templates::QUERY_RESET_GAME_PWD;
use crate::pages::{insert_security_headers, request_to_jar, CommonAuthInfo};
use crate::{DataBaseRo, DataBaseRw, WebData};

use uuid::Uuid;

#[derive(serde_derive::Serialize)]
struct PageData<'a> {
    common_auth_info: CommonAuthInfo<'a>,
    csrf: Uuid,
}

#[derive(serde_derive::Deserialize)]
pub struct FormData {
    login: String,
    contact_type: String,
    contact: String,
    password_type: String,
    csrf: Uuid,
}

pub async fn query_reset_game_pwd(
    request: HttpRequest,
    data: web::Data<WebData<'_>>,
) -> HttpResponse {
    let jar = request_to_jar(request);

    let user = jar
        .private(&data.cookies_key)
        .get("auth")
        .map(|x| x.value().to_string());

    let csrf = Uuid::new_v4();

    {
        let mut cache = data.cache_query_reset_game_pwd.lock().await;
        cache.insert(
            csrf,
            (),
            std::time::Duration::from_secs(data.cache_duration_sec),
        );
    }

    let body = match data.handlebars.render(
        QUERY_RESET_GAME_PWD,
        &PageData {
            common_auth_info: CommonAuthInfo {
                user: user.map(Into::into),
            },
            csrf,
        },
    ) {
        Ok(b) => b,
        Err(e) => {
            log::error!("Render error: {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    insert_security_headers(HttpResponse::Ok()).body(body)
}

async fn send_email(
    mailer: &lettre::AsyncSmtpTransport<lettre::Tokio1Executor>,
    mailer_from: &lettre::message::Mailbox,
    token_url: String,
    to: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let email = lettre::Message::builder()
        .from(mailer_from.clone())
        .to(to.parse()?)
        .subject("FreeOrion reset game password link")
        .body(format!(
            "You requested change for FreeOrion game password.\n\
                      Use next link {} to enter new password. Link is active for 24 hours.\n\
                        Don't send it to other people.",
            token_url
        ))?;

    use lettre::AsyncTransport;

    mailer.send(email).await?;

    Ok(())
}

async fn send_xmpp(
    xmpp_url: &str,
    token_url: String,
    to: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let client = awc::Client::default();

    client
        .post(xmpp_url)
        .insert_header(("X-XMPP-To", to))
        .send_body(format!(
            "You requested change for FreeOrion game password.\n\
                      Use next link {} to enter new password. Link is active for 24 hours.\n\
                        Don't send it to other people.",
            token_url
        ))
        .await?;

    Ok(())
}

enum Protocol {
    Email,
    Xmpp,
}

#[actix_web::post("/query-reset-game-pwd.do")]
pub async fn post_query_reset_game_pwd(
    form: web::Form<FormData>,
    data: web::Data<WebData<'_>>,
    data_ro: web::Data<DataBaseRo>,
    data_rw: web::Data<DataBaseRw>,
) -> HttpResponse {
    let cached_data = {
        let mut cache = data.cache_query_reset_game_pwd.lock().await;
        cache.remove(&form.csrf)
    };
    if cached_data.is_none() {
        log::warn!("Unknown data for CSRF: {}", form.csrf);
        return HttpResponse::BadRequest().body("Incorrect");
    }

    let protocol = match form.contact_type.as_str() {
        "email" => Protocol::Email,
        "xmpp" => Protocol::Xmpp,
        _ => {
            log::warn!("Unknown data for contact type: {}", form.contact_type);
            return HttpResponse::BadRequest().body("Incorrect");
        }
    };
    let is_game_pwd = match form.password_type.as_str() {
        "game" => true,
        "web" => false,
        _ => {
            log::warn!("Unknown data for password type: {}", form.password_type);
            return HttpResponse::BadRequest().body("Incorrect");
        }
    };

    // check existence of contact
    let dbclient_ro = match data_ro.0.get().await {
        Ok(c) => c,
        Err(e) => {
            log::error!("Pool RO error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let stmt = match dbclient_ro.prepare("select 1 from auth.users u inner join auth.contacts c on u.player_name = c.player_name and c.is_active = true and c.delete_ts is null where u.player_name = $1 and c.protocol = ($2::text)::auth.contact_protocol and c.address = $3 limit 1").await {
        Ok(stmt) => stmt,
        Err(e) => {
            log::error!("Pool RO statement error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let rows = match dbclient_ro
        .query_opt(&stmt, &[&form.login, &form.contact_type, &form.contact])
        .await
    {
        Ok(rows) => rows,
        Err(e) => {
            log::error!("Pool RO query error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    if rows.is_none() {
        return HttpResponse::NotFound().body("Not found");
    }

    let dbclient_rw = match data_rw.0.get().await {
        Ok(c) => c,
        Err(e) => {
            log::error!("Pool RW error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let stmt = match dbclient_rw
        .prepare("insert into auth.reset_tokens values ($1, $2, NULL, NOW(), NULL, $3);")
        .await
    {
        Ok(stmt) => stmt,
        Err(e) => {
            log::error!("Pool RW statement error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let token = Uuid::new_v4();
    let inserted = match dbclient_rw
        .execute(&stmt, &[&form.login, &token, &is_game_pwd])
        .await
    {
        Ok(c) => c,
        Err(e) => {
            log::error!("Pool RW execute insert error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    if inserted == 0 {
        log::error!("Pool RW execute insert row error");
        return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
    }

    let token_url = format!(
        "{}://{}/reset-game-pwd-{}.html",
        data.base_proto, data.base_domain, token
    );

    match protocol {
        Protocol::Email => {
            if let Err(e) =
                send_email(&data.mailer, &data.mailer_from, token_url, &form.contact).await
            {
                log::error!("Cann't send email: {}", e);
                return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
            }
        }
        Protocol::Xmpp => {
            if let Err(e) = send_xmpp(&data.xmpp_url, token_url, &form.contact).await {
                log::error!("Cann't send xmpp: {}", e);
                return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
            }
        }
    };

    HttpResponse::Found()
        .append_header((header::LOCATION, "index.html"))
        .finish()
}