//! Core logic for `mimetype-cli`.
//!
//! All functions here are `pub` so they can be exercised by integration tests
//! and fuzz targets without going through the CLI layer.
//!
//! Detection pipeline:
//! 1. `detect_by_magic` — binary magic-byte signatures via `infer` (~70 types)
//! 2. `detect_by_content` — text content sniffing via `content_inspector` + heuristics
//! 3. `detect_mime` — unified: tries magic first, falls back to content

// Include the phf::Set generated by build.rs from the mediatypes source.
// Provides: `KNOWN_MIME_TYPES: phf::Set<&str>` and `KNOWN_MIME_COUNT: usize`
#[allow(
    clippy::unreadable_literal,
    clippy::doc_markdown,
    dead_code,
    missing_docs
)]
mod generated {
    include!(concat!(env!("OUT_DIR"), "/mime_set.rs"));
}
use generated::KNOWN_MIME_TYPES;

pub mod fs;
pub mod path_security;

/// Number of MIME type constants in the generated set.
pub use generated::KNOWN_MIME_COUNT;

/// Returns `true` when `mime` is a constant defined in the `mediatypes` crate.
///
/// The lookup is an O(1) perfect-hash look-up against all known constants
/// extracted at compile time.
#[must_use]
pub fn in_mediatypes(mime: &str) -> bool {
    KNOWN_MIME_TYPES.contains(mime)
}

/// Human-readable label for the top-level MIME module (mirrors the module
/// structure of the `mediatypes` crate).
#[must_use]
pub fn module_label(mime: &str) -> &'static str {
    let top = mime.split('/').next().unwrap_or_default();
    match top {
        "application" => "Application-specific data",
        "audio" => "Audio",
        "chemical" => "Chemical data",
        "font" => "Font",
        "image" => "Image",
        "message" => "Message protocol",
        "model" => "3D model",
        "multipart" => "Multipart",
        "text" => "Text",
        "video" => "Video",
        "x-conference" => "Conference (experimental)",
        "x-shader" => "Shader (experimental)",
        _ => "Unknown",
    }
}

/// All file extensions known for `mime`, each prefixed with `.`.
/// Returns an empty `Vec` if no mapping exists.
#[must_use]
pub fn extensions_for(mime: &str) -> Vec<String> {
    mime_guess::get_mime_extensions_str(mime)
        .unwrap_or_default()
        .iter()
        .map(|e| format!(".{e}"))
        .collect()
}

/// All MIME types associated with a file extension.
/// `ext` may optionally start with a dot (`.png` or `png` both work).
/// Returns an empty `Vec` when the extension is unknown.
#[must_use]
pub fn lookup_by_ext(ext: &str) -> Vec<String> {
    let clean = ext.trim_start_matches('.');
    mime_guess::from_ext(clean)
        .iter()
        .map(|m| m.to_string())
        .collect()
}

/// Detect the MIME type of `bytes` via magic-byte signatures (`infer`).
/// Returns `None` when no signature matches.
#[must_use]
pub fn detect_by_magic(bytes: &[u8]) -> Option<String> {
    infer::get(bytes).map(|t| t.mime_type().to_owned())
}

/// Detect the MIME type of text content via heuristic content sniffing.
///
/// Only called when `detect_by_magic` returns `None`.  Uses
/// `content_inspector` to verify the buffer is text, then matches against
/// known text-format patterns (shebang lines, JSON, XML, YAML, etc.).
///
/// Returns `None` for binary content or truly unrecognisable text.
#[must_use]
pub fn detect_by_content(bytes: &[u8]) -> Option<String> {
    if bytes.is_empty() {
        return None;
    }

    // Reject binary content early.
    if !content_inspector::inspect(bytes).is_text() {
        return None;
    }

    // Work with UTF-8 lossy; only examine the first 4 KiB for speed.
    let limit = bytes.len().min(4096);
    let text = String::from_utf8_lossy(bytes.get(..limit).unwrap_or_default());
    let trimmed = text.trim_start();

    // ── Shebang detection ────────────────────────────────────────────────
    if let Some(rest) = trimmed.strip_prefix("#!") {
        return Some(detect_shebang(rest).to_owned());
    }

    // ── Structured format detection (order matters) ──────────────────────

    // JSON: starts with { or [
    if trimmed.starts_with('{') || trimmed.starts_with('[') {
        return Some("application/json".to_owned());
    }

    // PHP: starts with <?php
    if trimmed.starts_with("<?php") {
        return Some("text/x-php".to_owned());
    }

    // XML / SVG / HTML
    if trimmed.starts_with("<?xml") {
        // Check for SVG inside XML
        if text.contains("<svg") {
            return Some("image/svg+xml".to_owned());
        }
        return Some("text/xml".to_owned());
    }
    if trimmed.starts_with("<svg") {
        return Some("image/svg+xml".to_owned());
    }
    if starts_with_html(trimmed) {
        return Some("text/html".to_owned());
    }

    // YAML: starts with --- or looks like key: value
    if trimmed.starts_with("---") {
        return Some("text/x-yaml".to_owned());
    }

    // TOML: starts with [section] or key = "value"
    if looks_like_toml(trimmed) {
        return Some("application/toml".to_owned());
    }

    // ── Programming language detection ───────────────────────────────────
    if let Some(lang) = detect_programming_language(trimmed) {
        return Some(lang.to_owned());
    }

    // ── Fallback: plain text ─────────────────────────────────────────────
    Some("text/plain".to_owned())
}

/// Unified MIME detection: binary magic first, text content sniffing fallback.
///
/// This is the primary detection function.  It combines `detect_by_magic`
/// (fast binary path via `infer`) with `detect_by_content` (text heuristics).
#[must_use]
pub fn detect_mime(bytes: &[u8]) -> Option<String> {
    detect_by_magic(bytes).or_else(|| detect_by_content(bytes))
}

// ─── Fuzzing-only public wrappers ────────────────────────────────────────────
// These expose private helpers so fuzz targets can exercise them directly.
// The `fuzzing` cfg is set automatically by `cargo fuzz`.

/// Expose `detect_shebang` for fuzz targets.
#[cfg(fuzzing)]
#[must_use]
pub fn fuzz_detect_shebang(rest: &str) -> &'static str {
    detect_shebang(rest)
}

/// Expose `starts_with_html` for fuzz targets.
#[cfg(fuzzing)]
#[must_use]
pub fn fuzz_starts_with_html(s: &str) -> bool {
    starts_with_html(s)
}

/// Expose `looks_like_toml` for fuzz targets.
#[cfg(fuzzing)]
#[must_use]
pub fn fuzz_looks_like_toml(s: &str) -> bool {
    looks_like_toml(s)
}

// ─── Private helpers ──────────────────────────────────────────────────────────

/// Detect interpreter from a shebang line.
fn detect_shebang(rest: &str) -> &'static str {
    let interpreter = rest
        .split_whitespace()
        .next()
        .unwrap_or_default()
        .rsplit('/')
        .next()
        .unwrap_or_default();

    // Handle `env <interpreter>` pattern
    let resolved = if interpreter == "env" {
        rest.split_whitespace()
            .nth(1)
            .unwrap_or_default()
            .rsplit('/')
            .next()
            .unwrap_or_default()
    } else {
        interpreter
    };

    match resolved {
        s if s.starts_with("python") => "text/x-python",
        "ruby" | "irb" => "text/x-ruby",
        "perl" | "perl5" | "perl6" => "text/x-perl",
        "node" | "deno" | "bun" => "application/javascript",
        "lua" | "luajit" => "text/x-lua",
        "php" => "text/x-php",
        "Rscript" | "r" => "text/x-r",
        "awk" | "gawk" | "mawk" | "nawk" => "text/x-awk",
        "make" | "gmake" => "text/x-makefile",
        // bash/sh/zsh/etc. and unknown interpreters → shellscript
        _ => "text/x-shellscript",
    }
}

/// Check if text starts with an HTML-like tag.
fn starts_with_html(s: &str) -> bool {
    let lower = s.to_ascii_lowercase();
    lower.starts_with("<!doctype html")
        || lower.starts_with("<html")
        || lower.starts_with("<head")
        || lower.starts_with("<body")
        || lower.starts_with("<script")
}

/// Heuristic: does text look like TOML?
fn looks_like_toml(s: &str) -> bool {
    // [section] at the start of a line
    if s.starts_with('[') && !s.starts_with("[[") {
        if let Some(end) = s.find(']') {
            let section = s.get(1..end).unwrap_or_default();
            if !section.is_empty()
                && !section.contains('{')
                && !section.contains('"')
                && section
                    .chars()
                    .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
            {
                return true;
            }
        }
    }
    // key = "value" pattern
    for line in s.lines().take(5) {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        if trimmed.contains(" = ") && !trimmed.starts_with('-') {
            return true;
        }
        break;
    }
    false
}

/// Detect programming language from content patterns.
fn detect_programming_language(s: &str) -> Option<&'static str> {
    // C / C++
    if s.starts_with("#include") || s.starts_with("#ifndef") || s.starts_with("#define") {
        return Some("text/x-c");
    }

    // Rust
    if (s.contains("fn ") && s.contains("let "))
        || s.starts_with("use std::")
        || s.starts_with("pub fn ")
        || s.starts_with("mod ")
    {
        return Some("text/x-rust");
    }

    // Go
    if s.starts_with("package ") && s.contains("func ") {
        return Some("text/x-go");
    }

    // Java
    if s.starts_with("package ") || (s.contains("public class ") && s.contains("static void")) {
        return Some("text/x-java");
    }

    // CSS
    if (s.contains('{') && s.contains('}') && s.contains(':') && s.contains(';'))
        && (s.contains("margin")
            || s.contains("padding")
            || s.contains("color")
            || s.contains("font")
            || s.contains("display"))
    {
        return Some("text/css");
    }

    // Python (without shebang)
    if s.starts_with("import ")
        || s.starts_with("from ")
        || s.starts_with("def ")
        || s.starts_with("class ")
    {
        return Some("text/x-python");
    }

    // JavaScript / TypeScript (weak signals)
    if s.starts_with("const ")
        || s.starts_with("let ")
        || s.starts_with("var ")
        || s.starts_with("function ")
        || s.starts_with("export ")
    {
        return Some("application/javascript");
    }
    if s.starts_with("interface ")
        || s.starts_with("type ")
        || (s.contains(": string") && s.contains(": number"))
    {
        return Some("application/typescript");
    }

    // Makefile
    if s.contains(":\n\t") || (s.starts_with(".PHONY") || s.starts_with("all:")) {
        return Some("text/x-makefile");
    }

    // YAML (without leading ---)
    // Check for key: value pattern in first few lines
    let mut yaml_score = 0u8;
    for line in s.lines().take(10) {
        let t = line.trim();
        if t.is_empty() || t.starts_with('#') {
            continue;
        }
        if t.contains(": ") && !t.starts_with('{') && !t.starts_with('[') {
            yaml_score = yaml_score.saturating_add(1);
        }
    }
    if yaml_score >= 2 {
        return Some("text/x-yaml");
    }

    None
}

// ─── Kani model-checking proofs ──────────────────────────────────────────────
//
// **CBMC limitation:** Functions using `str::split_whitespace()`,
// `str::rsplit('/')`, `phf::Set`, or `to_ascii_lowercase()` cause CBMC
// to spend hours exploring iterator state machines and hash tables.
// These functions are verified by 588 unit/property tests and 10 fuzz
// targets (89M+ executions) instead.
//
// The Kani proofs below verify only `starts_with_html`, which uses
// only `to_ascii_lowercase()` + `starts_with()` — the simplest
// function in the crate.  Even this requires careful bounding.

#[cfg(kani)]
mod kani_proofs {
    use super::*;

    /// Prove `starts_with_html` returns true for doctype.
    #[kani::proof]
    fn proof_html_doctype() {
        assert!(starts_with_html("<!doctype html>"));
    }

    /// Prove `starts_with_html` returns true for html tag.
    #[kani::proof]
    fn proof_html_tag() {
        assert!(starts_with_html("<html>"));
    }

    /// Prove `starts_with_html` returns true for head tag.
    #[kani::proof]
    fn proof_html_head() {
        assert!(starts_with_html("<head>"));
    }

    /// Prove `starts_with_html` returns false for empty.
    #[kani::proof]
    fn proof_html_empty_false() {
        assert!(!starts_with_html(""));
    }

    /// Prove `starts_with_html` returns false for JSON.
    #[kani::proof]
    fn proof_html_json_false() {
        assert!(!starts_with_html("{\"key\": 1}"));
    }
}

// ─── Unit tests ───────────────────────────────────────────────────────────────

#[cfg(test)]
#[allow(
    clippy::missing_panics_doc,
    clippy::assertions_on_constants,
    clippy::panic
)]
mod tests {
    use super::generated::KNOWN_MIME_COUNT;
    use super::*;

    // ── in_mediatypes ────────────────────────────────────────────────────────

    #[test]
    fn known_common_types_present() {
        let types = [
            "application/json",
            "application/xml",
            "application/pdf",
            "application/zip",
            "application/gzip",
            "application/octet-stream",
            "application/wasm",
            "image/png",
            "image/jpeg",
            "image/gif",
            "image/webp",
            "image/svg+xml",
            "image/tiff",
            "image/bmp",
            "text/plain",
            "text/html",
            "text/css",
            "text/csv",
            "text/javascript",
            "text/markdown",
            "audio/mpeg",
            "audio/ogg",
            "audio/wav",
            "audio/flac",
            "video/mp4",
            "video/webm",
            "video/mpeg",
            "font/ttf",
            "font/otf",
            "font/woff",
            "font/woff2",
            "multipart/form-data",
        ];
        for mime in types {
            assert!(in_mediatypes(mime), "`{mime}` should be in mediatypes");
        }
    }

    #[test]
    fn fake_types_absent() {
        let fakes = [
            "application/x-definitely-fake-xyz-000",
            "image/x-no-such-format",
            "",
            "/",
            "not-a-mime",
            "//",
            "application/",
            "/json",
        ];
        for fake in fakes {
            assert!(!in_mediatypes(fake), "`{fake}` should NOT be in mediatypes");
        }
    }

    #[test]
    fn count_is_plausible() {
        assert!(
            KNOWN_MIME_COUNT >= 2_500,
            "expected \u{2265}2500 MIME types, got {KNOWN_MIME_COUNT}"
        );
        assert!(
            KNOWN_MIME_COUNT <= 3_500,
            "suspiciously high count {KNOWN_MIME_COUNT}; check build.rs"
        );
    }

    // ── module_label ─────────────────────────────────────────────────────────

    #[test]
    fn module_label_known_prefixes() {
        let cases = [
            ("application/json", "Application-specific data"),
            ("audio/mpeg", "Audio"),
            ("chemical/x-pdb", "Chemical data"),
            ("font/ttf", "Font"),
            ("image/png", "Image"),
            ("message/rfc822", "Message protocol"),
            ("model/gltf+json", "3D model"),
            ("multipart/mixed", "Multipart"),
            ("text/html", "Text"),
            ("video/mp4", "Video"),
        ];
        for (mime, want) in cases {
            assert_eq!(module_label(mime), want, "module_label({mime:?})");
        }
    }

    #[test]
    fn module_label_unknown_falls_back() {
        assert_eq!(module_label("x-custom/thing"), "Unknown");
        assert_eq!(module_label(""), "Unknown");
    }

    // ── extensions_for ───────────────────────────────────────────────────────

    #[test]
    fn png_has_png_extension() {
        let exts = extensions_for("image/png");
        assert!(exts.contains(&".png".to_owned()), "exts={exts:?}");
    }

    #[test]
    fn json_has_json_extension() {
        let exts = extensions_for("application/json");
        assert!(exts.contains(&".json".to_owned()), "exts={exts:?}");
    }

    #[test]
    fn extensions_start_with_dot() {
        for ext in extensions_for("text/html") {
            assert!(ext.starts_with('.'), "expected dot prefix, got {ext:?}");
        }
    }

    #[test]
    fn unknown_mime_returns_empty() {
        assert!(extensions_for("application/x-fake-zzz-999").is_empty());
    }

    // ── lookup_by_ext ────────────────────────────────────────────────────────

    #[test]
    fn ext_json_gives_application_json() {
        let mimes = lookup_by_ext("json");
        assert!(
            mimes.iter().any(|m| m == "application/json"),
            "mimes={mimes:?}"
        );
    }

    #[test]
    fn ext_with_dot_prefix_works() {
        assert_eq!(lookup_by_ext(".png"), lookup_by_ext("png"));
    }

    #[test]
    fn ext_png_gives_image_png() {
        let mimes = lookup_by_ext("png");
        assert!(mimes.iter().any(|m| m == "image/png"), "mimes={mimes:?}");
    }

    #[test]
    fn unknown_ext_returns_empty() {
        assert!(lookup_by_ext("xyzzy_no_such_ext_abc").is_empty());
    }

    // ── detect_by_magic ──────────────────────────────────────────────────────

    #[test]
    fn magic_png() {
        let bytes: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR";
        assert_eq!(detect_by_magic(bytes).as_deref(), Some("image/png"));
    }

    #[test]
    fn magic_jpeg() {
        let bytes: &[u8] = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00";
        assert_eq!(detect_by_magic(bytes).as_deref(), Some("image/jpeg"));
    }

    #[test]
    fn magic_gif() {
        let bytes: &[u8] = b"GIF89a\x01\x00\x01\x00";
        assert_eq!(detect_by_magic(bytes).as_deref(), Some("image/gif"));
    }

    #[test]
    fn magic_pdf() {
        let bytes: &[u8] = b"%PDF-1.4\n";
        assert_eq!(detect_by_magic(bytes).as_deref(), Some("application/pdf"));
    }

    #[test]
    fn magic_zip() {
        let bytes: &[u8] = b"PK\x03\x04\x14\x00\x00\x00";
        assert_eq!(detect_by_magic(bytes).as_deref(), Some("application/zip"));
    }

    #[test]
    fn magic_empty_is_none() {
        assert_eq!(detect_by_magic(b""), None);
    }

    #[test]
    fn magic_random_text_is_none() {
        assert_eq!(detect_by_magic(b"hello world"), None);
    }

    // ── detect_by_content (text sniffing) ────────────────────────────────────

    #[test]
    fn content_json_object() {
        assert_eq!(
            detect_by_content(b"{\"key\": \"value\"}").as_deref(),
            Some("application/json")
        );
    }

    #[test]
    fn content_json_array() {
        assert_eq!(
            detect_by_content(b"[1, 2, 3]").as_deref(),
            Some("application/json")
        );
    }

    #[test]
    fn content_yaml() {
        assert_eq!(
            detect_by_content(b"---\nkey: value\n").as_deref(),
            Some("text/x-yaml")
        );
    }

    #[test]
    fn content_xml() {
        assert_eq!(
            detect_by_content(b"<?xml version=\"1.0\"?>\n<root/>").as_deref(),
            Some("text/xml")
        );
    }

    #[test]
    fn content_svg() {
        assert_eq!(
            detect_by_content(
                b"<?xml version=\"1.0\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\"/>"
            )
            .as_deref(),
            Some("image/svg+xml")
        );
    }

    #[test]
    fn content_html() {
        assert_eq!(
            detect_by_content(b"<!DOCTYPE html>\n<html></html>").as_deref(),
            Some("text/html")
        );
    }

    #[test]
    fn content_php() {
        assert_eq!(
            detect_by_content(b"<?php echo 'hello'; ?>").as_deref(),
            Some("text/x-php")
        );
    }

    #[test]
    fn content_python_shebang() {
        assert_eq!(
            detect_by_content(b"#!/usr/bin/env python3\nimport sys").as_deref(),
            Some("text/x-python")
        );
    }

    #[test]
    fn content_bash_shebang() {
        assert_eq!(
            detect_by_content(b"#!/bin/bash\necho hello").as_deref(),
            Some("text/x-shellscript")
        );
    }

    #[test]
    fn content_c_include() {
        assert_eq!(
            detect_by_content(b"#include <stdio.h>\nint main() {}").as_deref(),
            Some("text/x-c")
        );
    }

    #[test]
    fn content_plain_text() {
        assert_eq!(
            detect_by_content(b"Hello, world. This is plain text.").as_deref(),
            Some("text/plain")
        );
    }

    #[test]
    fn content_empty_is_none() {
        assert_eq!(detect_by_content(b""), None);
    }

    #[test]
    fn content_binary_is_none() {
        assert_eq!(detect_by_content(&[0x00, 0x01, 0x02, 0x03]), None);
    }

    // ── detect_mime (unified) ────────────────────────────────────────────────

    #[test]
    fn unified_png_uses_magic() {
        let bytes: &[u8] = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR";
        assert_eq!(detect_mime(bytes).as_deref(), Some("image/png"));
    }

    #[test]
    fn unified_json_uses_content() {
        assert_eq!(
            detect_mime(b"{\"key\": 1}").as_deref(),
            Some("application/json")
        );
    }

    #[test]
    fn unified_empty_is_none() {
        assert_eq!(detect_mime(b""), None);
    }

    // ── round-trip ───────────────────────────────────────────────────────────

    #[test]
    fn extension_round_trip() {
        let ext = "svg";
        for mime in lookup_by_ext(ext) {
            let back = extensions_for(&mime);
            assert!(
                back.iter().any(|e| e == &format!(".{ext}")),
                "round-trip failed for {mime}: extensions={back:?}"
            );
        }
    }
}
