use std::{
    collections::{BTreeMap, BTreeSet},
    convert::Infallible,
    num::NonZeroUsize,
    path::{Path, PathBuf},
    str::FromStr,
};

use serde::{de::Error, Deserialize, Deserializer, Serialize};
use strum_macros::{Display, EnumIter};

use crate::{
    enums::{unit_enum_deserialize, unit_enum_from_str},
    error::Result,
    util::normalize_path,
};

#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TargetCache {
    #[serde(default)]
    invalidate_when: InvalidationStrategy,
}

impl TargetCache {
    pub fn invalidate_when(&self) -> &InvalidationStrategy {
        &self.invalidate_when
    }
}

#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InvalidationStrategy {
    #[serde(skip_serializing_if = "Option::is_none")]
    input_changes: Option<BTreeSet<FileChangesMatcher>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    output_changes: Option<BTreeSet<FileChangesMatcher>>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_files_missing"
    )]
    files_missing: Option<BTreeSet<PathBuf>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    expired: Option<TtlOptions>,
    #[serde(skip_serializing_if = "Option::is_none")]
    command_fails: Option<CommandFailsOptions>,
    #[serde(skip_serializing_if = "Option::is_none")]
    env_changes: Option<EnvChangesOptions>,
}

impl InvalidationStrategy {
    pub fn output_changes(&self) -> Option<&BTreeSet<FileChangesMatcher>> {
        self.output_changes.as_ref()
    }

    pub fn input_changes(&self) -> Option<&BTreeSet<FileChangesMatcher>> {
        self.input_changes.as_ref()
    }

    pub fn files_missing(&self) -> Option<&BTreeSet<PathBuf>> {
        self.files_missing.as_ref()
    }

    pub fn expired(&self) -> Option<&TtlOptions> {
        self.expired.as_ref()
    }

    pub fn command_fails(&self) -> Option<&CommandFailsOptions> {
        self.command_fails.as_ref()
    }

    pub fn env_changes(&self) -> Option<&EnvChangesOptions> {
        self.env_changes.as_ref()
    }
}

fn deserialize_files_missing<'de, D>(
    deserializer: D,
) -> std::result::Result<Option<BTreeSet<PathBuf>>, D::Error>
where
    D: Deserializer<'de>,
{
    Ok(Some(
        BTreeSet::<PathBuf>::deserialize(deserializer)?
            .into_iter()
            .map(normalize_path)
            .collect::<Result<_>>()
            .map_err(D::Error::custom)?,
    ))
}

#[derive(
    Default, Clone, Copy, Debug, Hash, EnumIter, PartialEq, Eq, PartialOrd, Ord, Display, Serialize,
)]
pub enum MatchingBehavior {
    Timestamps,
    #[default]
    Mixed,
    Hash,
}

unit_enum_from_str!(MatchingBehavior);
unit_enum_deserialize!(MatchingBehavior);

#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, PartialOrd, Ord)]
pub struct FileChangesMatcher {
    pattern: String,
    exclude: BTreeSet<String>,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    root: Option<PathBuf>,
    behavior: MatchingBehavior,
}

impl FileChangesMatcher {
    pub fn new(pattern: &str) -> Self {
        Self {
            pattern: pattern.to_owned(),
            exclude: BTreeSet::new(),
            root: None,
            behavior: MatchingBehavior::default(),
        }
    }

    pub fn with_exclude<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, patterns: I) -> Self {
        self.exclude = patterns
            .into_iter()
            .map(|s| s.as_ref().to_owned())
            .collect();
        self
    }

    pub fn with_root(mut self, root: &Path) -> Self {
        self.root = Some(root.to_owned());
        self
    }

    pub fn with_behavior(mut self, behavior: MatchingBehavior) -> Self {
        self.behavior = behavior;
        self
    }

    pub fn pattern(&self) -> &str {
        &self.pattern
    }

    pub fn exclude(&self) -> &BTreeSet<String> {
        &self.exclude
    }

    pub fn root(&self) -> Option<&Path> {
        self.root.as_deref()
    }

    pub fn behavior(&self) -> MatchingBehavior {
        self.behavior
    }
}

impl FromStr for FileChangesMatcher {
    type Err = Infallible;

    fn from_str(s: &str) -> std::result::Result<Self, Infallible> {
        Ok(Self {
            pattern: s.to_string(),
            behavior: MatchingBehavior::default(),
            exclude: BTreeSet::new(),
            root: None,
        })
    }
}

impl<'de> Deserialize<'de> for FileChangesMatcher {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(remote = "FileChangesMatcher")]
        struct FileChangesMatcherObject {
            pattern: String,
            #[serde(default)]
            exclude: BTreeSet<String>,
            root: Option<PathBuf>,
            #[serde(default)]
            behavior: MatchingBehavior,
        }

        #[derive(Deserialize)]
        #[serde(untagged)]
        enum FileChangesMatcherDeserializationMode {
            SinglePattern(String),
            #[serde(with = "FileChangesMatcherObject")]
            Full(FileChangesMatcher),
        }

        Ok(
            match FileChangesMatcherDeserializationMode::deserialize(deserializer)? {
                FileChangesMatcherDeserializationMode::SinglePattern(pattern) => {
                    FileChangesMatcher::from_str(pattern.as_str()).unwrap()
                }
                FileChangesMatcherDeserializationMode::Full(mut matcher) => {
                    if let Some(root) = &mut matcher.root {
                        *root = normalize_path(&root).map_err(D::Error::custom)?;
                    }
                    matcher
                }
            },
        )
    }
}

#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
pub struct CommandFailsOptions {
    program: String,
    #[serde(default)]
    arguments: Vec<String>,
    #[serde(default)]
    environment: BTreeMap<String, String>,
    #[serde(default)]
    verbose: bool,
    #[serde(deserialize_with = "deserialize_cwd", default)]
    cwd: Option<PathBuf>,
}

impl CommandFailsOptions {
    pub fn program(&self) -> &str {
        &self.program
    }

    pub fn arguments(&self) -> &[String] {
        &self.arguments
    }

    pub fn environment(&self) -> &BTreeMap<String, String> {
        &self.environment
    }

    pub fn verbose(&self) -> bool {
        self.verbose
    }

    pub fn cwd(&self) -> Option<&Path> {
        self.cwd.as_deref()
    }
}

fn deserialize_cwd<'de, D>(deserializer: D) -> std::result::Result<Option<PathBuf>, D::Error>
where
    D: Deserializer<'de>,
{
    Option::<PathBuf>::deserialize(deserializer)?
        .map(normalize_path)
        .transpose()
        .map_err(D::Error::custom)
}

#[derive(Debug, Clone, Copy, Hash, Display, Serialize, EnumIter)]
pub enum TimeUnit {
    Milliseconds,
    Seconds,
    Minutes,
    Hours,
    Days,
}

unit_enum_from_str!(TimeUnit);
unit_enum_deserialize!(TimeUnit);

#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
pub struct TtlOptions {
    unit: TimeUnit,
    amount: NonZeroUsize,
}

impl TtlOptions {
    pub fn unit(&self) -> TimeUnit {
        self.unit
    }

    pub fn amount(&self) -> usize {
        self.amount.get()
    }
}

#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
pub struct EnvChangesOptions {
    variables: BTreeSet<String>,
}

impl EnvChangesOptions {
    pub fn variables(&self) -> &BTreeSet<String> {
        &self.variables
    }
}
