use crate::{
    LintLevel, Spanned,
    cfg::{PackageSpec, PackageSpecOrExtended, Reason, ValidationContext},
    diag::{Diagnostic, FileId, Label},
};
use toml_span::{DeserError, Deserialize, de_helpers::TableHelper, value::Value};

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct CrateBanExtended {
    /// One or more crates that will allow this crate to be used if it is a
    /// direct dependency
    pub wrappers: Option<Spanned<Vec<Spanned<String>>>>,
    /// Setting this to true will only emit an error if multiple versions of the
    /// crate are found
    pub deny_multiple_versions: Option<Spanned<bool>>,
    /// The reason for banning the crate
    pub reason: Option<Reason>,
    /// The crate to use instead of the banned crate, could be just the crate name
    /// or a URL
    pub use_instead: Option<Spanned<String>>,
}

impl<'de> Deserialize<'de> for CrateBanExtended {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let mut th = TableHelper::new(value)?;

        let wrappers = th.optional("wrappers");
        let deny_multiple_versions = th.optional("deny-multiple-versions");
        let reason = th.optional_s("reason");
        let use_instead = th.optional("use-instead");
        th.finalize(None)?;

        Ok(Self {
            wrappers,
            deny_multiple_versions,
            reason: reason.map(Reason::from),
            use_instead,
        })
    }
}

//#[derive(Clone)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub struct CrateFeatures {
    pub spec: PackageSpec,
    /// All features that are allowed to be used.
    pub allow: Spanned<Vec<Spanned<String>>>,
    /// All features that are denied.
    pub deny: Vec<Spanned<String>>,
    /// The actual feature set has to exactly match the `allow` set.
    pub exact: Spanned<bool>,
    /// The reason for specifying the crate features
    pub reason: Option<Reason>,
}

impl<'de> Deserialize<'de> for CrateFeatures {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let spec = PackageSpec::deserialize(value)?;

        let mut th = TableHelper::new(value)?;

        let allow = th.optional("allow").unwrap_or_default();
        let deny = th.optional("deny").unwrap_or_default();
        let exact = th.optional("exact").unwrap_or_default();
        let reason = th.optional_s("reason");
        th.finalize(None)?;

        Ok(Self {
            spec,
            allow,
            deny,
            exact,
            reason: reason.map(Reason::from),
        })
    }
}

#[cfg_attr(test, derive(serde::Serialize))]
#[derive(PartialEq, Eq, Copy, Clone, Default, strum::VariantArray, strum::VariantNames)]
#[strum(serialize_all = "kebab-case")]
pub enum GraphHighlight {
    /// Highlights the path to a duplicate dependency with the fewest number
    /// of total edges, which tends to make it the best candidate for removing
    SimplestPath,
    /// Highlights the path to the duplicate dependency with the lowest version
    LowestVersion,
    /// Highlights with all of the other configs
    #[default]
    All,
}

crate::enum_deser!(GraphHighlight);

impl GraphHighlight {
    #[inline]
    pub(crate) fn simplest(self) -> bool {
        self == Self::SimplestPath || self == Self::All
    }

    #[inline]
    pub(crate) fn lowest_version(self) -> bool {
        self == Self::LowestVersion || self == Self::All
    }
}

#[derive(Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(Debug))]
pub struct Checksum(pub [u8; 32]);

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
pub enum ChecksumParseError {
    /// The checksum string had an invalid length
    InvalidLength(usize),
    /// The checksum string contained a non-hex character
    InvalidValue(char),
}

impl std::str::FromStr for Checksum {
    type Err = ChecksumParseError;

    fn from_str(data: &str) -> Result<Self, Self::Err> {
        if data.len() != 64 {
            return Err(ChecksumParseError::InvalidLength(data.len()));
        }

        let mut array = [0u8; 32];

        for (ind, chunk) in data.as_bytes().chunks(2).enumerate() {
            #[inline]
            fn parse_hex(b: u8) -> Result<u8, ChecksumParseError> {
                Ok(match b {
                    b'A'..=b'F' => b - b'A' + 10,
                    b'a'..=b'f' => b - b'a' + 10,
                    b'0'..=b'9' => b - b'0',
                    c => {
                        return Err(ChecksumParseError::InvalidValue(c as char));
                    }
                })
            }

            let mut cur = parse_hex(chunk[0])?;
            cur <<= 4;
            cur |= parse_hex(chunk[1])?;

            array[ind] = cur;
        }

        Ok(Self(array))
    }
}

impl std::fmt::Display for Checksum {
    #[allow(unsafe_code)]
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut hs = [0u8; 64];
        const CHARS: &[u8] = b"0123456789abcdef";
        for (i, &byte) in self.0.iter().enumerate() {
            let i = i * 2;
            hs[i] = CHARS[(byte >> 4) as usize];
            hs[i + 1] = CHARS[(byte & 0xf) as usize];
        }

        // SAFETY: we only insert hex ascii characters
        f.write_str(unsafe { std::str::from_utf8_unchecked(&hs) })
    }
}

impl<'de> Deserialize<'de> for Checksum {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let val = value.take_string(Some("a sha-256 hex encoded string"))?;

        val.parse().map_err(|err| {
            let err = match err {
                ChecksumParseError::InvalidLength(len) => {
                    toml_span::Error::from((toml_span::ErrorKind::Custom(format!("a sha-256 hex encoded string of length 64 but got a string of length '{len}'").into()), value.span))
                }
                ChecksumParseError::InvalidValue(c) => toml_span::Error::from((toml_span::ErrorKind::Unexpected(c), value.span)),
            };
            err.into()
        })
    }
}

#[cfg(test)]
impl serde::Serialize for Checksum {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut hexs = [0; 64];

        const CHARS: &[u8] = b"0123456789abcdef";

        for (i, &byte) in self.0.iter().enumerate() {
            let i = i * 2;
            hexs[i] = CHARS[(byte >> 4) as usize];
            hexs[i + 1] = CHARS[(byte & 0xf) as usize];
        }

        serializer.serialize_str(std::str::from_utf8(&hexs).unwrap())
    }
}

#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq, Eq, serde::Serialize))]
pub struct BypassPath {
    /// The crate-relative path to the executable
    pub path: Spanned<crate::PathBuf>,
    /// An optional sha-256 checksum to ensure that the executable is matched exactly
    pub checksum: Option<Spanned<Checksum>>,
}

impl<'de> Deserialize<'de> for BypassPath {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let mut th = TableHelper::new(value)?;

        let path: Spanned<String> = th.required_s("path")?;
        let checksum = th.optional("checksum");
        th.finalize(None)?;

        Ok(Self {
            path: path.map(),
            checksum,
        })
    }
}

#[derive(Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Bypass {
    pub spec: PackageSpec,
    pub build_script: Option<Spanned<Checksum>>,
    /// List of features that, if matched, means the build script/proc macro is
    /// not actually executed/run
    pub required_features: Vec<Spanned<String>>,
    /// List of glob patterns that are allowed. This is much more loose than
    /// `allow`, but can be useful in scenarios where things like test suites or
    /// the like that contain many scripts/test executables that are present in
    /// the packaged source, but are (hopefully) not actually read or executed
    /// during builds
    pub allow_globs: Option<Vec<Spanned<String>>>,
    /// One or more executables that are allowed. If not set all executables are
    /// allowed.
    pub allow: Vec<BypassPath>,
}

impl<'de> Deserialize<'de> for Bypass {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let spec = PackageSpec::deserialize(value)?;
        let mut th = TableHelper::new(value)?;
        let build_script = th.optional("build-script");
        let required_features = th.optional("required-features").unwrap_or_default();
        let allow_globs = th.optional("allow-globs");
        let allow = th.optional("allow").unwrap_or_default();
        th.finalize(None)?;

        Ok(Self {
            spec,
            build_script,
            required_features,
            allow_globs,
            allow,
        })
    }
}

pub struct BuildConfig {
    /// List of crates that are allowed to have build scripts. If this is set,
    /// any crates with a build script that aren't listed here will be banned
    pub allow_build_scripts: Option<Vec<PackageSpec>>,
    /// Lint level for when executables are detected within crates with build
    /// scripts or are proc macros, or are a dependency of either of them
    pub executables: LintLevel,
    /// The lint level for interpreted scripts
    pub interpreted: LintLevel,
    /// List of script extensions that are considered to be executable. These
    /// are always in addition to the builtin ones.
    pub script_extensions: Option<Vec<Spanned<String>>>,
    /// The list of allowed executables, by crate
    pub bypass: Option<Vec<Bypass>>,
    /// If true, enables the built-in glob patterns
    pub enable_builtin_globs: bool,
    /// If true, all dependencies of proc macro crates or crates with build
    /// scripts are also checked for executables/glob patterns
    pub include_dependencies: bool,
    /// If true, workspace crates are included
    pub include_workspace: bool,
    /// If true, archive files are counted as native executables
    pub include_archives: bool,
}

impl<'de> Deserialize<'de> for BuildConfig {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let mut th = TableHelper::new(value)?;
        let allow_build_scripts = th.optional("allow-build-scripts");
        let executables = th.optional("executables").unwrap_or(LintLevel::Deny);
        let interpreted = th.optional("interpreted").unwrap_or(LintLevel::Allow);
        let script_extensions = th.optional("script-extensions");
        let bypass = th.optional("bypass");
        let enable_builtin_globs = th.optional("enable-builtin-globs").unwrap_or_default();
        let include_dependencies = th.optional("include-dependencies").unwrap_or_default();
        let include_workspace = th.optional("include-workspace").unwrap_or_default();
        let include_archives = th.optional("include-archives").unwrap_or_default();
        th.finalize(None)?;

        Ok(Self {
            allow_build_scripts,
            executables,
            interpreted,
            script_extensions,
            bypass,
            enable_builtin_globs,
            include_dependencies,
            include_workspace,
            include_archives,
        })
    }
}

#[derive(Clone)]
#[cfg_attr(test, derive(Debug, PartialEq, Eq, serde::Serialize))]
pub struct TreeSkipExtended {
    pub depth: Option<usize>,
    /// Reason the tree is being skipped
    pub reason: Option<Reason>,
}

impl<'de> Deserialize<'de> for TreeSkipExtended {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let reason = if value.has_key("reason") {
            Some(Reason::deserialize(value)?)
        } else {
            None
        };

        let mut th = TableHelper::new(value)?;
        let depth = th.optional("depth");
        th.finalize(None)?;
        Ok(Self { depth, reason })
    }
}

pub type CrateBan = PackageSpecOrExtended<CrateBanExtended>;
pub type CrateAllow = PackageSpecOrExtended<Reason>;
pub type CrateSkip = PackageSpecOrExtended<Reason>;
pub type TreeSkip = PackageSpecOrExtended<TreeSkipExtended>;

#[cfg_attr(test, derive(serde::Serialize))]
pub struct WorkspaceDepsConfig {
    /// How to handle workspace dependencies on the same crate that aren't declared
    /// in `[workspace.dependencies]`
    pub duplicates: LintLevel,
    /// Whether path dependencies are treated as duplicates
    pub include_path_dependencies: bool,
    /// How to handle [`workspace.dependencies`] that are not used
    pub unused: LintLevel,
}

impl<'de> Deserialize<'de> for WorkspaceDepsConfig {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let mut th = TableHelper::new(value)?;

        let duplicates = th.optional("duplicates").unwrap_or(LintLevel::Deny);
        let include_path_dependencies = th.optional("include-path-dependencies").unwrap_or(true);
        let unused = th.optional("unused").unwrap_or(LintLevel::Deny);

        th.finalize(None)?;

        Ok(Self {
            duplicates,
            include_path_dependencies,
            unused,
        })
    }
}

#[cfg_attr(test, derive(serde::Serialize))]
pub struct StdReplacementConfig {
    /// What kind of dependency to include in checking
    ///
    /// By default we only want to check direct dependencies since they are something the user has control over
    pub scope: crate::cfg::Scope,
    /// The lint level used when emitting diagnostics for crates that have a std replacement
    ///
    /// Unlike most lint levels we default to deny since this is an optional sub-table of the outer bans config
    pub level: LintLevel,
    /// The maximum Rust version that can be considered for replacements
    ///
    /// Replacements that were added in rust versions higher than this are not considered. If not specified we attempt
    /// to determine the MSRV of the crate/workspace, otherwise we emit a diagnostic regardless of version
    pub rust_version: Option<semver::Version>,
    /// Crates to ignore emitting diagnostics for
    ///
    /// As usual, sometimes it is not possible to "fix" your graph to satisfy your constraints, so we need to allow
    /// configuration to ignore crates that we would otherwise emit diagnostics for
    pub ignore: Vec<SpecAndReason>,
    /// Whether a crate's `rust-version` is respected when considering a crate placed in std
    ///
    /// By default, the `rust-version` of a every crate that depends on a crate replaced in std is taken into account,
    /// ignoring the replacement if all of the versions in which APIs were stabilized are newer
    pub ignore_rust_version: crate::cfg::Scope,
}

#[inline]
pub fn parse_semver(mut v: toml_span::Value<'_>) -> Result<semver::Version, DeserError> {
    let rvs = v.take_string(Some("semver"))?;
    let span = v.span;

    // Note we don't use semver::Version parse directly here
    // - It's overly strict, disallowing versions without a patch, we want to support <major>.<minor> just the
    // same as cargo does in the rust-version field
    // - It includes parsing of prerelease versions which is just pointless for our needs
    let mut iter = memchr::memchr_iter(b'.', rvs.as_bytes());
    let mut start = 0;

    let Some(major) = iter.next() else {
        return Err(toml_span::Error {
            kind: toml_span::ErrorKind::UnexpectedValue {
                expected: &["non-empty semver"],
                value: Some(String::new()),
            },
            span,
            line_info: None,
        }
        .into());
    };

    let parse_int = |start: &mut usize, next: usize| -> Result<u32, DeserError> {
        let s = &rvs[*start..next];

        // + is allowed by std when parsing integers but not allowed in semver, the std parsing will take care of the
        // rest of the invalid inputs
        if s.starts_with('+') {
            return Err(toml_span::Error {
                kind: toml_span::ErrorKind::InvalidCharInString('+'),
                span: (span.start + *start..span.start + *start + 1).into(),
                line_info: None,
            }
            .into());
        }

        match s.parse() {
            Ok(v) => {
                *start = next + 1;
                Ok(v)
            }
            Err(err) => Err(toml_span::Error {
                kind: toml_span::ErrorKind::Custom(err.to_string().into()),
                span: (span.start + *start..span.start + next).into(),
                line_info: None,
            }
            .into()),
        }
    };

    let major = parse_int(&mut start, major)?;

    // There is only 1 major Rust version after more than 10 years, so this isn't really useful, but that's up to the user
    let Some(minor) = iter.next() else {
        return Ok(semver::Version::new(major as _, 0, 0));
    };

    let minor = parse_int(&mut start, minor)?;

    let Some(patch) = iter.next() else {
        return Ok(semver::Version::new(major as _, minor as _, 0));
    };

    let patch = parse_int(&mut start, patch)?;

    // Ensure there aren't more components
    if let Some(invalid) = iter.next() {
        return Err(toml_span::Error {
            kind: toml_span::ErrorKind::Custom("semver had additional components".into()),
            span: (invalid..span.end).into(),
            line_info: None,
        }
        .into());
    }

    Ok(semver::Version::new(major as _, minor as _, patch as _))
}

impl<'de> Deserialize<'de> for StdReplacementConfig {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let mut th = TableHelper::new(value)?;

        let scope = th.optional("scope").unwrap_or(crate::cfg::Scope::Workspace);
        let level = th.optional("level").unwrap_or(LintLevel::Deny);
        let rust_version = if let Some((_, rv)) = th.take("rust-version") {
            Some(parse_semver(rv)?)
        } else {
            None
        };
        let ignore = th.optional("ignore").unwrap_or_default();
        let ignore_rust_version = th
            .optional("ignore-rust-version")
            .unwrap_or(crate::cfg::Scope::None);

        th.finalize(None)?;

        Ok(Self {
            scope,
            level,
            rust_version,
            ignore,
            ignore_rust_version,
        })
    }
}

pub struct Config {
    /// How to handle multiple versions of the same crate
    pub multiple_versions: LintLevel,
    pub multiple_versions_include_dev: bool,
    pub workspace_dependencies: Option<WorkspaceDepsConfig>,
    /// How the duplicate graphs are highlighted
    pub highlight: GraphHighlight,
    /// The crates that will cause us to emit failures
    pub deny: Vec<CrateBan>,
    /// If specified, means only the listed crates are allowed
    pub allow: Vec<CrateAllow>,
    /// If true, automatically allow all workspace members even when using deny-by-default
    pub allow_workspace: bool,
    /// Allows specifying features that are or are not allowed on crates
    pub features: Vec<CrateFeatures>,
    /// The default lint level for default features for external, non-workspace
    /// crates, can be overridden in `features` on a crate by crate basis
    pub external_default_features: Option<Spanned<LintLevel>>,
    /// The default lint level for default features for workspace crates, can be
    /// overridden in `features` on a crate by crate basis
    pub workspace_default_features: Option<Spanned<LintLevel>>,
    /// If specified, disregards the crate completely
    pub skip: Vec<CrateSkip>,
    /// If specified, disregards the crate's transitive dependencies
    /// down to a certain depth
    pub skip_tree: Vec<TreeSkip>,
    /// How to handle wildcard dependencies
    pub wildcards: LintLevel,
    /// Wildcard dependencies defined using path attributes will be treated as
    /// if they were [`LintLevel::Allow`] for private crates, but other wildcard
    /// dependencies will be treated as [`LintLevel::Deny`].
    ///
    /// crates.io does not allow packages to be published with path dependencies,
    /// thus this rule will not effect public packages.
    pub allow_wildcard_paths: bool,
    /// Deprecated and moved into `build.allow_build_scripts`, will eventually
    /// be removed
    pub allow_build_scripts: Option<Spanned<Vec<PackageSpec>>>,
    /// Options for crates that run at build time
    pub build: Option<BuildConfig>,
    /// Options for checking crates against the std replacement list
    pub std_replacements: Option<StdReplacementConfig>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            multiple_versions: LintLevel::Warn,
            multiple_versions_include_dev: false,
            workspace_dependencies: None,
            highlight: GraphHighlight::All,
            deny: Vec::new(),
            allow: Vec::new(),
            allow_workspace: false,
            features: Vec::new(),
            external_default_features: None,
            workspace_default_features: None,
            skip: Vec::new(),
            skip_tree: Vec::new(),
            wildcards: LintLevel::Allow,
            allow_wildcard_paths: false,
            allow_build_scripts: None,
            build: None,
            std_replacements: None,
        }
    }
}

impl<'de> Deserialize<'de> for Config {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let mut th = TableHelper::new(value)?;

        let multiple_versions = th.optional("multiple-versions").unwrap_or(LintLevel::Warn);
        let multiple_versions_include_dev = th
            .optional("multiple-versions-include-dev")
            .unwrap_or_default();
        let highlight = th.optional("highlight").unwrap_or_default();
        let deny = th.optional("deny").unwrap_or_default();
        let allow = th.optional("allow").unwrap_or_default();
        let allow_workspace = th.optional("allow-workspace").unwrap_or_default();
        let features = th.optional("features").unwrap_or_default();
        let external_default_features = th.optional("external-default-features");
        let workspace_default_features = th.optional("workspace-default-features");
        let skip = th.optional("skip").unwrap_or_default();
        let skip_tree = th.optional("skip-tree").unwrap_or_default();
        let wildcards = th.optional("wildcards").unwrap_or(LintLevel::Allow);
        let allow_wildcard_paths = th.optional("allow-wildcard-paths").unwrap_or_default();
        let allow_build_scripts = th.optional("allow-build-scripts");
        let build = th.optional("build");

        let workspace_dependencies = th.optional("workspace-dependencies");
        let std_replacements = th.optional("std-replacements");

        th.finalize(None)?;

        Ok(Self {
            multiple_versions,
            multiple_versions_include_dev,
            workspace_dependencies,
            highlight,
            deny,
            allow,
            allow_workspace,
            features,
            external_default_features,
            workspace_default_features,
            skip,
            skip_tree,
            wildcards,
            allow_wildcard_paths,
            allow_build_scripts,
            build,
            std_replacements,
        })
    }
}

impl crate::cfg::UnvalidatedConfig for Config {
    type ValidCfg = ValidConfig;

    fn validate(self, mut ctx: ValidationContext<'_>) -> Self::ValidCfg {
        let cfg_id = ctx.cfg_id;

        let (denied_multiple_versions, denied) = {
            let mut dmulti = Vec::new();
            let mut denied = Vec::new();
            for deny_spec in self.deny {
                let spec = deny_spec.spec;

                let inner = if let Some(extended) = deny_spec.inner {
                    let dmv = extended.deny_multiple_versions;
                    let wrappers = extended.wrappers;

                    if let Some((dmv, wrappers)) = dmv.as_ref().zip(wrappers.as_ref())
                        && dmv.value
                        && !wrappers.value.is_empty()
                    {
                        ctx.push(
                                Diagnostic::error()
                                    .with_message(
                                        "a crate ban was specified with both `wrappers` and `deny-multiple-versions` = true",
                                    )
                                    .with_labels(vec![
                                        Label::secondary(cfg_id, wrappers.span)
                                            .with_message(format_args!("has {} `wrappers`", wrappers.value.len())),
                                        Label::secondary(cfg_id, dmv.span)
                                            .with_message("has `deny-multiple-versions` set to true"),
                                    ]),
                            );
                        continue;
                    }

                    if dmv.is_some_and(|d| d.value) {
                        dmulti.push(spec);
                        continue;
                    }

                    Some(KrateBan {
                        wrappers: wrappers.map(|sv| sv.value),
                        reason: extended.reason,
                        use_instead: extended.use_instead,
                    })
                } else {
                    None
                };

                denied.push(ValidKrateBan { spec, inner });
            }

            (dmulti, denied)
        };

        let allowed = self.allow;
        let skipped = self.skip;

        let dupe_crate_diag = |ctx: &mut ValidationContext<'_>,
                               first: (&PackageSpec, &str),
                               second: (&PackageSpec, &str)| {
            let diag = Diagnostic::error()
                .with_message(format_args!(
                    "a crate was specified in both `{}` and `{}`",
                    second.1, first.1
                ))
                .with_labels(vec![
                    Label::secondary(cfg_id, first.0.name.span)
                        .with_message(format_args!("marked as `{}`", first.1)),
                    Label::secondary(cfg_id, second.0.name.span)
                        .with_message(format_args!("marked as `{}`", second.1)),
                ]);

            ctx.push(diag);
        };

        let dupe_feature_diag = |ctx: &mut ValidationContext<'_>,
                                 krate: &PackageSpec,
                                 allow: &Spanned<String>,
                                 deny: &Spanned<String>| {
            let diag = Diagnostic::error()
                .with_message("a crate feature was specified as both allowed and denied")
                .with_labels(vec![
                    Label::primary(cfg_id, krate.name.span).with_message("crate ban entry"),
                    Label::secondary(cfg_id, allow.span).with_message("marked as `allow`"),
                    Label::secondary(cfg_id, deny.span).with_message("marked as `deny`"),
                ]);

            ctx.push(diag);
        };

        for d in &denied {
            if let Some(dupe) = exact_match(&allowed, &d.spec) {
                dupe_crate_diag(&mut ctx, (&d.spec, "deny"), (dupe, "allow"));
            }

            if let Some(dupe) = exact_match(&skipped, &d.spec) {
                dupe_crate_diag(&mut ctx, (&d.spec, "deny"), (dupe, "skip"));
            }
        }

        for all in &allowed {
            if let Some(dupe) = exact_match(&skipped, &all.spec) {
                dupe_crate_diag(&mut ctx, (&all.spec, "allow"), (dupe, "skip"));
            }
        }

        // Ensure that a feature isn't both allowed and denied
        let features = self
            .features
            .into_iter()
            .map(|cf| {
                let spec = cf.spec;
                for allowed in &cf.allow.value {
                    if let Some(denied) = cf.deny.iter().find(|df| df.value == allowed.value) {
                        dupe_feature_diag(&mut ctx, &spec, allowed, denied);
                    }
                }

                ValidKrateFeatures {
                    spec,
                    features: Features {
                        allow: cf.allow,
                        deny: cf.deny,
                        exact: cf.exact,
                    },
                    reason: cf.reason,
                }
            })
            .collect();

        let build = if let Some(bc) = self.build {
            // Give higher precedence to the user's extensions
            let mut gsb = GlobsetBuilder::new();
            if let Some(extensions) = bc.script_extensions {
                for ext in extensions {
                    // This top level config should only be extensions, not glob patterns
                    if !ext.value.is_ascii() {
                        ctx.diagnostics.push(
                            Diagnostic::error()
                                .with_message("non-ascii file extension provided")
                                .with_labels(vec![
                                    Label::primary(ctx.cfg_id, ext.span)
                                        .with_message("invalid extension"),
                                ]),
                        );
                        continue;
                    }

                    if let Some(i) = ext.value.chars().position(|c| !c.is_ascii_alphanumeric()) {
                        ctx.diagnostics.push(
                            Diagnostic::error()
                                .with_message("invalid file extension provided")
                                .with_labels(vec![
                                    Label::primary(ctx.cfg_id, ext.span).with_message("extension"),
                                    Label::secondary(
                                        ctx.cfg_id,
                                        ext.span.start + i..ext.span.start + i + 1,
                                    )
                                    .with_message("invalid character"),
                                ]),
                        );
                        continue;
                    }

                    match globset::Glob::new(&format!("*.{}", ext.value)) {
                        Ok(glob) => {
                            gsb.add(glob, GlobPattern::User(ext));
                        }
                        Err(err) => {
                            ctx.diagnostics.push(
                                Diagnostic::error()
                                    .with_message(format_args!("invalid glob pattern: {err}"))
                                    .with_labels(vec![
                                        Label::primary(ctx.cfg_id, ext.span)
                                            .with_message("extension"),
                                    ]),
                            );
                        }
                    }
                }
            }

            if bc.enable_builtin_globs {
                load_builtin_globs(ctx.files, &mut gsb);
            }

            let script_extensions = gsb.build().unwrap_or_else(|err| {
                ctx.diagnostics
                    .push(Diagnostic::error().with_message(format_args!(
                        "failed to build script extensions glob set: {err}"
                    )));
                ValidGlobSet::default()
            });

            let bypass = if let Some(aexes) = bc.bypass {
                let mut aex = Vec::new();

                for aexe in aexes {
                    let spec = aexe.spec;

                    let allow_globs = if let Some(allow_globs) = aexe.allow_globs {
                        let mut gsb = GlobsetBuilder::new();

                        for ag in allow_globs {
                            match globset::Glob::new(&ag.value) {
                                Ok(glob) => {
                                    gsb.add(glob, GlobPattern::User(ag));
                                }
                                Err(err) => {
                                    ctx.diagnostics.push(
                                        Diagnostic::error()
                                            .with_message(format_args!(
                                                "invalid glob pattern: {err}"
                                            ))
                                            .with_labels(vec![Label::primary(ctx.cfg_id, ag.span)]),
                                    );
                                }
                            }
                        }

                        match gsb.build() {
                            Ok(set) => Some(set),
                            Err(err) => {
                                ctx.diagnostics.push(Diagnostic::error().with_message(
                                    format_args!(
                                        "failed to build script extensions glob set: {err}"
                                    ),
                                ));
                                None
                            }
                        }
                    } else {
                        None
                    };

                    let mut allow = aexe.allow;
                    allow.retain(|ae| {
                        let keep = ae.path.value.is_relative();
                        if !keep {
                            ctx.diagnostics.push(
                                Diagnostic::error()
                                    .with_message("absolute paths are not allowed")
                                    .with_labels(vec![Label::primary(ctx.cfg_id, ae.path.span)]),
                            );
                        }

                        keep
                    });
                    allow.sort_by(|a, b| a.path.value.cmp(&b.path.value));

                    aex.push(ValidBypass {
                        spec,
                        build_script: aexe.build_script,
                        required_features: aexe.required_features,
                        allow,
                        allow_globs,
                    });
                }

                aex
            } else {
                Vec::new()
            };

            Some(ValidBuildConfig {
                allow_build_scripts: bc.allow_build_scripts,
                executables: bc.executables,
                script_extensions,
                bypass,
                include_dependencies: bc.include_dependencies,
                include_workspace: bc.include_workspace,
                include_archives: bc.include_archives,
                interpreted: bc.interpreted,
            })
        } else if let Some(abs) = self.allow_build_scripts {
            ctx.push(Diagnostic::warning()
                .with_message("[bans.allow-build-scripts] has been deprecated in favor of [bans.build.allow-build-scripts], this will become an error in the future")
                .with_labels(vec![
                    Label::primary(ctx.cfg_id, abs.span)
                ]));
            Some(ValidBuildConfig {
                allow_build_scripts: Some(abs.value),
                executables: LintLevel::Allow,
                script_extensions: ValidGlobSet::default(),
                bypass: Vec::new(),
                include_dependencies: false,
                include_workspace: false,
                include_archives: false,
                interpreted: LintLevel::Warn,
            })
        } else {
            None
        };

        ValidConfig {
            file_id: ctx.cfg_id,
            multiple_versions: self.multiple_versions,
            multiple_versions_include_dev: self.multiple_versions_include_dev,
            workspace_dependencies: self.workspace_dependencies,
            highlight: self.highlight,
            denied,
            denied_multiple_versions,
            allowed,
            allow_workspace: self.allow_workspace,
            features,
            external_default_features: self.external_default_features,
            workspace_default_features: self.workspace_default_features,
            skipped,
            wildcards: self.wildcards,
            allow_wildcard_paths: self.allow_wildcard_paths,
            tree_skipped: self.skip_tree,
            build,
            std_replacements: self.std_replacements,
        }
    }
}

fn load_builtin_globs(files: &mut crate::diag::Files, gsb: &mut GlobsetBuilder) {
    const BUILTIN_GLOBS: &str = include_str!("builtin_globs.toml");

    let mut biv = toml_span::parse(BUILTIN_GLOBS).expect("failed to parse builtin_globs.toml");
    let mut th = TableHelper::new(&mut biv).expect("builtin_globs.toml does not have a root table");

    let globs: Vec<Spanned<String>> = th.required("globs").expect("failed to find 'globs' array");

    let file_id = files.add("builtin_globs.toml", BUILTIN_GLOBS.to_owned());

    for glob in globs {
        gsb.add(
            globset::Glob::new(&glob.value).expect("failed to parse builtin glob"),
            GlobPattern::Builtin((glob, file_id)),
        );
    }
}

#[inline]
pub(crate) fn exact_match<'v, T>(
    arr: &'v [PackageSpecOrExtended<T>],
    id: &'_ PackageSpec,
) -> Option<&'v PackageSpec> {
    arr.iter()
        .find_map(|sid| (&sid.spec == id).then_some(&sid.spec))
}

#[cfg_attr(test, derive(serde::Serialize))]
pub(crate) struct KrateBan {
    pub wrappers: Option<Vec<Spanned<String>>>,
    pub reason: Option<Reason>,
    pub use_instead: Option<Spanned<String>>,
}

pub(crate) type ValidKrateBan = PackageSpecOrExtended<KrateBan>;

#[cfg_attr(test, derive(serde::Serialize))]
pub struct Features {
    pub allow: Spanned<Vec<Spanned<String>>>,
    pub deny: Vec<Spanned<String>>,
    pub exact: Spanned<bool>,
}

#[cfg_attr(test, derive(serde::Serialize))]
pub(crate) struct ValidKrateFeatures {
    pub spec: PackageSpec,
    pub features: Features,
    pub reason: Option<Reason>,
}

#[cfg_attr(test, derive(serde::Serialize))]
pub enum GlobPattern {
    Builtin((Spanned<String>, FileId)),
    User(Spanned<String>),
}

struct GlobsetBuilder {
    builder: globset::GlobSetBuilder,
    patterns: Vec<GlobPattern>,
}

impl GlobsetBuilder {
    fn new() -> Self {
        Self {
            builder: globset::GlobSetBuilder::new(),
            patterns: Vec::new(),
        }
    }

    fn add(&mut self, glob: globset::Glob, pattern: GlobPattern) {
        self.builder.add(glob);
        self.patterns.push(pattern);
    }

    fn build(self) -> anyhow::Result<ValidGlobSet> {
        use anyhow::Context as _;
        let set = self.builder.build().context("unable to build globset")?;

        Ok(ValidGlobSet {
            set,
            patterns: self.patterns,
        })
    }
}

pub struct ValidGlobSet {
    set: globset::GlobSet,
    /// Patterns in the globset for lint output
    pub(crate) patterns: Vec<GlobPattern>,
}

#[cfg(test)]
impl serde::Serialize for ValidGlobSet {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.collect_seq(self.patterns.iter().filter_map(|gp| {
            let GlobPattern::User(gp) = gp else {
                return None;
            };
            Some(gp)
        }))
    }
}

impl Default for ValidGlobSet {
    fn default() -> Self {
        Self {
            set: globset::GlobSet::empty(),
            patterns: Vec::new(),
        }
    }
}

impl ValidGlobSet {
    #[inline]
    pub fn matches(
        &self,
        path: &globset::Candidate<'_>,
        indices: &mut Vec<usize>,
    ) -> Option<Vec<&GlobPattern>> {
        self.set.matches_candidate_into(path, indices);
        (!indices.is_empty()).then(|| indices.iter().map(|i| &self.patterns[*i]).collect())
    }
}

#[cfg_attr(test, derive(serde::Serialize))]
pub struct ValidBypass {
    pub spec: PackageSpec,
    pub build_script: Option<Spanned<Checksum>>,
    pub required_features: Vec<Spanned<String>>,
    pub allow_globs: Option<ValidGlobSet>,
    pub allow: Vec<BypassPath>,
}

#[cfg_attr(test, derive(serde::Serialize))]
pub struct ValidBuildConfig {
    pub allow_build_scripts: Option<Vec<PackageSpec>>,
    pub executables: LintLevel,
    pub script_extensions: ValidGlobSet,
    pub bypass: Vec<ValidBypass>,
    pub include_dependencies: bool,
    pub include_workspace: bool,
    pub include_archives: bool,
    pub interpreted: LintLevel,
}

pub type ValidTreeSkip = PackageSpecOrExtended<TreeSkipExtended>;
pub type SpecAndReason = PackageSpecOrExtended<Reason>;

#[cfg_attr(test, derive(serde::Serialize))]
pub struct ValidConfig {
    pub file_id: FileId,
    pub multiple_versions: LintLevel,
    pub multiple_versions_include_dev: bool,
    pub workspace_dependencies: Option<WorkspaceDepsConfig>,
    pub highlight: GraphHighlight,
    pub(crate) denied: Vec<ValidKrateBan>,
    pub(crate) denied_multiple_versions: Vec<PackageSpec>,
    pub(crate) allowed: Vec<SpecAndReason>,
    pub allow_workspace: bool,
    pub(crate) features: Vec<ValidKrateFeatures>,
    pub external_default_features: Option<Spanned<LintLevel>>,
    pub workspace_default_features: Option<Spanned<LintLevel>>,
    pub(crate) skipped: Vec<SpecAndReason>,
    pub(crate) tree_skipped: Vec<ValidTreeSkip>,
    pub wildcards: LintLevel,
    pub allow_wildcard_paths: bool,
    pub build: Option<ValidBuildConfig>,
    pub std_replacements: Option<StdReplacementConfig>,
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::test_utils::ConfigData;

    #[test]
    fn deserializes_ban_cfg() {
        struct Bans {
            bans: Config,
        }

        impl<'de> toml_span::Deserialize<'de> for Bans {
            fn deserialize(
                value: &mut toml_span::value::Value<'de>,
            ) -> Result<Self, toml_span::DeserError> {
                let mut th = toml_span::de_helpers::TableHelper::new(value)?;
                let bans = th.required("bans").unwrap();
                th.finalize(None)?;
                Ok(Self { bans })
            }
        }

        let cd = ConfigData::<Bans>::load("tests/cfg/bans.toml");
        let validated = cd.validate(|b| b.bans);

        insta::assert_json_snapshot!(validated);
    }
}
