use std::fmt;
use std::path::{Path, PathBuf};

use crate::XdgPathError;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClientServerTrust {
    System,
    CaFile(PathBuf),
}

#[derive(Debug)]
pub enum ResolveClientServerTrustError {
    InvalidMode { value: String },
    UnexpectedServerCaFile,
    DefaultCaPath(XdgPathError),
}

impl fmt::Display for ResolveClientServerTrustError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidMode { value } => write!(
                formatter,
                "client.server-trust must be one of `system` or `ca-file`, got `{value}`"
            ),
            Self::UnexpectedServerCaFile => formatter.write_str(
                "client.server-ca-file may be set only when client.server-trust = \"ca-file\"",
            ),
            Self::DefaultCaPath(source) => write!(formatter, "{source}"),
        }
    }
}

impl std::error::Error for ResolveClientServerTrustError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::DefaultCaPath(source) => Some(source),
            Self::InvalidMode { .. } | Self::UnexpectedServerCaFile => None,
        }
    }
}

pub(crate) fn resolve_client_server_trust_with_default(
    trust_mode: Option<&str>,
    server_ca_file: Option<PathBuf>,
    config_dir: &Path,
    default_ca_path: impl FnOnce() -> Result<PathBuf, XdgPathError>,
) -> Result<ClientServerTrust, ResolveClientServerTrustError> {
    let trust_mode = match trust_mode.unwrap_or("system") {
        "system" => ClientServerTrustMode::System,
        "ca-file" => ClientServerTrustMode::CaFile,
        value => {
            return Err(ResolveClientServerTrustError::InvalidMode {
                value: value.to_owned(),
            });
        }
    };

    match trust_mode {
        ClientServerTrustMode::System => {
            if server_ca_file.is_some() {
                return Err(ResolveClientServerTrustError::UnexpectedServerCaFile);
            }
            Ok(ClientServerTrust::System)
        }
        ClientServerTrustMode::CaFile => Ok(ClientServerTrust::CaFile(match server_ca_file {
            Some(server_ca_file) => resolve_path(config_dir, &server_ca_file),
            None => default_ca_path().map_err(ResolveClientServerTrustError::DefaultCaPath)?,
        })),
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ClientServerTrustMode {
    System,
    CaFile,
}

fn resolve_path(config_dir: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        config_dir.join(path)
    }
}

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

    use super::{
        ClientServerTrust, ResolveClientServerTrustError, resolve_client_server_trust_with_default,
    };

    #[test]
    fn defaults_to_system_trust() {
        let trust = resolve_client_server_trust_with_default(
            None,
            None,
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap();

        assert_eq!(trust, ClientServerTrust::System);
    }

    #[test]
    fn ca_file_trust_uses_the_default_ca_path_when_no_explicit_file_is_set() {
        let trust = resolve_client_server_trust_with_default(
            Some("ca-file"),
            None,
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/xdg-data/runewarp/client/server-ca.crt")),
        )
        .unwrap();

        assert_eq!(
            trust,
            ClientServerTrust::CaFile(PathBuf::from("/xdg-data/runewarp/client/server-ca.crt"))
        );
    }

    #[test]
    fn ca_file_trust_resolves_relative_paths_from_the_config_directory() {
        let trust = resolve_client_server_trust_with_default(
            Some("ca-file"),
            Some(PathBuf::from("server-ca.pem")),
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap();

        assert_eq!(
            trust,
            ClientServerTrust::CaFile(PathBuf::from("/tmp/runewarp/server-ca.pem"))
        );
    }

    #[test]
    fn system_trust_rejects_server_ca_files() {
        let error = resolve_client_server_trust_with_default(
            Some("system"),
            Some(PathBuf::from("server-ca.pem")),
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap_err();

        assert!(matches!(
            error,
            ResolveClientServerTrustError::UnexpectedServerCaFile
        ));
    }

    #[test]
    fn rejects_unknown_trust_modes() {
        let error = resolve_client_server_trust_with_default(
            Some("hybrid"),
            None,
            PathBuf::from("/tmp/runewarp").as_path(),
            || Ok(PathBuf::from("/unused/server-ca.crt")),
        )
        .unwrap_err();

        assert!(matches!(
            error,
            ResolveClientServerTrustError::InvalidMode { value } if value == "hybrid"
        ));
    }
}
