use axum::extract::{Path, State};
use axum::http::{header, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};

use super::common::UiError;
use crate::api::AppState;

/// Serve a file from `{project_root}/assets/` by slug + relative path.
/// Validates that the resolved path stays under the assets directory.
pub async fn serve_asset_mp(
    State(state): State<AppState>,
    Path((slug, asset_path)): Path<(String, String)>,
) -> Result<Response, UiError> {
    let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
    serve_asset_from(&proj.root, &asset_path).await
}

pub async fn serve_asset_single(
    State(state): State<AppState>,
    Path(asset_path): Path<String>,
) -> Result<Response, UiError> {
    serve_asset_from(&state.project_root, &asset_path).await
}

async fn serve_asset_from(root: &std::path::Path, rel: &str) -> Result<Response, UiError> {
    let assets_root = root.join("assets");
    let Ok(canonical_root) = assets_root.canonicalize() else {
        return Err(UiError::NotConfigured);
    };

    // Resolve the requested path (tolerate leading `/`)
    let requested = canonical_root.join(rel.trim_start_matches('/'));
    let resolved = match requested.canonicalize() {
        Ok(p) => p,
        // Try appending index.html for bare directory URLs
        Err(_) => {
            let with_index = requested.join("index.html");
            with_index
                .canonicalize()
                .map_err(|_| UiError::NotConfigured)?
        }
    };

    // Security: must stay within assets root
    if !resolved.starts_with(&canonical_root) {
        return Ok(StatusCode::FORBIDDEN.into_response());
    }

    // Directory → serve index.html
    let file_path = if resolved.is_dir() {
        let idx = resolved.join("index.html");
        if idx.exists() {
            idx
        } else {
            return Ok(StatusCode::NOT_FOUND.into_response());
        }
    } else {
        resolved
    };

    let bytes = tokio::fs::read(&file_path)
        .await
        .map_err(|_| UiError::NotConfigured)?;

    let ct = asset_content_type(&file_path);
    let mut resp = bytes.into_response();
    resp.headers_mut()
        .insert(header::CONTENT_TYPE, HeaderValue::from_static(ct));
    Ok(resp)
}

pub(crate) fn asset_content_type(path: &std::path::Path) -> &'static str {
    match path.extension().and_then(|e| e.to_str()) {
        Some("html") | Some("htm") => "text/html; charset=utf-8",
        Some("svg") => "image/svg+xml",
        Some("css") => "text/css",
        Some("js") => "application/javascript",
        Some("json") => "application/json",
        Some("png") => "image/png",
        Some("jpg") | Some("jpeg") => "image/jpeg",
        Some("webp") => "image/webp",
        Some("woff2") => "font/woff2",
        Some("woff") => "font/woff",
        Some("md") => "text/plain; charset=utf-8",
        _ => "application/octet-stream",
    }
}

/// Generic file server for an arbitrary base directory.
pub(crate) async fn serve_dir_from(base: &std::path::Path, rel: &str) -> Result<Response, UiError> {
    let Ok(canonical_base) = base.canonicalize() else {
        return Ok(StatusCode::NOT_FOUND.into_response());
    };
    let requested = canonical_base.join(rel.trim_start_matches('/'));
    let resolved = match requested.canonicalize() {
        Ok(p) => p,
        Err(_) => {
            let with_index = requested.join("index.html");
            match with_index.canonicalize() {
                Ok(p) => p,
                Err(_) => return Ok(StatusCode::NOT_FOUND.into_response()),
            }
        }
    };
    if !resolved.starts_with(&canonical_base) {
        return Ok(StatusCode::FORBIDDEN.into_response());
    }
    let file_path = if resolved.is_dir() {
        let idx = resolved.join("index.html");
        if idx.exists() {
            idx
        } else {
            return Ok(StatusCode::NOT_FOUND.into_response());
        }
    } else {
        resolved
    };
    let bytes = tokio::fs::read(&file_path)
        .await
        .map_err(|_| UiError::NotConfigured)?;
    let ct = asset_content_type(&file_path);
    let mut resp = bytes.into_response();
    resp.headers_mut()
        .insert(header::CONTENT_TYPE, HeaderValue::from_static(ct));
    Ok(resp)
}

/// Serve a file from `{project_root}/public/` — generated, gitignored
/// artefacts.
pub async fn serve_public_mp(
    State(state): State<AppState>,
    Path((slug, pub_path)): Path<(String, String)>,
) -> Result<Response, UiError> {
    let proj = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
    serve_dir_from(&proj.root.join("public"), &pub_path).await
}

pub async fn serve_public_single(
    State(state): State<AppState>,
    Path(pub_path): Path<String>,
) -> Result<Response, UiError> {
    serve_dir_from(&state.project_root.join("public"), &pub_path).await
}
