//! MCP progress tracking for long-running operations.
//!
//! Provides progress notifications with unique tokens, tracking operation state,
//! and automatic cleanup of stale tokens. Tokens are formatted as:
//! `"{op_type}-{unix_timestamp}-{counter}"`

use dashmap::DashMap;
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;

use crate::transport::traits::JsonRpcNotification;

/// Tracks state of an active operation
#[derive(Debug, Clone)]
struct OperationState {
    /// Operation type: "stalint", "dictator", etc.
    #[allow(dead_code)]
    op_type: String,
    /// Stable operation ID (same as token)
    #[allow(dead_code)]
    op_id: String,
    /// Total items to process
    total: u32,
    /// Currently processed items
    current: u32,
    /// When operation started
    start_time: Instant,
}

/// Progress token for tracking an operation
pub type ProgressToken = String;

/// Progress tracker for long-running MCP operations
///
/// # Example
///
/// ```rust,ignore
/// let tracker = ProgressTracker::new(notification_tx);
/// let token = tracker.start("my-operation", 100);
/// tracker.progress(&token, 50);  // 50% complete
/// tracker.finish(&token);        // 100% complete
/// ```
#[derive(Clone)]
pub struct ProgressTracker {
    /// Global counter for unique progress tokens
    token_counter: Arc<AtomicU64>,
    /// Active operations: token -> state
    operations: Arc<DashMap<String, OperationState>>,
    /// Notification channel sender
    notif_tx: mpsc::UnboundedSender<JsonRpcNotification>,
}

impl ProgressTracker {
    /// Create a new progress tracker
    ///
    /// # Arguments
    ///
    /// * `notif_tx` - Notification channel for sending progress updates
    pub fn new(notif_tx: mpsc::UnboundedSender<JsonRpcNotification>) -> Self {
        Self {
            token_counter: Arc::new(AtomicU64::new(0)),
            operations: Arc::new(DashMap::new()),
            notif_tx,
        }
    }

    /// Generate a unique progress token: "{op_type}-{timestamp}-{counter}"
    fn generate_token(&self, op_type: &str) -> ProgressToken {
        let counter = self.token_counter.fetch_add(1, Ordering::SeqCst);
        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        format!("{op_type}-{timestamp}-{counter}")
    }

    /// Start a new operation using a token provided by the client (`_meta.progressToken`).
    ///
    /// Use this when the caller supplied a progress token in the request metadata.
    /// Per spec the server MUST use the client-provided token, not a self-generated one.
    /// Sends the initial progress notification (0%).
    ///
    /// # Arguments
    ///
    /// * `token` - Token provided by the client in `_meta.progressToken`
    /// * `op_type` - Operation type identifier (used for internal tracking only)
    /// * `total` - Total number of items to process
    pub fn start_with_token(&self, token: &str, op_type: &str, total: u32) -> ProgressToken {
        let token = token.to_owned();
        let op_id = token.clone();

        let op_state = OperationState {
            op_type: op_type.to_string(),
            op_id,
            total,
            current: 0,
            start_time: Instant::now(),
        };

        self.operations.insert(token.clone(), op_state);
        self.send_notification(&token, 0, total, None);
        token
    }

    /// Start a new operation and return its unique progress token
    ///
    /// Sends initial progress notification (0%)
    ///
    /// # Arguments
    ///
    /// * `op_type` - Operation type identifier (e.g., "lint", "fix")
    /// * `total` - Total number of items to process
    ///
    /// # Returns
    ///
    /// Unique progress token to use for updates
    pub fn start(&self, op_type: &str, total: u32) -> ProgressToken {
        let token = self.generate_token(op_type);
        let op_id = token.clone();

        let op_state = OperationState {
            op_type: op_type.to_string(),
            op_id,
            total,
            current: 0,
            start_time: Instant::now(),
        };

        self.operations.insert(token.clone(), op_state);

        // Send initial progress notification (0%)
        self.send_notification(&token, 0, total, None);

        token
    }

    /// Update progress for an operation (0..=total)
    ///
    /// # Arguments
    ///
    /// * `token` - Progress token from `start()`
    /// * `current` - Current progress (will be clamped to [0, total])
    ///
    /// # Returns
    ///
    /// `true` if operation is still active, `false` if unknown/expired
    pub fn progress(&self, token: &ProgressToken, current: u32) -> bool {
        self.progress_with_message(token, current, None)
    }

    /// Update progress with an optional human-readable message.
    ///
    /// # Arguments
    ///
    /// * `token` - Progress token from `start()`
    /// * `current` - Current progress (will be clamped to [0, total])
    /// * `message` - Optional status message (added in 2025-06-18 spec)
    ///
    /// # Returns
    ///
    /// `true` if operation is still active, `false` if unknown/expired
    pub fn progress_with_message(
        &self,
        token: &ProgressToken,
        current: u32,
        message: Option<String>,
    ) -> bool {
        if let Some(mut op) = self.operations.get_mut(token) {
            // Clamp progress to [0, total]
            op.current = current.min(op.total);
            let progress = op.current;
            let total = op.total;
            drop(op); // Release lock before sending notification

            self.send_notification(token, progress, total, message);
            true
        } else {
            false
        }
    }

    /// Mark operation complete (final notification at 100%)
    ///
    /// Removes the operation from tracking and sends final progress notification.
    ///
    /// # Arguments
    ///
    /// * `token` - Progress token from `start()`
    pub fn finish(&self, token: &ProgressToken) {
        self.finish_with_message(token, None);
    }

    /// Mark operation complete with an optional final message.
    ///
    /// # Arguments
    ///
    /// * `token` - Progress token from `start()`
    /// * `message` - Optional completion message
    pub fn finish_with_message(&self, token: &ProgressToken, message: Option<String>) {
        if let Some((_key, op)) = self.operations.remove(token) {
            self.send_notification(token, op.total, op.total, message);
        }
    }

    /// Send progress notification to client.
    ///
    /// Includes the optional `message` field added in the 2025-06-18 spec.
    fn send_notification(
        &self,
        token: &ProgressToken,
        progress: u32,
        total: u32,
        message: Option<String>,
    ) {
        let params = if let Some(msg) = message {
            json!({
                "progressToken": token,
                "progress": progress,
                "total": total,
                "message": msg
            })
        } else {
            json!({
                "progressToken": token,
                "progress": progress,
                "total": total
            })
        };

        let notification = JsonRpcNotification::new("notifications/progress", Some(params));
        let _ = self.notif_tx.send(notification);
    }

    /// Clean up stale operations (older than 10 minutes)
    ///
    /// Call periodically from background tasks. Only performs cleanup
    /// if at least 60 seconds have passed since last cleanup.
    pub fn cleanup_stale(&self) {
        let now = Instant::now();
        let timeout = Duration::from_secs(600); // 10 minutes

        self.operations
            .retain(|_token, op| now.duration_since(op.start_time) < timeout);
    }
}

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

    fn create_tracker() -> ProgressTracker {
        let (tx, _rx) = mpsc::unbounded_channel();
        ProgressTracker::new(tx)
    }

    #[test]
    fn test_token_format() {
        let tracker = create_tracker();
        let token = tracker.generate_token("stalint");

        // Format: "{op_type}-{timestamp}-{counter}"
        let parts: Vec<&str> = token.split('-').collect();
        assert_eq!(parts.len(), 3);
        assert_eq!(parts[0], "stalint");
        // parts[1] is unix timestamp
        assert!(parts[1].parse::<u64>().is_ok());
        // parts[2] is counter
        assert!(parts[2].parse::<u64>().is_ok());
    }

    #[test]
    fn test_token_uniqueness() {
        let tracker = create_tracker();
        let token1 = tracker.generate_token("stalint");
        let token2 = tracker.generate_token("stalint");

        assert_ne!(token1, token2);
    }

    #[test]
    fn test_start_registers_operation() {
        let tracker = create_tracker();
        let token = tracker.start("stalint", 100);

        assert!(tracker.operations.contains_key(&token));
        let op = tracker.operations.get(&token).unwrap();
        assert_eq!(op.current, 0);
        assert_eq!(op.total, 100);
    }

    #[test]
    fn test_progress_updates() {
        let tracker = create_tracker();
        let token = tracker.start("dictator", 50);

        // Update to 50%
        assert!(tracker.progress(&token, 25));
        let op = tracker.operations.get(&token).unwrap();
        assert_eq!(op.current, 25);
        assert_eq!(op.total, 50);
        drop(op);

        // Update to 100%
        assert!(tracker.progress(&token, 50));
        let op = tracker.operations.get(&token).unwrap();
        assert_eq!(op.current, 50);
        assert_eq!(op.total, 50);
    }

    #[test]
    fn test_progress_clamps_to_total() {
        let tracker = create_tracker();
        let token = tracker.start("supremecourt", 10);

        // Try to set beyond total
        assert!(tracker.progress(&token, 99));
        let op = tracker.operations.get(&token).unwrap();
        assert_eq!(op.current, 10); // Clamped to total
        assert_eq!(op.total, 10);
    }

    #[test]
    fn test_finish_removes_operation() {
        let tracker = create_tracker();
        let token = tracker.start("stalint", 100);

        assert!(tracker.operations.contains_key(&token));
        tracker.finish(&token);
        assert!(!tracker.operations.contains_key(&token));
    }

    #[test]
    fn test_progress_unknown_token() {
        let tracker = create_tracker();

        // Unknown token should return false
        assert!(!tracker.progress(&"unknown-token".to_string(), 50));
    }

    #[test]
    fn test_multiple_concurrent_operations() {
        let tracker = create_tracker();
        let token1 = tracker.start("stalint", 100);
        let token2 = tracker.start("dictator", 50);

        assert!(tracker.operations.contains_key(&token1));
        assert!(tracker.operations.contains_key(&token2));

        // Update both independently
        assert!(tracker.progress(&token1, 50));
        assert!(tracker.progress(&token2, 25));

        let op1 = tracker.operations.get(&token1).unwrap();
        let op2 = tracker.operations.get(&token2).unwrap();

        assert_eq!((op1.current, op1.total), (50, 100));
        assert_eq!((op2.current, op2.total), (25, 50));
        drop(op1);
        drop(op2);

        // Finish one
        tracker.finish(&token1);
        assert!(!tracker.operations.contains_key(&token1));
        assert!(tracker.operations.contains_key(&token2));
    }

    #[test]
    fn test_cleanup_stale() {
        let tracker = create_tracker();
        let token = tracker.start("test", 100);

        assert!(tracker.operations.contains_key(&token));

        // Manually age the operation
        if let Some(mut op) = tracker.operations.get_mut(&token) {
            op.start_time = Instant::now() - Duration::from_secs(700); // 11+ minutes ago
        }

        tracker.cleanup_stale();

        // Should be removed
        assert!(!tracker.operations.contains_key(&token));
    }

    #[tokio::test]
    async fn test_progress_with_message_includes_field() {
        let (tx, mut rx) = mpsc::unbounded_channel();
        let tracker = ProgressTracker::new(tx);
        let token = tracker.start("with-message", 100);

        // Drain start() notification.
        let _ = tokio::time::timeout(Duration::from_millis(250), rx.recv())
            .await
            .expect("timed out waiting for start notification")
            .expect("notification channel closed unexpectedly");

        assert!(tracker.progress_with_message(&token, 50, Some("half".to_string())));

        let notification = tokio::time::timeout(Duration::from_millis(250), rx.recv())
            .await
            .expect("timed out waiting for progress notification")
            .expect("notification channel closed unexpectedly");

        assert_eq!(notification.method, "notifications/progress");
        let params = notification.params.expect("expected notification params");
        assert_eq!(params["message"], json!("half"));
    }

    #[tokio::test]
    async fn test_progress_without_message_excludes_field() {
        let (tx, mut rx) = mpsc::unbounded_channel();
        let tracker = ProgressTracker::new(tx);
        let token = tracker.start("without-message", 100);

        // Drain start() notification.
        let _ = tokio::time::timeout(Duration::from_millis(250), rx.recv())
            .await
            .expect("timed out waiting for start notification")
            .expect("notification channel closed unexpectedly");

        assert!(tracker.progress(&token, 50));

        let notification = tokio::time::timeout(Duration::from_millis(250), rx.recv())
            .await
            .expect("timed out waiting for progress notification")
            .expect("notification channel closed unexpectedly");

        assert_eq!(notification.method, "notifications/progress");
        let params = notification.params.expect("expected notification params");
        let obj = params
            .as_object()
            .expect("progress notification params must be an object");
        assert!(!obj.contains_key("message"));
    }

    #[tokio::test]
    async fn test_finish_with_message_includes_field() {
        let (tx, mut rx) = mpsc::unbounded_channel();
        let tracker = ProgressTracker::new(tx);
        let token = tracker.start("finish-message", 100);

        // Drain start() notification.
        let _ = tokio::time::timeout(Duration::from_millis(250), rx.recv())
            .await
            .expect("timed out waiting for start notification")
            .expect("notification channel closed unexpectedly");

        tracker.finish_with_message(&token, Some("done".to_string()));

        let notification = tokio::time::timeout(Duration::from_millis(250), rx.recv())
            .await
            .expect("timed out waiting for finish notification")
            .expect("notification channel closed unexpectedly");

        assert_eq!(notification.method, "notifications/progress");
        let params = notification.params.expect("expected notification params");
        assert_eq!(params["message"], json!("done"));
    }
}
