use std::collections::BTreeSet;
use std::path::Path;

use tq_core::{InitModulesMode, QualifierStrategy, RuleId};
use tq_engine::Rule;

use crate::error::RulesError;
use crate::file_too_large::TestFileTooLargeRule;
use crate::mapping_missing_test::MappingMissingTestRule;
use crate::orphaned_test::OrphanedTestRule;
use crate::rule_docs::{
    BuiltinRuleDoc, mapping_missing_test_doc, orphaned_test_doc, structure_mismatch_doc,
    test_file_too_large_doc,
};
use crate::structure_mismatch::StructureMismatchRule;

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum BuiltinRule {
    MappingMissingTest,
    StructureMismatch,
    TestFileTooLarge,
    OrphanedTest,
}

impl BuiltinRule {
    const ALL: [Self; 4] = [
        Self::MappingMissingTest,
        Self::StructureMismatch,
        Self::TestFileTooLarge,
        Self::OrphanedTest,
    ];

    #[must_use]
    const fn as_str(self) -> &'static str {
        self.doc().id
    }

    #[must_use]
    pub const fn default_severity(self) -> tq_core::Severity {
        self.doc().default_severity
    }

    pub(crate) fn rule_id(self) -> Result<RuleId, RulesError> {
        parse_builtin_rule_id(self.as_str())
    }

    #[must_use]
    const fn doc(self) -> &'static BuiltinRuleDoc {
        match self {
            Self::MappingMissingTest => mapping_missing_test_doc(),
            Self::StructureMismatch => structure_mismatch_doc(),
            Self::TestFileTooLarge => test_file_too_large_doc(),
            Self::OrphanedTest => orphaned_test_doc(),
        }
    }

    #[must_use]
    fn from_rule_id(rule_id: &RuleId) -> Option<Self> {
        match rule_id.as_str() {
            "mapping-missing-test" => Some(Self::MappingMissingTest),
            "structure-mismatch" => Some(Self::StructureMismatch),
            "test-file-too-large" => Some(Self::TestFileTooLarge),
            "orphaned-test" => Some(Self::OrphanedTest),
            _ => None,
        }
    }

    fn build(self, options: &BuiltinRuleOptions) -> Result<Box<dyn Rule>, RulesError> {
        match self {
            Self::MappingMissingTest => Ok(Box::new(MappingMissingTestRule::new(
                options.init_modules(),
                options.qualifier_strategy(),
                options.allowed_qualifiers().clone(),
            )?)),
            Self::StructureMismatch => Ok(Box::new(StructureMismatchRule::new()?)),
            Self::TestFileTooLarge => Ok(Box::new(TestFileTooLargeRule::new(
                options.max_test_file_non_blank_lines(),
            )?)),
            Self::OrphanedTest => Ok(Box::new(OrphanedTestRule::new(
                options.qualifier_strategy(),
                options.allowed_qualifiers().clone(),
            )?)),
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct BuiltinRuleOptions {
    init_modules: InitModulesMode,
    max_test_file_non_blank_lines: u64,
    qualifier_strategy: QualifierStrategy,
    allowed_qualifiers: BTreeSet<String>,
}

impl BuiltinRuleOptions {
    pub fn new(
        init_modules: InitModulesMode,
        max_test_file_non_blank_lines: u64,
        qualifier_strategy: QualifierStrategy,
        allowed_qualifiers: impl IntoIterator<Item = String>,
    ) -> Result<Self, RulesError> {
        if max_test_file_non_blank_lines < 1 {
            return Err(RulesError::value_must_be_positive(
                "max_test_file_non_blank_lines",
            ));
        }

        let allowed_qualifiers = normalize_non_empty_trimmed_strings(allowed_qualifiers);
        if qualifier_strategy == QualifierStrategy::Allowlist && allowed_qualifiers.is_empty() {
            return Err(RulesError::allowlist_requires_qualifiers());
        }

        Ok(Self {
            init_modules,
            max_test_file_non_blank_lines,
            qualifier_strategy,
            allowed_qualifiers,
        })
    }

    #[must_use]
    pub const fn init_modules(&self) -> InitModulesMode {
        self.init_modules
    }

    #[must_use]
    pub const fn max_test_file_non_blank_lines(&self) -> u64 {
        self.max_test_file_non_blank_lines
    }

    #[must_use]
    pub const fn qualifier_strategy(&self) -> QualifierStrategy {
        self.qualifier_strategy
    }

    #[must_use]
    pub const fn allowed_qualifiers(&self) -> &BTreeSet<String> {
        &self.allowed_qualifiers
    }
}

impl Default for BuiltinRuleOptions {
    fn default() -> Self {
        Self {
            init_modules: InitModulesMode::Include,
            max_test_file_non_blank_lines: 600,
            qualifier_strategy: QualifierStrategy::AnySuffix,
            allowed_qualifiers: BTreeSet::new(),
        }
    }
}

#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct RuleSelection {
    select: Vec<RuleId>,
    ignore: Vec<RuleId>,
}

impl RuleSelection {
    #[must_use]
    pub const fn new(select: Vec<RuleId>, ignore: Vec<RuleId>) -> Self {
        Self { select, ignore }
    }

    #[must_use]
    pub fn select(&self) -> &[RuleId] {
        &self.select
    }

    #[must_use]
    pub fn ignore(&self) -> &[RuleId] {
        &self.ignore
    }
}

pub struct BuiltinRuleRegistry;

impl BuiltinRuleRegistry {
    pub fn build_rules(
        selection: &RuleSelection,
        options: &BuiltinRuleOptions,
    ) -> Result<Vec<Box<dyn Rule>>, RulesError> {
        let active_rules = resolve_active_rules(selection)?;

        let mut rules: Vec<Box<dyn Rule>> = Vec::with_capacity(active_rules.len());
        for builtin_rule in active_rules {
            rules.push(builtin_rule.build(options)?);
        }

        Ok(rules)
    }
}

pub fn validate_severity_override_rule_ids(
    overrides: &std::collections::BTreeMap<tq_core::RuleId, tq_core::Severity>,
) -> Result<(), RulesError> {
    let unknown: Vec<tq_core::RuleId> = overrides
        .keys()
        .filter(|rule_id| BuiltinRule::from_rule_id(rule_id).is_none())
        .cloned()
        .collect();
    if !unknown.is_empty() {
        return Err(RulesError::unknown_builtin_rule_ids(&unknown));
    }
    Ok(())
}

pub fn builtin_rule_ids() -> Result<Vec<RuleId>, RulesError> {
    BuiltinRule::ALL
        .into_iter()
        .map(BuiltinRule::rule_id)
        .collect()
}

pub fn resolve_active_rule_ids(selection: &RuleSelection) -> Result<Vec<RuleId>, RulesError> {
    resolve_active_rules(selection)?
        .into_iter()
        .map(BuiltinRule::rule_id)
        .collect::<Result<Vec<_>, _>>()
}

fn resolve_active_rules(selection: &RuleSelection) -> Result<Vec<BuiltinRule>, RulesError> {
    let unknown = selection
        .select()
        .iter()
        .chain(selection.ignore())
        .filter(|requested| BuiltinRule::from_rule_id(requested).is_none())
        .cloned()
        .collect::<Vec<_>>();
    if !unknown.is_empty() {
        return Err(RulesError::unknown_builtin_rule_ids(&unknown));
    }

    let selected_lookup = selection
        .select()
        .iter()
        .filter_map(BuiltinRule::from_rule_id)
        .collect::<BTreeSet<_>>();
    let ignored_lookup = selection
        .ignore()
        .iter()
        .filter_map(BuiltinRule::from_rule_id)
        .collect::<BTreeSet<_>>();

    let selected_base = if selected_lookup.is_empty() {
        BuiltinRule::ALL.to_vec()
    } else {
        BuiltinRule::ALL
            .into_iter()
            .filter(|rule_id| selected_lookup.contains(rule_id))
            .collect::<Vec<_>>()
    };

    Ok(selected_base
        .into_iter()
        .filter(|rule_id| !ignored_lookup.contains(rule_id))
        .collect())
}

fn normalize_non_empty_trimmed_strings(
    values: impl IntoIterator<Item = String>,
) -> BTreeSet<String> {
    values
        .into_iter()
        .map(|value| value.trim().to_owned())
        .filter(|value| !value.is_empty())
        .collect()
}

pub fn parse_builtin_rule_id(value: &'static str) -> Result<RuleId, RulesError> {
    RuleId::parse(value).map_err(|source| RulesError::invalid_builtin_rule_id(value, source))
}

#[must_use]
pub fn starts_with_path_prefix(test_file: &Path, prefix: &Path) -> bool {
    let prefix_parts = prefix.components().collect::<Vec<_>>();
    let test_parts = test_file.components().collect::<Vec<_>>();
    if test_parts.len() < prefix_parts.len() {
        return false;
    }

    test_parts[..prefix_parts.len()] == prefix_parts
}

#[must_use]
pub fn is_non_unit_test_path(test_file: &Path) -> bool {
    test_file.components().any(|component| {
        component
            .as_os_str()
            .to_str()
            .is_some_and(|segment| segment == "integration" || segment == "e2e")
    })
}

#[must_use]
pub fn is_unit_test_filename(file_name: &str) -> bool {
    file_name.starts_with("test_")
        && Path::new(file_name)
            .extension()
            .is_some_and(|extension| extension.eq_ignore_ascii_case("py"))
}

#[must_use]
pub fn path_to_forward_slashes(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}
