//! Admin API endpoints for node management.

use axum::{extract::State, http::HeaderMap, http::StatusCode, Json};
use chrono::{DateTime, Utc};
use deployer_types::NodeConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
use tracing::{debug, error, info};

use crate::auth::require_admin_auth;
use crate::state::AppState;
use crate::storage::StorageError;
use crate::validation::{validate_identifier, validate_ip};

fn is_stale_heartbeat(last_heartbeat: Option<DateTime<Utc>>, timeout_secs: u64) -> bool {
    if timeout_secs == 0 {
        return false;
    }
    let Some(last) = last_heartbeat else {
        return false;
    };
    let elapsed = Utc::now().signed_duration_since(last).num_seconds();
    elapsed > timeout_secs as i64
}

fn adjust_state_for_heartbeat(
    state: String,
    last_heartbeat: Option<DateTime<Utc>>,
    timeout_secs: u64,
) -> String {
    if is_stale_heartbeat(last_heartbeat, timeout_secs) {
        if matches!(state.as_str(), "failed" | "draining") {
            return state;
        }
        return "failed".to_string();
    }
    state
}

/// Pre-registration request payload.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreRegisterRequest {
    /// Machine ID (from /etc/machine-id)
    pub machine_id: String,
    /// Canonical bootstrap configuration that should be served back during phone-home.
    pub node_config: NodeConfig,
}

/// Pre-registration response payload.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreRegisterResponse {
    pub success: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    pub machine_id: String,
    pub node_id: String,
}

/// List nodes response payload.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListNodesResponse {
    pub nodes: Vec<NodeSummary>,
    pub total: usize,
}

/// Node summary for listing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeSummary {
    pub node_id: String,
    pub hostname: String,
    pub ip: String,
    pub role: String,
    pub state: String,
}

/// POST /api/v1/admin/nodes
pub async fn pre_register(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
    Json(request): Json<PreRegisterRequest>,
) -> Result<Json<PreRegisterResponse>, (StatusCode, String)> {
    require_admin_auth(&state, &headers)?;
    validate_identifier(&request.machine_id, "machine_id")?;
    validate_identifier(&request.node_config.assignment.node_id, "node_id")?;
    if !request.node_config.assignment.ip.is_empty() {
        validate_ip(&request.node_config.assignment.ip, "ip")?;
    }

    let node_id = request.node_config.assignment.node_id.clone();
    info!(
        machine_id = %request.machine_id,
        node_id = %node_id,
        role = %request.node_config.assignment.role,
        "Pre-registration request"
    );

    if let Some(local_storage) = &state.local_storage {
        let storage = local_storage.lock().await;
        if let Some(existing) = storage.get_node_config(&request.machine_id) {
            if existing.assignment.node_id != node_id {
                return Err((
                    StatusCode::CONFLICT,
                    format!(
                        "machine_id {} already mapped to {}",
                        request.machine_id, existing.assignment.node_id
                    ),
                ));
            }
        }
    }

    if let Some(storage_mutex) = &state.storage {
        let mut storage = storage_mutex.lock().await;
        if let Some(existing) = storage
            .get_node_config(&request.machine_id)
            .await
            .map_err(|e| {
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("Failed to check node mapping: {}", e),
                )
            })?
        {
            if existing.assignment.node_id != node_id {
                return Err((
                    StatusCode::CONFLICT,
                    format!(
                        "machine_id {} already mapped to {}",
                        request.machine_id, existing.assignment.node_id
                    ),
                ));
            }
        }
    }

    {
        let map = state.machine_configs.read().await;
        if let Some(existing) = map.get(&request.machine_id) {
            if existing.assignment.node_id != node_id {
                return Err((
                    StatusCode::CONFLICT,
                    format!(
                        "machine_id {} already mapped to {}",
                        request.machine_id, existing.assignment.node_id
                    ),
                ));
            }
        }
    }

    let mut stored_locally = false;
    if let Some(local_storage) = &state.local_storage {
        let mut storage = local_storage.lock().await;
        if let Err(e) = storage.register_node(&request.machine_id, &request.node_config) {
            error!(
                machine_id = %request.machine_id,
                error = %e,
                "Failed to pre-register in local storage"
            );
        } else {
            stored_locally = true;
            info!(
                machine_id = %request.machine_id,
                node_id = %node_id,
                "Node pre-registered in local storage"
            );
        }
    }

    if let Some(storage_mutex) = &state.storage {
        let mut storage = storage_mutex.lock().await;
        match storage
            .register_node(&request.machine_id, &request.node_config)
            .await
        {
            Ok(_) => {
                info!(
                    machine_id = %request.machine_id,
                    node_id = %node_id,
                    "Node pre-registered in ChainFire"
                );
                return Ok(Json(PreRegisterResponse {
                    success: true,
                    message: Some("Node pre-registered successfully".to_string()),
                    machine_id: request.machine_id,
                    node_id,
                }));
            }
            Err(StorageError::Conflict(msg)) => {
                return Err((StatusCode::CONFLICT, msg));
            }
            Err(e) => {
                error!(
                    machine_id = %request.machine_id,
                    error = %e,
                    "Failed to pre-register in ChainFire"
                );
                if !stored_locally {
                    return Err((
                        StatusCode::INTERNAL_SERVER_ERROR,
                        format!("Failed to pre-register node: {}", e),
                    ));
                }
            }
        }
    }

    state
        .machine_configs
        .write()
        .await
        .insert(request.machine_id.clone(), request.node_config.clone());

    debug!(
        machine_id = %request.machine_id,
        node_id = %node_id,
        "Node pre-registered in-memory (ChainFire unavailable)"
    );

    Ok(Json(PreRegisterResponse {
        success: true,
        message: Some(if stored_locally {
            "Node pre-registered (local storage)".to_string()
        } else {
            "Node pre-registered (in-memory)".to_string()
        }),
        machine_id: request.machine_id,
        node_id,
    }))
}

/// GET /api/v1/admin/nodes
pub async fn list_nodes(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
) -> Result<Json<ListNodesResponse>, (StatusCode, String)> {
    require_admin_auth(&state, &headers)?;

    debug!("Listing all nodes");

    let mut nodes = Vec::new();
    let mut seen = HashSet::new();

    let cluster_id = state
        .config
        .cluster_id
        .as_deref()
        .map(|id| id.trim())
        .filter(|id| !id.is_empty());
    let cluster_namespace = state.config.cluster_namespace.trim();
    let cluster_enabled = cluster_id.is_some() && !cluster_namespace.is_empty();

    if cluster_enabled {
        if let Some(storage_mutex) = &state.storage {
            let mut storage = storage_mutex.lock().await;
            match storage
                .list_cluster_nodes(cluster_namespace, cluster_id.unwrap())
                .await
            {
                Ok(cluster_nodes) => {
                    for record in cluster_nodes {
                        let role = if record.roles.is_empty() {
                            "unknown".to_string()
                        } else {
                            record.roles.join(",")
                        };
                        let node_state = adjust_state_for_heartbeat(
                            record.state.unwrap_or_else(|| "unknown".to_string()),
                            record.last_heartbeat,
                            state.config.heartbeat_timeout_secs,
                        );
                        nodes.push(NodeSummary {
                            node_id: record.node_id.clone(),
                            hostname: record.hostname.clone(),
                            ip: record.ip.clone(),
                            role,
                            state: node_state,
                        });
                        seen.insert(record.node_id);
                    }
                }
                Err(e) => {
                    error!(error = %e, "Failed to list cluster nodes from ChainFire");
                }
            }
        }
    }

    if cluster_enabled {
        if let Some(local_storage) = &state.local_storage {
            let storage = local_storage.lock().await;
            let cluster_nodes = storage.list_cluster_nodes(cluster_namespace, cluster_id.unwrap());
            for record in cluster_nodes {
                if seen.contains(&record.node_id) {
                    continue;
                }
                let role = if record.roles.is_empty() {
                    "unknown".to_string()
                } else {
                    record.roles.join(",")
                };
                let node_state = adjust_state_for_heartbeat(
                    record.state.unwrap_or_else(|| "unknown".to_string()),
                    record.last_heartbeat,
                    state.config.heartbeat_timeout_secs,
                );
                nodes.push(NodeSummary {
                    node_id: record.node_id.clone(),
                    hostname: record.hostname.clone(),
                    ip: record.ip.clone(),
                    role,
                    state: node_state,
                });
                seen.insert(record.node_id);
            }
        }
    }

    if let Some(storage_mutex) = &state.storage {
        let mut storage = storage_mutex.lock().await;
        match storage.list_nodes().await {
            Ok(node_infos) => {
                for info in node_infos {
                    if seen.contains(&info.id) {
                        continue;
                    }
                    let node_id = info.id.clone();
                    let node_state = adjust_state_for_heartbeat(
                        format!("{:?}", info.state).to_lowercase(),
                        Some(info.last_heartbeat),
                        state.config.heartbeat_timeout_secs,
                    );
                    nodes.push(NodeSummary {
                        node_id: node_id.clone(),
                        hostname: info.hostname,
                        ip: info.ip,
                        role: info
                            .metadata
                            .get("role")
                            .cloned()
                            .unwrap_or_else(|| "unknown".to_string()),
                        state: node_state,
                    });
                    seen.insert(node_id);
                }
            }
            Err(e) => {
                error!(error = %e, "Failed to list nodes from ChainFire");
            }
        }
    }

    if let Some(local_storage) = &state.local_storage {
        let storage = local_storage.lock().await;
        for info in storage.list_nodes() {
            if seen.contains(&info.id) {
                continue;
            }
            let node_id = info.id.clone();
            let node_state = adjust_state_for_heartbeat(
                format!("{:?}", info.state).to_lowercase(),
                Some(info.last_heartbeat),
                state.config.heartbeat_timeout_secs,
            );
            nodes.push(NodeSummary {
                node_id: node_id.clone(),
                hostname: info.hostname,
                ip: info.ip,
                role: info
                    .metadata
                    .get("role")
                    .cloned()
                    .unwrap_or_else(|| "unknown".to_string()),
                state: node_state,
            });
            seen.insert(node_id);
        }
    }

    let in_memory = state.nodes.read().await;
    for info in in_memory.values() {
        if seen.contains(&info.id) {
            continue;
        }
        let state_value = adjust_state_for_heartbeat(
            format!("{:?}", info.state).to_lowercase(),
            Some(info.last_heartbeat),
            state.config.heartbeat_timeout_secs,
        );
        nodes.push(NodeSummary {
            node_id: info.id.clone(),
            hostname: info.hostname.clone(),
            ip: info.ip.clone(),
            role: info
                .metadata
                .get("role")
                .cloned()
                .unwrap_or_else(|| "unknown".to_string()),
            state: state_value,
        });
        seen.insert(info.id.clone());
    }

    if let Some(storage_mutex) = &state.storage {
        let mut storage = storage_mutex.lock().await;
        match storage.list_machine_configs().await {
            Ok(configs) => {
                for (_machine_id, config) in configs {
                    let node_id = config.assignment.node_id.clone();
                    if seen.contains(&node_id) {
                        continue;
                    }
                    nodes.push(NodeSummary {
                        node_id: node_id.clone(),
                        hostname: config.assignment.hostname.clone(),
                        ip: config.assignment.ip.clone(),
                        role: config.assignment.role.clone(),
                        state: "pre-registered".to_string(),
                    });
                    seen.insert(node_id);
                }
            }
            Err(e) => {
                error!(error = %e, "Failed to list pre-registered nodes from ChainFire");
            }
        }
    }

    if let Some(local_storage) = &state.local_storage {
        let storage = local_storage.lock().await;
        for (_machine_id, config) in storage.list_machine_configs() {
            let node_id = config.assignment.node_id.clone();
            if seen.contains(&node_id) {
                continue;
            }
            nodes.push(NodeSummary {
                node_id: node_id.clone(),
                hostname: config.assignment.hostname.clone(),
                ip: config.assignment.ip.clone(),
                role: config.assignment.role.clone(),
                state: "pre-registered".to_string(),
            });
            seen.insert(node_id);
        }
    }

    let configs = state.machine_configs.read().await;
    for config in configs.values() {
        let node_id = config.assignment.node_id.clone();
        if seen.contains(&node_id) {
            continue;
        }
        nodes.push(NodeSummary {
            node_id: node_id.clone(),
            hostname: config.assignment.hostname.clone(),
            ip: config.assignment.ip.clone(),
            role: config.assignment.role.clone(),
            state: "pre-registered".to_string(),
        });
        seen.insert(node_id);
    }

    let total = nodes.len();
    Ok(Json(ListNodesResponse { nodes, total }))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::Config;
    use crate::state::AppState;
    use axum::http::HeaderMap;
    use deployer_types::{BootstrapPlan, BootstrapSecrets, NodeAssignment};

    fn test_headers() -> HeaderMap {
        let mut headers = HeaderMap::new();
        headers.insert("x-deployer-token", "test-token".parse().unwrap());
        headers
    }

    fn test_state() -> Arc<AppState> {
        let mut config = Config::default();
        config.bootstrap_token = Some("test-token".to_string());
        config.admin_token = Some("test-token".to_string());
        Arc::new(AppState::with_config(config))
    }

    fn test_node_config() -> NodeConfig {
        NodeConfig::from_parts(
            NodeAssignment {
                node_id: "node-test".to_string(),
                hostname: "node-test".to_string(),
                role: "worker".to_string(),
                ip: "10.0.1.50".to_string(),
                labels: std::collections::HashMap::new(),
                pool: None,
                node_class: None,
                failure_domain: None,
            },
            BootstrapPlan {
                services: vec!["chainfire".to_string()],
                nix_profile: None,
                install_plan: None,
            },
            BootstrapSecrets {
                ssh_authorized_keys: vec!["ssh-ed25519 AAAA... test".to_string()],
                ssh_host_key: None,
                tls_cert: None,
                tls_key: None,
            },
        )
    }

    #[tokio::test]
    async fn test_pre_register() {
        let state = test_state();

        let request = PreRegisterRequest {
            machine_id: "new-machine-abc".to_string(),
            node_config: test_node_config(),
        };

        let result =
            pre_register(State(state.clone()), test_headers(), Json(request.clone())).await;
        assert!(result.is_ok());

        let response = result.unwrap().0;
        assert!(response.success);
        assert_eq!(response.machine_id, "new-machine-abc");
        assert_eq!(response.node_id, "node-test");

        let configs = state.machine_configs.read().await;
        let config = configs.get("new-machine-abc").expect("stored config");
        assert_eq!(config.assignment.node_id, "node-test");
        assert_eq!(config.assignment.role, "worker");
    }

    #[tokio::test]
    async fn test_list_nodes_empty() {
        let state = test_state();

        let result = list_nodes(State(state), test_headers()).await;
        assert!(result.is_ok());

        let response = result.unwrap().0;
        assert_eq!(response.total, 0);
        assert!(response.nodes.is_empty());
    }
}
