pub mod utils;

use curl::easy::{Easy, List};
use std::env::var;
use std::io::Read;
use std::str::from_utf8;
use std::{error::Error, net::SocketAddr};
use unicase::Ascii;
use warp::hyper::HeaderMap;
use warp::{http::Method, http::Response, hyper::body::Bytes, path::FullPath, Filter};

const FOR_COPY: [&'static str; 3] = ["Content-Type", "Cookie", "Content-Encoding"];

const BASE: &str = "https://www.bankofmaldives.com.mv/internetbanking/api/";
const ERR: &str = "{ \"code\": 407, \"message\": \"Proxy failed\" }";

fn get_base_url() -> String {
    match var("BML_PROXY_BASE") {
        Ok(base) => base,
        Err(_) => BASE.to_string(),
    }
}

fn get_proxied_body(
    uri: &String,
    method: &Method,
    headers: &HeaderMap,
    body: &Bytes,
    base: &String,
) -> Result<(String, Vec<String>), Box<dyn Error>> {
    let mut handle = Easy::new();
    handle.url(format!("{}{}", base, uri).as_str())?;

    let mut custom_headers = List::new();
    for (name, value) in headers.iter() {
        if FOR_COPY.iter().any(|v| Ascii::new(v) == &name) {
            match value.to_str() {
                Ok(val) => custom_headers.append(format!("{}: {}", name.as_str(), val).as_str())?,
                Err(_) => (),
            };
        }
    }
    handle.http_headers(custom_headers)?;

    let body = std::str::from_utf8(body)?;
    let mut body_bytes = body.as_bytes();

    if method == Method::POST {
        handle.post_field_size(body_bytes.len() as u64)?;
    }

    let mut body_buffer = Vec::new();
    let mut headers = Vec::new();

    {
        let mut transfer = handle.transfer();

        if method == Method::POST {
            transfer.read_function(|into| {
                let read = body_bytes.read(into).unwrap_or(0);
                Ok(read)
            })?;
        }

        transfer.write_function(|data| {
            body_buffer.extend_from_slice(data);
            Ok(data.len())
        })?;

        transfer.header_function(|header| {
            match from_utf8(header) {
                Ok(v) => headers.push(v.to_string()),
                Err(_) => (),
            }

            true
        })?;

        transfer.perform()?;
    }

    let cloned_buf = body_buffer.clone();
    let s = from_utf8(&cloned_buf)?;

    Ok((s.to_string(), headers))
}

fn handler(
    uri: FullPath,
    headers: HeaderMap,
    method: Method,
    body: Bytes,
    ip: Option<SocketAddr>,
    base: String,
) -> Result<Response<String>, warp::http::Error> {
    let uri = uri.as_str().clone().to_string();
    let ip_str = match ip {
        Some(v) => format!("{}", v),
        None => "unknown".to_string(),
    };

    println!("({}) {}", ip_str, uri);

    let proxy_response = get_proxied_body(&uri, &method, &headers, &body, &base);

    let (res, headers) = match proxy_response {
        Ok(v) => v,
        Err(e) => {
            println!("error: {}", e);
            (ERR.to_string(), vec![])
        }
    };

    let mut builder = Response::builder();

    for header in headers.iter() {
        let split: Vec<&str> = header.split(": ").collect();
        if split.len() == 2 {
            builder = builder.header(split[0], split[1].replace("\r\n", ""));
        }
    }

    builder.body(res)
}

#[tokio::main]
async fn main() {
    let proxy_handler = warp::any()
        .and(utils::extract_request_data_filter())
        .and(warp::filters::addr::remote())
        .and(warp::any().map(move || get_base_url()))
        .map(handler);

    let log_warning = || {
        println!("This binary is meant to be used with LD_PRELOAD=\"/path/to/libcurl-impersonate-chrome.so\"");
        println!("If you're on Arch Linux, this library can be obtained via the AUR package 'libcurl-impersonate-bin'");
        println!(
            "A common location for this library is '/usr/local/lib/libcurl-impersonate-chrome.so'"
        );
        println!("\n[WARN] proceeding without libcurl-impersonate, expect cloudflare blockages\n");
    };

    match var("LD_PRELOAD") {
        Ok(v) => {
            if !v.contains("libcurl-impersonate-chrome.so") {
                log_warning();
            }
        }

        Err(_) => log_warning(),
    }

    let port = var("BML_PROXY_PORT")
        .unwrap_or_default()
        .parse::<u16>()
        .unwrap_or(3030);

    println!("Proxy running on port: {}", port);
    println!("Proxy base URL: {}", get_base_url());
    println!("Available environment variables for modification: BML_PROXY_PORT, BML_PROXY_BASE\n");

    warp::serve(proxy_handler).run(([0, 0, 0, 0], port)).await;
}
