use super::{DetectResult, ProjectDef};
use std::path::{Path, PathBuf};

use anyhow::Result;
use serde::Deserialize;

pub const DEF: ProjectDef = ProjectDef {
    name: "cargo",
    detect: detect,
    build_cmd: &["cargo", "build", "--release"],
    run_cmd: &["cargo", "run", "--release"],
};

#[derive(Deserialize)]
struct CargoManifest {
    workspace: Option<Workspace>,
    package: Option<Package>,
    bin: Option<Vec<BinTarget>>,
}

#[derive(Deserialize)]
struct Workspace {
    members: Option<Vec<String>>,
}

#[derive(Deserialize)]
struct Package {
    #[allow(dead_code)]
    name: Option<String>,
}

#[derive(Deserialize)]
struct BinTarget {
    #[allow(dead_code)]
    name: String,
}

fn detect(dir: &Path) -> Result<DetectResult> {
    let cargo_toml = dir.join("Cargo.toml");
    if !cargo_toml.exists() {
        return Ok(DetectResult::no_match());
    }

    let content = std::fs::read_to_string(&cargo_toml)?;
    let manifest: CargoManifest = toml::from_str(&content)?;

    match (&manifest.workspace, &manifest.package) {
        (Some(ws), None) => detect_workspace(dir, ws),
        (Some(ws), Some(_)) => {
            if has_binary_target(dir, &manifest) {
                Ok(DetectResult::matched()
                    .with_alert("Workspace detected. Using root package binary."))
            } else {
                detect_workspace(dir, ws)
            }
        }
        (None, Some(_)) => {
            if has_binary_target(dir, &manifest) {
                Ok(DetectResult::matched())
            } else {
                Ok(DetectResult::no_match())
            }
        }
        (None, None) => {
            Ok(DetectResult::no_match()
                .with_alert("Bad Cargo.toml: contains neither [package] nor [workspace]."))
        }
    }
}

fn detect_workspace(dir: &Path, ws: &Workspace) -> Result<DetectResult> {
    let members = ws.members.as_deref().unwrap_or(&[]);
    let member_dirs = resolve_members(dir, members);

    let mut binary_count = 0u32;
    for member_dir in &member_dirs {
        let member_toml = member_dir.join("Cargo.toml");
        if !member_toml.exists() {
            continue;
        }
        let content = std::fs::read_to_string(&member_toml)?;
        let manifest: CargoManifest = toml::from_str(&content)?;
        if has_binary_target(member_dir, &manifest) {
            binary_count += 1;
        }
    }

    match binary_count {
        0 => Ok(DetectResult::no_match()),
        1 => Ok(DetectResult::matched()),
        _ => Ok(DetectResult::no_match()
            .with_alert("Found workspace with multiple binaries, it is unclear which binary to run. Run donion again inside the target binary crate's folder.")),
    }
}

fn resolve_members(base: &Path, patterns: &[String]) -> Vec<PathBuf> {
    let mut dirs = Vec::new();
    for pattern in patterns {
        if pattern.contains('*') {
            let prefix = pattern.split('*').next().unwrap_or("");
            let search_dir = base.join(prefix);
            if let Ok(entries) = std::fs::read_dir(&search_dir) {
                for entry in entries.flatten() {
                    let path = entry.path();
                    if path.join("Cargo.toml").exists() {
                        dirs.push(path);
                    }
                }
            }
        } else {
            let path = base.join(pattern);
            if path.exists() {
                dirs.push(path);
            }
        }
    }
    dirs
}

fn has_binary_target(dir: &Path, manifest: &CargoManifest) -> bool {
    if manifest.bin.as_ref().is_some_and(|bins| !bins.is_empty()) {
        return true;
    }
    dir.join("src/main.rs").exists()
}
