use crate::dirs;
use crate::task::Task;
use crate::tera::{TeraEngine, contains_template_syntax, render_str};
use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Deserialize, Deserializer, Serialize};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::Path;

#[derive(Debug, Clone, Eq, PartialEq, strum::EnumIs)]
pub enum TaskOutputs {
    Files(Vec<String>),
    NoFiles,
    Auto,
}

/// Stores raw (pre-render) output templates and the original env context so they
/// can be re-rendered when dependency env overrides are applied after initial rendering.
#[derive(Debug, Clone, Default)]
pub struct RawOutputTemplates {
    pub templates: Option<Vec<String>>,
    pub original_env: Option<std::collections::BTreeMap<String, String>>,
}

impl Default for TaskOutputs {
    fn default() -> Self {
        TaskOutputs::Files(vec![])
    }
}

impl TaskOutputs {
    pub fn is_empty(&self) -> bool {
        match self {
            TaskOutputs::Files(files) => files.is_empty(),
            TaskOutputs::NoFiles | TaskOutputs::Auto => false,
        }
    }

    pub fn patterns(&self) -> Vec<String> {
        match self {
            TaskOutputs::Files(files) => files.clone(),
            TaskOutputs::NoFiles | TaskOutputs::Auto => vec![],
        }
    }

    pub fn paths(&self, task: &Task, root: &Path) -> Vec<String> {
        match self {
            TaskOutputs::Files(files) => files.clone(),
            TaskOutputs::NoFiles => vec![],
            TaskOutputs::Auto => vec![self.auto_path(task, root)],
        }
    }

    pub fn has_tera_template(&self) -> bool {
        match self {
            TaskOutputs::Files(files) => files.iter().any(|file| contains_template_syntax(file)),
            TaskOutputs::NoFiles | TaskOutputs::Auto => false,
        }
    }

    pub fn raw_templates_without_env(&self) -> RawOutputTemplates {
        match self {
            TaskOutputs::Files(files) => RawOutputTemplates {
                templates: Some(files.clone()),
                original_env: None,
            },
            TaskOutputs::NoFiles | TaskOutputs::Auto => RawOutputTemplates::default(),
        }
    }

    fn auto_path(&self, task: &Task, root: &Path) -> String {
        let mut hasher = DefaultHasher::new();
        task.hash(&mut hasher);
        task.config_sources().hash(&mut hasher);
        root.hash(&mut hasher);
        let hash = format!("{:x}", hasher.finish());
        dirs::STATE
            .join("task-auto-outputs")
            .join(&hash)
            .to_string_lossy()
            .to_string()
    }

    pub fn render(
        &mut self,
        tera: &mut TeraEngine,
        ctx: &tera::Context,
    ) -> eyre::Result<RawOutputTemplates> {
        match self {
            TaskOutputs::Files(files) => {
                let raw = files.clone();
                let original_env = ctx
                    .get("env")
                    .and_then(|v| serde::Deserialize::deserialize(v.clone()).ok());
                for file in files.iter_mut() {
                    if contains_template_syntax(file) {
                        *file = render_str(tera, file, ctx)?;
                    }
                }
                Ok(RawOutputTemplates {
                    templates: Some(raw),
                    original_env,
                })
            }
            TaskOutputs::NoFiles | TaskOutputs::Auto => Ok(RawOutputTemplates::default()),
        }
    }

    /// Re-render output templates with additional env vars injected into the
    /// tera context. Used after dependency env overrides are applied.
    pub fn re_render_with_env(
        &mut self,
        raw: &RawOutputTemplates,
        env: &indexmap::IndexMap<String, String>,
        config_root: &std::path::Path,
    ) -> eyre::Result<()> {
        if let TaskOutputs::Files(files) = self
            && let Some(raw_templates) = raw.templates.as_ref()
        {
            if raw_templates
                .iter()
                .any(|tmpl| contains_template_syntax(tmpl))
            {
                let mut tera = crate::tera::get_tera(Some(config_root));
                let mut ctx = tera::Context::new();
                // Start with original env from initial render, then overlay dependency env
                let mut env_map = raw.original_env.clone().unwrap_or_default();
                for (k, v) in env {
                    env_map.insert(k.clone(), v.clone());
                }
                ctx.insert("env", &env_map);
                ctx.insert("config_root", &config_root.to_string_lossy().to_string());
                *files = raw_templates
                    .iter()
                    .map(|tmpl| {
                        if contains_template_syntax(tmpl) {
                            render_str(&mut tera, tmpl, &ctx)
                        } else {
                            Ok(tmpl.clone())
                        }
                    })
                    .collect::<Result<Vec<_>, _>>()?;
            } else {
                *files = raw_templates.clone();
            }
        }
        Ok(())
    }
}

impl From<&toml::Value> for TaskOutputs {
    fn from(value: &toml::Value) -> Self {
        match value {
            toml::Value::String(file) => TaskOutputs::Files(vec![file.to_string()]),
            toml::Value::Array(files) if files.is_empty() => TaskOutputs::NoFiles,
            toml::Value::Array(files) => TaskOutputs::Files(
                files
                    .iter()
                    .map(|v| v.as_str().unwrap().to_string())
                    .collect(),
            ),
            toml::Value::Table(table) => {
                let auto = table
                    .get("auto")
                    .and_then(|v| v.as_bool())
                    .unwrap_or_default();
                if auto {
                    TaskOutputs::Auto
                } else {
                    TaskOutputs::default()
                }
            }
            _ => TaskOutputs::default(),
        }
    }
}

impl<'de> Deserialize<'de> for TaskOutputs {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        struct TaskOutputsVisitor;

        impl<'de> serde::de::Visitor<'de> for TaskOutputsVisitor {
            type Value = TaskOutputs;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a string, a sequence of strings, or a map")
            }

            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
                Ok(TaskOutputs::Files(vec![value.to_string()]))
            }

            fn visit_seq<A: serde::de::SeqAccess<'de>>(
                self,
                mut seq: A,
            ) -> Result<Self::Value, A::Error> {
                let mut files = vec![];
                while let Some(file) = seq.next_element()? {
                    files.push(file);
                }
                if files.is_empty() {
                    Ok(TaskOutputs::NoFiles)
                } else {
                    Ok(TaskOutputs::Files(files))
                }
            }

            fn visit_map<A: serde::de::MapAccess<'de>>(
                self,
                mut map: A,
            ) -> Result<Self::Value, A::Error> {
                if let Some(key) = map.next_key::<String>()? {
                    if key == "auto" {
                        if map.next_value::<bool>()? {
                            Ok(TaskOutputs::Auto)
                        } else {
                            Ok(TaskOutputs::default())
                        }
                    } else {
                        Err(serde::de::Error::custom("Invalid TaskOutputs map"))
                    }
                } else {
                    Ok(TaskOutputs::default())
                }
            }
        }

        deserializer.deserialize_any(TaskOutputsVisitor)
    }
}

impl Serialize for TaskOutputs {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self {
            TaskOutputs::Files(files) => {
                let mut seq = serializer.serialize_seq(Some(files.len()))?;
                for file in files {
                    seq.serialize_element(file)?;
                }
                seq.end()
            }
            TaskOutputs::NoFiles => serializer.serialize_seq(Some(0))?.end(),
            TaskOutputs::Auto => {
                let mut m = serializer.serialize_map(Some(1))?;
                m.serialize_entry("auto", &true)?;
                m.end()
            }
        }
    }
}

mod tests {
    #[allow(unused_imports)]
    use super::*;

    #[test]
    fn test_task_outputs_from_toml() {
        let value: toml::Table = toml::from_str("outputs = \"file1\"").unwrap();
        let value = value.get("outputs").unwrap();
        let outputs = TaskOutputs::from(value);
        assert_eq!(outputs, TaskOutputs::Files(vec!["file1".to_string()]));

        let value: toml::Table = toml::from_str("outputs = [\"file1\"]").unwrap();
        let value = value.get("outputs").unwrap();
        let outputs = TaskOutputs::from(value);
        assert_eq!(outputs, TaskOutputs::Files(vec!["file1".to_string()]));

        let value: toml::Table = toml::from_str("outputs = []").unwrap();
        let value = value.get("outputs").unwrap();
        let outputs = TaskOutputs::from(value);
        assert_eq!(outputs, TaskOutputs::NoFiles);

        let value: toml::Table = toml::from_str("outputs = { auto = true }").unwrap();
        let value = value.get("outputs").unwrap();
        let outputs = TaskOutputs::from(value);
        assert_eq!(outputs, TaskOutputs::Auto);
    }

    #[test]
    fn test_task_outputs_serialize() {
        let outputs = TaskOutputs::Files(vec!["file1".to_string()]);
        let serialized = serde_json::to_string(&outputs).unwrap();
        assert_eq!(serialized, "[\"file1\"]");

        let outputs = TaskOutputs::Auto;
        let serialized = serde_json::to_string(&outputs).unwrap();
        assert_eq!(serialized, "{\"auto\":true}");

        let outputs = TaskOutputs::NoFiles;
        let serialized = serde_json::to_string(&outputs).unwrap();
        assert_eq!(serialized, "[]");
    }

    #[test]
    fn test_task_outputs_deserialize() {
        let deserialized: TaskOutputs = serde_json::from_str("\"file1\"").unwrap();
        assert_eq!(deserialized, TaskOutputs::Files(vec!["file1".to_string()]));

        let deserialized: TaskOutputs = serde_json::from_str("[\"file1\"]").unwrap();
        assert_eq!(deserialized, TaskOutputs::Files(vec!["file1".to_string()]));

        let deserialized: TaskOutputs = serde_json::from_str("[]").unwrap();
        assert_eq!(deserialized, TaskOutputs::NoFiles);

        let deserialized: TaskOutputs = serde_json::from_str("{ \"auto\": true }").unwrap();
        assert_eq!(deserialized, TaskOutputs::Auto);
    }
}
