use axum::{
    Router,
    extract::ws::{self, Message},
    http::StatusCode,
    response::{self, IntoResponse},
    routing,
};
use futures_util::{
    SinkExt, StreamExt,
    stream::{SplitSink, SplitStream},
};
use open_tavern::api::{Request, RequestError, Response};
use tokio::sync::{broadcast, mpsc};
use tokio_util::io::ReaderStream;

#[tokio::main]
async fn main() {
    let (bsend, _) = broadcast::channel(10);
    let (msend, mrecv) = mpsc::channel(50);
    let bsend2 = bsend.clone();

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
    let app = Router::new()
        .route("/", routing::get(root))
        .route("/tavern.js", routing::get(socket))
        .route("/app.js", routing::get(app_js))
        .route("/style.css", routing::get(style))
        .route("/assets/{*asset}", routing::get(get_asset))
        .route(
            "/ws",
            routing::get(move |w| ws_handler(w, msend, bsend2.clone().subscribe())),
        );
    let game = tokio::fs::read_to_string(open_tavern::GAME_SAVE_FILE)
        .await
        .map(|s| serde_json::from_str(&s).ok())
        .ok()
        .flatten()
        .unwrap_or(open_tavern::GameServer::new());
    let game_server = tokio::spawn(game.server_loop(mrecv, bsend));
    println!("Server is running at http://localhost:3001");
    let axum_server = tokio::spawn(run_web_server(listener, app));

    // wait for one of the bunch to finish, then quit
    tokio::select! {
        _ = game_server => {},
        _ = axum_server => {},
    };
}
/// Runs the axum server so i would get a JoinHandle<()> instead of whatever axum::serve returns
async fn run_web_server(listener: tokio::net::TcpListener, app: Router) {
    axum::serve(listener, app).await.expect("Axum server crashed???");
}

/// Executes on a new WebSocket request, set update to [handle_socket]
async fn ws_handler(
    ws: ws::WebSocketUpgrade,
    msend: mpsc::Sender<(String, Request)>,
    brecv: broadcast::Receiver<(Option<String>, Response)>,
) -> impl axum::response::IntoResponse {
    ws.on_upgrade(|w| handle_socket(w, msend, brecv))
}

/// Receiver of a websocket after [handle_socket] handle the login
async fn socket_receiver(mut recv: SplitStream<ws::WebSocket>, msend: mpsc::Sender<(String, Request)>, id: String) {
    while let Some(msg) = recv.next().await {
        if let Ok(msg) = msg {
            match msg {
                Message::Text(t) => {
                    println!("Got message from {}: {}", &id, &t);
                    let req = serde_json::from_str::<Request>(&t).unwrap_or_default();
                    let erred = msend.send((id.clone(), req)).await.is_err();
                    if erred {
                        break;
                    }
                }
                ws::Message::Binary(_) => todo!(),
                ws::Message::Ping(_) => todo!(),
                ws::Message::Pong(_) => todo!(),
                ws::Message::Close(_) => {
                    // dont care if we fail the send as we are quitting regardless
                    _ = msend.send((id.clone(), open_tavern::api::Request::Quit)).await;
                    break;
                }
            }
        }
    }
}
/// Sender of a websocket after [handle_socket] handles the login
async fn socket_sender(
    id: String,
    mut send: SplitSink<ws::WebSocket, ws::Message>,
    mut brecv: broadcast::Receiver<(Option<String>, Response)>,
) {
    while let Ok((to_id, msg)) = brecv.recv().await {
        if to_id.is_none() || to_id.map(|t| t == id).unwrap_or(false) {
            println!("Sending a message to {}: {:?}", &id, &msg);
            let err = send
                .send(ws::Message::Text(serde_json::to_string(&msg).unwrap().into()))
                .await
                .is_err();
            if err || matches!(msg, Response::Shutdown) {
                _ = send.close().await;
                break;
            }
        }
    }
}
/// Socket login handler, upon a websocket upgrade it will check for login requests until a login
/// request succeeds, then launch [socket_sender] and [socket_receiver] to handle the actual gameplay part
async fn handle_socket(
    mut socket: ws::WebSocket,
    msend: mpsc::Sender<(String, Request)>,
    mut brecv: broadcast::Receiver<(Option<String>, Response)>,
) {
    let mut id: Option<String> = None;
    // this is a temp id, and as long as 2 people dont try to connect to the socket at the same milisecond and to the same user it would be fine (i hope)
    let temp_id = format!(
        "temp_id_{}",
        std::time::SystemTime::now()
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .map(|t| t.as_micros())
            .unwrap_or(0)
    );
    let mut last_login_req = std::time::Instant::now();
    loop {
        tokio::select! {
            b = brecv.recv() => {
                println!("{} trying to log in: {:?}", &temp_id, &b);
                if let Ok((to_id, msg)) = b {
                    if let Response::Login(open_tavern::api::login::LoginResult { success, username }) = &msg {
                        let to_id = to_id.map(|ti| ti == temp_id).unwrap_or(false);
                        if to_id && *success && id.as_ref().map(|id| id == username).unwrap_or(false) {
                            _ = socket.send(Message::Text(serde_json::to_string(&msg).unwrap_or_default().into())).await;
                            break;
                        }
                        else {
                            id = None;
                            _ = socket.send(Message::Text(serde_json::to_string(&msg).unwrap_or_default().into())).await;
                        }
                    }
                }
            },
            msg = socket.recv() => {
                if let Some(msg) = msg.map(|m| m.ok()).flatten() {
                    match msg {
                        Message::Text(t) =>  {
                            let req = serde_json::from_str::<Request>(&t).unwrap_or_default();
                            println!("{} trying to log in incoming: {:?}", &temp_id, t);
                            match req {
                                // TODO: Actual signing in mechanism with multiple ids :)
                                Request::Login(r) => {
                                    // allow 1 login attempt every 3 seconds
                                    if last_login_req.elapsed() > std::time::Duration::from_secs(1) {
                                        last_login_req = std::time::Instant::now();
                                        id = Some(r.username.clone());
                                        _ = msend.send((temp_id.clone(), Request::Login(r))).await;
                                    }
                                    else {
                                        println!("Failed too early");
                                    }
                                    },
                                _ => {
                                    _ = socket.send(Message::Text(serde_json::to_string(&Response::Error(RequestError::InvalidRequest)).unwrap().into())).await;
                                },
                            }
                        }
                        ws::Message::Close(_) => {
                            id = None;
                            break;
                        },
                        _ => {},
                    };
                }
            }
        };
    }
    if let Some(id) = id {
        // maybe i should not spawn 2 different routines and just have 1 routine that uses tokio::select!
        println!("Got id for socket: {}", &id);
        let (send, recv) = socket.split();
        tokio::spawn(socket_receiver(recv, msend, id.clone()));
        tokio::spawn(socket_sender(id, send, brecv));
    }
    println!("Done with so-cat");
}

async fn root() -> axum::response::Html<String> {
    response::Html(std::fs::read_to_string("./assets/web/index.html").expect("Failed finding index.html"))
}
async fn socket() -> impl response::IntoResponse {
    (
        [(axum::http::header::CONTENT_TYPE, "text/javascript")],
        std::fs::read_to_string("./assets/web/tavern.js").expect("Failed finding tavern.js"),
    )
}

async fn app_js() -> impl response::IntoResponse {
    (
        [(axum::http::header::CONTENT_TYPE, "text/javascript")],
        std::fs::read_to_string("./assets/web/app.js").expect("Failed finding app.js"),
    )
}

async fn style() -> impl response::IntoResponse {
    (
        [(axum::http::header::CONTENT_TYPE, "text/css")],
        std::fs::read_to_string("./assets/web/style.css").unwrap(),
    )
}

async fn get_asset(asset: axum::extract::Path<String>) -> impl IntoResponse {
    println!("Asset requested: {}", asset.0);
    let supported_file_types = [
        (".jpg", "image/jpeg"),
        (".jpeg", "image/jpeg"),
        (".png", "image/png"),
        (".jxl", "image/jxl"),
    ];
    // check if file is a folder. if so, list it
    // TODO: This should prob be moved into the websocket connection and into the game server, so players cant snoop around the files
    if let Ok(mut dir) = tokio::fs::read_dir(format!("assets/{}", asset.0)).await {
        let mut files = Vec::new();

        while let Ok(Some(entry)) = dir.next_entry().await {
            if let Ok(name) = entry.file_name().into_string() {
                files.push(name);
            }
        }
        return Ok((
            [(axum::http::header::CONTENT_TYPE, "application/json")],
            axum::body::Body::from(serde_json::to_string(&files).unwrap_or(String::new())),
        ));
    }
    let mime = match supported_file_types
        .iter()
        .filter(|t| asset.0.ends_with(t.0))
        .map(|t| t.1)
        .next()
    {
        Some(t) => t,
        None => return Err((StatusCode::UNSUPPORTED_MEDIA_TYPE, "Unsupported file type".to_string())),
    };
    let file = match tokio::fs::File::open(format!("assets/{}", asset.0)).await {
        Ok(f) => f,
        Err(err) => return Err((StatusCode::NOT_FOUND, format!("File not found: {}", err))),
    };
    let stream = ReaderStream::new(file);
    let body = axum::body::Body::from_stream(stream);

    let headers = [(axum::http::header::CONTENT_TYPE, mime)];
    Ok((headers, body))
}
