use std::collections::HashMap;
use std::path::PathBuf;

use log::*;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::AppState;

/*
 * Representation of the Synchronik YAML format
 */
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Yml {
    pub needs: Vec<String>,
    pub commands: Vec<String>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Scm {
    /*
     * The Nonexistent Scm is used for stubbing out the Scm properties when
     * inlining configuration
     */
    Nonexistent,
    GitHub {
        owner: String,
        repo: String,
        #[serde(rename = "ref")]
        scm_ref: String,
    },
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub struct Project {
    description: String,
    /*
     * Used for optionally defining an inline Yml configuration
     */
    pub inline: Option<Yml>,
    pub filename: Option<String>,
    #[serde(default = "default_scm", with = "serde_yaml::with::singleton_map")]
    pub scm: Scm,
}

/*
 * Simple default scm for use when nothing has been otherwise defined
 */
fn default_scm() -> Scm {
    Scm::Nonexistent
}

/*
 * Internal representation of an Agent that has been "loaded" by the server
 *
 * Loaded meaning the server has pinged the agent and gotten necessary bootstrap
 * information
 */
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Agent {
    pub name: String,
    pub url: Url,
    pub capabilities: Vec<synchronik::Capability>,
}

impl Default for Agent {
    fn default() -> Self {
        Self {
            name: "default-agent".into(),
            url: Url::parse("http://example.com").unwrap(),
            capabilities: vec![],
        }
    }
}

impl Agent {
    pub fn new(name: String, url: Url, capabilities: Vec<synchronik::Capability>) -> Self {
        Self {
            name,
            url,
            capabilities,
        }
    }

    pub fn render_compact(&self, _state: &AppState<'_>) -> String {
        self.name.clone()
        //let data = serde_json::to_str(self).unwrap_or(serde_json::Value::Array);

        //state.render("views/components/agent/compact.hbs",
        //             data: data).await.unwrap_or("".into())
    }

    /*
     * Determine if this agent can meet the specified needs
     */
    pub fn can_meet(&self, needs: &Vec<String>) -> bool {
        let capabilities: Vec<String> = self
            .capabilities
            .iter()
            .map(|c| c.name.to_lowercase())
            .collect();

        for need in needs {
            if !capabilities.contains(&need) {
                return false;
            }
        }
        true
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AgentConfig {
    pub url: Url,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ServerConfig {
    pub agents: HashMap<String, AgentConfig>,
    pub projects: HashMap<String, Project>,
}

impl ServerConfig {
    pub fn has_project(&self, name: &str) -> bool {
        self.projects.contains_key(name)
    }

    /*
     * Load the ServerConfig from the given file.
     */
    fn from_filepath(path: &PathBuf) -> anyhow::Result<Self> {
        let config_file = std::fs::File::open(path).expect("Failed to open config file");
        serde_yaml::from_reader(config_file).map_err(anyhow::Error::from)
    }

    /*
     * Load the ServerConfig from an amalgamation of yaml in the given directory
     */
    fn from_dirpath(path: &PathBuf) -> anyhow::Result<Self> {
        use glob::glob;
        use std::fs::File;

        let pattern = format!("{}/**/*.yml", path.as_path().to_string_lossy());
        debug!("Loading config from directory with pattern: {}", pattern);

        let mut values: Vec<serde_yaml::Value> = vec![];

        for entry in glob(&pattern).expect("Failed to read glob pattern") {
            match entry {
                Ok(path) => {
                    if let Ok(file) = File::open(path) {
                        if let Ok(value) = serde_yaml::from_reader(file) {
                            values.push(value);
                        }
                    }
                }
                Err(e) => error!("Failed to read entry: {:?}", e),
            }
        }

        /*
         * At this point we should have enough partials to do a coercion to the ServerConfig
         * structure
         */
        let mut v = serde_yaml::Value::Null;
        for m in values.drain(0..) {
            merge_yaml(&mut v, m);
        }
        serde_yaml::from_value(v).map_err(anyhow::Error::from)
    }

    /*
     * Take the given path and do the necessary deserialization whether a file or a directory
     */
    pub fn from_path(path: &PathBuf) -> anyhow::Result<Self> {
        if !path.exists() {
            error!("The provided configuration path does not exist: {:?}", path);
            return Err(std::io::Error::from(std::io::ErrorKind::NotFound).into());
        }

        match path.is_file() {
            true => Self::from_filepath(&path),
            false => Self::from_dirpath(&path),
        }
    }
}

/*
 * Merge two Valus from <https://stackoverflow.com/a/67743348>
 */
fn merge_yaml(a: &mut serde_yaml::Value, b: serde_yaml::Value) {
    match (a, b) {
        (a @ &mut serde_yaml::Value::Mapping(_), serde_yaml::Value::Mapping(b)) => {
            let a = a.as_mapping_mut().unwrap();
            for (k, v) in b {
                if v.is_sequence() && a.contains_key(&k) && a[&k].is_sequence() {
                    let mut _b = a.get(&k).unwrap().as_sequence().unwrap().to_owned();
                    _b.append(&mut v.as_sequence().unwrap().to_owned());
                    a[&k] = serde_yaml::Value::from(_b);
                    continue;
                }
                if !a.contains_key(&k) {
                    a.insert(k.to_owned(), v.to_owned());
                } else {
                    merge_yaml(&mut a[&k], v);
                }
            }
        }
        (a, b) => *a = b,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;
    use synchronik::Capability;

    #[test]
    fn test_serverconfig_from_filepath() {
        let path = PathBuf::from("./examples/server.yml");
        let config = ServerConfig::from_path(&path);
        match config {
            Ok(config) => {
                assert_eq!(
                    config.agents.len(),
                    1,
                    "Unexpected number of agents: {:?}",
                    config.agents
                );
            }
            Err(e) => {
                assert!(false, "Failed to process ServerConfig: {:?}", e);
            }
        }
    }

    #[test]
    fn test_serverconfig_non0xistent() {
        let path = PathBuf::from("./non-existing/path/withstuff");
        let config = ServerConfig::from_path(&path);
        assert!(config.is_err());
    }

    #[test]
    fn test_serverconfig_from_filedir() {
        let path = PathBuf::from("./examples/synchronik.d");
        let config = ServerConfig::from_path(&path);
        match config {
            Ok(config) => {
                assert_eq!(
                    config.agents.len(),
                    1,
                    "Unexpected number of agents: {:?}",
                    config.agents
                );
                assert_eq!(
                    config.projects.len(),
                    2,
                    "Unexpected number of projects: {:?}",
                    config.projects
                );
            }
            Err(e) => {
                assert!(false, "Failed to process ServerConfig: {:?}", e);
            }
        }
    }

    #[test]
    fn parse_config_with_scm() {
        let conf = r#"
---
agents:
  'Local':
    url: 'http://localhost:9000'
projects:
  'synchronik':
    description: |
      Self-hosted project
    filename: 'ci.synchronik.yml'
    scm:
      github:
        owner: 'rtyler'
        repo: 'synchronik'
        ref: 'main'
"#;
        let value: ServerConfig = serde_yaml::from_str(&conf).expect("Failed to parse");
        assert_eq!(value.agents.len(), 1);
    }

    #[test]
    fn parse_config_inline() {
        let conf = r#"
---
agents:
  'Local':
    url: 'http://localhost:9000'
projects:
  'synchronik':
    description: |
      Self-hosted project
    inline:
      needs:
        - git
      commands:
        - 'whoami'
"#;
        let value: ServerConfig = serde_yaml::from_str(&conf).expect("Failed to parse");
        assert_eq!(value.agents.len(), 1);
        assert_eq!(value.projects.len(), 1);

        let project = value.projects.get("synchronik").unwrap();
        match &project.inline {
            Some(yml) => {
                assert!(yml.commands.contains(&"whoami".to_string()));
            }
            None => {
                assert!(false);
            }
        }
    }

    #[test]
    fn agent_can_meet_false() {
        let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
        let capabilities = vec![Capability::with_name("rustc")];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert_eq!(false, agent.can_meet(&needs));
    }

    #[test]
    fn agent_can_meet_true() {
        let needs: Vec<String> = vec!["dotnet".into()];
        let capabilities = vec![Capability::with_name("dotnet")];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert!(agent.can_meet(&needs));
    }

    #[test]
    fn agent_can_meet_false_multiple() {
        let needs: Vec<String> = vec!["rspec".into(), "git".into(), "dotnet".into()];
        let capabilities = vec![Capability::with_name("dotnet")];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert_eq!(false, agent.can_meet(&needs));
    }

    #[test]
    fn agent_can_meet_true_multiple() {
        let needs: Vec<String> = vec!["rspec".into(), "dotnet".into()];
        let capabilities = vec![
            Capability::with_name("dotnet"),
            Capability::with_name("rspec"),
        ];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert!(agent.can_meet(&needs));
    }

    #[test]
    fn agent_can_meet_intersection() {
        let needs: Vec<String> = vec!["git".into()];
        let capabilities = vec![
            Capability::with_name("dotnet"),
            Capability::with_name("git"),
            Capability::with_name("rspec"),
        ];
        let agent = Agent {
            name: "test".into(),
            url: Url::parse("http://localhost").unwrap(),
            capabilities,
        };
        assert!(agent.can_meet(&needs));
    }
}
