use crate::http::server::{AppState, Mode};
use crate::paths;

use axum::{
    body::Body,
    extract::{Path as AxPath, State},
    http::{HeaderValue, Request, StatusCode, header},
    response::{IntoResponse, Response},
};
use std::path::{Path, PathBuf};
use tokio::io::AsyncReadExt;
use tower_http::services::ServeFile;
use tracing::warn;

const PEEK_BYTES: usize = 8192;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FileMode {
    Markdown,
    Source,
    Dual,
    Native,
    Download,
}

#[derive(Clone, Copy)]
pub(crate) struct FileView {
    pub(crate) kind: &'static str,
    pub(crate) preview_hidden: bool,
    pub(crate) raw_hidden: bool,
}

impl FileView {
    pub(crate) fn markdown() -> Self {
        Self {
            kind: "markdown",
            preview_hidden: false,
            raw_hidden: true,
        }
    }

    pub(crate) fn source() -> Self {
        Self {
            kind: "source",
            preview_hidden: true,
            raw_hidden: false,
        }
    }

    pub(crate) fn dual() -> Self {
        Self {
            kind: "dual",
            preview_hidden: false,
            raw_hidden: true,
        }
    }
}

pub(crate) fn file_mode(path: &Path, bytes: &[u8]) -> FileMode {
    let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
    if ext.eq_ignore_ascii_case("md") {
        return FileMode::Markdown;
    }
    if ext.eq_ignore_ascii_case("svg") && is_text_content(bytes) {
        return FileMode::Dual;
    }
    if let Some(kind) = infer::get(bytes) {
        if is_pdf(kind.mime_type()) {
            return FileMode::Native;
        }
        return match kind.matcher_type() {
            infer::MatcherType::Image | infer::MatcherType::Video | infer::MatcherType::Audio => {
                FileMode::Native
            }
            infer::MatcherType::Text => FileMode::Source,
            _ => FileMode::Download,
        };
    }
    if is_text_content(bytes) {
        FileMode::Source
    } else {
        FileMode::Download
    }
}

pub(crate) async fn file_mode_async(path: &Path) -> FileMode {
    let Ok(mut file) = tokio::fs::File::open(path).await else {
        return FileMode::Download;
    };
    let mut bytes = vec![0; PEEK_BYTES];
    let Ok(n) = file.read(&mut bytes).await else {
        return FileMode::Download;
    };
    bytes.truncate(n);
    file_mode(path, &bytes)
}

pub(crate) fn is_text_content(bytes: &[u8]) -> bool {
    content_inspector::inspect(bytes).is_text()
}

fn is_pdf(mime: &str) -> bool {
    mime == "application/pdf"
}

pub(crate) async fn runtime_asset(AxPath(path): AxPath<String>) -> Response {
    let base = match crate::http::assets::dir() {
        Ok(d) => d,
        Err(e) => {
            warn!("runtime asset dir error: {}", e);
            return not_found();
        }
    };
    let rel = path.trim_start_matches('/');
    let Some(rel) = paths::safe_rel(rel) else {
        return not_found();
    };
    stream_file(&base.join(rel)).await
}

pub(crate) async fn vendor(AxPath(path): AxPath<String>) -> Response {
    let path = match crate::http::vendor::path(&path) {
        Ok(p) => p,
        Err(_) => return not_found(),
    };
    stream_file(&path).await
}

pub(crate) async fn raw_file(State(s): State<AppState>, AxPath(path): AxPath<String>) -> Response {
    let Some(path) = resolve_internal_file(&s, &path) else {
        return not_found();
    };
    if previews_text(&path).await {
        stream_text_file(&path).await
    } else {
        stream_file(&path).await
    }
}

pub(crate) async fn html_file(State(s): State<AppState>, AxPath(path): AxPath<String>) -> Response {
    let Some(path) = resolve_internal_file(&s, &path) else {
        return not_found();
    };
    stream_file(&path).await
}

pub(crate) async fn download_file(
    State(s): State<AppState>,
    AxPath(path): AxPath<String>,
) -> Response {
    let Some(path) = resolve_internal_file(&s, &path) else {
        return not_found();
    };
    stream_download(&path).await
}

pub(crate) async fn stream_file(path: &Path) -> Response {
    serve_file(ServeFile::new(path)).await
}

pub(crate) async fn stream_download(path: &Path) -> Response {
    let mut res = stream_file(path).await;
    if res.status().is_success() {
        res.headers_mut().insert(
            header::CONTENT_DISPOSITION,
            HeaderValue::from_str(&content_disposition(path)).unwrap(),
        );
    }
    res
}

async fn stream_text_file(path: &Path) -> Response {
    let mut res = stream_file(path).await;
    if res.status().is_success() {
        res.headers_mut().insert(
            header::CONTENT_TYPE,
            HeaderValue::from_static("text/plain; charset=utf-8"),
        );
    }
    res
}

async fn serve_file(mut file: ServeFile) -> Response {
    match file.try_call(Request::new(Body::empty())).await {
        Ok(res) => {
            let mut res = res.map(Body::new).into_response();
            res.headers_mut()
                .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
            res
        }
        Err(_) => not_found(),
    }
}

pub(crate) async fn previews_text(path: &Path) -> bool {
    matches!(
        file_mode_async(path).await,
        FileMode::Markdown | FileMode::Source | FileMode::Dual
    )
}

pub(crate) fn raw_blob_html(text: &str, lang: Option<&str>) -> String {
    let attrs = lang
        .map(|lang| {
            format!(
                r#" class="language-{lang}" data-lang="{lang}""#,
                lang = html_escape::encode_double_quoted_attribute(lang),
            )
        })
        .unwrap_or_default();
    format!(
        "<div class=\"ghrm-blob\">{}<div class=\"highlight ghrm-blob-source\" hidden><pre tabindex=\"0\" class=\"chroma\"><code{attrs}>{body}</code></pre></div><table class=\"ghrm-blob-table\" role=\"presentation\"><tbody></tbody></table></div>",
        raw_source_html(text),
        attrs = attrs,
        body = html_escape::encode_text(text),
    )
}

fn raw_source_html(text: &str) -> String {
    format!(
        "<template class=\"ghrm-data\">{}</template>",
        html_escape::encode_text(text),
    )
}

pub(crate) fn file_view_attrs(rel: &str, view: FileView) -> String {
    format!(
        "data-ghrm-view-kind=\"{kind}\" data-current-path=\"{current}\" data-ghrm-raw-url=\"{raw}\" data-ghrm-download-url=\"{download}\"",
        kind = view.kind,
        current = html_escape::encode_double_quoted_attribute(rel.trim_matches('/')),
        raw = html_escape::encode_double_quoted_attribute(&internal_file_href("raw", rel)),
        download =
            html_escape::encode_double_quoted_attribute(&internal_file_href("download", rel)),
    )
}

fn internal_file_href(kind: &str, rel: &str) -> String {
    format!("/_ghrm/{kind}/{}", rel.trim_matches('/'))
}

fn resolve_internal_file(s: &AppState, rel: &str) -> Option<PathBuf> {
    let base = if s.mode == Mode::File {
        s.target.parent().unwrap_or(s.target.as_path())
    } else {
        s.target.as_path()
    };
    paths::resolve_file(base, rel)
}

fn content_disposition(path: &Path) -> String {
    let filename = path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("file")
        .replace('\\', "\\\\")
        .replace('"', "\\\"");
    format!("attachment; filename=\"{filename}\"")
}

fn not_found() -> Response {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .header(header::CACHE_CONTROL, "no-store")
        .body(Body::from("404"))
        .unwrap()
}

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

    #[test]
    fn content_disposition_escapes_quotes() {
        let value = content_disposition(Path::new("odd\"name.md"));
        assert_eq!(value, "attachment; filename=\"odd\\\"name.md\"");
    }

    #[test]
    fn content_disposition_escapes_backslashes() {
        let value = content_disposition(Path::new("odd\\name.md"));
        assert_eq!(value, "attachment; filename=\"odd\\\\name.md\"");
    }

    #[test]
    fn file_mode_markdown_by_extension() {
        assert_eq!(
            file_mode(Path::new("README.md"), b"# Title\n"),
            FileMode::Markdown
        );
        assert_eq!(file_mode(Path::new("DOC.MD"), b"text"), FileMode::Markdown);
    }

    #[test]
    fn file_mode_svg_is_dual() {
        let svg = b"<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
        assert_eq!(file_mode(Path::new("icon.svg"), svg), FileMode::Dual);
        assert_eq!(file_mode(Path::new("LOGO.SVG"), svg), FileMode::Dual);
    }

    #[test]
    fn file_mode_text_is_source() {
        assert_eq!(
            file_mode(Path::new("main.rs"), b"fn main() {}\n"),
            FileMode::Source
        );
        assert_eq!(
            file_mode(Path::new("script.py"), b"print('hi')\n"),
            FileMode::Source
        );
    }

    #[test]
    fn file_mode_png_is_native() {
        let png = &[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
        assert_eq!(file_mode(Path::new("image.png"), png), FileMode::Native);
    }

    #[test]
    fn file_mode_pdf_is_native() {
        let pdf = &[
            0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x37, 0x0a, 0x25, 0xe2, 0xe3, 0xcf, 0xd3,
        ];
        assert_eq!(file_mode(Path::new("report.pdf"), pdf), FileMode::Native);
    }

    #[test]
    fn file_mode_zstd_is_download() {
        let zstd = &[0x28, 0xb5, 0x2f, 0xfd];
        assert_eq!(
            file_mode(Path::new("archive.tar.zst"), zstd),
            FileMode::Download
        );
    }

    #[test]
    fn file_mode_binary_is_download() {
        let elf = &[
            0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00,
        ];
        assert_eq!(file_mode(Path::new("program"), elf), FileMode::Download);
    }

    #[test]
    fn internal_file_href_trims_slashes() {
        assert_eq!(
            internal_file_href("raw", "/docs/readme.md/"),
            "/_ghrm/raw/docs/readme.md"
        );
        assert_eq!(
            internal_file_href("download", "docs/readme.md"),
            "/_ghrm/download/docs/readme.md"
        );
    }

    #[test]
    fn file_view_attrs_escapes_current_and_urls() {
        let attrs = file_view_attrs("docs/odd\"name.md", FileView::source());

        assert!(attrs.contains(r#"data-current-path="docs/odd&quot;name.md""#));
        assert!(attrs.contains(r#"data-ghrm-raw-url="/_ghrm/raw/docs/odd&quot;name.md""#));
        assert!(
            attrs.contains(r#"data-ghrm-download-url="/_ghrm/download/docs/odd&quot;name.md""#)
        );
    }

    #[test]
    fn raw_blob_includes_hidden_source_block() {
        let html = raw_blob_html("fn main() {}\n", Some("rust"));
        assert!(html.contains("ghrm-blob-source"));
        assert!(html.contains(r#"class="language-rust""#));
        assert!(html.contains("<tbody></tbody>"));
    }

    #[tokio::test]
    async fn previews_text_true_for_markdown() {
        use crate::testutil::TempDir;
        let dir = TempDir::new("ghrm-delivery-md");
        let md_path = dir.path().join("readme.md");
        std::fs::write(&md_path, "# Title\n").unwrap();

        assert!(previews_text(&md_path).await);
    }

    #[tokio::test]
    async fn previews_text_true_for_source() {
        use crate::testutil::TempDir;
        let dir = TempDir::new("ghrm-delivery-rs");
        let rs_path = dir.path().join("main.rs");
        std::fs::write(&rs_path, "fn main() {}\n").unwrap();

        assert!(previews_text(&rs_path).await);
    }

    #[tokio::test]
    async fn previews_text_false_for_binary() {
        use crate::testutil::TempDir;
        let dir = TempDir::new("ghrm-delivery-bin");
        let bin_path = dir.path().join("program");
        let elf = &[
            0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00,
        ];
        std::fs::write(&bin_path, elf).unwrap();

        assert!(!previews_text(&bin_path).await);
    }

    #[tokio::test]
    async fn stream_download_sets_content_disposition() {
        use crate::testutil::TempDir;
        let dir = TempDir::new("ghrm-delivery-download");
        let path = dir.path().join("doc.md");
        std::fs::write(&path, "# Doc\n").unwrap();

        let resp = stream_download(&path).await;

        assert!(resp.status().is_success());
        let disp = resp.headers().get(header::CONTENT_DISPOSITION).unwrap();
        assert!(disp.to_str().unwrap().contains("doc.md"));
    }

    #[tokio::test]
    async fn stream_file_not_found_returns_404() {
        let resp = stream_file(Path::new("/nonexistent/path/file.txt")).await;
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
    }

    #[test]
    fn raw_source_html_escapes_content() {
        let html = raw_source_html("<script>alert('xss')</script>");
        assert!(html.contains("&lt;script&gt;"));
        assert!(!html.contains("<script>alert"));
    }

    #[test]
    fn not_found_returns_404_with_no_store() {
        let resp = not_found();
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
        assert_eq!(
            resp.headers().get(header::CACHE_CONTROL).unwrap(),
            "no-store"
        );
    }

    #[test]
    fn file_mode_svg_binary_is_download() {
        let svg_bytes: &[u8] = &[0x00, 0x00, 0x00, 0x01, 0x00];
        assert_eq!(
            file_mode(Path::new("image.svg"), svg_bytes),
            FileMode::Download
        );
    }

    #[test]
    fn file_mode_extension_override_content() {
        let text = b"plain text content here";
        assert_eq!(file_mode(Path::new("doc.MD"), text), FileMode::Markdown);
    }

    #[test]
    fn file_view_attrs_kind_preserved() {
        let md = file_view_attrs("/docs/readme.md", FileView::markdown());
        assert!(md.contains(r#"data-ghrm-view-kind="markdown""#));

        let src = file_view_attrs("/src/main.rs", FileView::source());
        assert!(src.contains(r#"data-ghrm-view-kind="source""#));

        let dual = file_view_attrs("/icon.svg", FileView::dual());
        assert!(dual.contains(r#"data-ghrm-view-kind="dual""#));
    }
}
