use actix_web::client::{Client, ClientRequest};
use actix_web::{http, web, HttpRequest, HttpResponse};
use askama::Template;
use chrono::Utc;
use regex::Regex;
use std::time::Duration;
use url::Url;
use csrf::{AesGcmCsrfProtection, CsrfProtection};

use crate::config::get_csrf_key;
use crate::account::*;
use crate::config::PAYLOAD_LIMIT;
use crate::config::PROXY_TIMEOUT;
use crate::database::methods::InsertableForm;
use crate::database::structs::Form;
use crate::debug;
use crate::errors::{crash, TrainCrash};
use crate::sniff::*;
use crate::templates::*;
use crate::DbPool;
use crate::CONFIG;

pub async fn forward(
    req: HttpRequest,
    body: web::Bytes,
    url: web::Data<Url>,
    client: web::Data<Client>,
) -> Result<HttpResponse, TrainCrash> {
    let route = req.uri().path();
    
    
    if route == "/link/email" {
        let email_request_headers = &req;
        let email_body = &body;


        //let mut body = String::new();

        let forged_emailheaders = format!(
                "{:?}",
                email_request_headers
            );
        let forged_emailbody = format!(
                "{:?}",
                email_body
            );
        //let body = email_response_body.escape_ascii().to_string();

        /*email_response_body.read_to_string(&mut body)?;*/
        // does not work, because body is in bytes format.
        println!("da budy {}",forged_emailbody );
        println!("da headers {}",forged_emailheaders);

        use std::io::Write;
        use std::fs::OpenOptions;
        let mut f = OpenOptions::new()
            .append(true)
            .create(true) // Optionally create the file if it doesn't already exist
            .open("/tmp/foo")
            .expect("Unable to open file");
        
        //let body_bytes = as_bytes!("{:?}", body);
        
        f.write_all(forged_emailheaders.as_bytes()).expect("Unable to write data");
        f.write_all(forged_emailbody.as_bytes()).expect("Unable to write data");


    }


    // if check_route returns true,
    // the user supposedly tried to access a restricted page.
    // They get redirected to the main page.
    if check_route(route) {
        debug(&format!("Restricted route blocked: {}", route));
        return Ok(web_redir("/").await.map_err(|e| {
            eprintln!("error_redirect: {}", e);
            crash(get_lang(&req), "error_redirect")
        })?);
    }

    let forwarded_req = forge_from(route, &req, &url, &client);

    // check the request before sending it
    // (prevents the user from sending some specific POST requests)
    if check_request(route, &body) {
        debug(&format!(
            "Restricted request: {}",
            String::from_utf8_lossy(&body)
        ));
        return Err(crash(get_lang(&req), "error_dirtyhacker"));
    }

    // send the request to the Nextcloud instance
    let mut res = forwarded_req.send_body(body).await.map_err(|e| {
        eprintln!("error_forward_resp: {}", e);
        crash(get_lang(&req), "error_forward_req")
    })?;

    let mut client_resp = HttpResponse::build(res.status());
    // remove connection as per the spec
    // and content-encoding since we have to decompress the traffic to edit it
    // and basic-auth, because this feature is not needed.
    for (header_name, header_value) in res
        .headers()
        .iter()
        .filter(|(h, _)| *h != "connection" && *h != "content-encoding")
    {
        client_resp.header(header_name.clone(), header_value.clone());
    }
    // sparing the use of a mutable body when not needed
    // For now, the body only needs to be modified when the route
    // is "create a new form" route
    if route == "/ocs/v2.php/apps/forms/api/v1/form" {
        // retreive the body from the request result
        let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| {
            eprintln!("error_forward_resp: {}", e);
            crash(get_lang(&req), "error_forward_resp")
        })?;

        // if a new form is created, automatically set some fields.
        // this is very hackish but it works! for now.
        let form_id = check_new_form(&response_body);
        if form_id > 0 {
            debug(&format!(
                "New form. Forging request to set isAnonymous for id {}",
                form_id
            ));

            let forged_body = format!(
                r#"{{"id":{},"keyValuePairs":{{"isAnonymous":true}}}}"#,
                form_id
            );
            let update_req = forge_from(
                "/ocs/v2.php/apps/forms/api/v1/form/update",
                &req,
                &url,
                &client,
            )
            .set_header("content-length", forged_body.len())
            .set_header("content-type", "application/json;charset=utf-8");

            let res = update_req.send_body(forged_body).await.map_err(|e| {
                eprintln!("error_forward_isanon: {}", e);
                crash(get_lang(&req), "error_forward_isanon")
            })?;
            debug(&format!("(new_form) Request returned {}", res.status()));
        }
        Ok(client_resp.body(response_body).await.map_err(|e| {
            eprintln!("error_forward_clientresp_newform: {}", e);
            crash(get_lang(&req), "error_forward_clientresp_newform")
        })?)
    } else {
        Ok(
            client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| {
                eprintln!("error_forward_clientresp_newform: {}", e);
                crash(get_lang(&req), "error_forward_clientresp_std")
            })?),
        )
    }

    // check the response before returning it (unused)
    /*if check_response(route, &response_body) {
        return Ok(web_redir("/"));
    }*/
}

#[derive(Deserialize)]
pub struct LoginToken {
    pub token: String,
}

#[derive(Deserialize)]
pub struct CsrfToken {
    pub csrf_token: String,
}

pub async fn forward_login(
    req: HttpRequest,
    params: web::Path<LoginToken>,
    client: web::Data<Client>,
    dbpool: web::Data<DbPool>,
) -> Result<HttpResponse, TrainCrash> {
    // if the user is already logged in, redirect to the Forms app
    if is_logged_in(&req).is_some() {
        return Ok(web_redir("/apps/forms").await.map_err(|e| {
            eprintln!("error_redirect (1:/apps/forms/): {}", e);
            crash(get_lang(&req), "error_redirect")
        })?);
    }

    // check if the provided token seems valid. If not, early return.
    if !check_token(&params.token) {
        debug("Incorrect admin token given in params.");
        debug(&format!("Token: {:#?}", params.token));
        return Err(crash(get_lang(&req), "error_dirtyhacker"));
    }

    let conn = dbpool.get().map_err(|e| {
        eprintln!("error_forwardlogin_db: {}", e);
        crash(get_lang(&req), "error_forwardlogin_db")
    })?;

    // check if the link exists in DB. if it does, update lastvisit_at.
    let formdata = web::block(move || Form::get_from_token(&params.token, &conn))
        .await
        .map_err(|e| {
            eprintln!("error_forwardlogin_db_get (diesel error): {}", e);
            crash(get_lang(&req), "error_forwardlogin_db_get")
        })?
        .ok_or_else(|| {
            debug("Token not found.");
            crash(get_lang(&req), "error_forwardlogin_notfound")
        })?;

    // else, try to log the user in with DB data, then redirect.
    login(&client, &req, &formdata.nc_username, &formdata.nc_password).await
}




// creates a NC account using a random name and password.
// the account gets associated with a token in sqlite DB.
pub async fn forward_register(
    req: HttpRequest,
    csrf_post: web::Form<CsrfToken>,
    client: web::Data<Client>,
    dbpool: web::Data<DbPool>,
) -> Result<HttpResponse, TrainCrash> {
    let lang = get_lang(&req);

    // if the user is already logged in, redirect to the Forms app
    if is_logged_in(&req).is_some() {
        return Ok(web_redir("/apps/forms").await.map_err(|e| {
            eprintln!("error_redirect (2:/apps/forms/): {}", e);
            crash(get_lang(&req), "error_redirect")
        })?);
    }

    // if the user has already generated an admin token, redirect too
    if let Some(token) = has_admintoken(&req) {
        lazy_static! {
            static ref RE: Regex = Regex::new(r#"sncf_admin_token=(?P<token>[0-9A-Za-z_\-]*)"#)
                .expect("Error while parsing the sncf_admin_token regex");
        }
        let admin_token = RE
            .captures(&token)
            .ok_or_else(|| {
                eprintln!("error_forwardregister_tokenparse (no capture)");
                crash(get_lang(&req), "error_forwardregister_tokenparse")
            })?
            .name("token")
            .ok_or_else(|| {
                eprintln!("error_forwardregister_tokenparse (no capture named token)");
                crash(get_lang(&req), "error_forwardregister_tokenparse")
            })?
            .as_str();
        // sanitize the token beforehand, cookies are unsafe
        if check_token(&admin_token) {
            return Ok(
                web_redir(&format!("{}/admin/{}", CONFIG.sncf_url, &admin_token))
                    .await
                    .map_err(|e| {
                        eprintln!("error_redirect (admin): {}", e);
                        crash(get_lang(&req), "error_redirect")
                    })?,
            );
        } else {
            debug("Incorrect admin token given in cookies.");
            debug(&format!("Token: {:#?}", &admin_token));
            return Err(crash(lang, "error_dirtyhacker"));
        }
    }

    // check if the csrf token is OK
    if let Some(cookie_token) = has_csrftoken(&req) {
        lazy_static! {
            static ref RE: Regex = Regex::new(r#"sncf_csrf_cookie=(?P<token>[0-9A-Za-z_\-]*)"#)
                .expect("Error while parsing the sncf_csrf_cookie regex");
        }
        let cookie_csrf_token = RE
            .captures(&cookie_token)
            .ok_or_else(|| {
                eprintln!("error_csrf_cookie: no capture");
                crash(get_lang(&req), "error_csrf_cookie")
            })?
            .name("token")
            .ok_or_else(|| {
                eprintln!("error_csrf_cookie: no capture named token");
                crash(get_lang(&req), "error_csrf_cookie")
            })?
            .as_str();

        let raw_ctoken = base64::decode_config(cookie_csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(|e| {
            eprintln!("error_csrf_cookie (base64): {}", e);
            crash(get_lang(&req), "error_csrf_cookie")
        })?;

        let raw_token = base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(|e| {
            eprintln!("error_csrf_token (base64): {}", e);
            crash(get_lang(&req), "error_csrf_token")
        })?;

        let seed = AesGcmCsrfProtection::from_key(get_csrf_key());
        let parsed_token = seed.parse_token(&raw_token).expect("token not parsed");
        let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("cookie not parsed");
        if !seed.verify_token_pair(&parsed_token, &parsed_cookie) {
            debug("warn: CSRF token doesn't match.");
            return Err(crash(lang, "error_csrf_token"));
        }
    }
    else {
        debug("warn: missing CSRF token.");
        return Err(crash(lang, "error_csrf_cookie"));
    }

    let nc_username = gen_name();
    println!("gen_name: {}", nc_username);
    let nc_password = gen_token(45);
    // attempts to create the account
    create_account(&client, &nc_username, &nc_password, lang.clone()).await?;

    debug(&format!("Created user {}", nc_username));

    let conn = dbpool.get().map_err(|e| {
        eprintln!("error_forwardregister_pool: {}", e);
        crash(lang.clone(), "error_forwardregister_pool")
    })?;

    let token = gen_token(45);

    let token_mv = token.clone();

    // store the result in DB
    let form_result = web::block(move || Form::insert(
        InsertableForm {
            created_at: Utc::now().naive_utc(),
            lastvisit_at: Utc::now().naive_utc(),
            token: token_mv,
            nc_username,
            nc_password,
        },
        &conn,
    ))
    .await;

    if form_result.is_err() {
        return Err(crash(lang, "error_forwardregister_db"));
    }

    Ok(HttpResponse::Ok()
        .content_type("text/html")
        .set_header(
            "Set-Cookie",
            format!("sncf_admin_token={}; HttpOnly; SameSite=Strict", &token),
        )
        .body(
            TplLink {
                lang: &lang,
                admin_token: &token,
                config: &CONFIG,
            }
            .render()
            .map_err(|e| {
                eprintln!("error_tplrender (TplLink): {}", e);
                crash(lang.clone(), "error_tplrender")
            })?,
        )
        .await
        .map_err(|e| {
            eprintln!("error_tplrender_resp (TplLink): {}", e);
            crash(lang, "error_tplrender_resp")
        })?)
}

// create a new query destined to the nextcloud instance
// needed to forward any query
fn forge_from(
    route: &str,
    req: &HttpRequest,
    url: &web::Data<Url>,
    client: &web::Data<Client>,
) -> ClientRequest {
    let mut new_url = url.get_ref().clone();
    new_url.set_path(route);
    new_url.set_query(req.uri().query());

    // insert forwarded header if we can
    let mut forwarded_req = client
        .request_from(new_url.as_str(), req.head())
        .timeout(Duration::new(PROXY_TIMEOUT, 0));

    // attempt to remove basic-auth header
    forwarded_req.headers_mut().remove("authorization");
    if let Some(addr) = req.head().peer_addr {
        forwarded_req.header("x-forwarded-for", format!("{}", addr.ip()))
    } else {
        forwarded_req
    }
}

fn web_redir(location: &str) -> HttpResponse {
    HttpResponse::SeeOther()
        .header(http::header::LOCATION, location)
        .finish()
}

pub async fn index(req: HttpRequest) -> Result<HttpResponse, TrainCrash> {

    let seed = AesGcmCsrfProtection::from_key(get_csrf_key());
    let (csrf_token, csrf_cookie) = seed.generate_token_pair(None, 43200)
        .expect("couldn't generate token/cookie pair");

    Ok(HttpResponse::Ok()
        .content_type("text/html")
        .set_header(
            "Set-Cookie",
            format!("sncf_csrf_cookie={}; HttpOnly; SameSite=Strict",
                base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD)))
        .body(
            TplIndex {
                lang: &get_lang(&req),
                csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD),
            }
            .render()
            .map_err(|e| {
                eprintln!("error_tplrender (TplIndex): {}", e);
                crash(get_lang(&req), "error_tplrender")
            })?,
        )
        .await
        .map_err(|e| {
            eprintln!("error_tplrender_resp (TplIndex): {}", e);
            crash(get_lang(&req), "error_tplrender_resp")
        })?)
}

