use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration};

use anyhow::{Context, Result};
use axum::{
    body::Body,
    extract::{
        ws::{Message, WebSocket},
        Path, State, WebSocketUpgrade,
    },
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::{get, post},
    Form, Router,
};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use rand::random;
use serde::Deserialize;
use tokio::sync::{
    broadcast::{self, Receiver, Sender},
    Mutex,
};
use tower_http::{
    catch_panic::CatchPanicLayer,
    services::ServeDir,
    trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer},
};
use tracing::{error, info, trace, warn, Level};
use tracing_subscriber::FmtSubscriber;

/// If a handler panics we run this function and return the [Response].
fn handle_panic(err: Box<dyn std::any::Any + Send + 'static>) -> Response {
    let details = if let Some(s) = err.downcast_ref::<String>() {
        s.clone()
    } else if let Some(s) = err.downcast_ref::<&str>() {
        s.to_string()
    } else {
        "Unknown panic message".to_string()
    };
    error!(details = details, "Handler paniced");

    (StatusCode::INTERNAL_SERVER_ERROR).into_response()
}

#[derive(Deserialize)]
struct NoteDatum {
    // Epoch time in ms
    time: u64,
    // Fraction up/down
    y: f64,
}

#[derive(Deserialize)]
struct Notes {
    notes: Vec<NoteDatum>,
}

#[derive(Deserialize)]
struct NotesForm {
    notes: String,
}

struct Game {
    id: u64,

    is_started: bool,

    submitted: Option<Notes>,

    /// Sender to broadcast things
    broadcast_tx: Sender<String>,
    /// Broadcast receiver to send to WebSocket
    broadcast_rx: Receiver<String>,
}

impl Game {
    /// Return the appropriate HTML for the game state.
    fn get_html(&self) -> Markup {
        let gid = self.id;

        if self.submitted.is_some() {
            return html! {
                h1 { "Submitted!" }
            };
        }
        if self.is_started {
            return html! {
                script { ({PreEscaped("document.notes = []; ")})}
                h1 { "Playing Game" }
                p { "Were playing here" }

                canvas
                    #canvas
                    height=(256) width=(256)
                    style="position:relative; background: cyan;"
                    {};
                script { (PreEscaped(r#"document.getElementById("canvas").onclick = (e) => {
                    const notes = JSON.parse(e.target.dataset.notes ?? "[]");
                    notes.push({ time: Date.now(), y: e.layerY / e.target.height});
                    e.target.dataset.notes = JSON.stringify(notes);
                }"#))}

                button hx-post=(format!("/game/{gid}/submit"))
                        hx-vals=r#"js:{ "notes": document.getElementById("canvas").dataset.notes }"#
                    { "Submit" }
            };
        }

        html! {
            h1 { "Game" }
            p { "Current game id is " (gid)}
            div #messages {};
            button hx-post=(format!("/game/{gid}/start")) { "Start" }
        }
    }
}

async fn submit_game_answer(lock: Arc<Mutex<Game>>, notes: Notes) {
    lock.lock().await.submitted = Some(notes);

    tokio::spawn(async move {
        let _ = tokio::time::sleep(Duration::from_secs(2)).await;
        let mut g = lock.lock().await;
        g.is_started = false;
        g.submitted = None;
        g.broadcast_tx.send(
            html! {
                section #content {
                    (g.get_html())
                }
            }
            .into_string(),
        )
    });
}

struct ServerState {
    games: HashMap<u64, Arc<Mutex<Game>>>,
}

impl ServerState {
    fn new_game(&mut self, gid: u64) {
        let (tx, rx) = broadcast::channel(16);
        let g = Game {
            id: gid,
            is_started: false,
            submitted: None,
            broadcast_rx: rx,
            broadcast_tx: tx,
        };
        self.games.insert(gid, Arc::new(Mutex::new(g)));
    }
}

type Server = Arc<Mutex<ServerState>>;

async fn route_index(State(st): State<Server>) -> Response {
    let game_ids = st.lock().await.games.keys().cloned().collect::<Vec<_>>();

    let game_list = if game_ids.is_empty() {
        html! { "No current games"}
    } else {
        html! {
            h3 { "Games" }
            ul {
                @for id in &game_ids {
                    li { a href=(format!("/game/{id}")) { (id) } }
                }
            }
        }
    };

    let html = html! {
        h1 { "Good game "}
        (game_list)

        button hx-post="/game" { "Create game" }
    };

    html_response(html! {
        (DOCTYPE)
        head {
            meta charset="utf-8";
            title { "Good Game" };
            meta name="viewport" content="width=device-width, initial-scale=1" {};
            script src="https://unpkg.com/htmx.org@2.0.4" {};
            link rel="stylesheet" type="text/css" href="static/style.css";
        }
        body {
            (html)
        }
    })
}

fn html_response(html: PreEscaped<String>) -> Response {
    Response::builder()
        .status(StatusCode::OK)
        .body(Body::from(html.into_string()))
        .expect("Failed to set body")
}

async fn create_game(State(st): State<Server>) -> Response {
    let gid = random::<u64>();

    let _ = {
        let mut s = st.lock().await;
        s.new_game(gid);
    };

    Response::builder()
        .header("hx-redirect", format!("/game/{gid}"))
        .body(Body::empty())
        .expect("failed to build response")
}

async fn start_game(Path(gid): Path<u64>, State(st): State<Server>) -> Response {
    let lock = {
        if let Some(g) = st.lock().await.games.get(&gid) {
            g.clone()
        } else {
            return StatusCode::NOT_FOUND.into_response();
        }
    };

    let mut g = lock.lock().await;

    g.is_started = true;
    let html = g.get_html();

    g.broadcast_tx
        .send(html! { section #content { (html) } }.into_string())
        .expect("failed to send");

    StatusCode::OK.into_response()
}

async fn get_game(Path(gid): Path<u64>, State(st): State<Server>) -> Response {
    let game = {
        if let Some(g) = st.lock().await.games.get(&gid) {
            g.clone()
        } else {
            return (StatusCode::NOT_FOUND).into_response();
        }
    };

    let html = game.lock().await.get_html();

    html_response(html! {
        (DOCTYPE)
        head {
            meta charset="utf-8";
            title { "Good Game" };
            meta name="viewport" content="width=device-width, initial-scale=1" {};
            script src="https://unpkg.com/htmx.org@2.0.4" {};
            script src="https://unpkg.com/htmx-ext-ws@2.0.1/ws.js" {};

            link rel="stylesheet" type="text/css" href="/static/style.css";
        }
        body hx-ext="ws" ws-connect=(format!("/game/{gid}/ws")) {
            footer { }
            section #content {
                (html)
            }
            footer { span { "Good Game" } }
        }
    })
}

async fn submit_game(
    Path(gid): Path<u64>,
    State(st): State<Server>,
    Form(form): Form<NotesForm>,
) -> Response {
    let glock = {
        if let Some(g) = st.lock().await.games.get(&gid) {
            g.clone()
        } else {
            return (StatusCode::NOT_FOUND).into_response();
        }
    };

    let Ok(notes) = serde_json::from_str(&form.notes) else {
        warn!(notes = form.notes);
        return (StatusCode::UNPROCESSABLE_ENTITY).into_response();
    };

    submit_game_answer(glock.clone(), Notes { notes }).await;

    let g = glock.lock().await;
    let html = g.get_html();
    g.broadcast_tx
        .send(html! { section #content { (html) } }.into_string())
        .expect("failed to send");

    StatusCode::OK.into_response()
}

async fn game_ws(ws: WebSocketUpgrade, Path(gid): Path<u64>, State(st): State<Server>) -> Response {
    let game = {
        let s = st.lock().await;
        if let Some(g) = s.games.get(&gid) {
            g.clone()
        } else {
            return StatusCode::NOT_FOUND.into_response();
        }
    };

    ws.on_upgrade(move |socket| handle_socket(socket, game))
        .into_response()
}

async fn handle_socket(mut socket: WebSocket, game: Arc<Mutex<Game>>) {
    if socket
        .send(Message::Ping(bytes::Bytes::from("wat")))
        .await
        .is_ok()
    {
        trace!("Pinged ws");
    } else {
        println!("Could not send ping!");
        return;
    }
    {
        game.lock()
            .await
            .broadcast_tx
            .send(
                html! {
                    div #messages hx-swap-oob="beforeend" { p { "Someone joined" } }
                }
                .into_string(),
            )
            .unwrap();
    }

    let mut rx = { game.lock().await.broadcast_rx.resubscribe() };
    while let Ok(a) = rx.recv().await {
        trace!(a, "send ");
        let res = socket.send(a.into()).await;
        trace!(?res);
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let subscriber = FmtSubscriber::builder()
        .with_max_level(Level::TRACE)
        .finish();
    tracing::subscriber::set_global_default(subscriber)
        .expect("set global default subscriber failed");

    let mut server_state = ServerState {
        games: HashMap::new(),
    };
    server_state.new_game(123);

    let app = Router::new()
        .route("/", get(route_index))
        .route("/game", post(create_game))
        .route("/game/{gid}", get(get_game))
        .route("/game/{gid}/start", post(start_game))
        .route("/game/{gid}/submit", post(submit_game))
        .route("/game/{gid}/ws", get(game_ws))
        .nest_service("/static", ServeDir::new("static"))
        .layer(CatchPanicLayer::custom(handle_panic))
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(DefaultMakeSpan::new().level(Level::INFO))
                .on_response(DefaultOnResponse::new().level(Level::INFO)),
        )
        .with_state(Arc::new(Mutex::new(server_state)));

    let addr = "192.168.0.106:4800";
    info!("Listening on {addr}");
    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    )
    .await
    .context("serve")
}
