// Developed by Manfred Lotz in cooperation with Claude (Anthropic).

use crate::detector::DetectResult;
use anyhow::Result;
use std::fmt;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;

pub struct CheckResult {
    pub mime_issue: Option<Issue>,
    pub perm_issue: Option<Issue>,
    /// true when the extension is not in our whitelist — for --verbose display
    pub unclassified: bool,
}

pub enum Issue {
    MimeMismatch {
        expected: &'static str,
        got: DetectResult,
    },
    PermMismatch {
        expected: &'static str,
        got: u32,
    },
}

impl fmt::Display for Issue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Issue::MimeMismatch { expected, got } => {
                write!(f, "MIME mismatch — expected {expected}, got {got}")
            }
            Issue::PermMismatch { expected, got } => {
                write!(
                    f,
                    "permission mismatch — expected {expected}, got {:03o}",
                    got
                )
            }
        }
    }
}

enum KnownExt {
    Pdf,
    PostScript,
    Zip,
    Archive,
    Image,
    Font,
    Text,
    WindowsScript,
}

fn extension_kind(ext: &str) -> Option<KnownExt> {
    match ext {
        "pdf" => Some(KnownExt::Pdf),
        "ps" | "eps" => Some(KnownExt::PostScript),
        "zip" => Some(KnownExt::Zip),
        "tar" | "tgz" | "txz" | "gz" | "bz2" | "xz" | "zst" | "7z" | "rar" | "lz" | "lzma"
        | "deb" | "rpm" => Some(KnownExt::Archive),
        "png" | "jpg" | "jpeg" | "gif" | "webp" | "tiff" | "tif" | "bmp" | "ico" => {
            Some(KnownExt::Image)
        }
        "otf" | "ttf" | "woff" | "woff2" => Some(KnownExt::Font),
        "txt" | "md" | "rst" | "csv" | "toml" | "yaml" | "yml" | "sh" | "bash" | "zsh" | "fish"
        | "py" | "rb" | "pl" | "js" | "ts" | "c" | "h" | "cpp" | "hpp" | "rs" | "tex" | "sty"
        | "cls" | "bib" | "dtx" | "ins" | "ltx" | "def" | "fd" | "bst" | "go" | "java" | "cs"
        | "kt" | "swift" | "zig" | "lua" | "php" | "r" | "jl" | "hs" | "ex" | "exs" | "dart"
        | "scala" | "html" | "htm" | "css" | "scss" | "less" | "svg" | "json" | "xml" | "ini"
        | "sql" | "tsv" | "env" | "nix" | "org" | "adoc" | "cmake" | "mk" => Some(KnownExt::Text),
        "cmd" | "bat" | "ps1" => Some(KnownExt::WindowsScript),
        _ => None,
    }
}

fn expected_mime(kind: &KnownExt) -> &'static str {
    match kind {
        KnownExt::Pdf => "PDF",
        KnownExt::PostScript => "PostScript",
        KnownExt::Zip => "ZIP",
        KnownExt::Archive => "archive",
        KnownExt::Image => "image",
        KnownExt::Font => "font",
        KnownExt::Text => "text",
        KnownExt::WindowsScript => "text",
    }
}

fn mime_matches(result: &DetectResult, kind: &KnownExt) -> bool {
    match (kind, result) {
        (KnownExt::Pdf, DetectResult::Pdf) => true,
        (KnownExt::PostScript, DetectResult::PostScript) => true,
        (KnownExt::Zip, DetectResult::Zip) => true,
        (KnownExt::Archive, DetectResult::Archive) => true,
        (KnownExt::Archive, DetectResult::Mime(m)) => matches!(
            m.as_str(),
            "application/x-7z-compressed"
                | "application/x-rar-compressed"
                | "application/vnd.rar"
                | "application/x-lzip"
                | "application/x-lzma"
                | "application/vnd.debian.binary-package"
                | "application/x-deb"
                | "application/x-rpm"
        ),
        (KnownExt::Image, DetectResult::Png) => true,
        (KnownExt::Image, DetectResult::Mime(m)) => m.starts_with("image/"),
        (KnownExt::Font, DetectResult::Mime(m)) => {
            m.starts_with("font/")
                || matches!(
                    m.as_str(),
                    "application/font-sfnt" | "application/font-woff"
                )
        }
        (
            KnownExt::Text,
            DetectResult::Text(_) | DetectResult::Script(_, _) | DetectResult::Bom(_),
        ) => true,
        (KnownExt::Text, DetectResult::Mime(m)) => {
            m.starts_with("text/")
                || matches!(
                    m.as_str(),
                    "application/json"
                        | "application/xml"
                        | "application/javascript"
                        | "image/svg+xml"
                )
        }
        (
            KnownExt::WindowsScript,
            DetectResult::Text(_) | DetectResult::Script(_, _) | DetectResult::Bom(_),
        ) => true,
        (KnownExt::WindowsScript, DetectResult::Mime(m)) => {
            m.starts_with("text/")
                || matches!(
                    m.as_str(),
                    "application/json"
                        | "application/xml"
                        | "application/javascript"
                        | "image/svg+xml"
                )
        }
        _ => false,
    }
}

fn is_executable(result: &DetectResult) -> bool {
    matches!(result, DetectResult::Elf)
}

pub fn check_file(path: &Path, result: &DetectResult) -> Result<CheckResult> {
    if matches!(result, DetectResult::Directory) {
        let mode = path.metadata()?.permissions().mode() & 0o777;
        let perm_issue = ((mode & 0o755) != 0o755).then_some(Issue::PermMismatch {
            expected: "at least 755",
            got: mode,
        });
        return Ok(CheckResult {
            mime_issue: None,
            perm_issue,
            unclassified: false,
        });
    }

    let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
    let kind = extension_kind(ext);
    let unclassified = kind.is_none();

    let mime_issue = if let Some(ref k) = kind
        && !mime_matches(result, k)
    {
        Some(Issue::MimeMismatch {
            expected: expected_mime(k),
            got: result.clone(),
        })
    } else {
        None
    };

    let mode = path.metadata()?.permissions().mode() & 0o777;
    let perm_issue = if is_executable(result) {
        ((mode & 0o755) != 0o755).then_some(Issue::PermMismatch {
            expected: "at least 755",
            got: mode,
        })
    } else if matches!(result, DetectResult::Script(_, _))
        || matches!(kind, Some(KnownExt::WindowsScript))
    {
        ((mode & 0o644) != 0o644).then_some(Issue::PermMismatch {
            expected: "at least 644",
            got: mode,
        })
    } else {
        ((mode & 0o111) != 0 || (mode & 0o644) != 0o644).then_some(Issue::PermMismatch {
            expected: "at least 644, no execute bits",
            got: mode,
        })
    };

    Ok(CheckResult {
        mime_issue,
        perm_issue,
        unclassified,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::detector::LineEnding;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;

    fn make_file(name: &str, content: &[u8], mode: u32) -> (tempfile::TempDir, std::path::PathBuf) {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(name);
        fs::write(&path, content).unwrap();
        fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap();
        (dir, path)
    }

    fn make_dir(mode: u32) -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        fs::set_permissions(dir.path(), fs::Permissions::from_mode(mode)).unwrap();
        dir
    }

    // ── MIME checks ──────────────────────────────────────────────────────────

    #[test]
    fn pdf_correct_type_and_perms() {
        let (_dir, path) = make_file("report.pdf", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Pdf).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
        assert!(!cr.unclassified);
    }

    #[test]
    fn pdf_mime_mismatch() {
        let (_dir, path) = make_file("report.pdf", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(matches!(cr.mime_issue, Some(Issue::MimeMismatch { .. })));
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn zip_correct() {
        let (_dir, path) = make_file("archive.zip", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Zip).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn zip_mime_mismatch() {
        let (_dir, path) = make_file("archive.zip", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Archive).unwrap();
        assert!(matches!(cr.mime_issue, Some(Issue::MimeMismatch { .. })));
        assert!(cr.perm_issue.is_none());
    }

    // ── permission checks ────────────────────────────────────────────────────

    #[test]
    fn script_with_correct_perms() {
        let (_dir, path) = make_file("deploy.sh", b"content", 0o755);
        let cr = check_file(
            &path,
            &DetectResult::Script(LineEnding::Lf, "/bin/sh".into()),
        )
        .unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn script_with_644_perms_ok() {
        let (_dir, path) = make_file("deploy.sh", b"content", 0o644);
        let cr = check_file(
            &path,
            &DetectResult::Script(LineEnding::Lf, "/bin/sh".into()),
        )
        .unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn script_with_bad_perms() {
        let (_dir, path) = make_file("deploy.sh", b"content", 0o600);
        let cr = check_file(
            &path,
            &DetectResult::Script(LineEnding::Lf, "/bin/sh".into()),
        )
        .unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(matches!(cr.perm_issue, Some(Issue::PermMismatch { .. })));
    }

    #[test]
    fn elf_with_correct_perms() {
        let (_dir, path) = make_file("binary", b"\x7fELF", 0o755);
        let cr = check_file(&path, &DetectResult::Elf).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn elf_missing_execute_bit() {
        let (_dir, path) = make_file("binary", b"\x7fELF", 0o644);
        let cr = check_file(&path, &DetectResult::Elf).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(matches!(cr.perm_issue, Some(Issue::PermMismatch { .. })));
    }

    #[test]
    fn text_file_with_execute_bit() {
        let (_dir, path) = make_file("lib.rs", b"fn main() {}", 0o755);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(matches!(cr.perm_issue, Some(Issue::PermMismatch { .. })));
    }

    #[test]
    fn text_file_missing_read_bit() {
        let (_dir, path) = make_file("lib.rs", b"fn main() {}", 0o200);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(matches!(cr.perm_issue, Some(Issue::PermMismatch { .. })));
    }

    // ── image checks ────────────────────────────────────────────────────────

    #[test]
    fn png_file_as_image_kind() {
        let (_dir, path) = make_file("photo.png", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Png).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn jpg_file_with_image_mime() {
        let (_dir, path) = make_file("photo.jpg", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Mime("image/jpeg".into())).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn jpg_file_mime_mismatch() {
        let (_dir, path) = make_file("photo.jpg", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(matches!(cr.mime_issue, Some(Issue::MimeMismatch { .. })));
    }

    // ── font checks ──────────────────────────────────────────────────────────

    #[test]
    fn ttf_correct() {
        let (_dir, path) = make_file("font.ttf", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Mime("application/font-sfnt".into())).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn woff_correct() {
        let (_dir, path) = make_file("font.woff", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Mime("application/font-woff".into())).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn ttf_mime_mismatch() {
        let (_dir, path) = make_file("font.ttf", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(matches!(cr.mime_issue, Some(Issue::MimeMismatch { .. })));
    }

    // ── extended archive checks ──────────────────────────────────────────────

    #[test]
    fn archive_7z_correct() {
        let (_dir, path) = make_file("data.7z", b"content", 0o644);
        let cr = check_file(
            &path,
            &DetectResult::Mime("application/x-7z-compressed".into()),
        )
        .unwrap();
        assert!(cr.mime_issue.is_none());
    }

    #[test]
    fn archive_rar_correct() {
        let (_dir, path) = make_file("data.rar", b"content", 0o644);
        let cr = check_file(
            &path,
            &DetectResult::Mime("application/x-rar-compressed".into()),
        )
        .unwrap();
        assert!(cr.mime_issue.is_none());
    }

    // ── extended text checks ─────────────────────────────────────────────────

    #[test]
    fn json_file_detected_as_text() {
        let (_dir, path) = make_file("data.json", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
    }

    #[test]
    fn json_file_detected_as_application_json() {
        let (_dir, path) = make_file("data.json", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Mime("application/json".into())).unwrap();
        assert!(cr.mime_issue.is_none());
    }

    #[test]
    fn svg_detected_as_image_svg_xml() {
        let (_dir, path) = make_file("figure.svg", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Mime("image/svg+xml".into())).unwrap();
        assert!(cr.mime_issue.is_none());
    }

    // ── Windows script checks ────────────────────────────────────────────────

    #[test]
    fn cmd_with_644_ok() {
        let (_dir, path) = make_file("setup.cmd", b"@echo off", 0o644);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn bat_with_755_ok() {
        let (_dir, path) = make_file("setup.bat", b"@echo off", 0o755);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn ps1_with_644_ok() {
        let (_dir, path) = make_file("deploy.ps1", b"Write-Host hello", 0o644);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
    }

    #[test]
    fn cmd_with_bad_perms() {
        let (_dir, path) = make_file("setup.cmd", b"@echo off", 0o600);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(matches!(cr.perm_issue, Some(Issue::PermMismatch { .. })));
    }

    // ── unclassified extension ───────────────────────────────────────────────

    #[test]
    fn unknown_extension_is_unclassified() {
        let (_dir, path) = make_file("data.xyz", b"content", 0o644);
        let cr = check_file(&path, &DetectResult::Text(LineEnding::Lf)).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
        assert!(cr.unclassified);
    }

    // ── directory permission checks ──────────────────────────────────────────

    #[test]
    fn directory_with_correct_perms() {
        let dir = make_dir(0o755);
        let cr = check_file(dir.path(), &DetectResult::Directory).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(cr.perm_issue.is_none());
        assert!(!cr.unclassified);
    }

    #[test]
    fn directory_missing_execute_bit() {
        let dir = make_dir(0o644);
        let cr = check_file(dir.path(), &DetectResult::Directory).unwrap();
        assert!(cr.mime_issue.is_none());
        assert!(matches!(cr.perm_issue, Some(Issue::PermMismatch { .. })));
    }
}
