use std::fs::File;
use std::sync::Mutex;

use anyhow::Context;
use anyhow::Result;
use serde::Deserialize;
use serde_yaml::Mapping;
use serde_yaml::Value;

mod crates;
mod image;
mod npm;
mod project;
mod release;

pub use crates::Crates;
pub use crates::CratesPublish;
pub use image::Image;
pub use image::VersionFrom;
pub use npm::Npm;
pub use project::Project;
pub use release::ExtractBinary;
pub use release::ExtractBinaryMode;
pub use release::ReleaseTag;

const CONF_FILE: &str = "replidev.yaml";
const CONF_FILE_LOCAL: &str = "replidev.local.yaml";
const CONF_LOAD_ERROR: &str =
    "Could not load configuration, are you in the root of a Replicante repository?";

// The first time an IP is detected cache it for consistency and performance.
lazy_static::lazy_static! {
    static ref DETECTED_IP_CACHE: Mutex<Option<String>> = Mutex::new(None);
}

/// Project specific configuration.
#[derive(Clone, Debug, Deserialize)]
pub struct Conf {
    /// Advanced configuration for crates in this project.
    #[serde(default)]
    pub crates: Crates,

    /// Command to execute easyrsa.
    #[serde(default = "Conf::default_easyrsa")]
    pub easyrsa: String,

    /// List of rules to extract binaries from container images.
    #[serde(default)]
    pub extract_binaries: Vec<ExtractBinary>,

    /// List of images to build for this project.
    #[serde(default)]
    pub images: Vec<Image>,

    /// Advanced configuration for npm packages in this project.
    #[serde(default)]
    pub npm: Npm,

    /// Command to execute openssl.
    #[serde(default = "Conf::default_openssl")]
    pub openssl: String,

    /// Set the address returned for agents in the play server discovery endpoint.
    #[serde(default = "Conf::default_play_server_agents_address")]
    pub play_server_agents_address: String,

    /// Bind address and port for the playground API server.
    #[serde(default = "Conf::default_play_server_bind")]
    pub play_server_bind: String,

    /// Current project to operate on.
    pub project: Project,

    /// Command to execute podman.
    #[serde(default = "Conf::default_podman")]
    pub podman: String,

    /// IP address the host server running podman.
    #[serde(default)]
    pub podman_host_ip: Option<String>,

    /// Set PODMAN_HOSTNAME inside pods to resolve to slirp4netns virtual router IP.
    #[serde(default = "Conf::default_podman_hostname_as_internal")]
    pub podman_hostname_as_internal: bool,

    /// Network mode to use for podman pod create.
    #[serde(default = "Conf::default_podman_network_mode")]
    pub podman_network_mode: Option<String>,

    /// IP of slirp4netns virtual router inside pods.
    #[serde(default = "Conf::default_podman_network_virtual_router_ip")]
    pub podman_network_virtual_router_ip: String,

    /// How to determine the git tag generated by `replidev release publish`.
    #[serde(default)]
    pub release_tag: Option<ReleaseTag>,
}

impl Conf {
    /// Load the local project's configuration file.
    pub fn from_file() -> Result<Conf> {
        // Read config file and optional override file.
        let base = Conf::load_file(CONF_FILE)?;
        let local = if std::path::Path::new(CONF_FILE_LOCAL).exists() {
            Conf::load_file(CONF_FILE_LOCAL)?
        } else {
            Mapping::new()
        };

        // Merge the config options and decode the result.
        let conf = Conf::merge(base, local);
        let conf = serde_yaml::from_value(conf).context(CONF_LOAD_ERROR)?;
        Ok(conf)
    }

    /// IP address the host server running podman.
    ///
    /// If an IP address is not provided in the configuration an attempt to
    /// auto-detect a non-local IPv4 address is made.
    pub fn podman_host_ip(&self) -> crate::error::Result<String> {
        // Use configure IP if possible.
        if let Some(ip) = &self.podman_host_ip {
            return Ok(ip.clone());
        }

        // Consult IP cache for consistency and performance.
        {
            let cache = DETECTED_IP_CACHE
                .lock()
                .expect("detected IP cache lock is poisoned");
            if let Some(ip) = cache.as_ref() {
                return Ok(ip.clone());
            }
        }

        // Attempt to auto detect a non-local IP.
        for iface in pnet_datalink::interfaces() {
            for ip in iface.ips {
                let ip = ip.ip();
                if ip.is_loopback() || !ip.is_ipv4() {
                    continue;
                }
                let ip = ip.to_string();
                let mut cache = DETECTED_IP_CACHE
                    .lock()
                    .expect("detected IP cache lock is poisoned");
                *cache = Some(ip.clone());
                return Ok(ip);
            }
        }

        // Could not find a non-loopback IP address.
        let error = crate::error::ErrorKind::ip_not_detected();
        Err(error.into())
        //anyhow::bail!("Could not find a non-loopback IP address");
    }

    /// Lookup the play_server_agents_address from the environment of the configuration.
    pub fn resolve_play_server_agents_address(&self) -> String {
        match std::env::var("REPLIDEV_PLAY_SERVER_AGENTS_ADDRESS") {
            Ok(address) => address,
            Err(_) => self.play_server_agents_address.clone(),
        }
    }
}

impl Conf {
    fn default_easyrsa() -> String {
        "easyrsa".into()
    }

    fn default_openssl() -> String {
        "openssl".into()
    }

    fn default_play_server_agents_address() -> String {
        "host.containers.internal".into()
    }

    fn default_play_server_bind() -> String {
        "0.0.0.0:9876".into()
    }

    fn default_podman() -> String {
        "podman".into()
    }

    fn default_podman_hostname_as_internal() -> bool {
        true
    }

    fn default_podman_network_mode() -> Option<String> {
        Some("slirp4netns:allow_host_loopback=true".into())
    }

    fn default_podman_network_virtual_router_ip() -> String {
        "10.0.2.2".into()
    }

    fn load_file(file: &str) -> Result<Mapping> {
        let conf = File::open(file).context(CONF_LOAD_ERROR)?;
        let conf = serde_yaml::from_reader(conf).context(CONF_LOAD_ERROR)?;
        Ok(conf)
    }

    fn merge(mut base: Mapping, local: Mapping) -> Value {
        for (key, value) in local {
            base.insert(key, value);
        }
        Value::Mapping(base)
    }
}
