use allow_core::{AllowEntry, CargoAllowError, CargoAllowResult, WorkspaceConfig, normalize_path};
use std::collections::BTreeSet;
use std::path::Path;

use crate::source_tree_scope::{normalize_source_tree_scope, validate_glob, validate_path_scope};
use crate::text_validation::validate_required_text;

pub(crate) fn validate_workspace(workspace: &WorkspaceConfig) -> CargoAllowResult<()> {
    validate_path_scope("workspace root", Path::new(&workspace.root))?;
    validate_required_text("workspace inventory", &workspace.inventory)?;
    if workspace.inventory != "git-tracked" {
        return Err(CargoAllowError::new(format!(
            "unsupported workspace inventory `{}`",
            workspace.inventory
        )));
    }
    validate_required_text("workspace default_mode", &workspace.default_mode)?;
    if !matches!(
        workspace.default_mode.as_str(),
        "audit" | "no-new" | "strict" | "release"
    ) {
        return Err(CargoAllowError::new(format!(
            "unsupported workspace default_mode `{}`",
            workspace.default_mode
        )));
    }
    for pattern in &workspace.ignored {
        validate_glob("source-tree ignored glob", pattern)?;
    }
    for pattern in &workspace.generated {
        validate_glob("source-tree generated glob", pattern)?;
    }
    validate_unique_workspace_globs("source-tree ignored glob", &workspace.ignored)?;
    validate_unique_workspace_globs("source-tree generated glob", &workspace.generated)?;
    Ok(())
}

pub(crate) fn validate_allow_entry_scope(entry: &AllowEntry) -> CargoAllowResult<()> {
    if entry.path.is_none() && entry.glob.is_none() && entry.selector.glob.is_none() {
        return Err(CargoAllowError::new(format!(
            "{} has no path or glob",
            entry.id
        )));
    }
    if let Some(path) = &entry.path {
        validate_path_scope(&entry.id, path)?;
    }
    if let Some(glob) = &entry.glob {
        validate_glob(&format!("{} glob", entry.id), glob)?;
    }
    if let Some(glob) = &entry.selector.glob {
        validate_glob(&format!("{} selector glob", entry.id), glob)?;
    }
    validate_scope_consistency(entry)
}

pub(crate) fn validate_scope_consistency(entry: &AllowEntry) -> CargoAllowResult<()> {
    if entry.path.is_some() && entry.glob.is_some() {
        return Err(CargoAllowError::new(format!(
            "{} must not define both path and glob",
            entry.id
        )));
    }
    if let (Some(path), Some(selector_glob)) = (&entry.path, &entry.selector.glob) {
        let path = normalize_path(path);
        let selector_glob = normalize_source_tree_scope(selector_glob);
        if selector_glob != path {
            return Err(CargoAllowError::new(format!(
                "{} selector glob `{selector_glob}` must match path `{path}` or omit one scope",
                entry.id
            )));
        }
    }
    if let (Some(glob), Some(selector_glob)) = (&entry.glob, &entry.selector.glob) {
        let glob = normalize_source_tree_scope(glob);
        let selector_glob = normalize_source_tree_scope(selector_glob);
        if selector_glob != glob {
            return Err(CargoAllowError::new(format!(
                "{} selector glob `{selector_glob}` must match glob `{glob}` or omit one scope",
                entry.id
            )));
        }
    }
    Ok(())
}

fn validate_unique_workspace_globs(label: &str, patterns: &[String]) -> CargoAllowResult<()> {
    let mut seen = BTreeSet::new();
    for (index, pattern) in patterns.iter().enumerate() {
        let normalized = normalize_source_tree_scope(pattern);
        if !seen.insert(normalized.clone()) {
            return Err(CargoAllowError::new(format!(
                "duplicate {label} `{normalized}` at position {}",
                index + 1
            )));
        }
    }
    Ok(())
}
