alopex-cli 0.6.0

Command-line interface for Alopex DB
//! Server management commands.

use std::io::Write;

use serde::Deserialize;

use crate::cli::{CompactionCommand, OutputFormat, ServerCommand};
use crate::client::http::{ClientError, HttpClient};
use crate::error::{CliError, Result};
use crate::models::{Column, Row};
use crate::output::server as server_output;
use crate::output::table::TableFormatter;
use crate::output::Formatter;
use crate::tui::renderer::render_output;

#[derive(Debug, Deserialize)]
struct ServerStatusResponse {
    version: Option<String>,
    uptime_secs: Option<u64>,
    connections: Option<u64>,
    queries_per_second: Option<f64>,
}

#[derive(Debug, Deserialize)]
struct ServerMetricsResponse {
    qps: Option<f64>,
    avg_latency_ms: Option<f64>,
    p99_latency_ms: Option<f64>,
    memory_usage_mb: Option<u64>,
    active_connections: Option<u64>,
}

#[derive(Debug, Deserialize)]
struct ServerHealthResponse {
    status: Option<String>,
    message: Option<String>,
}

#[derive(Debug, Deserialize)]
struct ServerCompactionResponse {
    success: Option<bool>,
    message: Option<String>,
}

/// Execute a server management command against a remote server.
pub async fn execute_remote<W: Write>(
    client: &HttpClient,
    cmd: &ServerCommand,
    writer: &mut W,
    quiet: bool,
) -> Result<()> {
    match cmd {
        ServerCommand::Status => {
            let response: ServerStatusResponse = client
                .get_json("api/admin/status")
                .await
                .map_err(map_client_error)?;
            if quiet {
                return Ok(());
            }
            render_table(
                writer,
                server_output::status_columns(),
                vec![server_output::status_row(
                    response.version.as_deref(),
                    response.uptime_secs,
                    response.connections,
                    response.queries_per_second,
                )],
            )
        }
        ServerCommand::Metrics => {
            let response: ServerMetricsResponse = client
                .get_json("api/admin/metrics")
                .await
                .map_err(map_client_error)?;
            if quiet {
                return Ok(());
            }
            render_table(
                writer,
                server_output::metrics_columns(),
                vec![server_output::metrics_row(
                    response.qps,
                    response.avg_latency_ms,
                    response.p99_latency_ms,
                    response.memory_usage_mb,
                    response.active_connections,
                )],
            )
        }
        ServerCommand::Health => {
            let response: ServerHealthResponse = client
                .get_json("api/admin/health")
                .await
                .map_err(map_client_error)?;
            if quiet {
                return Ok(());
            }
            render_table(
                writer,
                server_output::health_columns(),
                vec![server_output::health_row(
                    response.status.as_deref(),
                    response.message.as_deref(),
                )],
            )
        }
        ServerCommand::Compaction { command } => match command {
            CompactionCommand::Trigger => {
                let request = serde_json::json!({});
                let response: ServerCompactionResponse = client
                    .post_json("api/admin/compaction", &request)
                    .await
                    .map_err(map_client_error)?;
                if quiet {
                    return Ok(());
                }
                render_table(
                    writer,
                    server_output::compaction_columns(),
                    vec![server_output::compaction_row(
                        response.success,
                        response.message.as_deref(),
                    )],
                )
            }
        },
    }
}

pub async fn execute_remote_tui(
    client: &HttpClient,
    cmd: &ServerCommand,
    quiet: bool,
    connection_label: impl Into<String>,
    output_format: OutputFormat,
    admin_launcher: Option<Box<dyn FnMut() -> Result<()> + '_>>,
) -> Result<()> {
    match cmd {
        ServerCommand::Status => {
            let response: ServerStatusResponse = client
                .get_json("api/admin/status")
                .await
                .map_err(map_client_error)?;
            if quiet {
                return Ok(());
            }
            render_output(
                server_output::status_columns(),
                vec![server_output::status_row(
                    response.version.as_deref(),
                    response.uptime_secs,
                    response.connections,
                    response.queries_per_second,
                )],
                connection_label,
                Some(server_command_context(cmd)),
                true,
                None,
                output_format,
                admin_launcher,
            )
        }
        ServerCommand::Metrics => {
            let response: ServerMetricsResponse = client
                .get_json("api/admin/metrics")
                .await
                .map_err(map_client_error)?;
            if quiet {
                return Ok(());
            }
            render_output(
                server_output::metrics_columns(),
                vec![server_output::metrics_row(
                    response.qps,
                    response.avg_latency_ms,
                    response.p99_latency_ms,
                    response.memory_usage_mb,
                    response.active_connections,
                )],
                connection_label,
                Some(server_command_context(cmd)),
                true,
                None,
                output_format,
                admin_launcher,
            )
        }
        ServerCommand::Health => {
            let response: ServerHealthResponse = client
                .get_json("api/admin/health")
                .await
                .map_err(map_client_error)?;
            if quiet {
                return Ok(());
            }
            render_output(
                server_output::health_columns(),
                vec![server_output::health_row(
                    response.status.as_deref(),
                    response.message.as_deref(),
                )],
                connection_label,
                Some(server_command_context(cmd)),
                true,
                None,
                output_format,
                admin_launcher,
            )
        }
        ServerCommand::Compaction { command } => match command {
            CompactionCommand::Trigger => {
                let request = serde_json::json!({});
                let response: ServerCompactionResponse = client
                    .post_json("api/admin/compaction", &request)
                    .await
                    .map_err(map_client_error)?;
                if quiet {
                    return Ok(());
                }
                render_output(
                    server_output::compaction_columns(),
                    vec![server_output::compaction_row(
                        response.success,
                        response.message.as_deref(),
                    )],
                    connection_label,
                    Some(server_command_context(cmd)),
                    true,
                    None,
                    output_format,
                    admin_launcher,
                )
            }
        },
    }
}

fn server_command_context(cmd: &ServerCommand) -> String {
    match cmd {
        ServerCommand::Status => "server status".to_string(),
        ServerCommand::Metrics => "server metrics".to_string(),
        ServerCommand::Health => "server health".to_string(),
        ServerCommand::Compaction { .. } => "server compaction trigger".to_string(),
    }
}

fn render_table<W: Write>(writer: &mut W, columns: Vec<Column>, rows: Vec<Row>) -> Result<()> {
    let mut formatter = TableFormatter::new();
    formatter.write_header(writer, &columns)?;
    for row in rows {
        formatter.write_row(writer, &row)?;
    }
    formatter.write_footer(writer)
}

fn map_client_error(err: ClientError) -> CliError {
    match err {
        ClientError::Request { source, .. } => {
            CliError::ServerConnection(format!("request failed: {source}"))
        }
        ClientError::InvalidUrl(message) => CliError::InvalidArgument(message),
        ClientError::Build(message) => CliError::InvalidArgument(message),
        ClientError::Auth(err) => CliError::InvalidArgument(err.to_string()),
        ClientError::HttpStatus { status, body } => {
            CliError::InvalidArgument(format!("Server error: HTTP {} - {}", status.as_u16(), body))
        }
    }
}