use anyhow;
use hyperware_process_lib::http::client::send_request_await_response;
use hyperware_process_lib::http::server::{send_response, HttpBindingConfig, WsBindingConfig};
use hyperware_process_lib::http::{Method, StatusCode};
use hyperware_process_lib::logging::{info, init_logging, Level};
use hyperware_process_lib::{
    await_message, call_init, get_blob, get_typed_state, homepage, http, kiprintln, set_state,
    Address, Capability, Message, Request,
};
use process_macros::SerdeJsonInto;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use url::Url;
mod proxy;

// DEVS: replace with your Web2 site URL, login endpoint and login nonce
// TODO if testing with memedeck etc. disable the auth check below, see the other TODO comment
// const WEB2_URL: &str = "https://www.memedeck.xyz";
const WEB2_URL: &str = "http://localhost:3000";
const WEB2_LOGIN_ENDPOINT: &str = "http://localhost:3000/api/hypr-login";
const WEB2_LOGIN_NONCE: &str = "lorem ipsum";

wit_bindgen::generate!({
    path: "target/wit",
    world: "loginex-sys-v0",
    generate_unused_types: true,
    additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],
});

#[derive(Debug, Serialize, Deserialize)]
enum SignerRequest {
    Sign,
    Verify { node: String, signature: Vec<u8> },
}
#[derive(Debug, Serialize, Deserialize, SerdeJsonInto)]
struct SignResponse {
    signature: Vec<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
enum FrontendRequest {
    Sign,
    CheckCookie,
    Logout,
    Debug(String),
}
#[derive(Debug, Serialize, Deserialize)]
struct LoginMessage {
    pub site: String,
    pub time: u64,
    pub nonce: Option<String>,
}
const ICON: &str = include_str!("icon");

#[derive(Debug, Serialize, Deserialize)]
struct ProxyStateV1 {
    // TODO this should probably be generic and go on login:sys:sys
    pub cookie: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "version")]
enum VersionedState {
    /// State fully stored in memory, persisted using serde_json.
    /// Future state version will use SQLite.
    V1(ProxyStateV1),
}

impl VersionedState {
    fn load() -> Self {
        get_typed_state(|bytes| serde_json::from_slice(bytes))
            .unwrap_or(Self::V1(ProxyStateV1 { cookie: None }))
    }
    fn _save(&self) {
        set_state(&serde_json::to_vec(&self).expect("Failed to serialize state!"));
    }
    fn save_cookie(&self, cookie: String) {
        let ns = Self::V1(ProxyStateV1 {
            cookie: Some(cookie),
        });
        set_state(&serde_json::to_vec(&ns).expect("Failed to serialize state!"));
    }
    fn wipe_cookie(&self) {
        let ns = Self::V1(ProxyStateV1 { cookie: None });
        set_state(&serde_json::to_vec(&ns).expect("Failed to serialize state!"));
    }
    fn get_cookie(&self) -> Option<String> {
        match self {
            Self::V1(ps) => ps.cookie.clone(),
        }
    }
}

fn _signer_test(our: &Address) -> anyhow::Result<()> {
    let target = Address::new(our.node(), ("sign", "sign", "sys"));
    let login_message = LoginMessage {
        site: WEB2_URL.to_string(),
        nonce: Some(WEB2_LOGIN_NONCE.to_string()),
        time: get_now(),
    };
    let blob = serde_json::to_vec(&login_message)?;
    // Get the signature from login:sys:sys
    let res: SignResponse = Request::to(target.clone())
        .blob_bytes(blob.clone())
        .body(serde_json::to_vec(&SignerRequest::Sign)?)
        .send_and_await_response(10)??
        .body()
        .try_into()?;
    // Send signature to designated endpoint on Web2 app
    let body2 = SignerRequest::Verify {
        node: our.node().to_string(),
        signature: res.signature,
    };
    let verify_res = Request::to(target)
        .blob_bytes(blob)
        .body(serde_json::to_vec(&body2)?)
        .send_and_await_response(10)??;
    let is_good: bool = serde_json::from_slice(verify_res.body())?;
    kiprintln!("verify_res \n{:#?}", is_good);
    Ok(())
}

call_init!(initialize);
fn initialize(our: Address) {
    init_logging(Level::DEBUG, Level::INFO, None, None, None).unwrap();
    info!("begin");

    homepage::add_to_homepage("Login", Some(ICON), Some("/"), None);

    let mut state = VersionedState::load();
    let cookie = state.get_cookie();
    kiprintln!("cookie{:#?}", cookie);

    let mut http_server = http::server::HttpServer::new(5);
    let http_config = HttpBindingConfig::default().secure_subdomain(false);

    http_server
        .serve_ui("ui", vec!["/hypr-login"], http_config.clone())
        .expect("Failed to serve UI");
    http_server.bind_http_path("/", http_config).unwrap();
    http_server
        .bind_ws_path("/", WsBindingConfig::default())
        .unwrap();

    main_loop(&our, &mut state, &mut http_server);
}

fn main_loop(
    our: &Address,
    state: &mut VersionedState,
    http_server: &mut http::server::HttpServer,
) {
    loop {
        match await_message() {
            Err(_send_error) => {
                // ignore send errors, local-only process
                continue;
            }
            Ok(Message::Request {
                source,
                body,
                capabilities,
                ..
            }) => {
                // ignore messages from other nodes -- technically superfluous check
                // since manifest does not acquire networking capability
                if source.node() != our.node {
                    continue;
                }
                let _ = handle_request(our, &source, &body, capabilities, state, http_server);
            }
            _ => continue, // ignore responses
        }
    }
}

fn handle_request(
    our: &Address,
    source: &Address,
    body: &[u8],
    _capabilities: Vec<Capability>,
    state: &mut VersionedState,
    http_server: &mut http::server::HttpServer,
) -> anyhow::Result<()> {
    // source node is ALWAYS ourselves since networking is disabled
    if source.process == "http-server:distro:sys" {
        // receive HTTP requests and websocket connection messages from our server
        let server_request = http_server.parse_request(body).unwrap();
        match server_request {
            http::server::HttpServerRequest::Http(request) => {
                handle_http_request(our, state, &request)?;
            }
            // TODO handle websockets
            _ => (),
        };
    };
    Ok(())
}

/// Handle HTTP requests from our own frontend.
fn handle_http_request(
    our: &Address,
    state: &mut VersionedState,
    http_request: &http::server::IncomingHttpRequest,
) -> anyhow::Result<()> {
    let method = http_request.method().unwrap();
    let url = http_request.url().unwrap();
    let paths = url.path_segments().unwrap();
    let last = paths.last().unwrap();
    if last == "hypr-login" {
        handle_login_request(our, state, method)?;
    } else {
        handle_page_request(our, state, http_request)?;
    }
    Ok(())
}

fn handle_login_request(
    our: &Address,
    state: &mut VersionedState,
    method: Method,
) -> anyhow::Result<()> {
    match method {
        Method::POST => {
            let blob = get_blob().unwrap();
            let request_bytes = blob.bytes();
            let request = serde_json::from_slice::<FrontendRequest>(request_bytes)?;
            match request {
                FrontendRequest::Sign => {
                    let target = Address::new(our.node(), ("sign", "sign", "sys"));
                    let body = LoginMessage {
                        site: WEB2_URL.to_string(),
                        nonce: Some(WEB2_LOGIN_NONCE.to_string()),
                        time: get_now(),
                    };
                    let body_bytes = serde_json::to_vec(&body)?;
                    // Get the signature from login:sys:sys
                    let res: SignResponse = Request::to(target)
                        .blob_bytes(body_bytes)
                        .body(serde_json::to_vec(&SignerRequest::Sign)?)
                        .send_and_await_response(10)??
                        .body()
                        .try_into()?;
                    // Send signature to designated endpoint on Web2 app
                    attempt_login(our, state, res)?;
                }
                FrontendRequest::CheckCookie => {
                    let cookie = state.get_cookie();
                    let json = match cookie {
                        None => serde_json::Value::Null,
                        Some(s) => json!(s),
                    };
                    send_response(StatusCode::OK, None, serde_json::to_vec(&json)?)
                }
                FrontendRequest::Logout => {
                    state.wipe_cookie();
                    let json = serde_json::to_vec(&json!(true))?;
                    send_response(StatusCode::OK, None, json);
                }
                FrontendRequest::Debug(s) => {
                    let value = serde_json::from_str::<serde_json::Value>(&s)?;
                    kiprintln!("frontend log\n{:#?}\n", value);
                }
            }
        }
        _ => {
            send_response(StatusCode::METHOD_NOT_ALLOWED, None, vec![]);
        }
    };
    Ok(())
}
fn attempt_login(
    our: &Address,
    state: &mut VersionedState,
    signature_response: SignResponse,
) -> anyhow::Result<()> {
    let mut json_headers = HashMap::new();
    json_headers.insert("Content-type".to_string(), "application/json".to_string());
    let node = our.node();
    let blob = get_blob().ok_or(anyhow::anyhow!("no blob"))?;
    let message = blob.bytes();
    let signature = signature_response.signature;
    let json =
        serde_json::to_vec(&json!({"node":node, "message": message, "signature": signature}))?;
    let url = Url::parse(WEB2_LOGIN_ENDPOINT)?;
    let res = send_request_await_response(Method::POST, url, Some(json_headers), 5000, json)?;
    let resbody = res.body();
    let resjson = serde_json::from_slice::<serde_json::Value>(resbody)?;
    let okres = resjson.get("ok");
    match okres {
        None => {
            send_json_response(
                StatusCode::OK,
                &json!({"error": "Signature verification failed"}),
            )?;
        }
        Some(_) => {
            let cookie_header = res.headers().get("set-cookie");
            match cookie_header {
                None => {
                    send_json_response(
                        StatusCode::OK,
                        &json!({"error": "No auth string found in response"}),
                    )?;
                }
                Some(cookie_value) => {
                    let cookie = cookie_value.to_str()?;
                    state.save_cookie(cookie.to_string());
                    send_json_response(StatusCode::OK, &json!({"ok": "go ahead"}))?;
                }
            }
        }
    };
    Ok(())
}
fn handle_page_request(
    our: &Address,
    state: &mut VersionedState,
    http_request: &http::server::IncomingHttpRequest,
) -> anyhow::Result<()> {
    // TODO remove before release
    let cookie = state.get_cookie();
    // let auth = Some(String::new());
    match cookie {
        None => {
            let package = our.package_id();
            let process = our.process();
            let redirect_to = format!("/{}:{}/hypr-login", process, package);
            let mut redirect_headers = HashMap::new();
            redirect_headers.insert("Location".to_string(), redirect_to);
            send_response(
                StatusCode::TEMPORARY_REDIRECT,
                Some(redirect_headers),
                vec![],
            );
            Ok(())
        }
        Some(cookie) => proxy::run_proxy(http_request, WEB2_URL, &cookie),
    }
}

// fn _invalid_node(
//     hypermap: &hypermap::Hypermap,
//     node: &str,
// ) -> Option<(contacts::Response, Option<LazyLoadBlob>)> {
//     if hypermap
//         .get(&node)
//         .map(|(tba, _, _)| tba != eth::Address::ZERO)
//         .unwrap_or(false)
//     {
//         None
//     } else {
//         Some((
//             contacts::Response::Err("Node name invalid or does not exist".to_string()),
//             None,
//         ))
//     }
// }

fn send_json_response<T: serde::Serialize>(status: StatusCode, data: &T) -> anyhow::Result<()> {
    let json_data = serde_json::to_vec(data)?;
    send_response(
        status,
        Some(HashMap::from([(
            String::from("Content-Type"),
            String::from("application/json"),
        )])),
        json_data,
    );
    Ok(())
}

fn get_now() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs()
}
