//! Gateway service for HTTP request routing.
//!
//! The Gateway provides a stateless HTTP interface for clients, routing
//! requests to the appropriate shard nodes based on subject hashing.

use axum::http::{header, Method};
use axum::routing::{get, post};
use axum::{Extension, Router};
use dashmap::DashMap;
use metrics_exporter_prometheus::PrometheusHandle;
use parking_lot::RwLock;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::Notify;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tracing::{info, instrument};

use crate::gateway::handlers;
use crate::membership::{NodeId, SwimMembership};
use crate::sharding::RangeRouter;
use crate::{ClusterError, Result};

/// Shared state for gateway handlers.
pub struct GatewayState {
    /// Router for subject-to-shard mapping.
    pub router: Arc<RangeRouter>,

    /// Membership for discovering nodes.
    pub membership: Arc<SwimMembership>,

    /// RPC client pool (node ID -> client).
    /// In a full implementation, these would be gRPC clients.
    pub rpc_clients: DashMap<NodeId, ()>,

    /// Request counter for metrics.
    pub request_count: AtomicU64,

    /// Notify handles for anti-entropy workers (one per peer).
    /// Used by the /v1/admin/sync endpoint to trigger immediate sync.
    pub sync_notifiers: RwLock<Vec<Arc<Notify>>>,
}

impl GatewayState {
    /// Creates a new gateway state.
    pub fn new(router: Arc<RangeRouter>, membership: Arc<SwimMembership>) -> Self {
        Self {
            router,
            membership,
            rpc_clients: DashMap::new(),
            request_count: AtomicU64::new(0),
            sync_notifiers: RwLock::new(Vec::new()),
        }
    }

    /// Increments and returns the request count.
    pub fn inc_requests(&self) -> u64 {
        self.request_count.fetch_add(1, Ordering::Relaxed)
    }

    /// Registers an anti-entropy worker's notify handle.
    ///
    /// This allows the admin API to trigger immediate sync cycles.
    pub fn register_sync_notifier(&self, notify: Arc<Notify>) {
        self.sync_notifiers.write().push(notify);
    }
}

/// Stateless gateway for routing client requests to shards.
///
/// The gateway:
/// - Validates incoming requests
/// - Routes by subject hash to determine shard
/// - Forwards writes to shard leaders
/// - Load balances reads across replicas
/// - Provides cluster status endpoints
/// - Exposes Prometheus metrics at /metrics
/// - Provides admin endpoints for observability
pub struct Gateway {
    /// Shared state for handlers.
    state: Arc<GatewayState>,

    /// Bind address for the HTTP server.
    bind_addr: SocketAddr,

    /// Prometheus handle for /metrics endpoint.
    prometheus_handle: Option<Arc<PrometheusHandle>>,
}

impl Gateway {
    /// Creates a new gateway.
    pub fn new(
        router: Arc<RangeRouter>,
        membership: Arc<SwimMembership>,
        bind_addr: SocketAddr,
    ) -> Self {
        let state = Arc::new(GatewayState::new(router, membership));
        Self { state, bind_addr, prometheus_handle: None }
    }

    /// Sets the Prometheus handle for metrics export.
    pub fn with_prometheus_handle(mut self, handle: Arc<PrometheusHandle>) -> Self {
        self.prometheus_handle = Some(handle);
        self
    }

    /// Returns the axum router for this gateway.
    pub fn router(&self) -> Router {
        let router = Router::new()
            // Assertion endpoints
            .route("/v1/assert", post(handlers::handle_assert))
            .route("/v1/query", get(handlers::handle_query))
            .route("/v1/vote", post(handlers::handle_vote))
            // Cluster endpoints
            .route("/v1/health", get(handlers::handle_health))
            .route("/v1/cluster/status", get(handlers::handle_cluster_status))
            .route("/v1/shards/:shard_id", get(handlers::handle_shard_info))
            .route("/v1/route", get(handlers::handle_route_test))
            // Admin endpoints (Phase 8B Observability)
            .route("/v1/admin/cluster", get(handlers::handle_cluster_status))
            .route("/v1/admin/ranges", get(handlers::handle_ranges))
            .route("/v1/admin/sync", post(handlers::handle_force_sync))
            // Prometheus metrics endpoint
            .route("/metrics", get(handlers::handle_metrics))
            // Middleware
            .layer(TraceLayer::new_for_http())
            .layer(
                CorsLayer::new()
                    .allow_methods([Method::GET, Method::POST])
                    .allow_headers([header::CONTENT_TYPE]),
            )
            // State
            .with_state(self.state.clone());

        // Add Prometheus handle as Extension if available
        if let Some(handle) = &self.prometheus_handle {
            router.layer(Extension(handle.clone()))
        } else {
            router
        }
    }

    /// Starts the gateway HTTP server.
    ///
    /// This blocks until the server is shut down.
    #[instrument(skip(self), fields(addr = %self.bind_addr))]
    pub async fn serve(self) -> Result<()> {
        let listener = TcpListener::bind(self.bind_addr).await.map_err(|e| {
            ClusterError::Network(format!("Failed to bind to {}: {}", self.bind_addr, e))
        })?;

        info!(addr = %self.bind_addr, "Gateway listening");

        let app = self.router();

        axum::serve(listener, app)
            .await
            .map_err(|e| ClusterError::Network(format!("Gateway server error: {e}")))?;

        Ok(())
    }

    /// Returns the bind address.
    pub fn bind_addr(&self) -> SocketAddr {
        self.bind_addr
    }

    /// Returns the shared state for testing.
    pub fn state(&self) -> Arc<GatewayState> {
        self.state.clone()
    }
}

/// Builder for Gateway configuration.
pub struct GatewayBuilder {
    router: Option<Arc<RangeRouter>>,
    membership: Option<Arc<SwimMembership>>,
    bind_addr: SocketAddr,
    prometheus_handle: Option<Arc<PrometheusHandle>>,
}

impl GatewayBuilder {
    /// Creates a new gateway builder.
    pub fn new() -> Self {
        Self {
            router: None,
            membership: None,
            bind_addr: "0.0.0.0:8080".parse().unwrap_or_else(|_| {
                // Fallback that cannot fail
                SocketAddr::from(([0, 0, 0, 0], 8080))
            }),
            prometheus_handle: None,
        }
    }

    /// Sets the range router.
    pub fn with_router(mut self, router: Arc<RangeRouter>) -> Self {
        self.router = Some(router);
        self
    }

    /// Sets the membership.
    pub fn with_membership(mut self, membership: Arc<SwimMembership>) -> Self {
        self.membership = Some(membership);
        self
    }

    /// Sets the bind address.
    pub fn with_bind_addr(mut self, addr: SocketAddr) -> Self {
        self.bind_addr = addr;
        self
    }

    /// Sets the Prometheus handle for metrics export.
    pub fn with_prometheus_handle(mut self, handle: Arc<PrometheusHandle>) -> Self {
        self.prometheus_handle = Some(handle);
        self
    }

    /// Builds the gateway.
    ///
    /// # Errors
    ///
    /// Returns error if required components are missing.
    pub fn build(self) -> Result<Gateway> {
        let router =
            self.router.ok_or_else(|| ClusterError::Config("router is required".to_string()))?;

        let membership = self
            .membership
            .ok_or_else(|| ClusterError::Config("membership is required".to_string()))?;

        let mut gateway = Gateway::new(router, membership, self.bind_addr);
        if let Some(handle) = self.prometheus_handle {
            gateway = gateway.with_prometheus_handle(handle);
        }
        Ok(gateway)
    }
}

impl Default for GatewayBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::SwimConfig;
    use crate::membership::NodeInfo;
    use std::net::{IpAddr, Ipv4Addr};

    fn test_node_id(n: u8) -> NodeId {
        NodeId::from_bytes([n; 16])
    }

    fn test_addr(port: u16) -> SocketAddr {
        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port)
    }

    #[test]
    fn test_gateway_builder() {
        let local_id = test_node_id(1);
        let local_info = NodeInfo::new(local_id, test_addr(9090), test_addr(8080));

        let router = Arc::new(RangeRouter::new(local_id));
        let membership = Arc::new(SwimMembership::new(local_info, SwimConfig::default()));

        let gateway = GatewayBuilder::new()
            .with_router(router)
            .with_membership(membership)
            .with_bind_addr(test_addr(8081))
            .build();

        assert!(gateway.is_ok());
        let gateway = gateway.unwrap();
        assert_eq!(gateway.bind_addr().port(), 8081);
    }

    #[test]
    fn test_gateway_builder_missing_router() {
        let local_id = test_node_id(1);
        let local_info = NodeInfo::new(local_id, test_addr(9090), test_addr(8080));

        let membership = Arc::new(SwimMembership::new(local_info, SwimConfig::default()));

        let result = GatewayBuilder::new().with_membership(membership).build();

        assert!(result.is_err());
    }

    #[test]
    fn test_gateway_creates_router() {
        let local_id = test_node_id(1);
        let local_info = NodeInfo::new(local_id, test_addr(9090), test_addr(8080));

        let router = Arc::new(RangeRouter::new(local_id));
        let membership = Arc::new(SwimMembership::new(local_info, SwimConfig::default()));

        let gateway = Gateway::new(router, membership, test_addr(8080));
        // Verify router construction doesn't panic
        let _app = gateway.router();
    }

    #[test]
    fn test_gateway_state_request_count() {
        let local_id = test_node_id(1);
        let router = Arc::new(RangeRouter::new(local_id));
        let local_info = NodeInfo::new(local_id, test_addr(9090), test_addr(8080));
        let membership = Arc::new(SwimMembership::new(local_info, SwimConfig::default()));

        let state = GatewayState::new(router, membership);

        assert_eq!(state.inc_requests(), 0);
        assert_eq!(state.inc_requests(), 1);
        assert_eq!(state.inc_requests(), 2);
    }
}
