//! ML Trading Proxy - Zero-copy gRPC forwarding for ML-based trading operations
//!
//! This module implements a high-performance proxy for ML-specific trading operations:
//! - SubmitMLOrder: Submit orders based on ensemble ML predictions
//! - GetMLPredictions: Query historical ML prediction performance
//! - GetMLPerformance: Get ML model performance metrics
//!
//! Architecture:
//! - Zero-copy message forwarding (routing overhead <10μs)
//! - Connection pooling via tonic::transport::Channel
//! - Circuit breaker integration for backend failures
//! - Health checking integration
//!
//! Security:
//! - Permission checks: "trading.submit" for SubmitMLOrder
//! - Permission checks: "trading.view" for read operations
//! - Rate limiting: 100 requests/minute for GetMLPredictions, 20 requests/minute for GetMLPerformance
//! - Audit logging for all operations

use chrono::Utc;
use serde_json::json;
use std::sync::Arc;
use tonic::{Request, Response, Status};
use tracing::{error, info, instrument, warn};

// Import authentication components
use crate::auth::interceptor::JwtClaims;

// Import rate limiting components
use governor::{
    clock::DefaultClock, state::keyed::DefaultKeyedStateStore, Quota,
    RateLimiter as GovernorRateLimiter,
};
use std::num::NonZeroU32;

// Import the Trading Service backend proto (where ML methods are defined)
use crate::trading_backend::trading_service_client::TradingServiceClient;
use crate::trading_backend::{
    MlOrderRequest, MlOrderResponse, MlPerformanceRequest, MlPerformanceResponse,
    MlPredictionsRequest, MlPredictionsResponse,
};

/// ML Trading Proxy
///
/// Provides zero-copy forwarding of ML trading requests to the backend Trading Service.
///
/// The Trading Service handles:
/// - Ensemble ML prediction aggregation (DQN, MAMBA-2, PPO, TFT)
/// - Order execution based on ML signals
/// - ML prediction tracking and performance analysis
///
/// # Performance
/// - Uses connection pooling and circuit breakers for high availability
/// - Target routing overhead: <10μs per request
#[derive(Clone)]
pub struct MlTradingProxy {
    /// Backend Trading Service client with connection pooling
    client: TradingServiceClient<tonic::transport::Channel>,
    /// Rate limiter: 100 requests/minute per user for GetMLPredictions
    rate_limiter_predictions:
        Arc<GovernorRateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>>,
    /// Rate limiter: 20 requests/minute per user for GetMLPerformance (expensive queries)
    rate_limiter_performance:
        Arc<GovernorRateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>>,
}

impl MlTradingProxy {
    /// Create a new ML Trading proxy
    ///
    /// # Arguments
    /// * `client` - Pre-configured Trading Service client with circuit breaker
    ///
    /// # Performance
    /// - Uses Arc-based channel cloning for zero-copy client reuse
    /// - Connection pooling managed by tonic::transport::Channel
    pub fn new(client: TradingServiceClient<tonic::transport::Channel>) -> Self {
        // Create rate limiter for predictions: 100 requests per minute per user
        // SAFETY: NonZeroU32::new on positive constants always returns Some
        #[allow(clippy::unwrap_used)]
        let quota_predictions = Quota::per_minute(NonZeroU32::new(100).unwrap());
        let rate_limiter_predictions = Arc::new(GovernorRateLimiter::keyed(quota_predictions));

        // Create rate limiter for performance queries: 20 requests per minute per user (expensive)
        // SAFETY: NonZeroU32::new on positive constants always returns Some
        #[allow(clippy::unwrap_used)]
        let quota_performance = Quota::per_minute(NonZeroU32::new(20).unwrap());
        let rate_limiter_performance = Arc::new(GovernorRateLimiter::keyed(quota_performance));

        Self {
            client,
            rate_limiter_predictions,
            rate_limiter_performance,
        }
    }

    /// Submit ML-generated trading order with ensemble predictions
    ///
    /// # Security
    /// - Requires "trading.submit" permission (validated by auth interceptor)
    ///
    /// # Performance
    /// - Zero-copy message forwarding
    /// - Routing overhead target: <10μs
    ///
    /// # Flow
    /// 1. Receives MLOrderRequest with features and model selection
    /// 2. Forwards to Trading Service
    /// 3. Trading Service:
    ///    - Runs ensemble prediction (or specific model)
    ///    - Executes trading logic (BUY/SELL/HOLD)
    ///    - Records prediction in ensemble_predictions table
    ///    - Submits order if action is BUY/SELL
    /// 4. Returns order_id, prediction_id, action, confidence
    #[instrument(skip(self, request), fields(request_id = %uuid::Uuid::new_v4()), err)]
    pub async fn submit_ml_order(
        &self,
        request: Request<MlOrderRequest>,
    ) -> Result<Response<MlOrderResponse>, Status> {
        info!("Proxying SubmitMLOrder request");

        // Clone client (cheap Arc increment) for concurrent request handling
        let mut client = self.client.clone();

        // Forward request with zero-copy
        let response = client.submit_ml_order(request).await.map_err(|e| {
            error!("Backend SubmitMLOrder failed: {}", e);
            e
        })?;

        info!("SubmitMLOrder request forwarded successfully");
        Ok(response)
    }

    /// Get ML prediction history with outcomes
    ///
    /// # Security
    /// - Requires "trading.view" permission (validated by caller)
    /// - Rate limit: 100 requests/minute per user
    ///
    /// # Validation
    /// - symbol: required, must be valid format (alphanumeric + dots)
    /// - model_filter: optional, must be in [DQN, MAMBA2, PPO, TFT, TLOB, Liquid]
    /// - limit: optional, default 10, max 100
    ///
    /// # Performance
    /// - Zero-copy message forwarding
    /// - Routing overhead target: <10μs
    ///
    /// # Returns
    /// List of ML predictions with:
    /// - Ensemble voting results (action, signal, confidence)
    /// - Individual model predictions (DQN, MAMBA-2, PPO, TFT)
    /// - Actual P&L if order was executed and filled
    /// - Order ID linkage
    #[instrument(skip(self, request, claims), fields(request_id = %uuid::Uuid::new_v4(), user = %claims.sub), err)]
    pub async fn get_ml_predictions(
        &self,
        request: Request<MlPredictionsRequest>,
        claims: &JwtClaims,
    ) -> Result<Response<MlPredictionsResponse>, Status> {
        info!(
            "Processing GetMLPredictions request for user: {}",
            claims.sub
        );

        // Step 1: Check rate limit (100 requests/minute per user)
        if self
            .rate_limiter_predictions
            .check_key(&claims.sub)
            .is_err()
        {
            warn!(
                "Rate limit exceeded for user {} on GetMLPredictions",
                claims.sub
            );
            return Err(Status::resource_exhausted(
                "Rate limit exceeded: maximum 100 requests per minute for ML predictions queries",
            ));
        }

        // Step 2: Validate permission (requires "trading.view" scope)
        if !claims.permissions.contains(&"trading.view".to_string()) {
            warn!(
                "Permission denied: user {} lacks 'trading.view' scope for GetMLPredictions",
                claims.sub
            );
            return Err(Status::permission_denied(
                "Insufficient permissions: 'trading.view' scope required",
            ));
        }

        // Step 3: Extract and validate request parameters
        let req_inner = request.into_inner();
        let symbol = req_inner.symbol.trim();
        let model_filter = req_inner.model_name.as_deref();
        let limit = if req_inner.limit == 0 {
            10
        } else {
            req_inner.limit
        };

        // Validate symbol (required, must be alphanumeric + dots)
        if symbol.is_empty() {
            return Err(Status::invalid_argument(
                "Symbol is required and cannot be empty",
            ));
        }
        if !symbol.chars().all(|c| c.is_alphanumeric() || c == '.') {
            return Err(Status::invalid_argument(format!(
                "Invalid symbol format: '{}' (must be alphanumeric with optional dots)",
                symbol
            )));
        }

        // Validate model_filter (optional, must be valid model name)
        if let Some(model) = model_filter {
            let valid_models = ["DQN", "MAMBA2", "PPO", "TFT", "TLOB", "Liquid"];
            if !valid_models.contains(&model) {
                return Err(Status::invalid_argument(format!(
                    "Invalid model_filter: '{}' (must be one of: {})",
                    model,
                    valid_models.join(", ")
                )));
            }
        }

        // Validate limit (default 10, max 100)
        if limit < 1 {
            return Err(Status::invalid_argument("Limit must be at least 1"));
        }
        if limit > 100 {
            return Err(Status::invalid_argument(
                "Limit cannot exceed 100 (maximum predictions per query)",
            ));
        }

        info!(
            "Validated request: symbol={}, model_filter={:?}, limit={}",
            symbol, model_filter, limit
        );

        // Step 4: Forward request to Trading Service
        let mut client = self.client.clone();
        let backend_request = Request::new(MlPredictionsRequest {
            symbol: symbol.to_string(),
            model_name: model_filter.map(|s| s.to_string()),
            limit,
            start_time: None,
            end_time: None,
        });

        let response = client
            .get_ml_predictions(backend_request)
            .await
            .map_err(|e| {
                error!("Backend GetMLPredictions failed: {}", e);

                // Map backend errors to appropriate status codes
                match e.code() {
                    tonic::Code::Unavailable => Status::unavailable(
                        "Trading Service temporarily unavailable - please retry",
                    ),
                    tonic::Code::NotFound => {
                        Status::not_found(format!("No predictions found for symbol: {}", symbol))
                    },
                    tonic::Code::Internal => {
                        Status::internal("Database error occurred while retrieving predictions")
                    },
                    _ => e,
                }
            })?;

        let results_count = response.get_ref().predictions.len();
        info!(
            "GetMLPredictions successful: symbol={}, results_count={}",
            symbol, results_count
        );

        // Step 5: Audit log the query (non-blocking)
        let audit_log = json!({
            "action": "get_ml_predictions",
            "user": claims.sub,
            "symbol": symbol,
            "model_filter": model_filter,
            "limit": limit,
            "results_count": results_count,
            "timestamp": Utc::now().to_rfc3339(),
        });
        info!("Audit: {}", audit_log);

        // Step 6: Return predictions
        Ok(response)
    }

    /// Get ML model performance metrics
    ///
    /// # Security
    /// - Requires "trading.view" permission (validated by caller)
    /// - Rate limit: 20 requests/minute per user (performance queries are expensive)
    ///
    /// # Validation
    /// - model_name: optional, must be in [DQN, MAMBA_2, PPO, TFT]
    /// - time_range: start_time must be before end_time
    ///
    /// # Performance
    /// - Zero-copy message forwarding
    /// - Routing overhead target: <10μs
    /// - Response caching: 60 seconds (expensive queries)
    ///
    /// # Returns
    /// Performance metrics per model:
    /// - Total predictions made
    /// - Accuracy (correct/total)
    /// - Sharpe ratio (risk-adjusted returns)
    /// - Average P&L per prediction
    ///
    /// # Filters
    /// - By model name (optional): DQN, MAMBA_2, PPO, TFT
    /// - By time range (optional): start_time, end_time
    ///
    /// # Audit Logging
    /// All performance queries are logged for security and compliance monitoring.
    /// Performance queries are sensitive as they reveal ML model effectiveness.
    #[instrument(skip(self, request, claims), fields(
        request_id = %uuid::Uuid::new_v4(),
        user = %claims.sub,
        model_filter = ?request.get_ref().model_name
    ), err)]
    pub async fn get_ml_performance(
        &self,
        request: Request<MlPerformanceRequest>,
        claims: &JwtClaims,
    ) -> Result<Response<MlPerformanceResponse>, Status> {
        info!(
            "Processing GetMLPerformance request for user: {}",
            claims.sub
        );

        // Step 1: Check rate limit (20 requests/minute - performance queries are expensive)
        if self
            .rate_limiter_performance
            .check_key(&claims.sub)
            .is_err()
        {
            warn!(
                "Rate limit exceeded for user {} on GetMLPerformance",
                claims.sub
            );
            return Err(Status::resource_exhausted(
                "Rate limit exceeded: maximum 20 requests per minute for ML performance queries (expensive operation)"
            ));
        }

        // Step 2: Validate permission (requires "trading.view" scope)
        if !claims.permissions.contains(&"trading.view".to_string()) {
            warn!(
                "Permission denied: user {} lacks 'trading.view' scope for GetMLPerformance",
                claims.sub
            );
            return Err(Status::permission_denied(
                "Insufficient permissions: 'trading.view' scope required",
            ));
        }

        // Step 3: Extract and validate request parameters
        let req_inner = request.into_inner();
        let model_filter = req_inner.model_name.as_deref();
        let start_time = req_inner.start_time;
        let end_time = req_inner.end_time;

        // Validate model_name (optional, must be valid model ID)
        if let Some(model) = model_filter {
            let valid_models = ["DQN", "MAMBA_2", "PPO", "TFT"];
            if !valid_models.contains(&model) {
                warn!(
                    "Invalid model_name provided: {} (user: {})",
                    model, claims.sub
                );
                return Err(Status::invalid_argument(format!(
                    "Invalid model name: '{}' (must be one of: {})",
                    model,
                    valid_models.join(", ")
                )));
            }
        }

        // Validate time range (start_time must be before end_time)
        if let (Some(start), Some(end)) = (start_time, end_time) {
            if start > end {
                warn!(
                    "Invalid time range: start={}, end={} (user: {})",
                    start, end, claims.sub
                );
                return Err(Status::invalid_argument(format!(
                    "Invalid time range: start_time ({}) must be before end_time ({})",
                    start, end
                )));
            }
        }

        info!(
            "Validated request: model_filter={:?}, time_range=({:?}, {:?})",
            model_filter, start_time, end_time
        );

        // Step 4: Forward request to Trading Service
        let mut client = self.client.clone();
        let backend_request = Request::new(MlPerformanceRequest {
            model_name: model_filter.map(|s| s.to_string()),
            start_time,
            end_time,
        });

        let response = client
            .get_ml_performance(backend_request)
            .await
            .map_err(|e| {
                error!("Backend GetMLPerformance failed: {}", e);

                // Map backend errors to appropriate status codes
                match e.code() {
                    tonic::Code::Unavailable => Status::unavailable(
                        "Trading Service temporarily unavailable - please retry",
                    ),
                    tonic::Code::NotFound => {
                        Status::not_found("No performance data available for the specified filters")
                    },
                    tonic::Code::Internal => Status::internal(
                        "Database error occurred while retrieving performance metrics",
                    ),
                    _ => e,
                }
            })?;

        let models_count = response.get_ref().models.len();
        info!("GetMLPerformance successful: models_count={}", models_count);

        // Step 5: Audit log the query (performance queries are sensitive)
        // Log aggregated metrics for security monitoring
        let audit_log = json!({
            "action": "get_ml_performance",
            "user": claims.sub,
            "model_filter": model_filter,
            "time_range": {
                "start": start_time,
                "end": end_time
            },
            "results": {
                "models_count": models_count,
                "model_names": response.get_ref().models.iter().map(|m| &m.model_name).collect::<Vec<_>>()
            },
            "timestamp": Utc::now().to_rfc3339(),
        });
        info!("Audit: {}", audit_log);

        // Note: Response caching (60 seconds) would be implemented at a higher layer
        // (e.g., nginx/envoy proxy) to avoid adding Redis dependency to this proxy layer.
        // Cache key format: ml_performance:{model_filter}:{timestamp_minute}
        // This keeps the proxy layer lightweight and focused on routing/validation.

        // Step 6: Return performance metrics
        Ok(response)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ml_trading_proxy_creation() {
        // This test validates the proxy struct can be created
        // Full integration tests require running backend Trading Service
        // Integration tests are in services/api/tests/service_proxy_tests.rs
    }

    #[test]
    fn test_ml_trading_proxy_is_send_sync() {
        // Validate that MlTradingProxy can be shared across threads
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<MlTradingProxy>();
    }
}
