use std::{
    collections::{BTreeMap, BTreeSet},
    fmt::Display,
    path::{Path, PathBuf},
    str::FromStr,
};

use anyhow::bail;
use base64::Engine;
use hash_value::{to_value, Value};
use serde::{de::Error as DeError, ser::Error as SerError, Deserialize, Serialize};
use strum_macros::{Display, EnumIter};
use url::Url;

use crate::{cache::FileChangesMatcher, error::Error, unit_enum_deserialize, unit_enum_from_str};

#[derive(Hash, PartialEq, Eq, Serialize, EnumIter, Display, Debug, Clone, Copy)]
pub enum ExecutorKind {
    Rust,
    Node,
}

unit_enum_from_str!(ExecutorKind);
unit_enum_deserialize!(ExecutorKind);

#[derive(Serialize, Debug, Hash, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum ExecutorReference {
    Standard {
        url: Url,
    },
    Custom {
        url: Url,
        #[serde(flatten)]
        location: Location,
    },
}

impl Display for ExecutorReference {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Standard { url } => url.fmt(f),
            Self::Custom { url, .. } => url.fmt(f),
        }
    }
}

const URL_KEY: &str = "url";
const FORMAT_KEY: &str = "format";
const AUTHENTICATION_KEY: &str = "authentication";

impl<'de> Deserialize<'de> for ExecutorReference {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let root = Value::deserialize(deserializer)?;
        let url = root
            .at(URL_KEY)
            .and_then(Value::as_str)
            .or_else(|| root.as_str())
            .map(|s| Url::parse(s).map_err(D::Error::custom))
            .transpose()?
            .ok_or_else(|| D::Error::missing_field(URL_KEY))?;
        let is_single_url = matches!(root, Value::String(_));

        match url.scheme() {
            "std" => Ok(Self::Standard { url }),
            custom_executor_scheme => Ok(Self::Custom {
                location: match custom_executor_scheme {
                    "file" => Location::LocalFileSystem {
                        options: if is_single_url {
                            FileSystemOptions::default()
                        } else {
                            FileSystemOptions::deserialize(&root).map_err(D::Error::custom)?
                        },
                    },
                    "http" | "https" => {
                        let format = root
                            .at(FORMAT_KEY)
                            .map(|f| HttpFormatIdentifier::deserialize(f).map_err(D::Error::custom))
                            .transpose()?
                            .ok_or_else(|| D::Error::missing_field(FORMAT_KEY))?;

                        let transport =
                            HttpTransport::deserialize(&root).map_err(D::Error::custom)?;

                        match format {
                            HttpFormatIdentifier::Git => Location::GitOverHttp {
                                transport,
                                git_options: GitOptions::deserialize(&root)
                                    .map_err(D::Error::custom)?,
                                authentication: root
                                    .at(AUTHENTICATION_KEY)
                                    .map(|auth| {
                                        GitPlainAuthentication::deserialize(auth)
                                            .map_err(D::Error::custom)
                                    })
                                    .transpose()?,
                            },
                            HttpFormatIdentifier::Tarball => Location::TarballOverHttp {
                                transport,
                                tarball_options: TarballOptions::deserialize(&root)
                                    .map_err(D::Error::custom)?,
                                authentication: root
                                    .at(AUTHENTICATION_KEY)
                                    .map(|auth| {
                                        HttpAuthentication::deserialize(auth)
                                            .map_err(D::Error::custom)
                                    })
                                    .transpose()?,
                            },
                        }
                    }
                    "ssh" => Location::GitOverSsh {
                        transport: SshTransport::deserialize(&root).map_err(D::Error::custom)?,
                        git_options: GitOptions::deserialize(&root).map_err(D::Error::custom)?,
                        authentication: root
                            .at(AUTHENTICATION_KEY)
                            .map(|auth| {
                                SshAuthentication::deserialize(auth).map_err(D::Error::custom)
                            })
                            .transpose()?,
                    },
                    "git" => Location::Git {
                        options: GitOptions::deserialize(&root).map_err(D::Error::custom)?,
                    },
                    "npm" => Location::Npm {
                        options: NpmOptions::deserialize(&root).map_err(D::Error::custom)?,
                    },
                    "cargo" => Location::Cargo {
                        options: CargoOptions::deserialize(&root).map_err(D::Error::custom)?,
                    },
                    invalid_scheme => {
                        return Err(serde::de::Error::custom(&format!(
                            "invalid url scheme \"{invalid_scheme}\""
                        )))
                    }
                },
                url,
            }),
        }
    }
}

#[derive(Hash, Debug, PartialEq, Eq, Clone)]
pub enum Location {
    LocalFileSystem {
        options: FileSystemOptions,
    },

    TarballOverHttp {
        transport: HttpTransport,
        tarball_options: TarballOptions,
        authentication: Option<HttpAuthentication>,
    },

    GitOverHttp {
        transport: HttpTransport,
        git_options: GitOptions,
        authentication: Option<GitPlainAuthentication>,
    },

    GitOverSsh {
        transport: SshTransport,
        git_options: GitOptions,
        authentication: Option<SshAuthentication>,
    },
    Git {
        options: GitOptions,
    },
    Cargo {
        options: CargoOptions,
    },
    Npm {
        options: NpmOptions,
    },
}

impl Serialize for Location {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            Self::LocalFileSystem { options } => options.serialize(serializer),
            Self::GitOverHttp {
                transport,
                git_options,
                authentication,
            } => {
                let mut value = to_value(transport).map_err(S::Error::custom)?;
                value.overwrite(to_value(git_options).map_err(S::Error::custom)?);
                value.overwrite(Value::object([(
                    FORMAT_KEY,
                    Value::string(HttpFormatIdentifier::Git.to_string()),
                )]));
                if let Some(authentication) = authentication {
                    value.overwrite(Value::object([(
                        AUTHENTICATION_KEY,
                        to_value(authentication).map_err(S::Error::custom)?,
                    )]));
                }
                value.serialize(serializer)
            }
            Self::GitOverSsh {
                transport,
                git_options,
                authentication,
            } => {
                let mut value = to_value(transport).map_err(S::Error::custom)?;
                value.overwrite(to_value(git_options).map_err(S::Error::custom)?);
                if let Some(authentication) = authentication {
                    value.overwrite(to_value(authentication).map_err(S::Error::custom)?);
                }
                value.serialize(serializer)
            }
            Self::Npm { options } => options.serialize(serializer),
            Self::Cargo { options } => options.serialize(serializer),
            _ => todo!(),
        }
    }
}

#[derive(Default, EnumIter, Display, Serialize, Hash, PartialEq, Eq, Debug, Clone, Copy)]
pub enum RebuildStrategy {
    Always,
    #[default]
    OnChanges,
}

unit_enum_from_str!(RebuildStrategy);
unit_enum_deserialize!(RebuildStrategy);

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone, Default)]
pub struct FileSystemOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    kind: Option<ExecutorKind>,
    #[serde(default)]
    rebuild: RebuildStrategy,
    #[serde(skip_serializing_if = "Option::is_none")]
    watch: Option<BTreeSet<FileChangesMatcher>>,
}

impl FileSystemOptions {
    pub fn kind(&self) -> Option<ExecutorKind> {
        self.kind
    }

    pub fn rebuild(&self) -> RebuildStrategy {
        self.rebuild
    }

    pub fn watch(&self) -> Option<&BTreeSet<FileChangesMatcher>> {
        self.watch.as_ref()
    }
}

#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct HttpTransport {
    #[serde(default)]
    insecure: bool,
    #[serde(default)]
    headers: BTreeMap<String, String>,
}

impl HttpTransport {
    pub fn insecure(&self) -> bool {
        self.insecure
    }

    pub fn headers(&self) -> &BTreeMap<String, String> {
        &self.headers
    }
}

const HTTP_AUTH_MODE_KEY: &str = "mode";

#[derive(Serialize, EnumIter, Display, Hash, PartialEq, Eq, Clone)]
pub enum HttpAuthenticationMode {
    Basic,
    Bearer,
}

unit_enum_from_str!(HttpAuthenticationMode);
unit_enum_deserialize!(HttpAuthenticationMode);

#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct HttpBasicAuthentication {
    username: String,
    password: String,
}

impl HttpBasicAuthentication {
    pub fn username(&self) -> &str {
        &self.username
    }

    pub fn password(&self) -> &str {
        &self.password
    }
}

#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct HttpDigestAuthentication {
    username: String,
    password: String,
}

impl HttpDigestAuthentication {
    pub fn username(&self) -> &str {
        &self.username
    }

    pub fn password(&self) -> &str {
        &self.password
    }
}

#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct HttpBearerAuthentication {
    token: String,
}

impl HttpBearerAuthentication {
    pub fn token(&self) -> &str {
        &self.token
    }
}

#[derive(Hash, Debug, PartialEq, Eq, Clone)]
pub enum HttpAuthentication {
    Basic(HttpBasicAuthentication),
    Bearer(HttpBearerAuthentication),
}

impl<'de> Deserialize<'de> for HttpAuthentication {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let root = Value::deserialize(deserializer)?;
        let mode = root
            .at(HTTP_AUTH_MODE_KEY)
            .map(|m| HttpAuthenticationMode::deserialize(m).map_err(D::Error::custom))
            .transpose()?
            .ok_or_else(|| D::Error::missing_field(HTTP_AUTH_MODE_KEY))?;
        Ok(match mode {
            HttpAuthenticationMode::Basic => HttpAuthentication::Basic(
                HttpBasicAuthentication::deserialize(root).map_err(D::Error::custom)?,
            ),
            HttpAuthenticationMode::Bearer => HttpAuthentication::Bearer(
                HttpBearerAuthentication::deserialize(root).map_err(D::Error::custom)?,
            ),
        })
    }
}

impl Serialize for HttpAuthentication {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut value = Value::object([(
            HTTP_AUTH_MODE_KEY,
            to_value(match self {
                HttpAuthentication::Basic(_) => HttpAuthenticationMode::Basic,
                HttpAuthentication::Bearer(_) => HttpAuthenticationMode::Bearer,
            })
            .map_err(S::Error::custom)?,
        )]);

        value.overwrite(
            match self {
                HttpAuthentication::Basic(basic) => to_value(basic),
                HttpAuthentication::Bearer(bearer) => to_value(bearer),
            }
            .map_err(S::Error::custom)?,
        );

        value.serialize(serializer)
    }
}

#[derive(Serialize, EnumIter, Display)]
pub enum HttpFormatIdentifier {
    Git,
    Tarball,
}

#[derive(Serialize, Hash, Debug, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum HttpResource {
    Git {
        #[serde(flatten)]
        options: GitOptions,
    },
    Tarball {
        #[serde(flatten)]
        options: TarballOptions,
    },
}

#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum GitCheckout {
    Branch { branch: String },
    Tag { tag: String },
    Revision { rev: String },
}

unit_enum_from_str!(HttpFormatIdentifier);
unit_enum_deserialize!(HttpFormatIdentifier);

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct GitOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    kind: Option<ExecutorKind>,
    #[serde(default)]
    pull: bool,
    #[serde(skip_serializing_if = "Option::is_none", flatten)]
    checkout: Option<GitCheckout>,
    #[serde(skip_serializing_if = "Option::is_none")]
    path: Option<PathBuf>,
}

impl GitOptions {
    pub fn kind(&self) -> Option<ExecutorKind> {
        self.kind
    }

    pub fn pull(&self) -> bool {
        self.pull
    }

    pub fn checkout(&self) -> Option<&GitCheckout> {
        self.checkout.as_ref()
    }

    pub fn path(&self) -> Option<&Path> {
        self.path.as_deref()
    }
}

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct GitPlainAuthentication {
    username: String,
    password: String,
}

impl GitPlainAuthentication {
    pub fn username(&self) -> &str {
        &self.username
    }

    pub fn password(&self) -> &str {
        &self.password
    }
}

#[derive(Serialize, EnumIter, Display, Hash, Debug, PartialEq, Eq, Copy, Clone)]
pub enum Compression {
    Deflate,
    Zlib,
    Gzip,
}

unit_enum_from_str!(Compression);
unit_enum_deserialize!(Compression);

#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct TarballOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    kind: Option<ExecutorKind>,
    #[serde(skip_serializing_if = "Option::is_none")]
    compression: Option<Compression>,
}

impl TarballOptions {
    pub fn kind(&self) -> Option<ExecutorKind> {
        self.kind
    }

    pub fn compression(&self) -> Option<Compression> {
        self.compression
    }
}

#[derive(Serialize, EnumIter, Display, Hash, Debug, PartialEq, Eq, Copy, Clone)]
pub enum SshFingerprintAlgorithm {
    Md5,
    Sha1,
    Sha256,
}

unit_enum_from_str!(SshFingerprintAlgorithm);
unit_enum_deserialize!(SshFingerprintAlgorithm);

#[derive(Hash, Debug, PartialEq, Eq, Clone)]
pub enum SshFingerprint {
    Md5([u8; 16]),
    Sha1([u8; 20]),
    Sha256([u8; 32]),
}

macro_rules! parse_fingerprint {
    ($value:expr, $len:literal) => {{
        let decoded = base64::prelude::BASE64_STANDARD_NO_PAD
            .decode($value)
            .map_err(|err| {
                anyhow::anyhow!(
                    "failed to decode ssh fingerprint content {} ({})",
                    $value,
                    err
                )
            })?;
        let length = decoded.len();
        if length != $len {
            bail!(
                "fingerprint has invalid length (expected={}, actual={})",
                $len,
                length
            )
        }
        let mut bytes = [0_u8; $len];
        bytes.copy_from_slice(&decoded);
        Ok::<_, $crate::error::Error>(bytes)
    }};
}

const MD5: &str = "MD5";
const SHA1: &str = "SHA1";
const SHA256: &str = "SHA256";

impl FromStr for SshFingerprint {
    type Err = Error;

    fn from_str(s: &str) -> crate::error::Result<Self> {
        Ok(match s.split_once(':'){
            Some((algorithm, value)) => {
                match algorithm {
                    MD5 => Self::Md5(parse_fingerprint!(value, 16)?),
                    SHA1 => Self::Sha1(parse_fingerprint!(value, 20)?),
                    SHA256 => Self::Sha256(parse_fingerprint!(value, 32)?),
                    _ => bail!("unknown ssh fingerprint algorithm \"{algorithm}\". valid algorithms are MD5, SHA1 and SHA256")
                }
            },
            _ => bail!("bad ssh fingerprint. format must be <ALGORITHM>:<base64 hash>.")
        })
    }
}

impl Display for SshFingerprint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let (algorithm_id, slice) = match self {
            Self::Md5(md5) => (MD5, md5.as_slice()),
            Self::Sha1(sha1) => (SHA1, sha1.as_slice()),
            Self::Sha256(sha256) => (SHA256, sha256.as_slice()),
        };
        let mut base64 = String::new();
        base64::prelude::BASE64_STANDARD_NO_PAD.encode_string(slice, &mut base64);
        write!(f, "{algorithm_id}:{base64}")
    }
}

impl Serialize for SshFingerprint {
    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for SshFingerprint {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        SshFingerprint::from_str(&s).map_err(D::Error::custom)
    }
}

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SshTransport {
    #[serde(skip_serializing_if = "Option::is_none")]
    fingerprints: Option<Vec<SshFingerprint>>,
    #[serde(default)]
    insecure: bool,
}

impl SshTransport {
    pub fn fingerprints(&self) -> Option<&[SshFingerprint]> {
        self.fingerprints.as_deref()
    }

    pub fn insecure(&self) -> bool {
        self.insecure
    }
}

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum SshResource {
    Git {
        #[serde(flatten)]
        options: GitOptions,
    },
}

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum SshAuthentication {
    Password {
        #[serde(skip_serializing_if = "Option::is_none")]
        username: Option<String>,
        password: String,
    },
    PrivateKeyFile {
        #[serde(skip_serializing_if = "Option::is_none")]
        username: Option<String>,
        key: PathBuf,
        #[serde(skip_serializing_if = "Option::is_none")]
        passphrase: Option<String>,
    },
}

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct CargoOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    version: Option<String>,
    #[serde(default)]
    insecure: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    token: Option<String>,
}

impl CargoOptions {
    pub fn version(&self) -> Option<&str> {
        self.version.as_deref()
    }

    pub fn insecure(&self) -> bool {
        self.insecure
    }

    pub fn token(&self) -> Option<&str> {
        self.token.as_deref()
    }
}

#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
pub struct NpmOptions {
    #[serde(skip_serializing_if = "Option::is_none")]
    version: Option<String>,
    #[serde(default)]
    insecure: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    token: Option<String>,
}

impl NpmOptions {
    pub fn version(&self) -> Option<&str> {
        self.version.as_deref()
    }

    pub fn insecure(&self) -> bool {
        self.insecure
    }

    pub fn token(&self) -> Option<&str> {
        self.token.as_deref()
    }
}
