//! Nightlight server binary.
//!
//! Nightlight exposes:
//! - Prometheus remote_write ingestion over HTTP
//! - PromQL-compatible query endpoints over HTTP and gRPC
//! - gRPC admin endpoints for health and stats
//! - durable local storage backed by a WAL and snapshots

use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use anyhow::Result;
use axum::{routing::get, Router};
use clap::Parser;
use nightlight_api::nightlight::admin_server::AdminServer;
use nightlight_api::nightlight::metric_query_server::MetricQueryServer;
use tokio::time::MissedTickBehavior;
use tonic::transport::Server as TonicServer;
use tonic_health::server::health_reporter;
use tracing::{error, info, warn, Level};

mod config;
mod grpc;
mod ingestion;
mod query;
mod storage;

use config::{Config, StorageConfig};
use grpc::{AdminServiceImpl, MetricQueryServiceImpl};
use ingestion::IngestionService;
use query::QueryService;
use storage::Storage;

const DEFAULT_SNAPSHOT_INTERVAL_SECS: u64 = 30;

#[derive(Parser, Debug)]
#[command(name = "nightlight-server")]
struct Args {
    /// Configuration file path
    #[arg(short, long, default_value = "nightlight.yaml")]
    config: std::path::PathBuf,
}

#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();

    tracing_subscriber::fmt()
        .with_max_level(Level::INFO)
        .with_target(false)
        .with_thread_ids(true)
        .init();

    info!("Nightlight server starting");
    info!("Version: {}", env!("CARGO_PKG_VERSION"));

    let config = match Config::from_file(&args.config) {
        Ok(config) => {
            info!("Configuration loaded from {}", args.config.display());
            config
        }
        Err(error) => {
            info!(
                "Failed to load {}: {}, using defaults",
                args.config.display(),
                error
            );
            Config::default()
        }
    };

    if config.tls.is_some() {
        warn!("Nightlight TLS configuration is currently ignored; starting plaintext listeners");
    }

    let http_addr: SocketAddr = config.server.http_addr.parse()?;
    let grpc_addr: SocketAddr = config.server.grpc_addr.parse()?;
    let storage = Arc::new(Storage::new(&config.storage.data_dir)?);
    let query_service = QueryService::from_storage(storage.queryable());
    let ingestion_service = IngestionService::new(Arc::clone(&storage));
    let admin_service = AdminServiceImpl::new(
        Arc::clone(&storage),
        ingestion_service.metrics(),
        query_service.metrics(),
    );
    let metric_query_service = MetricQueryServiceImpl::new(query_service.clone());

    info!("Server configuration:");
    info!("  HTTP address: {}", http_addr);
    info!("  gRPC address: {}", grpc_addr);
    info!("  Data directory: {}", config.storage.data_dir);
    info!("  Retention: {} days", config.storage.retention_days);
    info!(
        "  Compaction interval: {} seconds",
        config.storage.compaction_interval_seconds
    );

    let http_listener = tokio::net::TcpListener::bind(http_addr).await?;
    let http_app = Router::new()
        .route("/healthz", get(healthz))
        .merge(ingestion_service.clone().router())
        .merge(query_service.clone().router());

    let (mut health_reporter, health_service) = health_reporter();
    health_reporter
        .set_serving::<MetricQueryServer<MetricQueryServiceImpl>>()
        .await;
    health_reporter
        .set_serving::<AdminServer<AdminServiceImpl>>()
        .await;

    let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
    let mut http_shutdown = shutdown_tx.subscribe();
    let mut grpc_shutdown = shutdown_tx.subscribe();
    let maintenance_shutdown = shutdown_tx.subscribe();

    let http_server = async move {
        axum::serve(http_listener, http_app)
            .with_graceful_shutdown(async move {
                let _ = http_shutdown.recv().await;
            })
            .await
    };

    let grpc_server = TonicServer::builder()
        .add_service(health_service)
        .add_service(MetricQueryServer::new(metric_query_service))
        .add_service(AdminServer::new(admin_service))
        .serve_with_shutdown(grpc_addr, async move {
            let _ = grpc_shutdown.recv().await;
        });

    let maintenance_handle = tokio::spawn(maintenance_loop(
        Arc::clone(&storage),
        config.storage.clone(),
        maintenance_shutdown,
    ));

    info!("HTTP server listening on {}", http_addr);
    info!("  - Ingestion: POST /api/v1/write");
    info!("  - Query: GET /api/v1/query, /api/v1/query_range");
    info!("  - Metadata: GET /api/v1/series, /api/v1/label/:name/values");
    info!("  - Health: GET /healthz");
    info!("gRPC server listening on {}", grpc_addr);
    info!("  - MetricQuery.InstantQuery / RangeQuery / SeriesQuery / LabelValuesQuery");
    info!("  - Admin.Health / Stats / BuildInfo");

    let shutdown = async {
        tokio::signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };
    tokio::pin!(shutdown);

    tokio::select! {
        result = http_server => {
            result?;
        }
        result = grpc_server => {
            result?;
        }
        _ = &mut shutdown => {
            info!("Shutdown signal received");
        }
    }

    let _ = shutdown_tx.send(());
    if let Err(error) = maintenance_handle.await {
        error!(error = %error, "Nightlight maintenance task failed to join");
    }

    info!("Nightlight server stopped");
    Ok(())
}

async fn maintenance_loop(
    storage: Arc<Storage>,
    config: StorageConfig,
    mut shutdown: tokio::sync::broadcast::Receiver<()>,
) {
    let snapshot_interval_secs = config
        .compaction_interval_seconds
        .clamp(5, DEFAULT_SNAPSHOT_INTERVAL_SECS);
    let mut snapshot_interval = tokio::time::interval(Duration::from_secs(snapshot_interval_secs));
    snapshot_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);

    let mut retention_interval = tokio::time::interval(Duration::from_secs(
        config.compaction_interval_seconds.max(1),
    ));
    retention_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);

    loop {
        tokio::select! {
            _ = shutdown.recv() => break,
            _ = snapshot_interval.tick() => {
                if let Err(error) = storage.flush().await {
                    error!(error = %error, "Nightlight snapshot flush failed");
                }
            }
            _ = retention_interval.tick() => {
                if let Err(error) = storage.enforce_retention(config.retention_days).await {
                    error!(error = %error, "Nightlight retention sweep failed");
                }
                if let Err(error) = storage.compact().await {
                    error!(error = %error, "Nightlight compaction checkpoint failed");
                }
            }
        }
    }

    if let Err(error) = storage.flush().await {
        error!(error = %error, "Nightlight final snapshot flush failed");
    }
}

async fn healthz() -> &'static str {
    "ok"
}
