#[cfg(debug_assertions)]
mod live_reload;
mod picture_upload;

use std::{net::Ipv4Addr, sync::Arc};

use axum::http::header::LOCATION;
use axum::{
    Router,
    extract::{Path, Query, RawQuery, State},
    http::{
        HeaderValue, StatusCode,
        header::{CACHE_CONTROL, CONTENT_TYPE},
    },
    response::{Html, IntoResponse, Response},
    routing::get,
};
use serde::Deserialize;
use tokio::net::TcpListener;
use tracing::{error, info, instrument};

use crate::dirs::Dirs;
use crate::sources::Sources;
use crate::state::Source;
use crate::vfs::asynch::AsyncDir;
use crate::vfs::{self, VPath};

mod system {
    use crate::vfs::VPath;

    pub const INDEX: &VPath = VPath::new_const("index");
    pub const FOUR_OH_FOUR: &VPath = VPath::new_const("_treehouse/404");
    pub const B_DOCS: &VPath = VPath::new_const("_treehouse/b");
}

struct Server {
    sources: Arc<Sources>,
    target: AsyncDir,
}

#[instrument(skip(sources, dirs, target))]
pub async fn serve(
    sources: Arc<Sources>,
    dirs: Arc<Dirs>,
    target: AsyncDir,
    port: u16,
) -> anyhow::Result<()> {
    let app = Router::new()
        .route("/", get(index)) // needed explicitly because * does not match empty paths
        .route("/*path", get(vfs_entry))
        .route("/b", get(branch))
        .route("/treehouse/quit", get(quit))
        .fallback(get(four_oh_four))
        .with_state(Arc::new(Server {
            sources: sources.clone(),
            target,
        }));

    #[cfg(debug_assertions)]
    let app = app
        .nest("/dev/live-reload", live_reload::router())
        .nest("/dev/picture-upload", picture_upload::router(dirs));

    info!("serving on port {port}");
    let listener = TcpListener::bind((Ipv4Addr::from([0u8, 0, 0, 0]), port)).await?;
    Ok(axum::serve(listener, app).await?)
}

#[derive(Debug, Deserialize)]
struct VfsQuery {
    #[serde(rename = "v")]
    content_version: Option<String>,
}

#[instrument(skip(state))]
async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option<Response> {
    let vpath = VPath::try_new(path).ok()?;

    if let Some(target) = state.sources.config.redirects.path.get(vpath) {
        let url = vfs::url(&state.sources.config.site, state.target.sync(), target)?;
        return Some((StatusCode::MOVED_PERMANENTLY, [(LOCATION, url)]).into_response());
    }

    let content = state.target.content(vpath).await?;
    let content_type = HeaderValue::from_str(content.kind()).inspect_err(
        |err| error!(?err, content_type = ?content.kind(), "content type cannot be used as an HTTP header"),
    ).ok()?;
    let mut response = content.bytes().into_response();
    response.headers_mut().insert(CONTENT_TYPE, content_type);

    if query.content_version.is_some() {
        response.headers_mut().insert(
            CACHE_CONTROL,
            HeaderValue::from_static("public, max-age=31536000, immutable"),
        );
    }

    Some(response)
}

async fn vfs_entry(
    Path(path): Path<String>,
    Query(query): Query<VfsQuery>,
    State(state): State<Arc<Server>>,
) -> Response {
    if let Some(response) = get_static_file(&path, &query, &state).await {
        response
    } else {
        four_oh_four(State(state)).await
    }
}

async fn system_page(target: &AsyncDir, path: &VPath, status_code: StatusCode) -> Response {
    if let Some(content) = target.content(path).await {
        (status_code, Html(content.bytes())).into_response()
    } else {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("500 Internal Server Error: system page {path} is not available"),
        )
            .into_response()
    }
}

async fn index(State(state): State<Arc<Server>>) -> Response {
    system_page(&state.target, system::INDEX, StatusCode::OK).await
}

async fn four_oh_four(State(state): State<Arc<Server>>) -> Response {
    system_page(&state.target, system::FOUR_OH_FOUR, StatusCode::NOT_FOUND).await
}

#[instrument(skip(state))]
async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>) -> Response {
    if let Some(named_id) = named_id {
        let branch_id = state
            .sources
            .treehouse
            .branches_by_named_id
            .get(&named_id)
            .copied()
            .or_else(|| {
                state
                    .sources
                    .treehouse
                    .branch_redirects
                    .get(&named_id)
                    .copied()
            });
        if let Some(branch_id) = branch_id {
            let branch = state.sources.treehouse.tree.branch(branch_id);
            if let Source::Tree { tree_path, .. } = state.sources.treehouse.source(branch.file_id)
                && let Some(url) =
                    vfs::url(&state.sources.config.site, &state.target.sync(), tree_path)
            {
                let url = format!("{url}#{}", branch.html_id);
                return (StatusCode::FOUND, [(LOCATION, url)]).into_response();
            }
        }

        system_page(&state.target, system::FOUR_OH_FOUR, StatusCode::NOT_FOUND).await
    } else {
        system_page(&state.target, system::B_DOCS, StatusCode::OK).await
    }
}

async fn quit() -> impl IntoResponse {
    info!("somebody just quit the treehouse. congration to them!");

    (
        StatusCode::FOUND,
        [(LOCATION, "https://www.youtube.com/watch?v=dQw4w9WgXcQ")],
    )
}
