use std::collections::HashMap;
use std::hash::Hash;
use std::ops::Deref;
use std::path::Path;

use serde::de;
use serde::{Deserialize, Deserializer, Serialize};
use validator::{Validate, ValidationError as ValidationErrorType};

use crate::error::{ColmenaError, ColmenaResult};

pub mod host;
use host::Ssh;
pub use host::{CopyDirection, CopyOptions, Host, RebootOptions};

pub mod hive;
pub use hive::{Hive, HivePath};

pub mod store;
pub use store::{BuildResult, StoreDerivation, StorePath};

pub mod key;
pub use key::Key;

pub mod profile;
pub use profile::{Profile, ProfileDerivation};

pub mod deployment;
pub use deployment::Goal;

pub mod info;
pub use info::NixCheck;

pub mod flake;
pub use flake::Flake;

pub mod node_filter;
pub use node_filter::NodeFilter;

pub mod evaluator;

pub mod expression;
pub use expression::{NixExpression, SerializedNixExpression};

/// Path to the main system profile.
pub const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system";

/// Path to the system profile that's currently active.
pub const CURRENT_PROFILE: &str = "/run/current-system";

/// A node's attribute name.
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
#[serde(transparent)]
pub struct NodeName(#[serde(deserialize_with = "NodeName::deserialize")] String);

#[derive(Debug, Clone, Validate, Deserialize)]
pub struct NodeConfig {
    #[serde(rename = "targetHost")]
    target_host: Option<String>,

    #[serde(rename = "targetUser")]
    target_user: Option<String>,

    #[serde(rename = "targetPort")]
    target_port: Option<u16>,

    #[serde(rename = "allowLocalDeployment")]
    allow_local_deployment: bool,

    #[serde(rename = "buildOnTarget")]
    build_on_target: bool,

    tags: Vec<String>,

    #[serde(rename = "replaceUnknownProfiles")]
    replace_unknown_profiles: bool,

    #[serde(rename = "privilegeEscalationCommand")]
    privilege_escalation_command: Vec<String>,

    #[validate(custom = "validate_keys")]
    keys: HashMap<String, Key>,
}

#[derive(Debug, Clone, Validate, Deserialize)]
pub struct MetaConfig {
    #[serde(rename = "allowApplyAll")]
    pub allow_apply_all: bool,

    #[serde(rename = "machinesFile")]
    pub machines_file: Option<String>,
}

/// Nix CLI flags.
#[derive(Debug, Clone, Default)]
pub struct NixFlags {
    /// Whether to pass --show-trace.
    show_trace: bool,

    /// Whether to pass --pure-eval.
    pure_eval: bool,

    /// Whether to pass --impure.
    impure: bool,

    /// Designated builders.
    ///
    /// See <https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds.html>.
    ///
    /// Valid examples:
    /// - `@/path/to/machines`
    /// - `builder@host.tld riscv64-linux /home/nix/.ssh/keys/builder.key 8 1 kvm`
    builders: Option<String>,

    /// Options to pass as --option name value.
    options: HashMap<String, String>,
}

impl NodeName {
    /// Returns the string.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Creates a NodeName from a String.
    pub fn new(name: String) -> ColmenaResult<Self> {
        let validated = Self::validate(name)?;
        Ok(Self(validated))
    }

    /// Deserializes a potentially-invalid node name.
    fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
    where
        D: Deserializer<'de>,
    {
        use de::Error;
        String::deserialize(deserializer)
            .and_then(|s| Self::validate(s).map_err(|e| Error::custom(e.to_string())))
    }

    fn validate(s: String) -> ColmenaResult<String> {
        // FIXME: Elaborate
        if s.is_empty() {
            return Err(ColmenaError::EmptyNodeName);
        }

        Ok(s)
    }
}

impl Deref for NodeName {
    type Target = str;

    fn deref(&self) -> &str {
        self.0.as_str()
    }
}

impl NodeConfig {
    pub fn tags(&self) -> &[String] {
        &self.tags
    }

    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
    pub fn allows_local_deployment(&self) -> bool {
        self.allow_local_deployment
    }

    pub fn privilege_escalation_command(&self) -> &Vec<String> {
        &self.privilege_escalation_command
    }

    pub fn build_on_target(&self) -> bool {
        self.build_on_target
    }
    pub fn set_build_on_target(&mut self, enable: bool) {
        self.build_on_target = enable;
    }

    pub fn to_ssh_host(&self) -> Option<Ssh> {
        self.target_host.as_ref().map(|target_host| {
            let mut host = Ssh::new(self.target_user.clone(), target_host.clone());
            host.set_privilege_escalation_command(self.privilege_escalation_command.clone());

            if let Some(target_port) = self.target_port {
                host.set_port(target_port);
            }

            host
        })
    }
}

impl NixFlags {
    pub fn set_show_trace(&mut self, show_trace: bool) {
        self.show_trace = show_trace;
    }

    pub fn set_pure_eval(&mut self, pure_eval: bool) {
        self.pure_eval = pure_eval;
    }

    pub fn set_impure(&mut self, impure: bool) {
        self.impure = impure;
    }

    pub fn set_builders(&mut self, builders: Option<String>) {
        self.builders = builders;
    }

    pub fn set_options(&mut self, options: HashMap<String, String>) {
        self.options = options;
    }

    pub fn to_args(&self) -> Vec<String> {
        let mut args = Vec::new();

        if let Some(builders) = &self.builders {
            args.append(&mut vec![
                "--option".to_string(),
                "builders".to_string(),
                builders.clone(),
            ]);
        }

        if self.show_trace {
            args.push("--show-trace".to_string());
        }

        if self.pure_eval {
            args.push("--pure-eval".to_string());
        }

        if self.impure {
            args.push("--impure".to_string());
        }

        for (name, value) in self.options.iter() {
            args.push("--option".to_string());
            args.push(name.to_string());
            args.push(value.to_string());
        }

        args
    }
}

fn validate_keys(keys: &HashMap<String, Key>) -> Result<(), ValidationErrorType> {
    // Bad secret names:
    // - /etc/passwd
    // - ../../../../../etc/passwd

    for name in keys.keys() {
        let path = Path::new(name);
        if path.has_root() {
            return Err(ValidationErrorType::new(
                "Secret key name cannot be absolute",
            ));
        }

        if path.components().count() != 1 {
            return Err(ValidationErrorType::new(
                "Secret key name cannot contain path separators",
            ));
        }
    }
    Ok(())
}