//! LightningStor storage node daemon

use clap::Parser;
use lightningstor_node::{
    proto::node_service_server::NodeServiceServer, LocalChunkStore, NodeConfig, NodeServiceImpl,
};
use metrics_exporter_prometheus::PrometheusBuilder;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tonic::transport::Server;
use tonic_health::server::health_reporter;
use tracing_subscriber::EnvFilter;

const MAX_NODE_GRPC_MESSAGE_SIZE: usize = 1024 * 1024 * 1024;
const NODE_GRPC_INITIAL_STREAM_WINDOW: u32 = 64 * 1024 * 1024;
const NODE_GRPC_INITIAL_CONNECTION_WINDOW: u32 = 512 * 1024 * 1024;
const NODE_GRPC_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);
const NODE_GRPC_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(10);

/// LightningStor storage node
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Configuration file path
    #[arg(short, long, default_value = "lightningstor-node.toml")]
    config: PathBuf,

    /// Node ID (overrides config)
    #[arg(long, env = "LIGHTNINGSTOR_NODE_ID")]
    node_id: Option<String>,

    /// gRPC address to listen on (overrides config)
    #[arg(long)]
    grpc_addr: Option<String>,

    /// Data directory (overrides config)
    #[arg(long)]
    data_dir: Option<PathBuf>,

    /// Zone identifier (overrides config)
    #[arg(long)]
    zone: Option<String>,

    /// Region identifier (overrides config)
    #[arg(long)]
    region: Option<String>,

    /// Log level (overrides config)
    #[arg(short, long)]
    log_level: Option<String>,

    /// Maximum storage capacity in bytes (overrides config)
    #[arg(long)]
    max_capacity: Option<u64>,

    /// Metrics port for Prometheus scraping
    #[arg(long)]
    metrics_port: Option<u16>,

    /// Flush chunk data before acknowledging writes
    #[arg(long)]
    sync_on_write: Option<bool>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();

    // Load configuration from file or use defaults
    let mut config = if args.config.exists() {
        let contents = tokio::fs::read_to_string(&args.config).await?;
        toml::from_str(&contents)?
    } else {
        eprintln!(
            "Config file not found: {}, using defaults",
            args.config.display()
        );
        NodeConfig::default()
    };

    // Apply command line overrides
    if let Some(node_id) = args.node_id {
        config.node_id = node_id;
    }
    if let Some(grpc_addr) = args.grpc_addr {
        config.grpc_addr = grpc_addr.parse()?;
    }
    if let Some(data_dir) = args.data_dir {
        config.data_dir = data_dir;
    }
    if let Some(zone) = args.zone {
        config.zone = zone;
    }
    if let Some(region) = args.region {
        config.region = region;
    }
    if let Some(log_level) = args.log_level {
        config.log_level = log_level;
    }
    if let Some(max_capacity) = args.max_capacity {
        config.max_capacity_bytes = max_capacity;
    }
    if let Some(metrics_port) = args.metrics_port {
        config.metrics_port = metrics_port;
    }
    if let Some(sync_on_write) = args.sync_on_write {
        config.sync_on_write = sync_on_write;
    }

    // Initialize tracing
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new(&config.log_level)),
        )
        .init();

    tracing::info!("Starting LightningStor storage node");
    tracing::info!("  Node ID: {}", config.node_id);
    tracing::info!("  gRPC: {}", config.grpc_addr);
    tracing::info!("  Data dir: {}", config.data_dir.display());
    if !config.zone.is_empty() {
        tracing::info!("  Zone: {}", config.zone);
    }
    if !config.region.is_empty() {
        tracing::info!("  Region: {}", config.region);
    }
    if config.max_capacity_bytes > 0 {
        tracing::info!(
            "  Max capacity: {} bytes",
            config.max_capacity_bytes
        );
    }
    tracing::info!("  Sync on write: {}", config.sync_on_write);

    // Initialize Prometheus metrics exporter
    let metrics_addr = format!("0.0.0.0:{}", config.metrics_port);
    let builder = PrometheusBuilder::new();
    builder
        .with_http_listener(metrics_addr.parse::<std::net::SocketAddr>()?)
        .install()
        .expect("Failed to install Prometheus metrics exporter");

    tracing::info!(
        "Prometheus metrics available at http://{}/metrics",
        metrics_addr
    );

    // Create local chunk store
    let store = Arc::new(
        LocalChunkStore::new(
            config.data_dir.clone(),
            config.max_capacity_bytes,
            config.sync_on_write,
        )
            .await
            .expect("Failed to create chunk store"),
    );

    tracing::info!(
        "Chunk store initialized: {} chunks, {} bytes",
        store.chunk_count(),
        store.total_bytes()
    );

    // Create service
    let config = Arc::new(config);
    let service = NodeServiceImpl::new(store.clone(), config.clone());

    // Setup health service
    let (mut health_reporter, health_service) = health_reporter();
    health_reporter
        .set_serving::<NodeServiceServer<NodeServiceImpl>>()
        .await;

    // Start gRPC server
    let addr = config.grpc_addr;
    tracing::info!("gRPC server listening on {}", addr);

    Server::builder()
        .tcp_nodelay(true)
        .initial_stream_window_size(NODE_GRPC_INITIAL_STREAM_WINDOW)
        .initial_connection_window_size(NODE_GRPC_INITIAL_CONNECTION_WINDOW)
        .http2_keepalive_interval(Some(NODE_GRPC_KEEPALIVE_INTERVAL))
        .http2_keepalive_timeout(Some(NODE_GRPC_KEEPALIVE_TIMEOUT))
        .add_service(health_service)
        .add_service(
            NodeServiceServer::new(service)
                .max_decoding_message_size(MAX_NODE_GRPC_MESSAGE_SIZE)
                .max_encoding_message_size(MAX_NODE_GRPC_MESSAGE_SIZE),
        )
        .serve(addr)
        .await?;

    Ok(())
}
