use axum::extract::{Form, Path, Query, State};
use axum::http::StatusCode;
use axum::response::Json;
use hyper::{
    header::{HeaderName, HeaderValue, LOCATION},
    HeaderMap,
};
use maud::Markup;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs;
use std::sync::Arc;

use crate::state::AppState;
use crate::url::CreateUrl;
use crate::views::url::{AutogeneratedUrl, CustomUrl, RootData};
use crate::{emoji, leaderboard, url, views};

// TODO: Move stuff to their own modules
// TODO: Implement SSE for URL stats page

pub async fn root(Query(params): Query<HashMap<String, String>>) -> Markup {
    let root_data = match params.get("custom_url") {
        Some(_) => RootData::Custom(CustomUrl::NoIdentifier),
        None => RootData::Auto(AutogeneratedUrl::NoIdentifier),
    };

    views::url::render(root_data)
}

pub async fn insert_url(
    State(app_state): State<Arc<AppState>>,
    Query(params): Query<HashMap<String, String>>,
    Form(form_data): Form<CreateUrl>,
) -> (StatusCode, Markup) {
    let custom_url = params.get(&String::from("custom_url")).is_some();

    match url::insert_url(&(*app_state).db_handle, form_data).await {
        Ok(i) => {
            let root_data = if custom_url {
                RootData::Custom(CustomUrl::HasIdentifier(i))
            } else {
                RootData::Auto(AutogeneratedUrl::HasIdentifier(i))
            };

            let content = views::url::render(root_data);

            (StatusCode::CREATED, content)
        }
        Err(e) => {
            let root_data = if custom_url {
                RootData::Custom(CustomUrl::Error(e))
            } else {
                RootData::Auto(AutogeneratedUrl::Error(e))
            };

            let content = views::url::render(root_data);

            (StatusCode::BAD_REQUEST, content)
        }
    }
}

pub async fn rpc_insert_url(
    State(app_state): State<Arc<AppState>>,
    Json(data): Json<CreateUrl>,
) -> (StatusCode, Json<Value>) {
    match url::insert_url(&(*app_state).db_handle, data).await {
        Ok(identifier) => (
            StatusCode::CREATED,
            Json(json!({ "identifier": identifier })),
        ),
        Err(e) => parse_url_error(e),
    }
}

pub async fn url_stats(
    State(app_state): State<Arc<AppState>>,
    Path(identifier): Path<String>,
) -> (StatusCode, Markup) {
    match url::url_stats(&(*app_state).db_handle, identifier).await {
        Ok(url_stat) => (StatusCode::OK, views::url::view_stats(&url_stat)),
        Err(_e) => {
            // TODO: Give back 50x when it's a problem with the DB
            (StatusCode::NOT_FOUND, views::status::not_found())
        }
    }
}

pub async fn fetch_url(
    State(app_state): State<Arc<AppState>>,
    Path(identifier): Path<String>,
) -> (StatusCode, HeaderMap, Markup) {
    let mut headers = HeaderMap::new();

    if emoji::is_valid(&identifier) {
        match url::fetch_url(&(*app_state).db_handle, identifier).await {
            Ok(u) => {
                headers.insert(LOCATION, u.parse().unwrap());

                // Not 301 cause 301 gets cached while 307 (temp redirect)
                // isn't cached by the browser.
                (StatusCode::TEMPORARY_REDIRECT, headers, maud::html! {})
            }
            Err(_e) => (StatusCode::NOT_FOUND, headers, views::status::not_found()),
        }
    } else {
        (StatusCode::BAD_REQUEST, headers, views::status::not_found())
    }
}

pub async fn leaderboard(State(app_state): State<Arc<AppState>>) -> (StatusCode, Markup) {
    match leaderboard::fetch(&(*app_state).db_handle).await {
        Ok(entries) => (StatusCode::OK, views::leaderboard::render(entries)),
        Err(_e) => (StatusCode::INTERNAL_SERVER_ERROR, maud::html! {}),
    }
}

// TODO: Static assets controller
pub async fn js(State(app_state): State<Arc<AppState>>) -> (StatusCode, HeaderMap, String) {
    let mut headers = HeaderMap::new();
    let mut static_assets_path = (*app_state).config.static_assets_path.clone();

    static_assets_path.push("app.js");

    match fs::read_to_string(static_assets_path) {
        Ok(content) => {
            headers.insert(
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/javascript; charset=utf-8"),
            );

            (StatusCode::OK, headers, content)
        }

        Err(_e) => (StatusCode::NOT_FOUND, headers, String::from("Not found")),
    }
}

pub async fn purifyjs(State(app_state): State<Arc<AppState>>) -> (StatusCode, HeaderMap, String) {
    let mut headers = HeaderMap::new();
    let mut static_assets_path = (*app_state).config.static_assets_path.clone();

    static_assets_path.push("purify.min.js");

    match fs::read_to_string(static_assets_path) {
        Ok(content) => {
            headers.insert(
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("application/javascript; charset=utf-8"),
            );

            (StatusCode::OK, headers, content)
        }

        Err(_e) => (StatusCode::NOT_FOUND, headers, String::from("Not found")),
    }
}

pub async fn stylesheet(State(app_state): State<Arc<AppState>>) -> (StatusCode, HeaderMap, String) {
    let mut headers = HeaderMap::new();
    let mut static_assets_path = (*app_state).config.static_assets_path.clone();

    static_assets_path.push("app.css");

    match fs::read_to_string(static_assets_path) {
        Ok(content) => {
            headers.insert(
                HeaderName::from_static("content-type"),
                HeaderValue::from_static("text/css; charset=utf-8"),
            );

            (StatusCode::OK, headers, content)
        }

        Err(_e) => (StatusCode::NOT_FOUND, headers, String::from("Not found")),
    }
}

pub async fn not_found(_uri: axum::http::Uri) -> impl axum::response::IntoResponse {
    (
        axum::http::StatusCode::NOT_FOUND,
        views::status::not_found(),
    )
}

fn parse_url_error(e: url::Error) -> (StatusCode, Json<Value>) {
    let internal_error_msg = "Something terribly wrong happened on my end";

    match e {
        url::Error::DbConnectionFailed(_) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({ "message": internal_error_msg, "type": "E001" })),
        ),
        url::Error::DbCommunicationFailed => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({ "message": internal_error_msg, "type": "E002" })),
        ),
        url::Error::FailedToPrepareQuery => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({ "message": internal_error_msg, "type": "E003" })),
        ),
        url::Error::EmptyColumn => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({ "message": internal_error_msg, "type": "E004" })),
        ),
        url::Error::DuplicateIdentifier => (
            StatusCode::BAD_REQUEST,
            Json(json!({"message": "This emoji ID is taken. Use another one!", "type": "E005"})),
        ),
        url::Error::InvalidIdentifier => (
            StatusCode::BAD_REQUEST,
            Json(json!({"message": "The ID has to be emojis. Only emojis.", "type": "E006"})),
        ),
        url::Error::InvalidURLFormat => (
            StatusCode::BAD_REQUEST,
            Json(
                json!({"message": "This is not a valid URL format. e.g https://example.com", "type": "E007"}),
            ),
        ),
        url::Error::MissingScheme => (
            StatusCode::BAD_REQUEST,
            Json(json!({
                "message": "URL must have a scheme. Valid schemes are: https, or http.\ne.g https://example.com",
                "type": "E008"
            })),
        ),
        url::Error::UnsupportedScheme => (
            StatusCode::BAD_REQUEST,
            Json(json!({
                "message": "Unsupported scheme. Acceptable schemes are: https, or http.\ne.g https://example.com",
                "type": "E009"
            })),
        ),
        url::Error::MissingHost => (
            StatusCode::BAD_REQUEST,
            Json(json!({"message": "URL host does not exist, or is invalid.", "type": "E010"})),
        ),
        url::Error::MissingPath => (
            StatusCode::BAD_REQUEST,
            Json(json!({"message": "URL path does not exist, or is invalid.", "type": "E011"})),
        ),
    }
}
