use std::{
    any,
    borrow::Cow,
    collections::{BTreeMap, BTreeSet, HashMap},
    iter,
};
use anyhow::Context;
use self::mount::{MountingPoint, MountingPoints};
use crate::{
    metadata::{
        AliasOptions, BasicTypes, ConfigMetadata, ConfigVariant, NestedConfigMetadata,
        ParamMetadata,
    },
    utils::EnumVariant,
    value::Pointer,
};
mod mount;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy)]
struct ParentLink {
    parent_ty: any::TypeId,
    this_ref: &'static NestedConfigMetadata,
}
#[derive(Debug, Clone)]
pub(crate) struct ConfigData {
    pub(crate) metadata: &'static ConfigMetadata,
    parent_link: Option<ParentLink>,
    pub(crate) is_top_level: bool,
    pub(crate) coerce_serde_enums: bool,
    all_paths: Vec<(Cow<'static, str>, AliasOptions)>,
}
impl ConfigData {
    pub(crate) fn prefix(&self) -> Pointer<'_> {
        Pointer(self.all_paths[0].0.as_ref())
    }
    pub(crate) fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
        self.all_paths
            .iter()
            .skip(1)
            .map(|(path, options)| (path.as_ref(), *options))
    }
    pub(crate) fn all_paths_for_param(
        &self,
        param: &'static ParamMetadata,
    ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
        self.all_paths_for_child(param.name, param.aliases, param.tag_variant)
    }
    fn all_paths_for_child(
        &self,
        name: &'static str,
        aliases: &'static [(&'static str, AliasOptions)],
        tag_variant: Option<&'static ConfigVariant>,
    ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
        let local_names =
            iter::once((name, AliasOptions::default())).chain(aliases.iter().copied());
        let enum_names = if let (true, Some(variant)) = (self.coerce_serde_enums, tag_variant) {
            let variant_names = iter::once(variant.name)
                .chain(variant.aliases.iter().copied())
                .filter_map(|name| Some(EnumVariant::new(name)?.to_snake_case()));
            let local_names_ = local_names.clone();
            let paths = variant_names.flat_map(move |variant_name| {
                local_names_
                    .clone()
                    .filter_map(move |(name_or_path, options)| {
                        if name_or_path.starts_with('.') {
                            return None;
                        }
                        let full_path = Pointer(&variant_name).join(name_or_path);
                        Some((Cow::Owned(full_path), options))
                    })
            });
            Some(paths)
        } else {
            None
        };
        let enum_names = enum_names.into_iter().flatten();
        let local_names = local_names
            .map(|(name, options)| (Cow::Borrowed(name), options))
            .chain(enum_names);
        self.all_paths
            .iter()
            .flat_map(move |(alias, config_options)| {
                local_names
                    .clone()
                    .filter_map(move |(name_or_path, options)| {
                        let full_path = Pointer(alias).join_path(Pointer(&name_or_path))?;
                        Some((full_path, options.combine(*config_options)))
                    })
            })
    }
}
#[derive(Debug, Clone, Copy)]
pub struct ConfigRef<'a> {
    schema: &'a ConfigSchema,
    prefix: &'a str,
    pub(crate) data: &'a ConfigData,
}
impl<'a> ConfigRef<'a> {
    pub fn prefix(&self) -> &'a str {
        self.prefix
    }
    pub fn metadata(&self) -> &'static ConfigMetadata {
        self.data.metadata
    }
    pub fn is_top_level(&self) -> bool {
        self.data.parent_link.is_none()
    }
    #[doc(hidden)] pub fn parent_link(&self) -> Option<(Self, &'static NestedConfigMetadata)> {
        let link = self.data.parent_link?;
        let parent_prefix = if link.this_ref.name.is_empty() {
            self.prefix
        } else {
            let (parent, _) = Pointer(self.prefix).split_last().unwrap();
            parent.0
        };
        let parent_ref = Self {
            schema: self.schema,
            prefix: parent_prefix,
            data: self.schema.get_ll(parent_prefix, link.parent_ty)?,
        };
        Some((parent_ref, link.this_ref))
    }
    pub fn aliases(&self) -> impl Iterator<Item = (&'a str, AliasOptions)> + '_ {
        self.data.aliases()
    }
    #[doc(hidden)] pub fn all_paths_for_param(
        &self,
        param: &'static ParamMetadata,
    ) -> impl Iterator<Item = (String, AliasOptions)> + '_ {
        self.data.all_paths_for_param(param)
    }
}
#[derive(Debug)]
pub struct ConfigMut<'a> {
    schema: &'a mut ConfigSchema,
    prefix: String,
    type_id: any::TypeId,
}
impl ConfigMut<'_> {
    pub fn prefix(&self) -> &str {
        &self.prefix
    }
    pub fn aliases(&self) -> impl Iterator<Item = (&str, AliasOptions)> + '_ {
        let data = &self.schema.configs[self.prefix.as_str()].inner[&self.type_id];
        data.aliases()
    }
    pub fn push_alias(self, alias: &'static str) -> anyhow::Result<Self> {
        self.push_alias_inner(alias, AliasOptions::new())
    }
    pub fn push_deprecated_alias(self, alias: &'static str) -> anyhow::Result<Self> {
        self.push_alias_inner(
            alias,
            AliasOptions {
                is_deprecated: true,
            },
        )
    }
    fn push_alias_inner(self, alias: &'static str, options: AliasOptions) -> anyhow::Result<Self> {
        let mut patched = PatchedSchema::new(self.schema);
        patched.insert_alias(self.prefix.clone(), self.type_id, Pointer(alias), options)?;
        patched.commit();
        Ok(self)
    }
}
#[derive(Debug, Clone, Default)]
struct ConfigsForPrefix {
    inner: HashMap<any::TypeId, ConfigData>,
    by_depth: BTreeSet<(usize, any::TypeId)>,
}
impl ConfigsForPrefix {
    fn by_depth(&self) -> impl Iterator<Item = &ConfigData> + '_ {
        self.by_depth.iter().map(|(_, ty)| &self.inner[ty])
    }
    fn insert(&mut self, ty: any::TypeId, depth: Option<usize>, data: ConfigData) {
        self.inner.insert(ty, data);
        if let Some(depth) = depth {
            self.by_depth.insert((depth, ty));
        }
    }
    fn extend(&mut self, other: Self) {
        self.inner.extend(other.inner);
        self.by_depth.extend(other.by_depth);
    }
}
#[derive(Debug, Clone, Default)]
pub struct ConfigSchema {
    configs: BTreeMap<Cow<'static, str>, ConfigsForPrefix>,
    mounting_points: MountingPoints,
    coerce_serde_enums: bool,
}
impl ConfigSchema {
    #[allow(clippy::missing_panics_doc)]
    pub fn new(metadata: &'static ConfigMetadata, prefix: &'static str) -> Self {
        let mut this = Self::default();
        this.insert(metadata, prefix)
            .expect("internal error: failed inserting first config to the schema");
        this
    }
    pub fn coerce_serde_enums(&mut self, coerce: bool) -> &mut Self {
        self.coerce_serde_enums = coerce;
        self
    }
    pub(crate) fn iter_ll(&self) -> impl Iterator<Item = (Pointer<'_>, &ConfigData)> + '_ {
        self.configs
            .iter()
            .flat_map(|(prefix, data)| data.inner.values().map(move |data| (Pointer(prefix), data)))
    }
    pub(crate) fn contains_canonical_param(&self, at: Pointer<'_>) -> bool {
        self.mounting_points.get(at.0).is_some_and(|mount| {
            matches!(
                mount,
                MountingPoint::Param {
                    is_canonical: true,
                    ..
                }
            )
        })
    }
    pub(crate) fn params_with_kv_path<'s>(
        &'s self,
        kv_path: &'s str,
    ) -> impl Iterator<Item = (Pointer<'s>, BasicTypes)> + 's {
        self.mounting_points
            .by_kv_path(kv_path)
            .filter_map(|(path, mount)| {
                let expecting = match mount {
                    MountingPoint::Param { expecting, .. } => *expecting,
                    MountingPoint::Config => return None,
                };
                Some((path, expecting))
            })
    }
    pub fn iter(&self) -> impl Iterator<Item = ConfigRef<'_>> + '_ {
        self.configs.iter().flat_map(move |(prefix, data)| {
            data.by_depth().map(move |data| ConfigRef {
                schema: self,
                prefix: prefix.as_ref(),
                data,
            })
        })
    }
    pub fn locate(&self, metadata: &'static ConfigMetadata) -> impl Iterator<Item = &str> + '_ {
        let config_type_id = metadata.ty.id();
        self.configs.iter().filter_map(move |(prefix, data)| {
            data.inner
                .contains_key(&config_type_id)
                .then_some(prefix.as_ref())
        })
    }
    pub fn get<'s>(
        &'s self,
        metadata: &'static ConfigMetadata,
        prefix: &'s str,
    ) -> Option<ConfigRef<'s>> {
        let data = self.get_ll(prefix, metadata.ty.id())?;
        Some(ConfigRef {
            schema: self,
            prefix,
            data,
        })
    }
    fn get_ll(&self, prefix: &str, ty: any::TypeId) -> Option<&ConfigData> {
        self.configs.get(prefix)?.inner.get(&ty)
    }
    pub fn get_mut(
        &mut self,
        metadata: &'static ConfigMetadata,
        prefix: &str,
    ) -> Option<ConfigMut<'_>> {
        let ty = metadata.ty.id();
        if !self.configs.get(prefix)?.inner.contains_key(&ty) {
            return None;
        }
        Some(ConfigMut {
            schema: self,
            prefix: prefix.to_owned(),
            type_id: ty,
        })
    }
    #[allow(clippy::missing_panics_doc)] pub fn single(&self, metadata: &'static ConfigMetadata) -> anyhow::Result<ConfigRef<'_>> {
        let prefixes: Vec<_> = self.locate(metadata).take(2).collect();
        match prefixes.as_slice() {
            [] => anyhow::bail!(
                "configuration `{}` is not registered in schema",
                metadata.ty.name_in_code()
            ),
            &[prefix] => Ok(ConfigRef {
                schema: self,
                prefix,
                data: &self.configs[prefix].inner[&metadata.ty.id()],
            }),
            [first, second] => anyhow::bail!(
                "configuration `{}` is registered in at least 2 locations: {first:?}, {second:?}",
                metadata.ty.name_in_code()
            ),
            _ => unreachable!(),
        }
    }
    #[allow(clippy::missing_panics_doc)] pub fn single_mut(
        &mut self,
        metadata: &'static ConfigMetadata,
    ) -> anyhow::Result<ConfigMut<'_>> {
        let mut it = self.locate(metadata);
        let first_prefix = it.next().with_context(|| {
            format!(
                "configuration `{}` is not registered in schema",
                metadata.ty.name_in_code()
            )
        })?;
        if let Some(second_prefix) = it.next() {
            anyhow::bail!(
                "configuration `{}` is registered in at least 2 locations: {first_prefix:?}, {second_prefix:?}",
                metadata.ty.name_in_code()
            );
        }
        drop(it);
        let prefix = first_prefix.to_owned();
        Ok(ConfigMut {
            schema: self,
            type_id: metadata.ty.id(),
            prefix,
        })
    }
    pub fn insert(
        &mut self,
        metadata: &'static ConfigMetadata,
        prefix: &'static str,
    ) -> anyhow::Result<ConfigMut<'_>> {
        let coerce_serde_enums = self.coerce_serde_enums;
        let mut patched = PatchedSchema::new(self);
        patched.insert_config(prefix, metadata, coerce_serde_enums)?;
        patched.commit();
        Ok(ConfigMut {
            schema: self,
            type_id: metadata.ty.id(),
            prefix: prefix.to_owned(),
        })
    }
}
#[derive(Debug)]
#[must_use = "Should be `commit()`ted"]
struct PatchedSchema<'a> {
    base: &'a mut ConfigSchema,
    patch: ConfigSchema,
}
impl<'a> PatchedSchema<'a> {
    fn new(base: &'a mut ConfigSchema) -> Self {
        Self {
            base,
            patch: ConfigSchema::default(),
        }
    }
    fn mount(&self, path: &str) -> Option<&MountingPoint> {
        self.patch
            .mounting_points
            .get(path)
            .or_else(|| self.base.mounting_points.get(path))
    }
    fn insert_config(
        &mut self,
        prefix: &'static str,
        metadata: &'static ConfigMetadata,
        coerce_serde_enums: bool,
    ) -> anyhow::Result<()> {
        self.insert_recursively(
            prefix.into(),
            true,
            ConfigData {
                metadata,
                parent_link: None,
                is_top_level: true,
                coerce_serde_enums,
                all_paths: vec![(prefix.into(), AliasOptions::new())],
            },
        )
    }
    fn insert_recursively(
        &mut self,
        prefix: Cow<'static, str>,
        is_new: bool,
        data: ConfigData,
    ) -> anyhow::Result<()> {
        let depth = is_new.then_some(0_usize);
        let mut pending_configs = vec![(prefix, data, depth)];
        while let Some((prefix, data, depth)) = pending_configs.pop() {
            if is_new && self.base.get_ll(&prefix, data.metadata.ty.id()).is_some() {
                continue;
            }
            let child_depth = depth.map(|d| d + 1);
            let new_configs = Self::list_nested_configs(Pointer(&prefix), &data)
                .map(|(prefix, data)| (prefix.into(), data, child_depth));
            pending_configs.extend(new_configs);
            self.insert_inner(prefix, depth, data)?;
        }
        Ok(())
    }
    fn insert_alias(
        &mut self,
        prefix: String,
        config_id: any::TypeId,
        alias: Pointer<'static>,
        options: AliasOptions,
    ) -> anyhow::Result<()> {
        let config_data = &self.base.configs[prefix.as_str()].inner[&config_id];
        if config_data
            .all_paths
            .iter()
            .any(|(name, _)| name == alias.0)
        {
            return Ok(()); }
        let metadata = config_data.metadata;
        self.insert_recursively(
            prefix.into(),
            false,
            ConfigData {
                metadata,
                parent_link: config_data.parent_link,
                is_top_level: config_data.is_top_level,
                coerce_serde_enums: config_data.coerce_serde_enums,
                all_paths: vec![(alias.0.into(), options)],
            },
        )
    }
    fn list_nested_configs<'i>(
        prefix: Pointer<'i>,
        data: &'i ConfigData,
    ) -> impl Iterator<Item = (String, ConfigData)> + 'i {
        data.metadata.nested_configs.iter().map(move |nested| {
            let all_paths =
                data.all_paths_for_child(nested.name, nested.aliases, nested.tag_variant);
            let all_paths = all_paths
                .map(|(path, options)| (Cow::Owned(path), options))
                .collect();
            let config_data = ConfigData {
                metadata: nested.meta,
                parent_link: Some(ParentLink {
                    parent_ty: data.metadata.ty.id(),
                    this_ref: nested,
                }),
                is_top_level: false,
                coerce_serde_enums: data.coerce_serde_enums,
                all_paths,
            };
            (prefix.join(nested.name), config_data)
        })
    }
    fn insert_inner(
        &mut self,
        prefix: Cow<'static, str>,
        depth: Option<usize>,
        mut data: ConfigData,
    ) -> anyhow::Result<()> {
        let config_name = data.metadata.ty.name_in_code();
        let config_paths = data.all_paths.iter().map(|(name, _)| name.as_ref());
        let config_paths = iter::once(prefix.as_ref()).chain(config_paths);
        for path in config_paths {
            if let Some(mount) = self.mount(path) {
                match mount {
                    MountingPoint::Config => { }
                    MountingPoint::Param { .. } => {
                        anyhow::bail!(
                            "Cannot mount config `{}` at `{path}` because parameter(s) are already mounted at this path",
                            data.metadata.ty.name_in_code()
                        );
                    }
                }
            }
            self.patch
                .mounting_points
                .insert(path.to_owned(), MountingPoint::Config);
        }
        for param in data.metadata.params {
            let all_paths = data.all_paths_for_param(param);
            for (name_i, (full_name, _)) in all_paths.enumerate() {
                let mut was_canonical = false;
                if let Some(mount) = self.mount(&full_name) {
                    let prev_expecting = match mount {
                        MountingPoint::Param {
                            expecting,
                            is_canonical,
                        } => {
                            was_canonical = *is_canonical;
                            *expecting
                        }
                        MountingPoint::Config => {
                            anyhow::bail!(
                                "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
                                 config(s) are already mounted at this path",
                                name = param.name,
                                field = param.rust_field_name
                            );
                        }
                    };
                    if prev_expecting != param.expecting {
                        anyhow::bail!(
                            "Cannot insert param `{name}` [Rust field: `{field}`] from config `{config_name}` at `{full_name}`: \
                             it expects {expecting}, while the existing param(s) mounted at this path expect {prev_expecting}",
                            name = param.name,
                            field = param.rust_field_name,
                            expecting = param.expecting
                        );
                    }
                }
                let is_canonical = was_canonical || name_i == 0;
                self.patch.mounting_points.insert(
                    full_name,
                    MountingPoint::Param {
                        expecting: param.expecting,
                        is_canonical,
                    },
                );
            }
        }
        let config_id = data.metadata.ty.id();
        let prev_data = self.base.get_ll(&prefix, config_id);
        if let Some(prev_data) = prev_data {
            let mut all_paths = prev_data.all_paths.clone();
            all_paths.extend_from_slice(&data.all_paths);
            data.all_paths = all_paths;
        }
        self.patch
            .configs
            .entry(prefix)
            .or_default()
            .insert(config_id, depth, data);
        Ok(())
    }
    fn commit(self) {
        for (prefix, data) in self.patch.configs {
            let prev_data = self.base.configs.entry(prefix).or_default();
            prev_data.extend(data);
        }
        self.base.mounting_points.extend(self.patch.mounting_points);
    }
}