use crate::Db;
use crate::glob::{
    AbsolutePortableGlobPattern, PortableGlobError, PortableGlobKind, PortableGlobPattern,
};
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_macros::Combine;
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Deserializer};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::fmt;
use std::fmt::Formatter;
use std::hash::{Hash, Hasher};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use toml::Spanned;
use ty_combine::Combine;

#[derive(Clone, Debug, get_size2::GetSize)]
pub enum ValueSource {
    /// Value loaded from a project's configuration file.
    ///
    /// Ideally, we'd use [`ruff_db::files::File`] but we can't because the database hasn't been
    /// created when loading the configuration.
    File(Arc<SystemPathBuf>),

    /// The value comes from a CLI argument, while it's left open if specified using a short argument,
    /// long argument (`--extra-paths`) or `--config key=value`.
    Cli,

    /// The value comes from the user's editor,
    /// while it's left open if specified as a setting
    /// or if the value was auto-discovered by the editor
    /// (e.g., the Python environment)
    Editor,
}

impl ValueSource {
    pub fn file(&self) -> Option<&SystemPath> {
        match self {
            ValueSource::File(path) => Some(&**path),
            ValueSource::Cli => None,
            ValueSource::Editor => None,
        }
    }

    pub const fn is_cli(&self) -> bool {
        matches!(self, ValueSource::Cli)
    }
}

thread_local! {
    /// Serde doesn't provide any easy means to pass a value to a [`Deserialize`] implementation,
    /// but we want to associate each deserialized [`RelativePath`] with the source from
    /// which it originated. We use a thread local variable to work around this limitation.
    ///
    /// Use the [`ValueSourceGuard`] to initialize the thread local before calling into any
    /// deserialization code. It ensures that the thread local variable gets cleaned up
    /// once deserialization is done (once the guard gets dropped).
    static VALUE_SOURCE: RefCell<Option<(ValueSource, bool)>> = const { RefCell::new(None) };
}

/// Guard to safely change the [`VALUE_SOURCE`] for the current thread.
#[must_use]
pub(super) struct ValueSourceGuard {
    prev_value: Option<(ValueSource, bool)>,
}

impl ValueSourceGuard {
    pub(super) fn new(source: ValueSource, is_toml: bool) -> Self {
        let prev = VALUE_SOURCE.replace(Some((source, is_toml)));
        Self { prev_value: prev }
    }
}

impl Drop for ValueSourceGuard {
    fn drop(&mut self) {
        VALUE_SOURCE.set(self.prev_value.take());
    }
}

/// A value that "remembers" where it comes from (source) and its range in source.
///
/// ## Equality, Hash, and Ordering
/// The equality, hash, and ordering are solely based on the value. They disregard the value's range
/// or source.
///
/// This ensures that two resolved configurations are identical even if the position of a value has changed
/// or if the values were loaded from different sources.
#[derive(Clone, serde::Serialize, get_size2::GetSize)]
#[serde(transparent)]
pub struct RangedValue<T> {
    value: T,
    #[serde(skip)]
    source: ValueSource,

    /// The byte range of `value` in `source`.
    ///
    /// Can be `None` because not all sources support a range.
    /// For example, arguments provided on the CLI won't have a range attached.
    #[serde(skip)]
    range: Option<TextRange>,
}

#[cfg(feature = "schemars")]
impl<T> schemars::JsonSchema for RangedValue<T>
where
    T: schemars::JsonSchema,
{
    fn schema_name() -> std::borrow::Cow<'static, str> {
        T::schema_name()
    }

    fn schema_id() -> std::borrow::Cow<'static, str> {
        T::schema_id()
    }

    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
        T::json_schema(generator)
    }

    fn _schemars_private_non_optional_json_schema(
        generator: &mut schemars::SchemaGenerator,
    ) -> schemars::Schema {
        T::_schemars_private_non_optional_json_schema(generator)
    }

    fn _schemars_private_is_option() -> bool {
        T::_schemars_private_is_option()
    }
}

impl<T> RangedValue<T> {
    pub fn new(value: T, source: ValueSource) -> Self {
        Self::with_range(value, source, TextRange::default())
    }

    pub fn cli(value: T) -> Self {
        Self::with_range(value, ValueSource::Cli, TextRange::default())
    }

    pub fn python_extension(value: T) -> Self {
        Self::with_range(value, ValueSource::Editor, TextRange::default())
    }

    pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self {
        Self {
            value,
            range: Some(range),
            source,
        }
    }

    pub fn range(&self) -> Option<TextRange> {
        self.range
    }

    pub fn source(&self) -> &ValueSource {
        &self.source
    }

    #[must_use]
    pub fn with_source(mut self, source: ValueSource) -> Self {
        self.source = source;
        self
    }

    #[must_use]
    pub fn map_value<R>(self, f: impl FnOnce(T) -> R) -> RangedValue<R> {
        RangedValue {
            value: f(self.value),
            source: self.source,
            range: self.range,
        }
    }

    pub fn into_inner(self) -> T {
        self.value
    }
}

impl<T> Combine for RangedValue<T>
where
    T: Combine,
{
    fn combine_with(&mut self, other: Self) {
        self.value.combine_with(other.value);
    }
}

impl<T> IntoIterator for RangedValue<T>
where
    T: IntoIterator,
{
    type Item = T::Item;
    type IntoIter = T::IntoIter;
    fn into_iter(self) -> Self::IntoIter {
        self.value.into_iter()
    }
}

// The type already has an `iter` method thanks to `Deref`.
#[expect(clippy::into_iter_without_iter)]
impl<'a, T> IntoIterator for &'a RangedValue<T>
where
    &'a T: IntoIterator,
{
    type Item = <&'a T as IntoIterator>::Item;
    type IntoIter = <&'a T as IntoIterator>::IntoIter;
    fn into_iter(self) -> Self::IntoIter {
        self.value.into_iter()
    }
}

// The type already has a `into_iter_mut` method thanks to `DerefMut`.
#[expect(clippy::into_iter_without_iter)]
impl<'a, T> IntoIterator for &'a mut RangedValue<T>
where
    &'a mut T: IntoIterator,
{
    type Item = <&'a mut T as IntoIterator>::Item;
    type IntoIter = <&'a mut T as IntoIterator>::IntoIter;
    fn into_iter(self) -> Self::IntoIter {
        self.value.into_iter()
    }
}

impl<T> fmt::Debug for RangedValue<T>
where
    T: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.value.fmt(f)
    }
}

impl<T> fmt::Display for RangedValue<T>
where
    T: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.value.fmt(f)
    }
}

impl<T> Deref for RangedValue<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl<T> DerefMut for RangedValue<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.value
    }
}

impl<T, U: ?Sized> AsRef<U> for RangedValue<T>
where
    T: AsRef<U>,
{
    fn as_ref(&self) -> &U {
        self.value.as_ref()
    }
}

impl<T: PartialEq> PartialEq for RangedValue<T> {
    fn eq(&self, other: &Self) -> bool {
        self.value.eq(&other.value)
    }
}

impl<T: PartialEq<T>> PartialEq<T> for RangedValue<T> {
    fn eq(&self, other: &T) -> bool {
        self.value.eq(other)
    }
}

impl<T: Eq> Eq for RangedValue<T> {}

impl<T: Hash> Hash for RangedValue<T> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.value.hash(state);
    }
}

impl<T: PartialOrd> PartialOrd for RangedValue<T> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.value.partial_cmp(&other.value)
    }
}

impl<T: PartialOrd<T>> PartialOrd<T> for RangedValue<T> {
    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
        self.value.partial_cmp(other)
    }
}

impl<T: Ord> Ord for RangedValue<T> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.value.cmp(&other.value)
    }
}

impl<'de, T> Deserialize<'de> for RangedValue<T>
where
    T: Deserialize<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        VALUE_SOURCE.with_borrow(|source| {
            let (source, has_span) = source.clone().unwrap();

            if has_span {
                let spanned: Spanned<T> = Spanned::deserialize(deserializer)?;
                let span = spanned.span();
                let range = TextRange::new(
                    TextSize::try_from(span.start)
                        .expect("Configuration file to be smaller than 4GB"),
                    TextSize::try_from(span.end)
                        .expect("Configuration file to be smaller than 4GB"),
                );

                Ok(Self::with_range(spanned.into_inner(), source, range))
            } else {
                Ok(Self::new(T::deserialize(deserializer)?, source))
            }
        })
    }
}

/// A possibly relative path in a configuration file.
///
/// Relative paths in configuration files or from CLI options
/// require different anchoring:
///
/// * CLI: The path is relative to the current working directory
/// * Configuration file: The path is relative to the project's root.
#[derive(
    Debug,
    Clone,
    serde::Serialize,
    serde::Deserialize,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Combine,
    get_size2::GetSize,
)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RelativePathBuf(RangedValue<SystemPathBuf>);

impl RelativePathBuf {
    pub fn new(path: impl AsRef<SystemPath>, source: ValueSource) -> Self {
        Self(RangedValue::new(path.as_ref().to_path_buf(), source))
    }

    pub fn cli(path: impl AsRef<SystemPath>) -> Self {
        Self::new(path, ValueSource::Cli)
    }

    pub fn python_extension(path: impl AsRef<SystemPath>) -> Self {
        Self::new(path, ValueSource::Editor)
    }

    /// Returns the relative path as specified by the user.
    pub fn path(&self) -> &SystemPath {
        &self.0
    }

    pub fn source(&self) -> &ValueSource {
        self.0.source()
    }

    pub fn range(&self) -> Option<TextRange> {
        self.0.range()
    }

    /// Returns the owned relative path.
    pub fn into_path_buf(self) -> SystemPathBuf {
        self.0.into_inner()
    }

    /// Resolves the absolute path for `self` based on its origin.
    pub fn absolute_with_db(&self, db: &dyn Db) -> SystemPathBuf {
        self.absolute(db.project().root(db), db.system())
    }

    /// Resolves the absolute path for `self` based on its origin.
    pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf {
        let relative_to = match &self.0.source {
            ValueSource::File(_) => project_root,
            ValueSource::Cli | ValueSource::Editor => system.current_directory(),
        };

        // Expand tildes and environment variables in the path (e.g. `~/.cache/foo`).
        // Use `full_with_context` to route lookups through the `System` trait,
        // ensuring correct behavior in tests, WASM, and the LSP.
        let expanded = shellexpand::full_with_context(
            self.0.as_str(),
            || system.env_var("HOME").ok(),
            |var| match system.env_var(var) {
                Ok(val) => Ok(Some(val)),
                Err(std::env::VarError::NotPresent) => Ok(None),
                Err(e) => Err(e),
            },
        );

        match &expanded {
            Ok(path) => SystemPath::absolute(SystemPath::new(path.as_ref()), relative_to),
            Err(err) => {
                tracing::warn!(
                    "Failed to expand variables in path `{}`: {err}",
                    self.0.as_str()
                );
                SystemPath::absolute(&self.0, relative_to)
            }
        }
    }
}

impl fmt::Display for RelativePathBuf {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

#[derive(
    Debug,
    Clone,
    serde::Serialize,
    serde::Deserialize,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Combine,
    get_size2::GetSize,
)]
#[serde(transparent)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RelativeGlobPattern(RangedValue<String>);

impl RelativeGlobPattern {
    pub fn new(pattern: impl AsRef<str>, source: ValueSource) -> Self {
        Self(RangedValue::new(pattern.as_ref().to_string(), source))
    }

    pub fn cli(pattern: impl AsRef<str>) -> Self {
        Self::new(pattern, ValueSource::Cli)
    }

    /// Resolves the absolute pattern for `self` based on its origin.
    pub(crate) fn absolute(
        &self,
        project_root: &SystemPath,
        system: &dyn System,
        kind: PortableGlobKind,
    ) -> Result<AbsolutePortableGlobPattern, PortableGlobError> {
        let relative_to = match &self.0.source {
            ValueSource::File(_) => project_root,
            ValueSource::Cli | ValueSource::Editor => system.current_directory(),
        };

        let pattern = PortableGlobPattern::parse(&self.0, kind)?;
        Ok(pattern.into_absolute(relative_to))
    }

    pub(crate) fn value(&self) -> &RangedValue<String> {
        &self.0
    }
}

impl std::fmt::Display for RelativeGlobPattern {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use ruff_db::system::{SystemPath, SystemPathBuf, TestSystem};

    use super::{RelativePathBuf, ValueSource};

    #[test]
    fn tilde_expansion_uses_system_env() {
        let system = TestSystem::default();
        system.set_env_var("HOME", "/test/home");

        let path = RelativePathBuf::new(
            "~/projects",
            ValueSource::File(Arc::new(SystemPathBuf::from("/project"))),
        );
        let resolved = path.absolute(SystemPath::new("/project"), &system);

        assert_eq!(resolved, SystemPathBuf::from("/test/home/projects"));
    }

    #[test]
    fn env_var_expansion_uses_system_env() {
        let system = TestSystem::default();
        system.set_env_var("MY_DIR", "/custom/dir");

        let path = RelativePathBuf::new(
            "$MY_DIR/sub",
            ValueSource::File(Arc::new(SystemPathBuf::from("/project"))),
        );
        let resolved = path.absolute(SystemPath::new("/project"), &system);

        assert_eq!(resolved, SystemPathBuf::from("/custom/dir/sub"));
    }

    #[test]
    fn undefined_env_var_falls_back_to_literal() {
        let system = TestSystem::default();

        let path = RelativePathBuf::new(
            "$NONEXISTENT/foo",
            ValueSource::File(Arc::new(SystemPathBuf::from("/project"))),
        );
        let resolved = path.absolute(SystemPath::new("/project"), &system);

        // When the variable is not found, the original path is used as a fallback.
        assert_eq!(resolved, SystemPathBuf::from("/project/$NONEXISTENT/foo"));
    }

    #[test]
    fn no_expansion_needed() {
        let system = TestSystem::default();

        let path = RelativePathBuf::new(
            "src/lib.rs",
            ValueSource::File(Arc::new(SystemPathBuf::from("/project"))),
        );
        let resolved = path.absolute(SystemPath::new("/project"), &system);

        assert_eq!(resolved, SystemPathBuf::from("/project/src/lib.rs"));
    }

    #[test]
    fn cli_source_resolves_relative_to_cwd() {
        let system = TestSystem::default();
        system.set_env_var("HOME", "/test/home");

        let path = RelativePathBuf::cli("~/config");
        let resolved = path.absolute(SystemPath::new("/project"), &system);

        assert_eq!(resolved, SystemPathBuf::from("/test/home/config"));
    }
}
