use std::{collections::BTreeMap, fmt};

use colored::Colorize;
use serde::{
    Deserialize, Serialize,
    de::{Deserializer, Error as SerdeDeError},
};

pub(crate) fn count_statuses<'a>(
    statuses: impl Iterator<Item = &'a Status>,
) -> BTreeMap<String, u64> {
    let mut counts = BTreeMap::<String, u64>::new();
    for status in statuses {
        let key = String::from(status);

        let count = counts.entry(key).or_insert(0);
        *count += 1;
    }
    counts
}

/// Satisfies the [`needs`](Metadata::needs) of other jobs
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Satisfying {
    Changed(String, String),
    NoChange(String),
}
impl Default for Satisfying {
    fn default() -> Self {
        Satisfying::Changed(String::from("unknown"), String::from("unknown"))
    }
}
impl fmt::Display for Satisfying {
    // TODO: should Display include terminal output concerns?
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Changed(from, to) => write!(
                f,
                "{}: {} => {}",
                "changed".yellow(),
                from.yellow().dimmed(),
                to.yellow()
            ),
            Self::NoChange(s) => write!(f, "{}: {}", "nochange".green(), s.green()),
        }
    }
}
impl From<&Satisfying> for String {
    fn from(source: &Satisfying) -> Self {
        Self::from(match source {
            Satisfying::Changed(_, _) => "changed",
            Satisfying::NoChange(_) => "nochange",
        })
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Status {
    /// Runner is executing this job.
    /// Possible next states:
    /// [`Satisfying`](Status::Satisfying), [`Unsatisfying`](Status::Unsatisfying).
    InProgress,
    /// Job has no [`needs`](Metadata::needs) or all of them are [`Satisfying`](Status::Satisfying).
    /// Possible next states: [`InProgress`](Status::InProgress).
    Pending,
    /// Settled and satisfies the [`needs`](Metadata::needs) of other jobs
    Satisfying(Satisfying),
    /// Settled and does not satisfy the [`needs`](Metadata::needs) of other jobs
    Unsatisfying(Unsatisfying),
    /// Job has [`needs`](Metadata::needs) that are not yet [`Satisfying`](Status::Satisfying).
    /// Possible next states: [`Unsatisfying`](Status::Unsatisfying), [`Pending`](Status::Pending).
    Waiting,
}
impl fmt::Display for Status {
    // TODO: should Display include terminal output concerns?
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::InProgress => write!(f, "{}", "inprogress".cyan()),
            Self::Pending => write!(f, "{}", "pending".white()),
            Self::Satisfying(s) => write!(f, "{s}"),
            Self::Unsatisfying(u) => write!(f, "{u}"),
            Self::Waiting => write!(f, "{}", "waiting".white()),
        }
    }
}
impl From<&Status> for String {
    fn from(source: &Status) -> Self {
        match source {
            Status::InProgress => Self::from("inprogress"),
            Status::Pending => Self::from("pending"),
            Status::Satisfying(s) => Self::from(s),
            Status::Unsatisfying(u) => Self::from(u),
            Status::Waiting => Self::from("waiting"),
        }
    }
}
impl<'de> Deserialize<'de> for Status {
    // TODO: find a way to tell serde how to deal with enum variants that contain enum variants
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = match toml::value::Value::deserialize(deserializer)? {
            toml::Value::String(s) => match s.as_str() {
                "blocked" => Status::Unsatisfying(Unsatisfying::Blocked),
                "changed" => Status::Satisfying(Satisfying::Changed(String::new(), String::new())),
                "error" => Status::Unsatisfying(Unsatisfying::Error),
                "inprogress" => Status::InProgress,
                "nochange" => Status::Satisfying(Satisfying::NoChange(String::new())),
                "pending" => Status::Pending,
                "skipped" => Status::Unsatisfying(Unsatisfying::Skipped),
                "waiting" => Status::Waiting,
                _ => {
                    return Err(SerdeDeError::custom("invalid status"));
                }
            },
            _ => {
                return Err(SerdeDeError::custom(
                    "must provide a string to convert to status",
                ));
            }
        };

        Ok(value)
    }
}
impl Serialize for Status {
    // TODO: find a way to tell serde how to deal with enum variants that contain enum variants
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(String::from(self).as_str())
    }
}
impl Status {
    pub fn changed(before: String, after: String) -> Self {
        if before == after {
            Self::no_change(before)
        } else {
            Self::Satisfying(Satisfying::Changed(before, after))
        }
    }

    /// Returns `true` if the job is successful and should no longer block dependant jobs.
    pub fn is_satisfying(&self) -> bool {
        matches!(&self, Self::Satisfying(_))
    }

    /// Returns `true` if the job has reached a terminal state and will never change state again.
    #[cfg(test)]
    pub fn is_settled(&self) -> bool {
        matches!(&self, Self::Satisfying(_) | Self::Unsatisfying(_))
    }

    pub fn no_change(before: String) -> Self {
        Self::Satisfying(Satisfying::NoChange(before))
    }
}

/// Does not satisfy the [`needs`](Metadata::needs) of other jobs
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Unsatisfying {
    /// Job has [`needs`](Metadata::needs) that can never reach [`Done`](Status::Done).
    Blocked,
    /// Job encountered an error.
    Error,
    /// Job has a [`when`](Metadata::when) that evaluated to `false`.
    Skipped,
}
impl fmt::Display for Unsatisfying {
    // TODO: should Display include terminal output concerns?
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Blocked => write!(f, "{}", "blocked".red().dimmed()),
            Self::Error => write!(f, "{}", "error".red()),
            Self::Skipped => write!(f, "{}", "skipped".blue()),
        }
    }
}
impl From<&Unsatisfying> for String {
    fn from(source: &Unsatisfying) -> Self {
        Self::from(match source {
            Unsatisfying::Blocked => "blocked",
            Unsatisfying::Error => "error",
            Unsatisfying::Skipped => "skipped",
        })
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use super::*;

    #[test]
    fn from_status_for_string() {
        let input = Status::Satisfying(Satisfying::NoChange(String::from("something")));

        assert_eq!(String::from(&input), "nochange");
    }

    #[test]
    fn from_toml() {
        let input = r#"blocked = "blocked"
changed = "changed"
error = "error"
inprogress = "inprogress"
nochange = "nochange"
pending = "pending"
skipped = "skipped"
waiting = "waiting"
"#;
        let want: BTreeMap<String, Status> = BTreeMap::from([
            (
                String::from("blocked"),
                Status::Unsatisfying(Unsatisfying::Blocked),
            ),
            (
                String::from("changed"),
                Status::Satisfying(Satisfying::Changed(String::new(), String::new())),
            ),
            (
                String::from("error"),
                Status::Unsatisfying(Unsatisfying::Error),
            ),
            (String::from("inprogress"), Status::InProgress),
            (
                String::from("nochange"),
                Status::Satisfying(Satisfying::NoChange(String::new())),
            ),
            (String::from("pending"), Status::Pending),
            (
                String::from("skipped"),
                Status::Unsatisfying(Unsatisfying::Skipped),
            ),
            (String::from("waiting"), Status::Waiting),
        ]);

        let got = toml::from_str::<BTreeMap<String, Status>>(input).expect("unable to deserialize");

        assert_eq!(got, want);
    }

    #[test]
    fn into_toml() {
        let input: BTreeMap<&str, Status> = BTreeMap::from([
            ("blocked", Status::Unsatisfying(Unsatisfying::Blocked)),
            (
                "changed",
                Status::Satisfying(Satisfying::Changed(String::new(), String::new())),
            ),
            ("error", Status::Unsatisfying(Unsatisfying::Error)),
            ("inprogress", Status::InProgress),
            (
                "nochange",
                Status::Satisfying(Satisfying::NoChange(String::new())),
            ),
            ("pending", Status::Pending),
            ("skipped", Status::Unsatisfying(Unsatisfying::Skipped)),
            ("waiting", Status::Waiting),
        ]);
        let want = r#"blocked = "blocked"
changed = "changed"
error = "error"
inprogress = "inprogress"
nochange = "nochange"
pending = "pending"
skipped = "skipped"
waiting = "waiting"
"#;

        let got = toml::to_string(&input).expect("unable to serialize");

        assert_eq!(got, want);
    }
}
