use std::error::Error;
use std::net::SocketAddr;
use std::path::PathBuf;

use log::warn;
use prometheus::Encoder;
use rust_embed::RustEmbed;
use serde::Deserialize;
use warp;
use warp::http::header::{HeaderMap, HeaderValue};
use warp::reject::Rejection;
use warp::Filter;

use crate::library::Library;

fn metrics() -> impl Filter<Extract = (impl warp::reply::Reply,), Error = Rejection> + Clone {
    let mut text_headers = HeaderMap::new();
    text_headers.insert("content-type", HeaderValue::from_static("text/plain"));
    warp::path("metrics")
        .map(|| {
            let mut buffer = Vec::new();
            let encoder = prometheus::TextEncoder::new();

            // Gather the metrics.
            let metric_families = prometheus::gather();
            // Encode them to send.
            encoder.encode(&metric_families, &mut buffer).unwrap();
            // TODO(wathiede): see if there's a wrapper like html()
            buffer
        })
        .with(warp::reply::with::headers(text_headers))
}

// TODO(wathiede): add caching for hashed files.  Add at least etag for everything.
fn index(path: warp::path::FullPath) -> Result<impl warp::Reply, warp::Rejection> {
    let path = path.as_str();
    let path = if path.ends_with("/") {
        format!("{}index.html", path.to_string())
    } else {
        path.to_string()
    };
    let path = &path[1..];

    match Asset::get(path) {
        Some(bytes) => {
            let mime = mime_guess::from_path(path).first_or_octet_stream();

            Ok(warp::http::Response::builder()
                .header("Content-Type", mime.essence_str())
                .body(bytes.into_owned()))
        }
        None => Err(warp::reject::not_found()),
    }
}

fn albums(lib: Library) -> Result<impl warp::Reply, warp::Rejection> {
    let albums = lib.albums().map_err(|e| {
        warn!("Couldn't find albums: {}", e);
        warp::reject::not_found()
    })?;
    Ok(warp::reply::json(&albums))
}

fn album(lib: Library, id: String) -> Result<impl warp::Reply, warp::Rejection> {
    let album = lib.album(&id).map_err(|e| {
        warn!("Couldn't find album {}: {}", id, e);
        warp::reject::not_found()
    })?;
    Ok(warp::reply::json(&album))
}

#[derive(Debug, Deserialize)]
struct ImageParams {
    w: Option<u32>,
    h: Option<u32>,
}

fn image(
    lib: Library,
    media_items_id: String,
    params: ImageParams,
) -> Result<impl warp::Reply, warp::Rejection> {
    // TODO(wathiede): add caching headers.
    match lib.thumbnail(&media_items_id, (params.w, params.h)) {
        None => {
            warn!("Couldn't find original {}", &media_items_id);
            Err(warp::reject::not_found())
        }
        Some(bytes) => Ok(warp::http::Response::builder()
            .header("Content-Type", "image/jpeg")
            .body(bytes)),
    }
}

#[derive(RustEmbed)]
#[folder = "react-debug/build/"]
struct Asset;

pub fn run(addr: SocketAddr, root: PathBuf) -> Result<(), Box<dyn Error>> {
    let lib = Library::new(root)?;
    let lib = warp::any().map(move || lib.clone());

    let index = warp::get2().and(warp::path::full()).and_then(index);

    let albums = warp::path("albums").and(lib.clone()).and_then(albums);

    let album = warp::path("album")
        .and(lib.clone())
        .and(warp::path::param())
        .and_then(album);

    let image = warp::path("image")
        .and(lib.clone())
        .and(warp::path::param())
        .and(warp::query::<ImageParams>())
        .and_then(image);

    let api = albums.or(album).or(image);
    let api = warp::path("api").and(api);

    // Fallback, always keep this last.
    let api = api.or(index);

    let api = api.with(warp::log("photosync"));
    // We don't want metrics & heath checking filling up the logs, so we add this handler after
    // wrapping with the log filter.
    let routes = metrics().or(api);

    warp::serve(routes).run(addr);
    Ok(())
}
