use std::collections::HashMap;

use chrono::{DateTime, Duration, Utc};

/// Execution record interface for learning calculations.
/// Implementations should provide timestamp and success flag.
pub trait ExecutionRecord: Send + Sync {
    fn timestamp(&self) -> DateTime<Utc>;
    fn success(&self) -> bool;
    fn duration_ms(&self) -> u64;
}

/// Calculate learning curve as time-series of expertise evolution.
/// Groups executions into daily windows and computes success rate per window.
/// Returns sorted Vec<(timestamp, success_rate)> where timestamp is start of
/// day.
pub fn calculate_learning_curve(
    executions: Vec<impl ExecutionRecord>,
    window_days: u32,
) -> Vec<(DateTime<Utc>, f64)> {
    if executions.is_empty() {
        return Vec::new();
    }

    let mut by_window: HashMap<DateTime<Utc>, (u32, u32)> = HashMap::new();

    for execution in executions {
        let window_start = align_to_window(execution.timestamp(), window_days);
        let (total, success) = by_window.entry(window_start).or_insert((0, 0));
        *total += 1;
        if execution.success() {
            *success += 1;
        }
    }

    let mut curve: Vec<_> = by_window
        .iter()
        .map(|(window, (total, success))| (*window, *success as f64 / *total as f64))
        .collect();

    curve.sort_by_key(|entry| entry.0);
    curve
}

/// Apply recency bias weighting to execution records.
/// Recent performance (last 7 days) weighted 3x higher than historical
/// averages. Returns weighted success rate accounting for time decay.
///
/// Formula: weight = 3.0 * e^(-days_ago / 7.0) for days_ago < 7, else
/// e^(-days_ago / 7.0)
pub fn apply_recency_bias(executions: Vec<impl ExecutionRecord>, decay_days: u32) -> f64 {
    if executions.is_empty() {
        return 0.5;
    }

    let now = Utc::now();
    let decay_factor = decay_days as f64;
    let mut weighted_success = 0.0;
    let mut total_weight = 0.0;

    for execution in executions {
        let days_ago = (now - execution.timestamp()).num_days() as f64;
        let weight = if days_ago < decay_factor {
            3.0 * (-days_ago / decay_factor).exp()
        } else {
            (-days_ago / decay_factor).exp()
        };

        weighted_success += weight * if execution.success() { 1.0 } else { 0.0 };
        total_weight += weight;
    }

    if total_weight > 0.0 {
        weighted_success / total_weight
    } else {
        0.5
    }
}

/// Aggregate execution records into time windows.
/// Returns HashMap<window_start, (total_count, success_count)>.
pub fn aggregate_by_time_window(
    executions: Vec<impl ExecutionRecord>,
    window_days: u32,
) -> HashMap<DateTime<Utc>, (u32, u32)> {
    let mut by_window: HashMap<DateTime<Utc>, (u32, u32)> = HashMap::new();

    for execution in executions {
        let window_start = align_to_window(execution.timestamp(), window_days);
        let (total, success) = by_window.entry(window_start).or_insert((0, 0));
        *total += 1;
        if execution.success() {
            *success += 1;
        }
    }

    by_window
}

/// Align timestamp to window boundary (e.g., start of day).
fn align_to_window(timestamp: DateTime<Utc>, window_days: u32) -> DateTime<Utc> {
    if window_days == 1 {
        // Align to midnight UTC
        timestamp
            .date_naive()
            .and_hms_opt(0, 0, 0)
            .map(|dt| dt.and_utc())
            .unwrap_or(timestamp)
    } else {
        // Align to start of week/custom window
        let epoch = DateTime::<Utc>::UNIX_EPOCH;
        let elapsed = timestamp - epoch;
        let window_secs = (window_days as i64) * 86400;
        let windows_since_epoch = elapsed.num_seconds() / window_secs;
        let window_start_secs = windows_since_epoch * window_secs;
        epoch + Duration::seconds(window_start_secs)
    }
}

/// Calculate task-specific success metrics with confidence bounds.
/// Returns (success_rate, confidence_score) where confidence reflects execution
/// count.
pub fn calculate_task_type_metrics(
    executions: Vec<impl ExecutionRecord>,
    min_executions_for_confidence: u32,
) -> (f64, f64) {
    let total = executions.len() as u32;
    if total == 0 {
        return (0.5, 0.0);
    }

    let successes = executions.iter().filter(|e| e.success()).count() as u32;
    let success_rate = successes as f64 / total as f64;
    let confidence = (total as f64 / min_executions_for_confidence as f64).min(1.0);

    (success_rate, confidence)
}

/// Calculate average execution metrics (duration, etc).
pub fn calculate_execution_averages(executions: Vec<impl ExecutionRecord>) -> (f64, f64, f64) {
    if executions.is_empty() {
        return (0.0, 0.0, 0.0);
    }

    let total = executions.len() as f64;
    let total_duration: u64 = executions.iter().map(|e| e.duration_ms()).sum();
    let avg_duration = total_duration as f64 / total;

    let min_duration = executions
        .iter()
        .map(|e| e.duration_ms())
        .min()
        .unwrap_or(0) as f64;
    let max_duration = executions
        .iter()
        .map(|e| e.duration_ms())
        .max()
        .unwrap_or(0) as f64;

    (avg_duration, min_duration, max_duration)
}

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

    #[derive(Debug, Clone)]
    struct MockExecution {
        timestamp: DateTime<Utc>,
        success: bool,
        duration_ms: u64,
    }

    impl ExecutionRecord for MockExecution {
        fn timestamp(&self) -> DateTime<Utc> {
            self.timestamp
        }

        fn success(&self) -> bool {
            self.success
        }

        fn duration_ms(&self) -> u64 {
            self.duration_ms
        }
    }

    #[test]
    fn test_recency_bias_weights_recent_higher() {
        let now = Utc::now();
        let executions = vec![
            MockExecution {
                timestamp: now - Duration::hours(1),
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now - Duration::days(8),
                success: false,
                duration_ms: 100,
            },
        ];

        let biased = apply_recency_bias(executions, 7);
        assert!(
            biased > 0.5,
            "Recent success should pull weighted average above 0.5"
        );
    }

    #[test]
    fn test_empty_executions() {
        let executions: Vec<MockExecution> = Vec::new();
        let curve = calculate_learning_curve(executions.clone(), 1);
        assert_eq!(curve.len(), 0);

        let biased = apply_recency_bias(executions.clone(), 7);
        assert_eq!(biased, 0.5);

        let metrics = calculate_task_type_metrics(executions, 20);
        assert_eq!(metrics, (0.5, 0.0));
    }

    #[test]
    fn test_learning_curve_sorts_chronologically() {
        let now = Utc::now();
        let executions = vec![
            MockExecution {
                timestamp: now - Duration::hours(25),
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now - Duration::hours(1),
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now - Duration::hours(49),
                success: false,
                duration_ms: 100,
            },
        ];

        let curve = calculate_learning_curve(executions, 1);
        assert!(curve.len() >= 2);
        for i in 1..curve.len() {
            assert!(
                curve[i - 1].0 <= curve[i].0,
                "Curve must be chronologically sorted"
            );
        }
    }

    #[test]
    fn test_aggregate_by_time_window() {
        let now = Utc::now();
        let executions = vec![
            MockExecution {
                timestamp: now - Duration::hours(1),
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now - Duration::hours(2),
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now - Duration::days(1) - Duration::hours(1),
                success: false,
                duration_ms: 100,
            },
        ];

        let aggregated = aggregate_by_time_window(executions, 1);
        assert_eq!(aggregated.len(), 2, "Should have 2 different days");

        for (_, (total, success)) in aggregated.iter() {
            assert!(*total > 0);
            assert!(*success <= *total);
        }
    }

    #[test]
    fn test_calculate_task_type_metrics() {
        let now = Utc::now();
        let executions = vec![
            MockExecution {
                timestamp: now,
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now,
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now,
                success: false,
                duration_ms: 100,
            },
        ];

        let (success_rate, confidence) = calculate_task_type_metrics(executions, 20);
        assert!((success_rate - 2.0 / 3.0).abs() < 0.01);
        assert!(confidence > 0.0 && confidence <= 1.0);
    }

    #[test]
    fn test_execution_averages() {
        let now = Utc::now();
        let executions = vec![
            MockExecution {
                timestamp: now,
                success: true,
                duration_ms: 100,
            },
            MockExecution {
                timestamp: now,
                success: true,
                duration_ms: 200,
            },
            MockExecution {
                timestamp: now,
                success: false,
                duration_ms: 150,
            },
        ];

        let (avg, min, max) = calculate_execution_averages(executions);
        assert!((avg - 150.0).abs() < 0.01);
        assert_eq!(min, 100.0);
        assert_eq!(max, 200.0);
    }
}
