use std::collections::HashMap;

use anyhow::bail;
use log::debug;
use serde_derive::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Global {
    pub author: libpijul::change::Author,
    pub unrecord_changes: Option<usize>,
}

const CONFIG_DIR: &str = "pijul";

impl Global {
    pub fn load() -> Result<Global, anyhow::Error> {
        if let Some(mut dir) = dirs_next::config_dir() {
            dir.push(CONFIG_DIR);
            dir.push("config.toml");
            let s = std::fs::read(&dir)
                .or_else(|e| {
                    // Read from `$HOME/.config/pijul` dir
                    if let Some(mut dir) = dirs_next::home_dir() {
                        dir.push(".config");
                        dir.push(CONFIG_DIR);
                        dir.push("config.toml");
                        std::fs::read(&dir)
                    } else {
                        Err(e.into())
                    }
                })
                .or_else(|e| {
                    // Read from `$HOME/.pijulconfig`
                    if let Some(mut dir) = dirs_next::home_dir() {
                        dir.push(".pijulconfig");
                        std::fs::read(&dir)
                    } else {
                        Err(e.into())
                    }
                })?;
            debug!("s = {:?}", s);
            if let Ok(t) = toml::from_slice(&s) {
                Ok(t)
            } else {
                bail!("Could not read configuration file at {:?}", dir)
            }
        } else {
            bail!("Global configuration file missing")
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Config {
    pub current_channel: Option<String>,
    pub default_remote: Option<String>,
    #[serde(default)]
    pub remotes: HashMap<String, String>,
    #[serde(default)]
    pub hooks: Hooks,
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Hooks {
    #[serde(default)]
    pub record: Vec<String>,
}

impl Config {
    pub fn get_current_channel<'a>(&'a self, alt: Option<&'a String>) -> &'a str {
        if let Some(channel) = alt {
            channel.as_ref()
        } else if let Some(ref channel) = self.current_channel {
            channel.as_str()
        } else {
            crate::DEFAULT_CHANNEL
        }
    }

    pub fn current_channel(&self) -> Option<&str> {
        if let Some(ref channel) = self.current_channel {
            Some(channel.as_str())
        } else {
            None
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct Remote_ {
    ssh: Option<SshRemote>,
    local: Option<String>,
    url: Option<String>,
}

#[derive(Debug)]
pub enum Remote {
    Ssh(SshRemote),
    Local { local: String },
    Http { url: String },
    None,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SshRemote {
    pub addr: String,
}

impl<'de> serde::Deserialize<'de> for Remote {
    fn deserialize<D>(deserializer: D) -> Result<Remote, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        let r = Remote_::deserialize(deserializer)?;
        if let Some(ssh) = r.ssh {
            Ok(Remote::Ssh(ssh))
        } else if let Some(local) = r.local {
            Ok(Remote::Local { local })
        } else if let Some(url) = r.url {
            Ok(Remote::Http { url })
        } else {
            Ok(Remote::None)
        }
    }
}

impl serde::Serialize for Remote {
    fn serialize<D>(&self, serializer: D) -> Result<D::Ok, D::Error>
    where
        D: serde::ser::Serializer,
    {
        let r = match *self {
            Remote::Ssh(ref ssh) => Remote_ {
                ssh: Some(ssh.clone()),
                local: None,
                url: None,
            },
            Remote::Local { ref local } => Remote_ {
                local: Some(local.to_string()),
                ssh: None,
                url: None,
            },
            Remote::Http { ref url } => Remote_ {
                local: None,
                ssh: None,
                url: Some(url.to_string()),
            },
            Remote::None => Remote_ {
                local: None,
                ssh: None,
                url: None,
            },
        };
        r.serialize(serializer)
    }
}