use std::{collections::HashMap, sync::Arc};

use axum::{
    extract::{Path, Query, State},
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Redirect, Response},
    routing::{get, post},
    Router,
};
use data::{CombinedFish, Data, Filters};
use maud::{html, Markup};
use nanoid::nanoid;
use serde::Deserialize;
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
use templates::layout;
use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

pub mod changelog;
pub mod clock;
pub mod data;
pub mod db;
pub mod forecast;
pub mod templates;

pub struct AppState {
    pub data: Data,
    pub pool: Pool<Postgres>,
}

pub struct AppError(anyhow::Error);

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Something went wrong: {}", self.0),
        )
            .into_response()
    }
}

impl<E> From<E> for AppError
where
    E: Into<anyhow::Error>,
{
    fn from(err: E) -> Self {
        Self(err.into())
    }
}

async fn root_handler() -> Result<Markup, AppError> {
    Ok(templates::root())
}

async fn new_account_handler(state: State<Arc<AppState>>) -> Result<Markup, AppError> {
    let id = nanoid!(10);
    db::insert_account(&id, &state.pool).await?;
    Ok(templates::new_account(id))
}

#[derive(Deserialize)]
pub struct FilterQuery {
    pub caught: Option<String>,
    pub big: Option<String>,
    pub patches: Option<Vec<f32>>,
}

async fn main_handler(
    headers: HeaderMap,
    state: State<Arc<AppState>>,
    Path(acc_id): Path<String>,
    axum_extra::extract::Query(query): axum_extra::extract::Query<FilterQuery>,
) -> Result<Response, AppError> {
    let exists = db::get_account(&acc_id, &state.pool).await?;
    if !exists {
        return Ok(html! { "The provided account ID doesn't eixst." }.into_response());
    }
    let caught_fish = db::get_caught_fish(&acc_id, &state.pool).await?;
    let pinned_fish = db::get_pinned_fish(&acc_id, &state.pool).await?;
    let filters = Filters::from_query(query);

    // If this is a HTMX-sent request, don't resend the entire page, just the list.
    let is_htmx = headers.contains_key("HX-Request");

    // Extract the list of fish we want to render.
    let meta = state.data.fish_with_meta();
    let mut values: Vec<CombinedFish> = filters.filter(meta.values().collect(), &caught_fish);
    values.sort_by(|afish, bfish| {
        pinned_fish
            .contains(&(bfish.entry.id as i32))
            .cmp(&pinned_fish.contains(&(afish.entry.id as i32)))
            .then(bfish.is_up.cmp(&afish.is_up))
            .then(bfish.is_always_up.cmp(&afish.is_always_up))
            .then(bfish.rarity.total_cmp(&afish.rarity).reverse())
            .then(bfish.meta.name_en.cmp(&afish.meta.name_en))
    });

    let state = state.clone();
    let filters = filters.clone();
    Ok(templates::main_page(templates::ViewData {
        state,
        fish: values,
        caught_fish,
        pinned_fish,
        acc_id,
        filters,
        only_list: is_htmx,
    })
    .into_response())
}

async fn insert_cf_handler(
    state: State<Arc<AppState>>,
    Path((acc_id, fish_id)): Path<(String, String)>,
) -> Result<Response, AppError> {
    let exists = db::get_account(&acc_id, &state.pool).await?;
    if !exists {
        return Ok(html! { "The provided account ID doesn't eixst." }.into_response());
    }

    db::insert_caught_fish(&acc_id, &fish_id.parse::<i32>().unwrap(), &state.pool).await?;

    Ok(Redirect::to(&format!("/{}", acc_id)).into_response())
}

async fn insert_pin_handler(
    state: State<Arc<AppState>>,
    Path((acc_id, fish_id)): Path<(String, String)>,
) -> Result<Response, AppError> {
    let exists = db::get_account(&acc_id, &state.pool).await?;
    if !exists {
        return Ok(html! { "The provided account ID doesn't exist." }.into_response());
    }

    db::insert_pinned_fish(&acc_id, &fish_id.parse::<i32>()?, &state.pool).await?;

    Ok(Redirect::to(&format!("/{}", acc_id)).into_response())
}

async fn delete_pin_handler(
    state: State<Arc<AppState>>,
    Path((acc_id, fish_id)): Path<(String, String)>,
) -> Result<Response, AppError> {
    let exists = db::get_account(&acc_id, &state.pool).await?;
    if !exists {
        return Ok(html! { "The provided account ID doesn't exist." }.into_response());
    }

    db::delete_pinned_fish(&acc_id, &fish_id.parse::<i32>()?, &state.pool).await?;

    Ok(Redirect::to(&format!("/{}", acc_id)).into_response())
}

async fn to_handler(
    state: State<Arc<AppState>>,
    query: Query<HashMap<String, String>>,
) -> Result<Response, AppError> {
    let rejection_html = html! {
        p { "The provided account ID doesn't exist. " }
    };
    if let Some(id) = query.get("id") {
        let exists = db::get_account(id, &state.pool).await?;
        if exists {
            Ok(Redirect::to(&format!("/{}", id)).into_response())
        } else {
            Ok(rejection_html.into_response())
        }
    } else {
        Ok(rejection_html.into_response())
    }
}

#[tokio::main]
async fn main() {
    #[cfg(debug_assertions)]
    dotenvy::dotenv().unwrap();

    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
                format!(
                    "{}=debug,tower_http=debug,axum::rejection=trace",
                    env!("CARGO_CRATE_NAME")
                )
                .into()
            }),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&std::env::var("DATABASE_URL").unwrap())
        .await
        .unwrap();

    sqlx::migrate!("./migrations").run(&pool).await.unwrap();

    let app = Router::new()
        .route("/", get(root_handler).post(new_account_handler))
        .route("/to", get(to_handler))
        .route("/{id}", get(main_handler))
        .route("/{acc_id}/catch/{fish_id}", post(insert_cf_handler))
        .route("/{acc_id}/pin/{fish_id}", post(insert_pin_handler))
        .route("/{acc_id}/pin/{fish_id}/delete", post(delete_pin_handler))
        .route("/changelog", get(|| async { changelog::changelog_page() }))
        .route(
            "/logout",
            get(|| async {
                layout(html! {
                    h1 { "Logging you out, please wait to be redirected..." }
                    script src="/static/scripts/logout.js" type="text/javascript" {}
                })
            }),
        )
        .layer(TraceLayer::new_for_http())
        .nest_service("/static", ServeDir::new("static"))
        .with_state(Arc::new(AppState {
            data: Data::new(),
            pool,
        }));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    println!("Listening on http://localhost:3000!");
    axum::serve(listener, app).await.unwrap();
}
