use actix_web::{http::header, web, HttpRequest, HttpResponse};
use mime_guess2::MimeGuess;

fn cache_control_header(path: &str) -> &'static str {
    if path == "index.html" {
        "no-cache, no-store, must-revalidate"
    } else if path.ends_with(".js")
        || path.ends_with(".css")
        || path.ends_with(".woff2")
        || path.ends_with(".woff")
        || path.ends_with(".ttf")
        || path.ends_with(".otf")
        || path.ends_with(".png")
        || path.ends_with(".jpg")
        || path.ends_with(".jpeg")
        || path.ends_with(".gif")
        || path.ends_with(".svg")
        || path.ends_with(".ico")
        || path.ends_with(".webp")
        || path.ends_with(".avif")
        || path.ends_with(".map")
    {
        "public, max-age=31536000, immutable"
    } else {
        "no-cache"
    }
}

/// Returns true if the client explicitly accepts the given encoding via Accept-Encoding header.
/// "*" is treated as accepting all encodings.
fn accepts_encoding(req: &HttpRequest, target: &str) -> bool {
    let header_val = match req.headers().get(header::ACCEPT_ENCODING) {
        Some(v) => v.to_str().unwrap_or(""),
        None => return false,
    };
    for part in header_val.split(',') {
        let part = part.trim();
        // Strip quality value if present
        let enc = part.split(';').next().unwrap_or(part).trim();
        if enc.eq_ignore_ascii_case(target) || enc == "*" {
            return true;
        }
    }
    false
}

/// Determines the Content-Type for a path, with explicit fallback for common frontend extensions.
fn content_type_for_path(path: &str) -> String {
    if let Some(ext) = path.rsplit('.').next() {
        match ext {
            "html" | "htm" => return "text/html; charset=utf-8".into(),
            "js" | "mjs" => return "application/javascript; charset=utf-8".into(),
            "jsx" | "ts" | "tsx" => return "text/plain; charset=utf-8".into(),
            "css" => return "text/css; charset=utf-8".into(),
            "json" => return "application/json; charset=utf-8".into(),
            "svg" => return "image/svg+xml".into(),
            "png" => return "image/png".into(),
            "jpg" | "jpeg" => return "image/jpeg".into(),
            "gif" => return "image/gif".into(),
            "webp" => return "image/webp".into(),
            "ico" => return "image/x-icon".into(),
            "woff" => return "font/woff".into(),
            "woff2" => return "font/woff2".into(),
            "ttf" => return "font/ttf".into(),
            "otf" => return "font/otf".into(),
            "txt" => return "text/plain; charset=utf-8".into(),
            "xml" => return "application/xml; charset=utf-8".into(),
            _ => {}
        }
    }
    MimeGuess::from_path(path).first_or_octet_stream().to_string()
}

/// Build an HttpResponse for the given asset.
/// Sets Content-Type, Cache-Control, ETag, and Content-Encoding (only if non-empty).
fn build_asset_response(
    req: &HttpRequest,
    data: &[u8],
    etag: &str,
    path: &str,
    cc: &str,
    content_encoding: &str,
) -> HttpResponse {
    // 304 Not Modified when client has the same ETag
    if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) {
        if let Ok(client_etag) = if_none_match.to_str() {
            if client_etag == etag {
                let mut resp = HttpResponse::NotModified();
                resp.insert_header(("Cache-Control", cc));
                resp.insert_header(("ETag", etag));
                if !content_encoding.is_empty() {
                    resp.insert_header(("Content-Encoding", content_encoding));
                }
                return resp.finish();
            }
        }
    }

    let mime = content_type_for_path(path);
    let mut resp = HttpResponse::Ok();
    resp.content_type(mime);
    resp.insert_header(("Cache-Control", cc));
    resp.insert_header(("ETag", etag));
    if !content_encoding.is_empty() {
        resp.insert_header(("Content-Encoding", content_encoding));
    }
    resp.body(data.to_vec())
}

pub async fn serve_frontend(req: HttpRequest, path: web::Path<String>) -> HttpResponse {
    let path = path.into_inner();
    let path_str = if path.is_empty() || path == "/" {
        "index.html"
    } else {
        path.as_str()
    };
    let cc = cache_control_header(path_str);

    // Try brotli first (best compression), then gzip, then uncompressed.
    // Only serve compressed variant if client explicitly accepts it AND we have one.
    let (data, encoding, etag, content_path) =
        match frontend::get_frontend_asset_compressed(path_str) {
            Some(r) => (r.0, r.1, r.2, path_str),
            None => {
                // Path not found — try index.html as SPA fallback.
                // Also use "index.html" for Content-Type detection (text/html).
                match frontend::get_frontend_asset_with_etag("index.html") {
                    Some((data, etag)) => (data, "", etag, "index.html"),
                    None => return HttpResponse::NotFound().finish(),
                }
            }
        };

    if !encoding.is_empty() && accepts_encoding(&req, &encoding) {
        build_asset_response(&req, data, etag, content_path, cc, &encoding)
    } else {
        build_asset_response(&req, data, etag, content_path, cc, "")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_content_type_for_html() {
        assert_eq!(
            content_type_for_path("index.html"),
            "text/html; charset=utf-8"
        );
    }

    #[test]
    fn test_content_type_for_hashed_js() {
        assert_eq!(
            content_type_for_path("index-De8T6ILu.js"),
            "application/javascript; charset=utf-8"
        );
    }

    #[test]
    fn test_content_type_for_extensionless() {
        let ct = content_type_for_path("auth/login");
        assert!(!ct.is_empty());
    }
}
