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

use uuid::Uuid;

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

#[derive(serde_derive::Serialize)]
struct PageData {
    csrf: Uuid,
}

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

#[derive(serde_derive::Deserialize)]
pub struct FormDataMastodon {
    login: String,
    csrf: Uuid,
}

pub async fn get_log_in(request: HttpRequest, data: web::Data<WebData<'_>>) -> HttpResponse {
    let csrf = Uuid::new_v4();

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

    let jar = request_to_jar(&request);
    if jar
        .private(&data.cookies_key)
        .get("i_accept_cookie")
        .is_none_or(|x| x.value() != "yes")
    {
        return HttpResponse::Found()
            .append_header((header::LOCATION, "cookies-policy.html"))
            .finish();
    }

    let body = match data.handlebars.render(LOG_IN, &PageData { csrf }) {
        Ok(b) => b,
        Err(e) => {
            log::error!("Render login error: {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    insert_security_headers(HttpResponse::Ok()).body(body)
}

fn log_in_response(
    player_name: &str,
    request: &HttpRequest,
    cookies_key: &actix_web::cookie::Key,
    set_cookie_domain: bool,
    base_domain: &str,
    check_cookie: bool,
) -> HttpResponse {
    let mut jar = request_to_jar(request);
    // ToDo: skip this check if player accepted them
    if check_cookie
        && jar
            .private(cookies_key)
            .get("i_accept_cookie")
            .is_none_or(|x| x.value() != "yes")
    {
        return HttpResponse::Found()
            .append_header((header::LOCATION, "cookies-policy.html"))
            .finish();
    }

    let mut builder = cookie::Cookie::build("auth", player_name.to_lowercase())
        .path("/")
        .secure(true)
        .http_only(true)
        .same_site(cookie::SameSite::Strict)
        .max_age(cookie::time::Duration::weeks(4));
    if set_cookie_domain {
        builder = builder.domain(base_domain.to_string());
    }
    jar.private_mut(cookies_key).add(builder.finish());

    let mut response = HttpResponse::Found()
        .append_header((header::LOCATION, "my.html"))
        .finish();
    if let Some(c) = jar.get("auth") {
        if let Err(e) = response.add_cookie(c) {
            log::error!("Cann't set cookie {}", e);
        }
    }
    response
}

pub async fn post_log_in(
    request: HttpRequest,
    form: web::Form<FormData>,
    data: web::Data<WebData<'_>>,
    data_ro: web::Data<DataBaseRo>,
) -> HttpResponse {
    let cached_data = {
        let mut cache = data.cache_login.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 dbclient = 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.prepare("select web_password = crypt($1, web_password) from auth.users where player_name = $2 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
        .query_opt(&stmt, &[&form.password, &form.login])
        .await
    {
        Ok(rows) => rows,
        Err(e) => {
            log::error!("Pool RO query error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let row = match rows {
        Some(row) => row,
        None => {
            return HttpResponse::NotFound().body("Not found");
        }
    };

    if !row.get::<_, Option<bool>>(0).unwrap_or(false) {
        return HttpResponse::NotFound().body("Not found");
    }

    log_in_response(
        &form.login,
        &request,
        &data.cookies_key,
        data.set_cookie_domain,
        &data.base_domain,
        true,
    )
}

fn get_mastodon_redirect_url(base_proto: &str, base_domain: &str, domain: &str) -> String {
    format!(
        "{}://{}/mastodon-redirect-{}.html",
        base_proto, base_domain, domain
    )
}

async fn register_in_mastodon(
    domain: &str,
    reg_sess_id: &Uuid,
    data: &web::Data<WebData<'_>>,
) -> Result<(String, String), ()> {
    log::info!(
        "Mastodon domain {} registration session {} started",
        domain,
        reg_sess_id
    );

    let client = awc::Client::default();
    let website = format!("{}://{}/", data.base_proto, data.base_domain);
    let website_redirect = get_mastodon_redirect_url(&data.base_proto, &data.base_domain, domain);
    match client
        .post(format!("https://{}/api/v1/apps", domain))
        .content_type("application/x-www-form-urlencoded")
        .send_body(format!(
            "client_name=FreeOrion%20Test%20Web&redirect_uris={}&scopes=read:accounts&website={}",
            pct_str::PctString::encode(website_redirect.chars(), pct_str::URIReserved).as_str(),
            pct_str::PctString::encode(website.chars(), pct_str::URIReserved).as_str()
        ))
        .await
    {
        Ok(mut resp) => match resp.json::<serde_json::Value>().await {
            Ok(serde_json::Value::Object(resp_value_obj)) => {
                let client_id_value = resp_value_obj.get("client_id");
                let client_secret_value = resp_value_obj.get("client_secret");
                if let (
                    Some(serde_json::Value::String(client_id)),
                    Some(serde_json::Value::String(client_secret)),
                ) = (client_id_value, client_secret_value)
                {
                    Ok((client_id.clone(), client_secret.clone()))
                } else {
                    log::error!(
                        "Mastodon domain {} registration session {} missing fields JSON {:?}",
                        domain,
                        reg_sess_id,
                        resp_value_obj
                    );
                    Err(())
                }
            }
            Ok(resp_value) => {
                log::error!(
                    "Mastodon domain {} registration session {} non-object JSON {:?}",
                    domain,
                    reg_sess_id,
                    resp_value
                );
                Err(())
            }
            Err(err) => {
                log::error!(
                    "Mastodon domain {} registration session {} failed JSON {:?}",
                    domain,
                    reg_sess_id,
                    err
                );
                Err(())
            }
        },
        Err(err) => {
            log::error!(
                "Mastodon domain {} registration session {} failed {:?}",
                domain,
                reg_sess_id,
                err
            );
            Err(())
        }
    }
}

async fn obtain_token_mastodon(
    base_proto: &str,
    base_domain: &str,
    domain: &str,
    client_id: &str,
    client_secret: &str,
    code: &str,
) -> Result<String, ()> {
    let client = awc::Client::default();
    let website_redirect = get_mastodon_redirect_url(base_proto, base_domain, domain);
    match client
        .post(format!("https://{}/oauth/token", domain))
        .content_type("application/x-www-form-urlencoded")
        .send_body(format!(
            "client_id={}&client_secret={}&redirect_uri={}&grant_type=authorization_code&code={}&scopes=read:accounts",
            pct_str::PctString::encode(client_id.chars(), pct_str::URIReserved).as_str(),
            pct_str::PctString::encode(client_secret.chars(), pct_str::URIReserved).as_str(),
            pct_str::PctString::encode(website_redirect.chars(), pct_str::URIReserved).as_str(),
            pct_str::PctString::encode(code.chars(), pct_str::URIReserved).as_str()
        ))
        .await {
            Ok(mut resp) => match resp.json::<serde_json::Value>().await {
                Ok(serde_json::Value::Object(resp_value_obj)) => {
                    let access_token_value = resp_value_obj.get("access_token");
                    if let Some(serde_json::Value::String(access_token)) = access_token_value {
                        Ok(access_token.clone())
                    } else {
                        log::error!(
                            "Mastodon domain {} login failed missing fields JSON {:?}",
                            domain,
                            resp_value_obj
                        );
                        Err(())
                    }
                }
                Ok(resp_value) => {
                    log::error!(
                        "Mastodon domain {} login failed non-object JSON {:?}",
                        domain,
                        resp_value
                    );
                    Err(())
                }
                Err(err) => {
                    log::error!(
                        "Mastodon domain {} login failed JSON {:?}",
                        domain,
                        err
                    );
                    Err(())
                }
            }
            Err(err) => {
                log::error!(
                    "Mastodon domain {} login failed {:?}",
                    domain,
                    err
                );
                Err(())
            }
        }
}

async fn check_token_mastodon(domain: &str, token: &str) -> Result<String, ()> {
    let client = awc::Client::default();
    match client
        .get(format!(
            "https://{}/api/v1/accounts/verify_credentials",
            domain
        ))
        .insert_header(("Authorization", format!("Bearer {}", token)))
        .send()
        .await
    {
        Ok(mut resp) => match resp.json::<serde_json::Value>().await {
            Ok(serde_json::Value::Object(resp_value_obj)) => {
                let username_value = resp_value_obj.get("username");
                if let Some(serde_json::Value::String(username)) = username_value {
                    Ok(username.clone())
                } else {
                    log::error!(
                        "Mastodon domain {} login failed missing fields JSON {:?}",
                        domain,
                        resp_value_obj
                    );
                    Err(())
                }
            }
            Ok(resp_value) => {
                log::error!(
                    "Mastodon domain {} login failed non-object JSON {:?}",
                    domain,
                    resp_value
                );
                Err(())
            }
            Err(err) => {
                log::error!("Mastodon domain {} login failed JSON {:?}", domain, err);
                Err(())
            }
        },
        Err(err) => {
            log::error!("Mastodon domain {} login failed {:?}", domain, err);
            Err(())
        }
    }
}

async fn player_name_by_mastodon(
    dbclient: &deadpool::managed::Object<deadpool_postgres::Manager>,
    user: &str,
    domain: &str,
) -> Result<Option<String>, ()> {
    let stmt = match dbclient.prepare("select player_name from auth.contacts where protocol = 'mastodon' and is_active = true and delete_ts is null and address = $1 limit 1;").await {
        Ok(stmt) => stmt,
        Err(e) => {
            log::error!("Pool RO statement error {}", e);
            return Err(())
        }
    };
    let address = format!("{}@{}", user, domain);
    let rows = match dbclient.query_opt(&stmt, &[&address]).await {
        Ok(rows) => rows,
        Err(e) => {
            log::error!("Pool RO query error {}", e);
            return Err(());
        }
    };
    let row = match rows {
        Some(row) => row,
        None => {
            return Ok(None);
        }
    };

    Ok(Some(row.get::<_, String>(0)))
}

pub async fn post_log_in_mastodon(
    form: web::Form<FormDataMastodon>,
    data: web::Data<WebData<'_>>,
    data_ro: web::Data<DataBaseRo>,
    data_rw: web::Data<DataBaseRw>,
) -> HttpResponse {
    let cached_data = {
        let mut cache = data.cache_login.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 mut parts = form.login.splitn(4, '@');
    let (user, domain) = match (parts.next(), parts.next(), parts.next(), parts.next()) {
        (Some(u), Some(d), None, None) | (Some(""), Some(u), Some(d), None) => {
            (u.to_ascii_lowercase(), d.to_ascii_lowercase())
        }
        other => {
            log::error!("Unknown Mastodon address {:?}", other);
            return HttpResponse::NotFound().body("Not found");
        }
    };

    let dbclient = 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 player_name = match player_name_by_mastodon(&dbclient, &user, &domain).await {
        Ok(Some(player_name)) => player_name,
        Ok(None) => {
            return HttpResponse::NotFound().body("Not found");
        }
        Err(()) => {
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };

    log::info!("Found player {} to login with mastodon", player_name);

    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.mastodon_apps (domain, refresh_ts, reg_sess_id) VALUES ($1, $2, $3)
            ON CONFLICT (domain) DO UPDATE SET refresh_ts = $2
            RETURNING client_id, client_secret, reg_sess_id;",
        )
        .await
    {
        Ok(stmt) => stmt,
        Err(e) => {
            log::error!("Pool RW statement error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let stmt_save = match dbclient_rw
        .prepare(
            "UPDATE auth.mastodon_apps
            SET client_id = $1
            , client_secret = $2
            WHERE client_id IS NULL
            AND client_secret IS NULL
            AND reg_sess_id = $3
            AND domain = $4;",
        )
        .await
    {
        Ok(stmt) => stmt,
        Err(e) => {
            log::error!("Pool RW statement save error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let ts = chrono::Utc::now().naive_utc();
    let reg_sess_id = Uuid::new_v4();
    log::info!(
        "Mastodon domain {} registration session {} created",
        domain,
        reg_sess_id
    );
    let rows = match dbclient_rw
        .query_opt(&stmt, &[&domain, &ts, &reg_sess_id])
        .await
    {
        Ok(rows) => rows,
        Err(e) => {
            log::error!("Pool RW query error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let row = match rows {
        Some(row) => row,
        None => {
            log::error!("Pool RW query error");
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };

    let opt_client_id = row.get::<_, Option<String>>(0);
    let opt_client_secret = row.get::<_, Option<String>>(1);
    let old_reg_sess_id = row.get::<_, uuid::Uuid>(2);

    let (client_id, _client_secret) = if let (Some(client_id), Some(client_secret)) =
        (opt_client_id, opt_client_secret)
    {
        (client_id, client_secret)
    } else {
        if reg_sess_id != old_reg_sess_id {
            // Some one else registering same domain, try again later
            log::warn!(
                "Mastodon domain {} registration session {} conflict with {}",
                domain,
                reg_sess_id,
                old_reg_sess_id
            );
            return HttpResponse::Found()
                .append_header((header::LOCATION, "index.html"))
                .finish();
        }

        // Try to register on Mastodon domain
        match register_in_mastodon(&domain, &reg_sess_id, &data).await {
            Ok(res) => {
                let updated = match dbclient_rw
                    .execute(&stmt_save, &[&res.0, &res.1, &reg_sess_id, &domain])
                    .await
                {
                    Ok(c) => c,
                    Err(e) => {
                        log::error!("Pool RW execute update error {}", e);
                        return HttpResponse::ServiceUnavailable()
                            .body(actix_web::body::None::new());
                    }
                };
                if updated == 1 {
                    res
                } else {
                    log::error!("Pool RW execute update row error {}", updated);
                    return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
                }
            }
            Err(_) => {
                return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
            }
        }
    };

    log::info!(
        "Mastodon domain {} registration session {} completed with client_id {}",
        domain,
        reg_sess_id,
        client_id
    );

    let state = Uuid::new_v4().to_string();
    let website_redirect = get_mastodon_redirect_url(&data.base_proto, &data.base_domain, &domain);
    let location = format!("https://{}/oauth/authorize?client_id={}&scope=read:accounts&redirect_uri={}&response_type=code&state={}", 
        domain,
        pct_str::PctString::encode(client_id.chars(), pct_str::URIReserved).as_str(),
        pct_str::PctString::encode(website_redirect.chars(), pct_str::URIReserved).as_str(),
        state);

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

    HttpResponse::Found()
        .append_header((header::LOCATION, location))
        .finish()
}

#[actix_web::get("/mastodon-redirect-{domain}.html")]
pub async fn get_log_in_mastodon_redirect(
    req: HttpRequest,
    domain: web::Path<String>,
    data: web::Data<WebData<'_>>,
    data_ro: web::Data<DataBaseRo>,
) -> HttpResponse {
    let domain = domain.into_inner();
    let domain = match domain.char_indices().nth(128) {
        None => domain,
        Some((idx, _)) => domain[..idx].to_string(),
    };

    let mut code = None;
    let mut state = None;
    for params in req.query_string().split('&') {
        if let Some((k, v)) = params.split_once('=') {
            if k == "code" {
                code = Some(v);
            } else if k == "state" {
                state = Some(v);
            }
        }
    }
    let (code, state) = match (code, state) {
        (Some(c), Some(s)) => (c, s),
        _ => {
            log::warn!(
                "Mastodon not redirected with code or state in {}",
                req.query_string()
            );
            return HttpResponse::BadRequest().body("Incorrect query");
        }
    };

    let cached_data = {
        let mut cache = data.cache_mastodon_state.lock().await;
        cache.remove(state)
    };
    if cached_data.is_none_or(|x| x != domain) {
        log::warn!("Unknown state for mastodon redirect: {}", state);
        return HttpResponse::BadRequest().body("Incorrect");
    }

    let dbclient = match data_ro.0.get().await {
        Ok(client) => client,
        Err(e) => {
            log::error!("{}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };

    let stmt = match dbclient
        .prepare("select client_id, client_secret from auth.mastodon_apps where domain = $1;")
        .await
    {
        Ok(stmt) => stmt,
        Err(e) => {
            log::error!("{}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let rows = match dbclient.query_opt(&stmt, &[&domain]).await {
        Ok(rows) => rows,
        Err(e) => {
            log::error!("Pool RO query error {}", e);
            return HttpResponse::ServiceUnavailable().body(actix_web::body::None::new());
        }
    };
    let row = match rows {
        Some(r) => r,
        None => {
            return HttpResponse::NotFound().body("Not found");
        }
    };

    let opt_client_id = row.get::<_, Option<&str>>(0);
    let opt_client_secret = row.get::<_, Option<&str>>(1);

    if let (Some(client_id), Some(client_secret)) = (opt_client_id, opt_client_secret) {
        match obtain_token_mastodon(
            &data.base_proto,
            &data.base_domain,
            &domain,
            client_id,
            client_secret,
            code,
        )
        .await
        {
            Ok(token) => match check_token_mastodon(&domain, &token).await {
                Ok(user) => match player_name_by_mastodon(&dbclient, &user, &domain).await {
                    Ok(Some(player_name)) => log_in_response(
                        &player_name,
                        &req,
                        &data.cookies_key,
                        data.set_cookie_domain,
                        &data.base_domain,
                        false,
                    ),
                    Ok(None) => HttpResponse::NotFound().body("Not found"),
                    Err(()) => {
                        HttpResponse::ServiceUnavailable().body(actix_web::body::None::new())
                    }
                },
                Err(()) => HttpResponse::NotFound().body("Not found"),
            },
            Err(()) => HttpResponse::NotFound().body("Not found"),
        }
    } else {
        log::warn!("Missing mastodon client data for domain {}", domain);
        HttpResponse::NotFound().body("Not found")
    }
}