use std::hash::Hash;

use aide::axum::{
    routing::{get_with, post_with},
    ApiRouter,
};
use axum::{extract::Query, http::StatusCode, Json};
use cached::proc_macro::cached;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;

mod description;
mod kind;
mod names;
mod position;
mod size;

pub fn routes() -> ApiRouter {
    ApiRouter::new()
        .api_route(
            "/discover",
            post_with(discover, |docs| {
                docs.response::<200, Json<Star>>()
                    .summary("add a new planet")
                    .id("discover")
                    .tag("stars")
            }),
        )
        .api_route(
            "/chart",
            get_with(chart, |docs| {
                docs.response::<200, Json<Vec<Protostar>>>()
                    .summary("the whole chart")
                    .id("chart")
                    .tag("stars")
            }),
        )
        .api_route_with(
            "/visit/",
            get_with(visit, |docs| {
                docs.response::<200, Json<Star>>()
                    .summary("visit a single planet")
                    .id("visit")
                    .tag("stars")
            }),
            |docs| docs,
        )
}

/// this gets stored in the db,
/// the other stuff is generated using
/// the id as a seed
#[derive(Serialize, FromRow, Clone, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
pub struct Protostar {
    pub id: i32,
    /// hex code of the stars color
    pub color: String,
}

#[derive(Serialize, Deserialize, JsonSchema, Clone)]
pub struct Star {
    pub core: Protostar,
    pub position: position::Position,
    pub kind: kind::Kind,
    pub name: String,
    pub description: String,
}

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct DiscoveryLog {
    pub color: String,
}

impl Into<Star> for Protostar {
    fn into(self) -> Star {
        star_from_seed(self.id.abs() as u64, self.clone())
    }
}

#[cached]
fn star_from_seed(seed: u64, protostar: Protostar) -> Star {
    let mut rng = fastrand::Rng::with_seed(seed);
    let range = 0..u64::MAX;
    let kind = kind::Kind::random(rng.u64(range.clone()));
    Star {
        core: protostar.clone(),
        position: position::Position::random(rng.u64(range.clone())),
        name: format!("{}-{}", names::random_name(rng.u64(range.clone())), seed,).into(),
        description: protostar.generate_description(&kind, rng.u64(range.clone())),
        kind,
    }
}

/// discover a new star in the solar system
async fn discover(Json(log): Json<DiscoveryLog>) -> Result<Json<Star>, StatusCode> {
    let query = "INSERT INTO stars (color) VALUES ($1) RETURNING id, color";

    let protostar: Protostar = sqlx::query_as(query)
        .bind(log.color)
        .fetch_one(crate::db::pool().await)
        .await
        .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?;
    let star: Star = protostar.into();
    Ok(Json(star))
}

/// show all stars
async fn chart() -> Json<Vec<Star>> {
    let query = "SELECT * FROM stars ORDER BY id DESC";
    let protostars: Vec<Protostar> = sqlx::query_as(query)
        .fetch_all(crate::db::pool().await)
        .await
        .unwrap_or_default();
    Json(
        protostars
            .iter()
            .map(|proto| Into::<Star>::into(proto.clone()))
            .collect::<Vec<Star>>(),
    )
}

#[derive(Serialize, Deserialize, JsonSchema)]
struct VisitorData {
    /// the planet to visit
    planet_id: i32,
}

async fn visit(data: Query<VisitorData>) -> Result<Json<Star>, StatusCode> {
    let query = "SELECT * FROM stars WHERE id = ($1)";
    let star: Protostar = sqlx::query_as(query)
        .bind(data.planet_id)
        .fetch_one(crate::db::pool().await)
        .await
        .or(Err(StatusCode::NOT_FOUND))?;
    let star: Star = star.into();
    Ok(Json(star))
}
