//! Embedded configuration support for alien-deploy-cli and alien-operator binaries.
//!
//! The package builder appends a JSON-encoded config struct to the end of the
//! binary, followed by a 4-byte little-endian length and 8-byte magic trailer.
//! This allows a single binary to be customized per deployment group without
//! recompilation.

use serde::{de::DeserializeOwned, Deserialize, Serialize};

/// Magic bytes at the end of a binary with embedded config.
pub const MAGIC_BYTES: &[u8; 8] = b"WLCFG001";

/// Size of the footer: 4 bytes (length) + 8 bytes (magic).
pub const FOOTER_SIZE: usize = 12;

/// Configuration embedded in alien-deploy-cli binaries.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeployCliConfig {
    // --- Connection (for pre-configured binaries) ---
    /// Authentication token for the platform/manager API.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token: Option<String>,
    /// Deployment group ID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deployment_group_id: Option<String>,
    /// Default platform for deployments.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default_platform: Option<String>,
    /// Platform API base URL used for manager discovery.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub api_base_url: Option<String>,
    /// Exact agent binary URL to install for local pull-model deployments.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_binary_url: Option<String>,
    /// Machine bootstrap bundle manifest URL used by `alien-deploy join`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub machine_bundle_url: Option<String>,
    /// Branded environment variable that contains the deployment token.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token_env_var: Option<String>,
    // --- Branding (for rebranded binaries) ---
    /// Binary name (e.g., "acme-deploy").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Human-friendly display name (e.g., "Acme Deploy CLI").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
}

/// Configuration embedded in alien-operator binaries.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperatorConfig {
    // --- Connection (for pre-configured/OSS binaries) ---
    /// Manager URL to connect to.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub manager_url: Option<String>,
    /// Authentication token for the manager API.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub token: Option<String>,
    /// Deployment ID this operator manages.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deployment_id: Option<String>,
    /// Sync interval in seconds (default: 30).
    #[serde(default = "default_sync_interval")]
    pub sync_interval_secs: u64,
    // --- Branding (for rebranded binaries) ---
    /// Binary name (e.g., "acme-operator").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Short brand slug used for generated resource names.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brand: Option<String>,
    /// Human-friendly display name (e.g., "Acme Operator").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
    /// Branded environment variable prefix (e.g., "ACME").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub env_prefix: Option<String>,
    /// Branded Kubernetes/cloud label domain (e.g., "acme.dev").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label_domain: Option<String>,
}

fn default_sync_interval() -> u64 {
    30
}

/// Load embedded configuration from the current binary.
///
/// Reads the binary's own file, checks for the magic trailer, extracts the
/// JSON payload, and deserializes it into the requested type.
///
/// Returns `None` if the binary has no embedded config (no magic trailer).
pub fn load_embedded_config<T: DeserializeOwned>() -> Result<Option<T>, EmbeddedConfigError> {
    let exe_path = std::env::current_exe().map_err(EmbeddedConfigError::Io)?;
    load_embedded_config_from_path(&exe_path)
}

/// Load embedded configuration from a specific binary path.
pub fn load_embedded_config_from_path<T: DeserializeOwned>(
    path: &std::path::Path,
) -> Result<Option<T>, EmbeddedConfigError> {
    let data = std::fs::read(path).map_err(EmbeddedConfigError::Io)?;

    if data.len() < FOOTER_SIZE {
        return Ok(None);
    }

    // Check magic bytes at the end
    let magic_start = data.len() - MAGIC_BYTES.len();
    if &data[magic_start..] != MAGIC_BYTES {
        return Ok(None);
    }

    // Read the 4-byte little-endian length before the magic
    let len_start = magic_start - 4;
    let len_bytes: [u8; 4] = data[len_start..magic_start]
        .try_into()
        .map_err(|_| EmbeddedConfigError::InvalidFormat("invalid length bytes".into()))?;
    let json_len = u32::from_le_bytes(len_bytes) as usize;

    if json_len == 0 || len_start < json_len {
        return Err(EmbeddedConfigError::InvalidFormat(
            "config length exceeds binary size".into(),
        ));
    }

    let json_start = len_start - json_len;
    let json_bytes = &data[json_start..len_start];

    let config: T =
        serde_json::from_slice(json_bytes).map_err(EmbeddedConfigError::Deserialization)?;

    Ok(Some(config))
}

/// Append embedded configuration to a binary.
///
/// Writes: original binary bytes + JSON payload + 4-byte LE length + magic bytes.
pub fn append_embedded_config<T: Serialize>(
    binary_data: &[u8],
    config: &T,
) -> Result<Vec<u8>, EmbeddedConfigError> {
    let json_bytes = serde_json::to_vec(config).map_err(EmbeddedConfigError::Deserialization)?;
    let json_len = json_bytes.len() as u32;

    let mut result = Vec::with_capacity(binary_data.len() + json_bytes.len() + FOOTER_SIZE);
    result.extend_from_slice(binary_data);
    result.extend_from_slice(&json_bytes);
    result.extend_from_slice(&json_len.to_le_bytes());
    result.extend_from_slice(MAGIC_BYTES);

    Ok(result)
}

/// Errors from embedded config operations.
#[derive(Debug)]
pub enum EmbeddedConfigError {
    Io(std::io::Error),
    InvalidFormat(String),
    Deserialization(serde_json::Error),
}

impl std::fmt::Display for EmbeddedConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(e) => write!(f, "IO error reading embedded config: {}", e),
            Self::InvalidFormat(msg) => write!(f, "invalid embedded config format: {}", msg),
            Self::Deserialization(e) => write!(f, "failed to deserialize embedded config: {}", e),
        }
    }
}

impl std::error::Error for EmbeddedConfigError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_roundtrip_deploy_cli_config() {
        let config = DeployCliConfig {
            token: Some("ax_dg_abc123".into()),
            deployment_group_id: Some("dg_xyz".into()),
            display_name: Some("Production".into()),
            default_platform: Some("aws".into()),
            api_base_url: Some("https://api.example.com".into()),
            agent_binary_url: Some("https://packages.example.com/acme/agent".into()),
            machine_bundle_url: Some(
                "https://packages.example.com/acme/machine-bundle.json".into(),
            ),
            token_env_var: Some("ACME_DEPLOYMENT_TOKEN".into()),
            name: Some("acme-deploy".into()),
        };

        let binary = b"fake binary content";
        let embedded = append_embedded_config(binary, &config).unwrap();

        let loaded: Option<DeployCliConfig> =
            load_embedded_config_from_path_bytes(&embedded).unwrap();
        let loaded = loaded.unwrap();

        assert_eq!(loaded.token, config.token);
        assert_eq!(loaded.deployment_group_id, config.deployment_group_id);
        assert_eq!(loaded.default_platform, config.default_platform);
        assert_eq!(loaded.api_base_url, config.api_base_url);
        assert_eq!(loaded.agent_binary_url, config.agent_binary_url);
        assert_eq!(loaded.machine_bundle_url, config.machine_bundle_url);
        assert_eq!(loaded.token_env_var, config.token_env_var);
        assert_eq!(loaded.display_name, config.display_name);
        assert_eq!(loaded.name, config.name);
    }

    #[test]
    fn test_no_embedded_config() {
        let binary = b"just a regular binary";
        let result: Option<DeployCliConfig> = load_embedded_config_from_path_bytes(binary).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_roundtrip_operator_config() {
        let config = OperatorConfig {
            manager_url: Some("https://manager.example.com".into()),
            token: Some("ax_dep_operator123".into()),
            deployment_id: Some("dep_abc".into()),
            sync_interval_secs: 60,
            name: Some("acme-operator".into()),
            brand: Some("acme".into()),
            display_name: Some("Acme Operator".into()),
            env_prefix: Some("ACME".into()),
            label_domain: Some("acme.dev".into()),
        };

        let binary = b"operator binary";
        let embedded = append_embedded_config(binary, &config).unwrap();

        let loaded: Option<OperatorConfig> =
            load_embedded_config_from_path_bytes(&embedded).unwrap();
        let loaded = loaded.unwrap();

        assert_eq!(loaded.manager_url, config.manager_url);
        assert_eq!(loaded.deployment_id, config.deployment_id);
        assert_eq!(loaded.sync_interval_secs, 60);
        assert_eq!(loaded.name, config.name);
        assert_eq!(loaded.brand, config.brand);
        assert_eq!(loaded.display_name, config.display_name);
        assert_eq!(loaded.env_prefix, config.env_prefix);
        assert_eq!(loaded.label_domain, config.label_domain);
    }

    /// Helper that works on in-memory bytes (for tests that don't need files).
    fn load_embedded_config_from_path_bytes<T: DeserializeOwned>(
        data: &[u8],
    ) -> Result<Option<T>, EmbeddedConfigError> {
        if data.len() < FOOTER_SIZE {
            return Ok(None);
        }

        let magic_start = data.len() - MAGIC_BYTES.len();
        if &data[magic_start..] != MAGIC_BYTES {
            return Ok(None);
        }

        let len_start = magic_start - 4;
        let len_bytes: [u8; 4] = data[len_start..magic_start]
            .try_into()
            .map_err(|_| EmbeddedConfigError::InvalidFormat("invalid length bytes".into()))?;
        let json_len = u32::from_le_bytes(len_bytes) as usize;

        if json_len == 0 || len_start < json_len {
            return Err(EmbeddedConfigError::InvalidFormat(
                "config length exceeds binary size".into(),
            ));
        }

        let json_start = len_start - json_len;
        let json_bytes = &data[json_start..len_start];

        let config: T =
            serde_json::from_slice(json_bytes).map_err(EmbeddedConfigError::Deserialization)?;

        Ok(Some(config))
    }
}
