//! Machine activation and management.
//!
//! This module provides functionality for activating machines against licenses,
//! managing machine heartbeats, and checking out machine files for offline use.

use crate::certificate::CertificateFileResponse;
use crate::client::{Client, ClientOptions, Response};
use crate::config::get_config;
use crate::config::KeygenConfig;
use crate::errors::Error;
use crate::machine_file::MachineFile;
use crate::KeygenResponseData;
use chrono::{DateTime, Utc};
use futures::future::{BoxFuture, FutureExt};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;

/// Heartbeat status as returned by the Keygen API
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum HeartbeatStatus {
    Alive,
    Dead,
    NotStarted,
}

impl HeartbeatStatus {
    /// Parses a HeartbeatStatus from a string, returning None for unknown values
    pub fn parse(s: &str) -> Option<Self> {
        match s.to_uppercase().as_str() {
            "ALIVE" => Some(Self::Alive),
            "DEAD" => Some(Self::Dead),
            "NOT_STARTED" => Some(Self::NotStarted),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MachineAttributes {
    pub fingerprint: String,
    pub name: Option<String>,
    pub platform: Option<String>,
    pub hostname: Option<String>,
    pub ip: Option<String>,
    pub cores: Option<i32>,
    pub metadata: Option<HashMap<String, Value>>,
    #[serde(rename = "requireHeartbeat")]
    pub require_heartbeat: bool,
    #[serde(rename = "heartbeatStatus")]
    pub heartbeat_status: String,
    #[serde(rename = "heartbeatDuration")]
    pub heartbeat_duration: Option<i32>,
    pub created: DateTime<Utc>,
    pub updated: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MachineResponse {
    pub data: KeygenResponseData<MachineAttributes>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MachinesResponse {
    pub data: Vec<KeygenResponseData<MachineAttributes>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Machine {
    pub id: String,
    pub fingerprint: String,
    pub name: Option<String>,
    pub platform: Option<String>,
    pub hostname: Option<String>,
    pub ip: Option<String>,
    pub cores: Option<i32>,
    pub metadata: Option<HashMap<String, Value>>,
    #[serde(rename = "requireHeartbeat")]
    pub require_heartbeat: bool,
    #[serde(rename = "heartbeatStatus")]
    pub heartbeat_status: String,
    #[serde(rename = "heartbeatDuration")]
    pub heartbeat_duration: Option<i32>,
    pub created: DateTime<Utc>,
    pub updated: DateTime<Utc>,
    pub account_id: Option<String>,
    pub environment_id: Option<String>,
    pub product_id: Option<String>,
    pub license_id: Option<String>,
    pub owner_id: Option<String>,
    pub group_id: Option<String>,
    #[serde(skip)]
    pub config: Option<Arc<KeygenConfig>>,
}

#[derive(Debug, Clone, Default)]
pub struct MachineCheckoutOpts {
    pub ttl: Option<i64>,
    pub include: Option<Vec<String>>,
}

impl MachineCheckoutOpts {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_ttl(ttl: i64) -> Self {
        Self {
            ttl: Some(ttl),
            ..Self::default()
        }
    }

    pub fn with_include(include: Vec<String>) -> Self {
        Self {
            include: Some(include),
            ..Self::default()
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MachineListFilters {
    pub license: Option<String>,
    pub user: Option<String>,
    pub platform: Option<String>,
    pub name: Option<String>,
    pub fingerprint: Option<String>,
    pub ip: Option<String>,
    pub hostname: Option<String>,
    pub product: Option<String>,
    pub owner: Option<String>,
    pub group: Option<String>,
    pub metadata: Option<HashMap<String, Value>>,
    pub page_number: Option<i32>,
    pub page_size: Option<i32>,
    pub limit: Option<i32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MachineCreateRequest {
    pub fingerprint: String,
    pub name: Option<String>,
    pub platform: Option<String>,
    pub hostname: Option<String>,
    pub ip: Option<String>,
    pub cores: Option<i32>,
    pub metadata: Option<HashMap<String, Value>>,
    pub license_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MachineUpdateRequest {
    pub name: Option<String>,
    pub platform: Option<String>,
    pub hostname: Option<String>,
    pub ip: Option<String>,
    pub cores: Option<i32>,
    pub metadata: Option<HashMap<String, Value>>,
}

impl Machine {
    pub(crate) fn from(data: KeygenResponseData<MachineAttributes>) -> Machine {
        Machine {
            id: data.id,
            fingerprint: data.attributes.fingerprint,
            name: data.attributes.name,
            platform: data.attributes.platform,
            hostname: data.attributes.hostname,
            ip: data.attributes.ip,
            cores: data.attributes.cores,
            metadata: data.attributes.metadata,
            require_heartbeat: data.attributes.require_heartbeat,
            heartbeat_status: data.attributes.heartbeat_status,
            heartbeat_duration: data.attributes.heartbeat_duration,
            created: data.attributes.created,
            updated: data.attributes.updated,
            account_id: data.relationships.account_id(),
            environment_id: data.relationships.environment_id(),
            product_id: data.relationships.product_id(),
            license_id: data.relationships.license_id(),
            owner_id: data.relationships.owner_id(),
            group_id: data.relationships.group_id(),
            config: None,
        }
    }

    /// Associates a configuration with this Machine
    pub fn with_config(mut self, config: KeygenConfig) -> Self {
        self.config = Some(Arc::new(config));
        self
    }

    /// Gets a client for this machine, using the associated config or global config
    fn get_client(&self) -> Result<Client, Error> {
        let config = if let Some(ref cfg) = self.config {
            cfg.as_ref().clone()
        } else {
            get_config()?
        };
        Client::new(ClientOptions::from(config))
    }

    pub async fn deactivate(&self) -> Result<(), Error> {
        let client = self.get_client()?;
        let _response = client
            .delete::<(), serde_json::Value>(&format!("machines/{}", self.id), None::<&()>)
            .await?;
        Ok(())
    }

    pub async fn checkout(&self, options: &MachineCheckoutOpts) -> Result<MachineFile, Error> {
        let mut query = json!({
            "encrypt": 1
        });

        if let Some(ttl) = options.ttl {
            query["ttl"] = ttl.into();
        }

        if let Some(ref include) = options.include {
            query["include"] = json!(include.join(","));
        } else {
            query["include"] = "license.entitlements".into();
        }

        let client = self.get_client()?;
        let response = client
            .post(
                &format!("machines/{}/actions/check-out", self.id),
                None::<&()>,
                Some(&query),
            )
            .await?;

        let machine_file_response: CertificateFileResponse = serde_json::from_value(response.body)?;
        let machine_file = MachineFile::from(machine_file_response.data);
        Ok(machine_file)
    }

    pub async fn ping(&self) -> Result<Machine, Error> {
        let client = self.get_client()?;
        let response: Response<MachineResponse> = client
            .post(
                &format!("machines/{}/actions/ping", self.id),
                None::<&()>,
                None::<&()>,
            )
            .await?;
        let machine = Machine::from(response.body.data).with_config(
            self.config
                .as_ref()
                .ok_or(Error::MissingConfiguration)?
                .as_ref()
                .clone(),
        );
        Ok(machine)
    }

    pub fn monitor(
        self: Arc<Self>,
        heartbeat_interval: Duration,
        tx: Option<mpsc::Sender<Result<Machine, Error>>>,
        mut cancel_rx: Option<mpsc::Receiver<()>>,
    ) -> BoxFuture<'static, ()> {
        async move {
            async fn send(
                tx: &Option<mpsc::Sender<Result<Machine, Error>>>,
                result: Result<Machine, Error>,
            ) {
                if let Some(tx) = tx {
                    let _ = tx.send(result).await;
                }
            }

            let mut interval = tokio::time::interval(heartbeat_interval);
            interval.tick().await;

            send(&tx, self.ping().await).await;

            loop {
                tokio::select! {
                    _ = interval.tick() => {
                        send(&tx, self.ping().await).await;
                    }
                    _ = async {
                        if let Some(ref mut rx) = cancel_rx {
                            rx.recv().await
                        } else {
                            std::future::pending::<Option<()>>().await
                        }
                    } => {
                        break;
                    }
                }
            }
        }
        .boxed()
    }

    /// Create a new machine
    #[cfg(feature = "token")]
    pub async fn create(request: MachineCreateRequest) -> Result<Machine, Error> {
        let config = get_config()?;
        let client = Client::new(ClientOptions::from(config))?;
        let mut attributes = serde_json::Map::new();
        attributes.insert("fingerprint".to_string(), json!(request.fingerprint));

        if let Some(name) = request.name {
            attributes.insert("name".to_string(), json!(name));
        }
        if let Some(platform) = request.platform {
            attributes.insert("platform".to_string(), json!(platform));
        }
        if let Some(hostname) = request.hostname {
            attributes.insert("hostname".to_string(), json!(hostname));
        }
        if let Some(ip) = request.ip {
            attributes.insert("ip".to_string(), json!(ip));
        }
        if let Some(cores) = request.cores {
            attributes.insert("cores".to_string(), json!(cores));
        }
        if let Some(metadata) = request.metadata {
            attributes.insert("metadata".to_string(), json!(metadata));
        }

        let body = json!({
            "data": {
                "type": "machines",
                "attributes": attributes,
                "relationships": {
                    "license": {
                        "data": {
                            "type": "licenses",
                            "id": request.license_id
                        }
                    }
                }
            }
        });

        let response = client.post("machines", Some(&body), None::<&()>).await?;
        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
        Ok(Machine::from(machine_response.data))
    }

    /// List machines with optional filters
    #[cfg(feature = "token")]
    pub async fn list(filters: Option<MachineListFilters>) -> Result<Vec<Machine>, Error> {
        let config = get_config()?;
        let client = Client::new(ClientOptions::from(config))?;
        let mut query_params = Vec::new();
        if let Some(filters) = filters {
            if let Some(license) = filters.license {
                query_params.push(("license".to_string(), license));
            }
            if let Some(user) = filters.user {
                query_params.push(("user".to_string(), user));
            }
            if let Some(platform) = filters.platform {
                query_params.push(("platform".to_string(), platform));
            }
            if let Some(name) = filters.name {
                query_params.push(("name".to_string(), name));
            }
            if let Some(fingerprint) = filters.fingerprint {
                query_params.push(("fingerprint".to_string(), fingerprint));
            }
            if let Some(ip) = filters.ip {
                query_params.push(("ip".to_string(), ip));
            }
            if let Some(hostname) = filters.hostname {
                query_params.push(("hostname".to_string(), hostname));
            }
            if let Some(product) = filters.product {
                query_params.push(("product".to_string(), product));
            }
            if let Some(owner) = filters.owner {
                query_params.push(("owner".to_string(), owner));
            }
            if let Some(group) = filters.group {
                query_params.push(("group".to_string(), group));
            }
            if let Some(metadata) = filters.metadata {
                for (key, value) in metadata {
                    query_params.push((format!("metadata[{key}]"), value.to_string()));
                }
            }
            // Add pagination parameters
            if let Some(page_number) = filters.page_number {
                query_params.push(("page[number]".to_string(), page_number.to_string()));
            }
            if let Some(page_size) = filters.page_size {
                query_params.push(("page[size]".to_string(), page_size.to_string()));
            }
            if let Some(limit) = filters.limit {
                query_params.push(("limit".to_string(), limit.to_string()));
            }
        }

        let query = if query_params.is_empty() {
            None
        } else {
            Some(
                query_params
                    .into_iter()
                    .collect::<HashMap<String, String>>(),
            )
        };

        let response = client.get("machines", query.as_ref()).await?;
        let machines_response: MachinesResponse = serde_json::from_value(response.body)?;
        Ok(machines_response
            .data
            .into_iter()
            .map(Machine::from)
            .collect())
    }

    /// Get a machine by ID
    #[cfg(feature = "token")]
    pub async fn get(id: &str) -> Result<Machine, Error> {
        let config = get_config()?;
        let client = Client::new(ClientOptions::from(config))?;
        let endpoint = format!("machines/{id}");
        let response = client.get(&endpoint, None::<&()>).await?;
        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
        Ok(Machine::from(machine_response.data))
    }

    /// Update a machine
    #[cfg(feature = "token")]
    pub async fn update(&self, request: MachineUpdateRequest) -> Result<Machine, Error> {
        let client = self.get_client()?;
        let endpoint = format!("machines/{}", self.id);

        let mut attributes = serde_json::Map::new();
        if let Some(name) = request.name {
            attributes.insert("name".to_string(), json!(name));
        }
        if let Some(platform) = request.platform {
            attributes.insert("platform".to_string(), json!(platform));
        }
        if let Some(hostname) = request.hostname {
            attributes.insert("hostname".to_string(), json!(hostname));
        }
        if let Some(ip) = request.ip {
            attributes.insert("ip".to_string(), json!(ip));
        }
        if let Some(cores) = request.cores {
            attributes.insert("cores".to_string(), json!(cores));
        }
        if let Some(metadata) = request.metadata {
            attributes.insert("metadata".to_string(), json!(metadata));
        }

        let body = json!({
            "data": {
                "type": "machines",
                "attributes": attributes
            }
        });

        let response = client.patch(&endpoint, Some(&body), None::<&()>).await?;
        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
        Ok(Machine::from(machine_response.data))
    }

    /// Reset machine heartbeat
    #[cfg(feature = "token")]
    pub async fn reset(&self) -> Result<Machine, Error> {
        let client = self.get_client()?;
        let endpoint = format!("machines/{}/actions/reset", self.id);
        let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
        let machine_response: MachineResponse = serde_json::from_value(response.body)?;
        Ok(Machine::from(machine_response.data))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        KeygenRelationship, KeygenRelationshipData, KeygenRelationships, KeygenResponseData,
    };
    use chrono::Utc;

    #[test]
    fn test_machine_relationships() {
        // Test that all relationship IDs are properly extracted
        let machine_data = KeygenResponseData {
            id: "test-machine-id".to_string(),
            r#type: "machines".to_string(),
            attributes: MachineAttributes {
                fingerprint: "test-fingerprint".to_string(),
                name: Some("Test Machine".to_string()),
                platform: Some("linux".to_string()),
                hostname: Some("test-host".to_string()),
                ip: Some("192.168.1.1".to_string()),
                cores: Some(8),
                metadata: Some(HashMap::new()),
                require_heartbeat: true,
                heartbeat_status: "ALIVE".to_string(),
                heartbeat_duration: Some(3600),
                created: Utc::now(),
                updated: Utc::now(),
            },
            relationships: KeygenRelationships {
                policy: None,
                account: Some(KeygenRelationship {
                    data: Some(KeygenRelationshipData {
                        r#type: "accounts".to_string(),
                        id: "test-account-id".to_string(),
                    }),
                    links: None,
                }),
                product: Some(KeygenRelationship {
                    data: Some(KeygenRelationshipData {
                        r#type: "products".to_string(),
                        id: "test-product-id".to_string(),
                    }),
                    links: None,
                }),
                group: Some(KeygenRelationship {
                    data: Some(KeygenRelationshipData {
                        r#type: "groups".to_string(),
                        id: "test-group-id".to_string(),
                    }),
                    links: None,
                }),
                owner: Some(KeygenRelationship {
                    data: Some(KeygenRelationshipData {
                        r#type: "users".to_string(),
                        id: "test-owner-id".to_string(),
                    }),
                    links: None,
                }),
                users: None,
                machines: None,
                environment: Some(KeygenRelationship {
                    data: Some(KeygenRelationshipData {
                        r#type: "environments".to_string(),
                        id: "test-environment-id".to_string(),
                    }),
                    links: None,
                }),
                license: Some(KeygenRelationship {
                    data: Some(KeygenRelationshipData {
                        r#type: "licenses".to_string(),
                        id: "test-license-id".to_string(),
                    }),
                    links: None,
                }),
                release: None,
                other: HashMap::new(),
            },
        };

        let machine = Machine::from(machine_data);

        assert_eq!(machine.account_id, Some("test-account-id".to_string()));
        assert_eq!(
            machine.environment_id,
            Some("test-environment-id".to_string())
        );
        assert_eq!(machine.product_id, Some("test-product-id".to_string()));
        assert_eq!(machine.license_id, Some("test-license-id".to_string()));
        assert_eq!(machine.owner_id, Some("test-owner-id".to_string()));
        assert_eq!(machine.group_id, Some("test-group-id".to_string()));
        assert_eq!(machine.id, "test-machine-id");
        assert_eq!(machine.fingerprint, "test-fingerprint");
    }

    #[test]
    fn test_machine_without_relationships() {
        // Test that all relationship IDs are None when no relationships exist
        let machine_data = KeygenResponseData {
            id: "test-machine-id".to_string(),
            r#type: "machines".to_string(),
            attributes: MachineAttributes {
                fingerprint: "test-fingerprint".to_string(),
                name: Some("Test Machine".to_string()),
                platform: Some("linux".to_string()),
                hostname: Some("test-host".to_string()),
                ip: Some("192.168.1.1".to_string()),
                cores: Some(8),
                metadata: Some(HashMap::new()),
                require_heartbeat: true,
                heartbeat_status: "ALIVE".to_string(),
                heartbeat_duration: Some(3600),
                created: Utc::now(),
                updated: Utc::now(),
            },
            relationships: KeygenRelationships {
                policy: None,
                account: None,
                product: None,
                group: None,
                owner: None,
                users: None,
                machines: None,
                environment: None,
                license: None,
                release: None,
                other: HashMap::new(),
            },
        };

        let machine = Machine::from(machine_data);

        assert_eq!(machine.account_id, None);
        assert_eq!(machine.environment_id, None);
        assert_eq!(machine.product_id, None);
        assert_eq!(machine.license_id, None);
        assert_eq!(machine.owner_id, None);
        assert_eq!(machine.group_id, None);
    }
}
