use crate::{Spanned, cfg::Span};
use semver::VersionReq;
use std::fmt;
use toml_span::{
    DeserError, Deserialize,
    de_helpers::{TableHelper, expected},
    value::{Value, ValueInner},
};

/// A package identifier, consisting of a package name and a version requirement
///
/// This is specified similarly to [Cargo Package Ids](https://doc.rust-lang.org/cargo/reference/pkgid-spec.html),
/// however we change semantics a bit to instead use a [`semver::VersionReq`] instead
/// of a [`semver::Version`] as Cargo's are meant for disambiguating graph operations
/// whereas ours may be targeting single or multiple packages. In practice this
/// is mainly just a superset of Cargo's version
#[derive(Clone, PartialEq, Eq)]
pub struct PackageSpec {
    pub name: Spanned<String>,
    pub version_req: Option<VersionReq>,
}

impl fmt::Display for PackageSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.name.value)?;

        if let Some(vr) = &self.version_req {
            write!(f, " = {vr}")?;
        }

        Ok(())
    }
}

impl fmt::Debug for PackageSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} = {:?}", self.name.value, self.version_req)
    }
}

impl<'de> Deserialize<'de> for PackageSpec {
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        use std::borrow::Cow;

        struct Ctx<'de> {
            inner: Cow<'de, str>,
            split: Option<(usize, bool)>,
            span: Span,
        }

        impl<'de> Ctx<'de> {
            fn from_str(bs: Cow<'de, str>, span: Span) -> Self {
                let split = bs
                    .find('@')
                    .map(|i| (i, true))
                    .or_else(|| bs.find(':').map(|i| (i, false)));
                Self {
                    inner: bs,
                    split,
                    span,
                }
            }
        }

        let ctx = match value.take() {
            ValueInner::String(s) => Ctx::from_str(s, value.span),
            ValueInner::Table(tab) => {
                let mut th = TableHelper::from((tab, value.span));

                if let Some(mut val) = th.table.remove("crate") {
                    let s = val.take_string(Some("a crate spec"))?;
                    th.finalize(Some(value))?;

                    Ctx::from_str(s, val.span)
                } else {
                    // Encourage user to use the 'crate' spec instead
                    let name = th.required("name").map_err(|e| {
                        if matches!(e.kind, toml_span::ErrorKind::MissingField(_)) {
                            (toml_span::ErrorKind::MissingField("crate"), e.span).into()
                        } else {
                            e
                        }
                    })?;
                    let version = th.optional::<Spanned<Cow<'_, str>>>("version");

                    th.finalize(Some(value))?;

                    let version_req = if let Some(vr) = version {
                        Some(vr.value.parse().map_err(|e: semver::Error| {
                            toml_span::Error::from((
                                toml_span::ErrorKind::Custom(e.to_string().into()),
                                vr.span,
                            ))
                        })?)
                    } else {
                        None
                    };

                    return Ok(Self { name, version_req });
                }
            }
            other => return Err(expected("a string or table", other, value.span).into()),
        };

        let (name, version_req) = if let Some((i, make_exact)) = ctx.split {
            let mut v: VersionReq = ctx.inner[i + 1..].parse().map_err(|e: semver::Error| {
                toml_span::Error::from((
                    toml_span::ErrorKind::Custom(e.to_string().into()),
                    Span::new(ctx.span.start + i + 1, ctx.span.end),
                ))
            })?;
            if make_exact && let Some(comp) = v.comparators.get_mut(0) {
                comp.op = semver::Op::Exact;
            }

            (Spanned::with_span(ctx.inner[..i].into(), ctx.span), Some(v))
        } else {
            (Spanned::with_span(ctx.inner.into(), ctx.span), None)
        };

        Ok(Self { name, version_req })
    }
}

#[cfg(test)]
impl serde::Serialize for PackageSpec {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        use serde::ser::SerializeMap;
        let mut map = serializer.serialize_map(Some(3))?;
        map.serialize_entry("name", &self.name.value)?;
        map.serialize_entry("version-req", &self.version_req)?;
        map.end()
    }
}

use std::cmp::Ordering;

impl Ord for PackageSpec {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.name.value.cmp(&other.name.value) {
            Ordering::Equal => match (&self.version_req, &other.version_req) {
                (None, None) => Ordering::Equal,
                (Some(_), None) => Ordering::Less,
                (None, Some(_)) => Ordering::Greater,
                (Some(a), Some(b)) => {
                    if a == b {
                        Ordering::Equal
                    } else {
                        match a.comparators.len().cmp(&b.comparators.len()) {
                            Ordering::Equal => {
                                #[derive(PartialOrd, PartialEq, Ord, Eq)]
                                enum Op {
                                    Exact,
                                    Greater,
                                    GreaterEq,
                                    Less,
                                    LessEq,
                                    Tilde,
                                    Caret,
                                    Wildcard,
                                }

                                #[allow(clippy::fallible_impl_from)]
                                impl From<semver::Op> for Op {
                                    fn from(op: semver::Op) -> Self {
                                        match op {
                                            semver::Op::Exact => Self::Exact,
                                            semver::Op::Greater => Self::Greater,
                                            semver::Op::GreaterEq => Self::GreaterEq,
                                            semver::Op::Less => Self::Less,
                                            semver::Op::LessEq => Self::LessEq,
                                            semver::Op::Tilde => Self::Tilde,
                                            semver::Op::Caret => Self::Caret,
                                            semver::Op::Wildcard => Self::Wildcard,
                                            // I fucking despise non_exhaustive
                                            _ => panic!(
                                                "semver has added a new Op, but non_exhaustive means we can't detect that at compile time, so please open an issue so that the additional match arm can be added"
                                            ),
                                        }
                                    }
                                }

                                for (acmp, bcmp) in a.comparators.iter().zip(b.comparators.iter()) {
                                    match Op::from(acmp.op).cmp(&Op::from(bcmp.op)) {
                                        Ordering::Equal => {}
                                        o => return o,
                                    }

                                    match acmp.major.cmp(&bcmp.major) {
                                        Ordering::Equal => {}
                                        o => return o,
                                    }

                                    match acmp.minor.cmp(&bcmp.minor) {
                                        Ordering::Equal => {}
                                        o => return o,
                                    }

                                    match acmp.patch.cmp(&bcmp.patch) {
                                        Ordering::Equal => {}
                                        o => return o,
                                    }

                                    match acmp.pre.cmp(&bcmp.pre) {
                                        Ordering::Equal => {}
                                        o => return o,
                                    }
                                }

                                Ordering::Equal
                            }
                            o => o,
                        }
                    }
                }
            },
            o => o,
        }
    }
}

impl PartialOrd for PackageSpec {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

#[cfg_attr(test, derive(serde::Serialize))]
pub struct PackageSpecOrExtended<T> {
    pub spec: PackageSpec,
    pub inner: Option<T>,
}

impl<T> PackageSpecOrExtended<T> {
    pub fn try_convert<V, E>(self) -> Result<PackageSpecOrExtended<V>, E>
    where
        V: TryFrom<T, Error = E>,
    {
        let inner = if let Some(i) = self.inner {
            Some(V::try_from(i)?)
        } else {
            None
        };

        Ok(PackageSpecOrExtended {
            spec: self.spec,
            inner,
        })
    }

    pub fn convert<V>(self) -> PackageSpecOrExtended<V>
    where
        V: From<T>,
    {
        PackageSpecOrExtended {
            spec: self.spec,
            inner: self.inner.map(V::from),
        }
    }
}

impl<'de, T> toml_span::Deserialize<'de> for PackageSpecOrExtended<T>
where
    T: toml_span::Deserialize<'de>,
{
    fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
        let spec = PackageSpec::deserialize(value)?;

        // If more keys exist in the table (or string) then try to deserialize
        // the rest as the "extended" portion
        let inner = if value.has_keys() {
            Some(T::deserialize(value)?)
        } else {
            None
        };

        Ok(Self { spec, inner })
    }
}

impl<T> fmt::Debug for PackageSpecOrExtended<T>
where
    T: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PackageSpecOrExtended")
            .field("spec", &self.spec)
            .field("inner", &self.inner)
            .finish()
    }
}

impl<T> Clone for PackageSpecOrExtended<T>
where
    T: Clone,
{
    fn clone(&self) -> Self {
        Self {
            spec: self.spec.clone(),
            inner: self.inner.clone(),
        }
    }
}

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

impl<T> Eq for PackageSpecOrExtended<T> {}

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

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

#[cfg(test)]
#[allow(dead_code)]
mod test {
    use super::*;
    use crate::{cfg::ValidationContext, test_utils::ConfigData};

    #[test]
    fn deserializes_package_id() {
        struct Boop {
            data: Option<Spanned<u32>>,
        }

        impl<'de> Deserialize<'de> for Boop {
            fn deserialize(value: &mut toml_span::value::Value<'de>) -> Result<Self, DeserError> {
                let mut th = TableHelper::new(value)?;
                let data = th.optional_s("data");
                th.finalize(None)?;
                Ok(Self { data })
            }
        }

        #[derive(serde::Serialize)]
        struct ValidBoop {
            data: Option<Spanned<u32>>,
        }

        impl From<Boop> for ValidBoop {
            fn from(value: Boop) -> Self {
                Self { data: value.data }
            }
        }

        struct TestCfg {
            bare: PackageSpecOrExtended<Boop>,
            specific: PackageSpecOrExtended<Boop>,
            range: PackageSpecOrExtended<Boop>,
            mixed: Vec<PackageSpecOrExtended<Boop>>,
        }

        impl<'de> Deserialize<'de> for TestCfg {
            fn deserialize(value: &mut toml_span::value::Value<'de>) -> Result<Self, DeserError> {
                let mut th = TableHelper::new(value)?;
                let bare = th.required("bare")?;
                let specific = th.required("specific")?;
                let range = th.required("range")?;
                let mixed = th.required("mixed")?;
                th.finalize(None)?;

                Ok(Self {
                    bare,
                    specific,
                    range,
                    mixed,
                })
            }
        }

        #[derive(serde::Serialize)]
        struct ValidTestCfg {
            bare: PackageSpecOrExtended<ValidBoop>,
            specific: PackageSpecOrExtended<ValidBoop>,
            range: PackageSpecOrExtended<ValidBoop>,
            mixed: Vec<PackageSpecOrExtended<ValidBoop>>,
        }

        impl crate::cfg::UnvalidatedConfig for TestCfg {
            type ValidCfg = ValidTestCfg;

            fn validate(self, mut _ctx: ValidationContext<'_>) -> Self::ValidCfg {
                ValidTestCfg {
                    bare: self.bare.convert(),
                    specific: self.specific.convert(),
                    range: self.range.convert(),
                    mixed: self.mixed.into_iter().map(|m| m.convert()).collect(),
                }
            }
        }

        let cd = ConfigData::<TestCfg>::load("tests/cfg/package-specs.toml");
        let validated = cd.validate(|s| s);

        insta::assert_json_snapshot!(validated);
    }
}
