use std::ffi::OsStr;
use std::fs;
use std::path::Path;

use walkdir::WalkDir;

use crate::detection::finding::{Finding, FindingCategory, LineRange, Severity};
use crate::domain::errors::PapertowelError;

pub const DETECTOR_NAME: &str = "maintenance";

const CODE_EXTENSIONS: [&str; 8] = ["rs", "go", "py", "ts", "tsx", "js", "cs", "zig"];
const DOC_EXTENSIONS: [&str; 4] = ["md", "rst", "txt", "adoc"];
const CONFIG_EXTENSIONS: [&str; 4] = ["toml", "yaml", "yml", "json"];
const PLACEHOLDER_MARKERS: [&str; 8] = [
    "coming soon",
    "todo",
    "tbd",
    "placeholder",
    "lorem ipsum",
    "to be added",
    "wip",
    "generated by",
];

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MaintenanceDetectionConfig {
    pub max_code_files_for_hollow_repo: usize,
    pub min_non_code_files: usize,
    pub min_placeholder_hits: usize,
    pub min_doc_to_code_ratio: f32,
}

impl Default for MaintenanceDetectionConfig {
    fn default() -> Self {
        Self {
            max_code_files_for_hollow_repo: 3,
            min_non_code_files: 5,
            min_placeholder_hits: 2,
            min_doc_to_code_ratio: 1.2,
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq)]
struct MaintenanceMetrics {
    code_files: usize,
    doc_files: usize,
    config_files: usize,
    placeholder_hits: usize,
    has_tests_dir: bool,
}

pub fn detect_repo(repo_root: impl AsRef<Path>) -> Result<Vec<Finding>, PapertowelError> {
    detect_repo_with_config(repo_root, MaintenanceDetectionConfig::default())
}

#[expect(
    clippy::cast_precision_loss,
    reason = "confidence score: bounded usize counts"
)]
pub fn detect_repo_with_config(
    repo_root: impl AsRef<Path>,
    config: MaintenanceDetectionConfig,
) -> Result<Vec<Finding>, PapertowelError> {
    let repo_root = repo_root.as_ref();
    let metrics = scan_repo(repo_root)?;

    let non_code_files = metrics.doc_files + metrics.config_files;
    let doc_to_code_ratio = if metrics.code_files == 0 {
        metrics.doc_files as f32
    } else {
        metrics.doc_files as f32 / metrics.code_files as f32
    };

    let hollow_repo = metrics.code_files <= config.max_code_files_for_hollow_repo
        && non_code_files >= config.min_non_code_files
        && metrics.placeholder_hits >= config.min_placeholder_hits
        && doc_to_code_ratio >= config.min_doc_to_code_ratio
        && !metrics.has_tests_dir;

    if !hollow_repo {
        return Ok(Vec::new());
    }

    let severity = if metrics.code_files <= 1 && metrics.placeholder_hits >= 3 {
        Severity::High
    } else {
        Severity::Medium
    };
    let confidence = (non_code_files as f32 / 8.0)
        .mul_add(
            0.5,
            (metrics.placeholder_hits as f32 / 6.0)
                .mul_add(0.3, doc_to_code_ratio.min(3.0) / 3.0 * 0.2),
        )
        .min(1.0);

    let mut finding = Finding::new(
        "maintenance.hollow_repo",
        FindingCategory::Maintenance,
        severity,
        confidence,
        repo_root.join("."),
        format!(
            "Repository appears maintenance-light (code files: {}, docs: {}, config: {}, placeholder hits: {}).",
            metrics.code_files, metrics.doc_files, metrics.config_files, metrics.placeholder_hits
        ),
    )?;
    finding.line_range = Some(LineRange::new(1, 1)?);
    finding.suggestion = Some(
		"Prioritize substantive code/test evolution and prune generated placeholder docs that overstate project maturity."
.to_owned(),
	);

    Ok(vec![finding])
}

fn scan_repo(repo_root: &Path) -> Result<MaintenanceMetrics, PapertowelError> {
    let mut metrics = MaintenanceMetrics::default();

    for entry in WalkDir::new(repo_root) {
        let entry = entry.map_err(|error| {
            let io_error = std::io::Error::other(error.to_string());
            let path = error
                .path()
                .map_or_else(|| repo_root.to_path_buf(), Path::to_path_buf);
            PapertowelError::io_with_path(path, io_error)
        })?;

        let path = entry.path();
        if path.components().any(|part| {
            part.as_os_str() == OsStr::new(".git")
                || part.as_os_str() == OsStr::new("target")
                || part.as_os_str() == OsStr::new(".coraline")
        }) {
            continue;
        }

        if path.is_dir() {
            if path.ends_with("tests") {
                metrics.has_tests_dir = true;
            }
            continue;
        }

        let Some(ext) = path.extension().and_then(OsStr::to_str) else {
            continue;
        };

        if CODE_EXTENSIONS.contains(&ext) {
            metrics.code_files += 1;
        } else if DOC_EXTENSIONS.contains(&ext) {
            metrics.doc_files += 1;
            metrics.placeholder_hits += count_placeholder_hits(path)?;
        } else if CONFIG_EXTENSIONS.contains(&ext) {
            metrics.config_files += 1;
        }
    }

    Ok(metrics)
}

fn count_placeholder_hits(path: &Path) -> Result<usize, PapertowelError> {
    let content =
        fs::read_to_string(path).map_err(|error| PapertowelError::io_with_path(path, error))?;
    let lowered = content.to_ascii_lowercase();
    Ok(PLACEHOLDER_MARKERS
        .iter()
        .filter(|marker| lowered.contains(**marker))
        .count())
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use crate::scrubber::maintenance::{
        DETECTOR_NAME, MaintenanceDetectionConfig, detect_repo, detect_repo_with_config,
    };

    #[test]
    fn detector_name_is_stable() {
        assert_eq!(DETECTOR_NAME, "maintenance");
    }

    #[test]
    fn maintenance_detector_ignores_active_repo_shape() -> Result<(), Box<dyn std::error::Error>> {
        let temp = TempDir::new()?;

        let src = temp.path().join("src");
        let tests = temp.path().join("tests");
        fs::create_dir_all(&src)?;
        fs::create_dir_all(&tests)?;
        fs::write(src.join("lib.rs"), "pub fn a() {}\n")?;
        fs::write(src.join("mod.rs"), "pub fn b() {}\n")?;
        fs::write(
            tests.join("integration.rs"),
            "#[test] fn ok() { assert!(true); }\n",
        )?;
        fs::write(temp.path().join("README.md"), "Project docs\n")?;

        let findings = detect_repo_with_config(temp.path(), MaintenanceDetectionConfig::default())?;
        assert!(findings.is_empty());
        Ok(())
    }

    #[test]
    fn maintenance_detector_flags_hollow_repo_shape() -> Result<(), Box<dyn std::error::Error>> {
        let temp = TempDir::new()?;

        let src = temp.path().join("src");
        fs::create_dir_all(&src)?;
        fs::write(src.join("main.rs"), "fn main() {}\n")?;

        let docs = [
            ("README.md", "Coming soon. Generated by template."),
            ("CONTRIBUTING.md", "To be added soon."),
            ("SECURITY.md", "TBD placeholder."),
            ("ROADMAP.md", "WIP and lorem ipsum."),
        ];
        for (name, content) in docs {
            fs::write(temp.path().join(name), content)?;
        }
        fs::write(temp.path().join("config.toml"), "mode = \"demo\"\n")?;

        let findings = detect_repo_with_config(temp.path(), MaintenanceDetectionConfig::default())?;
        assert_eq!(findings.len(), 1);
        Ok(())
    }

    #[test]
    fn detect_repo_delegates_to_with_config() -> Result<(), Box<dyn std::error::Error>> {
        use tempfile::TempDir;
        let temp = TempDir::new()?;
        let findings = detect_repo(temp.path())?;
        let _ = findings;
        Ok(())
    }

    #[test]
    fn hollow_repo_produces_medium_severity_with_many_code_files()
    -> Result<(), Box<dyn std::error::Error>> {
        // Triggers Severity::Medium path (line 89): code_files > 1 even if placeholder_hits are high.
        let temp = TempDir::new()?;
        let src = temp.path().join("src");
        fs::create_dir_all(&src)?;
        // 2 code files → code_files > 1 → Medium severity
        fs::write(src.join("main.rs"), "fn main() {}\n")?;
        fs::write(src.join("lib.rs"), "pub fn helper() {}\n")?;
        // Docs with enough placeholder hits
        let docs = [
            ("README.md", "Coming soon. Generated by template."),
            ("CONTRIBUTING.md", "To be added soon."),
            ("SECURITY.md", "TBD placeholder."),
            ("ROADMAP.md", "WIP and lorem ipsum."),
            ("CHANGELOG.md", "Coming soon. More TBD."),
        ];
        for (name, content) in docs {
            fs::write(temp.path().join(name), content)?;
        }
        let findings = detect_repo_with_config(temp.path(), MaintenanceDetectionConfig::default())?;
        if let Some(f) = findings.first() {
            assert_eq!(f.severity, crate::detection::finding::Severity::Medium);
        }
        Ok(())
    }

    #[test]
    fn no_extension_file_and_git_dir_do_not_panic() -> Result<(), Box<dyn std::error::Error>> {
        // Covers the extension-less file skip path and.git dir skip in scan_repo.
        use tempfile::TempDir;
        let temp = TempDir::new()?;
        fs::write(temp.path().join("Makefile"), "all:\n")?;
        fs::write(temp.path().join("Dockerfile"), "FROM ubuntu\n")?;
        fs::create_dir_all(temp.path().join(".git"))?;
        let findings = detect_repo(temp.path())?;
        let _ = findings;
        Ok(())
    }
}
