use http::{status::StatusCode, Response};
use std::path::{Path, PathBuf};

#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AssetServeError {
    #[error("Failed to infer mime type for asset: {0}")]
    InferringMimeType(std::io::Error),

    #[error("Failed to serve asset: {0}")]
    IoError(#[from] std::io::Error),

    #[error("Failed to construct response: {0}")]
    ResponseError(#[from] http::Error),
}

pub fn serve_asset_from_raw_path(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
    // If the user provided a custom asset handler, then call it and return the response if the request was handled.
    // The path is the first part of the URI, so we need to trim the leading slash.
    let mut uri_path = PathBuf::from(
        urlencoding::decode(path)
            .expect("expected URL to be UTF-8 encoded")
            .as_ref(),
    );

    // Attempt to serve from the asset dir on android using its loader
    #[cfg(target_os = "android")]
    {
        if let Some(asset) = to_java_load_asset(path) {
            return Ok(Response::builder()
                .header("Content-Type", get_mime_by_ext(&uri_path))
                .header("Access-Control-Allow-Origin", "*")
                .body(asset)?);
        }
    }

    // If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
    // This lets us handle both absolute and relative paths without being too "special"
    // It just means that our macos bundle is a little "special" because we need to place an `assets`
    // dir in the `Resources` dir.
    //
    // If there's no asset root, we use the cargo manifest dir as the root, or the current dir
    if !uri_path.exists() || uri_path.starts_with("/assets/") {
        let bundle_root = get_asset_root();
        let relative_path = uri_path.strip_prefix("/").unwrap();
        uri_path = bundle_root.join(relative_path);
    }

    // If the asset exists, then we can serve it!
    if uri_path.exists() {
        let mime_type =
            get_mime_from_path(&uri_path).map_err(AssetServeError::InferringMimeType)?;
        let body = std::fs::read(uri_path)?;
        return Ok(Response::builder()
            .header("Content-Type", mime_type)
            .header("Access-Control-Allow-Origin", "*")
            .body(body)?);
    }

    Ok(Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(String::from("Not Found").into_bytes())?)
}

/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
///
/// Currently supports:
/// - [x] macOS
/// - [x] iOS
/// - [x] Windows
/// - [x] Linux (appimage)
/// - [ ] Linux (rpm)
/// - [ ] Linux (deb)
/// - [ ] Android
#[allow(unreachable_code)]
fn get_asset_root() -> PathBuf {
    let cur_exe = std::env::current_exe().unwrap();

    #[cfg(target_os = "macos")]
    {
        return cur_exe
            .parent()
            .unwrap()
            .parent()
            .unwrap()
            .join("Resources");
    }

    // For all others, the structure looks like this:
    // app.(exe/appimage)
    //   main.exe
    //   assets/
    cur_exe.parent().unwrap().to_path_buf()
}

/// Get the mime type from a path-like string
fn get_mime_from_path(asset: &Path) -> std::io::Result<&'static str> {
    if asset.extension().is_some_and(|ext| ext == "svg") {
        return Ok("image/svg+xml");
    }

    match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
        Some(f) if f != "text/plain" => Ok(f),
        _other => Ok(get_mime_by_ext(asset)),
    }
}

/// Get the mime type from a URI using its extension
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
    match trimmed.extension().and_then(|e| e.to_str()) {
        // The common assets are all utf-8 encoded
        Some("js") => "text/javascript; charset=utf-8",
        Some("css") => "text/css; charset=utf-8",
        Some("json") => "application/json; charset=utf-8",
        Some("svg") => "image/svg+xml; charset=utf-8",
        Some("html") => "text/html; charset=utf-8",

        // the rest... idk? probably not
        Some("mjs") => "text/javascript; charset=utf-8",
        Some("bin") => "application/octet-stream",
        Some("csv") => "text/csv",
        Some("ico") => "image/vnd.microsoft.icon",
        Some("jsonld") => "application/ld+json",
        Some("rtf") => "application/rtf",
        Some("mp4") => "video/mp4",
        // Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
        Some(_) => "text/html; charset=utf-8",
        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
        // using octet stream according to this:
        None => "application/octet-stream",
    }
}

#[cfg(target_os = "android")]
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
    let normalized = filepath
        .trim_start_matches("/assets/")
        .trim_start_matches('/');

    // in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
    #[cfg(debug_assertions)]
    {
        let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
        if path.exists() {
            return std::fs::read(path).ok();
        }
    }

    use std::ptr::NonNull;

    let ctx = ndk_context::android_context();
    let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
    let mut env = vm.attach_current_thread().unwrap();

    // Query the Asset Manager
    let asset_manager_ptr = env
        .call_method(
            unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
            "getAssets",
            "()Landroid/content/res/AssetManager;",
            &[],
        )
        .expect("Failed to get asset manager")
        .l()
        .expect("Failed to get asset manager as object");

    unsafe {
        let asset_manager =
            ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);

        let asset_manager = ndk::asset::AssetManager::from_ptr(
            NonNull::new(asset_manager).expect("Invalid asset manager"),
        );

        let cstr = std::ffi::CString::new(normalized).unwrap();

        let mut asset = asset_manager.open(&cstr)?;
        Some(asset.buffer().unwrap().to_vec())
    }
}
