use std::{
    collections::{BTreeMap, BTreeSet},
    convert::TryInto,
};

use miette::{IntoDiagnostic, Result, SourceSpan};
use mit_lint::{Lint, Lints, CONFIG_KEY_PREFIX};

use crate::{external::Vcs, lints::cmd::errors::SerialiseLintError};

/// # Errors
///
/// If we fail to parse the toml
///
/// # Panics
///
/// Will panic if the lint prefix isn't delimited by dots. This should never
/// happen as it's a constant
pub fn read_from_toml_or_else_vcs(config: &str, vcs: &dyn Vcs) -> Result<Lints> {
    let vcs_lints = try_from_vcs(vcs)?;
    // contains PB  // contains lint // contains config
    let config: BTreeMap<String, BTreeMap<String, BTreeMap<String, bool>>> = toml::from_str(config)
        .map_err(|x| SerialiseLintError {
            src: config.to_string(),
            message: x.to_string(),
            span: x
                .span()
                .map_or_else(|| SourceSpan::new(0.into(), 0), Into::into),
        })?;

    let lint_prefix = CONFIG_KEY_PREFIX.split('.').collect::<Vec<_>>();
    let namespace = (*lint_prefix.first().unwrap()).to_string();

    let Some(config) = config.get(&namespace) else {
        return Ok(vcs_lints);
    };

    let group = (*lint_prefix.get(1).unwrap()).to_string();

    let Some(lint_names) = config.get(&group) else {
        return Ok(vcs_lints);
    };

    let to_add: Lints = lint_names
        .iter()
        .filter_map(|(key, value)| if *value { Some(key as &str) } else { None })
        .collect::<Vec<&str>>()
        .try_into()
        .into_diagnostic()?;

    let to_remove: Lints = lint_names
        .iter()
        .filter_map(|(key, value)| if *value { None } else { Some(key as &str) })
        .collect::<Vec<&str>>()
        .try_into()
        .into_diagnostic()?;

    Ok(vcs_lints.subtract(&to_remove).merge(&to_add))
}

/// Create lints from the VCS configuration
///
/// # Errors
/// If reading from the VCS fails
fn try_from_vcs(config: &dyn Vcs) -> Result<Lints> {
    Ok(Lints::new(
        Lint::all_lints()
            .filter_map(|lint| {
                get_config_or_default(config, lint, lint.enabled_by_default()).transpose()
            })
            .collect::<Result<BTreeSet<Lint>>>()?,
    ))
}

fn get_config_or_default(config: &dyn Vcs, lint: Lint, default: bool) -> Result<Option<Lint>> {
    Ok(config
        .get_bool(&lint.config_key())?
        .or(Some(default))
        .filter(|lint_value| lint_value == &true)
        .map(|_| lint))
}
