use std::collections::BTreeMap;

use codex_config::Constrained;
use codex_config::ConstrainedWithSource;
use codex_config::ConstraintError;
use codex_config::ConstraintResult;
use codex_config::FeatureRequirementsToml;
use codex_config::RequirementSource;
use codex_config::Sourced;

use codex_config::config_toml::ConfigToml;
use codex_features::Feature;
use codex_features::FeatureConfigSource;
use codex_features::FeatureOverrides;
use codex_features::Features;
use codex_features::canonical_feature_for_key;
use codex_features::feature_for_key;

/// Wrapper around [`Features`] which enforces constraints defined in
/// `FeatureRequirementsToml` and provides normalization to ensure constraints
/// are satisfied. Constraints are enforced on construction and mutation of
/// `ManagedFeatures`.
#[derive(Debug, Clone, PartialEq)]
pub struct ManagedFeatures {
    value: ConstrainedWithSource<Features>,
    pinned_features: BTreeMap<Feature, bool>,
}

impl Default for ManagedFeatures {
    fn default() -> Self {
        Self {
            value: ConstrainedWithSource::new(
                Constrained::allow_any(Features::default()),
                /*source*/ None,
            ),
            pinned_features: BTreeMap::new(),
        }
    }
}

impl ManagedFeatures {
    pub(crate) fn from_configured(
        configured_features: Features,
        feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
    ) -> std::io::Result<Self> {
        Self::from_configured_with_optional_warnings(
            configured_features,
            feature_requirements,
            /*startup_warnings*/ None,
        )
    }

    pub(crate) fn from_configured_with_warnings(
        configured_features: Features,
        feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
        startup_warnings: &mut Vec<String>,
    ) -> std::io::Result<Self> {
        Self::from_configured_with_optional_warnings(
            configured_features,
            feature_requirements,
            Some(startup_warnings),
        )
    }

    fn from_configured_with_optional_warnings(
        configured_features: Features,
        feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
        startup_warnings: Option<&mut Vec<String>>,
    ) -> std::io::Result<Self> {
        let (pinned_features, source) = match feature_requirements {
            Some(Sourced {
                value: feature_requirements,
                source,
            }) => (
                parse_feature_requirements(feature_requirements, &source, startup_warnings),
                Some(source),
            ),
            None => (BTreeMap::new(), None),
        };

        let normalized_features = normalize_candidate(configured_features, &pinned_features);
        validate_pinned_features(&normalized_features, &pinned_features, source.as_ref())?;
        Ok(Self {
            value: ConstrainedWithSource::new(Constrained::allow_any(normalized_features), source),
            pinned_features,
        })
    }

    pub fn get(&self) -> &Features {
        self.value.get()
    }

    fn normalize_and_validate(&self, candidate: Features) -> ConstraintResult<Features> {
        let normalized = normalize_candidate(candidate, &self.pinned_features);
        self.value.can_set(&normalized)?;
        validate_pinned_features_constraint(
            &normalized,
            &self.pinned_features,
            self.value.source.as_ref(),
        )?;
        Ok(normalized)
    }

    pub fn can_set(&self, candidate: &Features) -> ConstraintResult<()> {
        self.normalize_and_validate(candidate.clone()).map(|_| ())
    }

    pub fn set(&mut self, candidate: Features) -> ConstraintResult<()> {
        let normalized = self.normalize_and_validate(candidate)?;
        self.value.value.set(normalized)
    }

    pub fn set_enabled(&mut self, feature: Feature, enabled: bool) -> ConstraintResult<()> {
        let mut next = self.get().clone();
        next.set_enabled(feature, enabled);
        self.set(next)
    }

    pub fn enable(&mut self, feature: Feature) -> ConstraintResult<()> {
        self.set_enabled(feature, /*enabled*/ true)
    }

    pub fn disable(&mut self, feature: Feature) -> ConstraintResult<()> {
        self.set_enabled(feature, /*enabled*/ false)
    }
}

/// Only available for tests to ensure `ManagedFeatures` is constructed with
/// any required constraints taken into account.
#[cfg(test)]
impl From<Features> for ManagedFeatures {
    fn from(features: Features) -> Self {
        Self {
            value: ConstrainedWithSource::new(
                Constrained::allow_any(features),
                /*source*/ None,
            ),
            pinned_features: BTreeMap::new(),
        }
    }
}

impl std::ops::Deref for ManagedFeatures {
    type Target = Features;

    fn deref(&self) -> &Self::Target {
        self.get()
    }
}

fn normalize_candidate(
    mut candidate: Features,
    pinned_features: &BTreeMap<Feature, bool>,
) -> Features {
    for (feature, enabled) in pinned_features {
        candidate.set_enabled(*feature, *enabled);
    }
    candidate.normalize_dependencies();
    candidate
}

fn validate_pinned_features_constraint(
    normalized_features: &Features,
    pinned_features: &BTreeMap<Feature, bool>,
    source: Option<&RequirementSource>,
) -> ConstraintResult<()> {
    let Some(source) = source else {
        return Ok(());
    };
    let allowed = feature_requirements_display(pinned_features);
    for (feature, enabled) in pinned_features {
        if normalized_features.enabled(*feature) != *enabled {
            return Err(ConstraintError::InvalidValue {
                field_name: "features",
                candidate: format!(
                    "{}={}",
                    feature.key(),
                    normalized_features.enabled(*feature)
                ),
                allowed,
                requirement_source: source.clone(),
            });
        }
    }

    Ok(())
}

fn validate_pinned_features(
    normalized_features: &Features,
    pinned_features: &BTreeMap<Feature, bool>,
    source: Option<&RequirementSource>,
) -> std::io::Result<()> {
    validate_pinned_features_constraint(normalized_features, pinned_features, source)
        .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
}

fn feature_requirements_display(feature_requirements: &BTreeMap<Feature, bool>) -> String {
    let values = feature_requirements
        .iter()
        .map(|(feature, enabled)| format!("{}={enabled}", feature.key()))
        .collect::<Vec<_>>();
    format!("[{}]", values.join(", "))
}

fn parse_feature_requirements(
    feature_requirements: FeatureRequirementsToml,
    source: &RequirementSource,
    mut startup_warnings: Option<&mut Vec<String>>,
) -> BTreeMap<Feature, bool> {
    let mut pinned_features = BTreeMap::new();
    for (key, enabled) in feature_requirements.entries {
        if key == "auto_review" {
            pinned_features.insert(Feature::GuardianApproval, enabled);
            continue;
        }

        if let Some(feature) = canonical_feature_for_key(&key) {
            pinned_features.insert(feature, enabled);
            continue;
        }

        if let Some(feature) = feature_for_key(&key) {
            push_feature_requirement_warning(
                &mut startup_warnings,
                format!(
                    "Using legacy `features` requirement `{key}` from {source}; prefer canonical feature key `{}`",
                    feature.key()
                ),
            );
            pinned_features.insert(feature, enabled);
            continue;
        }

        push_feature_requirement_warning(
            &mut startup_warnings,
            format!("Ignoring unknown `features` requirement `{key}` from {source}"),
        );
    }

    pinned_features
}

fn push_feature_requirement_warning(
    startup_warnings: &mut Option<&mut Vec<String>>,
    message: String,
) {
    tracing::warn!("{message}");
    if let Some(startup_warnings) = startup_warnings.as_deref_mut() {
        startup_warnings.push(message);
    }
}

fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature, bool)> {
    let mut explicit_settings = Vec::new();

    if let Some(features) = cfg.features.as_ref() {
        for (key, enabled) in features.entries() {
            if let Some(feature) = feature_for_key(&key) {
                explicit_settings.push((format!("features.{key}"), feature, enabled));
            }
        }
    }
    if let Some(enabled) = cfg.experimental_use_unified_exec_tool {
        explicit_settings.push((
            "experimental_use_unified_exec_tool".to_string(),
            Feature::UnifiedExec,
            enabled,
        ));
    }
    explicit_settings
}

pub(crate) fn validate_explicit_feature_settings_in_config_toml(
    cfg: &ConfigToml,
    feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<()> {
    let Some(Sourced {
        value: feature_requirements,
        source,
    }) = feature_requirements
    else {
        return Ok(());
    };

    let pinned_features = parse_feature_requirements(
        feature_requirements.clone(),
        source,
        /*startup_warnings*/ None,
    );
    if pinned_features.is_empty() {
        return Ok(());
    }

    let allowed = feature_requirements_display(&pinned_features);
    for (path, feature, enabled) in explicit_feature_settings_in_config(cfg) {
        if pinned_features
            .get(&feature)
            .is_some_and(|required| *required != enabled)
        {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                ConstraintError::InvalidValue {
                    field_name: "features",
                    candidate: format!("{path}={enabled}"),
                    allowed,
                    requirement_source: source.clone(),
                },
            ));
        }
    }

    Ok(())
}

pub(crate) fn validate_feature_requirements_in_config_toml(
    cfg: &ConfigToml,
    feature_requirements: Option<&Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<()> {
    let configured_features = Features::from_sources(
        FeatureConfigSource {
            features: cfg.features.as_ref(),
            experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool,
        },
        FeatureConfigSource::default(),
        FeatureOverrides::default(),
    );
    ManagedFeatures::from_configured(configured_features, feature_requirements.cloned()).map(|_| ())
}
