use std::time::Duration;

use anyhow::Result;

/// Configuration for retry behavior on transient errors.
#[derive(Debug, Clone)]
pub struct RetryConfig {
    pub max_retries: u32,
    pub initial_delay: Duration,
    pub max_delay: Duration,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_retries: 0,
            initial_delay: Duration::from_secs(1),
            max_delay: Duration::from_secs(30),
        }
    }
}

/// Execute an async operation with retry on transient errors.
///
/// Retries with exponential backoff: `initial_delay * 2^attempt`, capped at `max_delay`.
/// Only retries on transient errors (timeout, connection, 429, 502, 503).
/// Retry events are emitted to stderr in JSON mode to avoid corrupting stdout.
pub async fn with_retry<F, Fut, T>(config: &RetryConfig, json_mode: bool, mut op: F) -> Result<T>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = Result<T>>,
{
    let mut attempt = 0u32;
    loop {
        match op().await {
            Ok(val) => return Ok(val),
            Err(e) => {
                if attempt >= config.max_retries || !is_transient(&e) {
                    return Err(e);
                }
                attempt += 1;
                let delay = compute_delay(config, attempt);
                if json_mode {
                    eprintln!(
                        "{}",
                        serde_json::json!({
                            "event": "retry",
                            "attempt": attempt,
                            "max_retries": config.max_retries,
                            "delay_ms": delay.as_millis(),
                            "error": e.to_string(),
                        })
                    );
                } else {
                    eprintln!(
                        "Retry {}/{}: {} (waiting {}ms)",
                        attempt,
                        config.max_retries,
                        e,
                        delay.as_millis()
                    );
                }
                tokio::time::sleep(delay).await;
            }
        }
    }
}

fn compute_delay(config: &RetryConfig, attempt: u32) -> Duration {
    let multiplier = 2u64.saturating_pow(attempt.saturating_sub(1));

    config
        .initial_delay
        .saturating_mul(multiplier as u32)
        .min(config.max_delay)
}

/// Determine if an error is transient and worth retrying.
fn is_transient(err: &anyhow::Error) -> bool {
    let msg = err.to_string().to_lowercase();
    // Network/connection errors
    msg.contains("timeout")
        || msg.contains("timed out")
        || msg.contains("connection")
        || msg.contains("connect error")
        || msg.contains("broken pipe")
        || msg.contains("reset by peer")
        // HTTP status codes that are transient
        || msg.contains("429")
        || msg.contains("too many requests")
        || msg.contains("502")
        || msg.contains("bad gateway")
        || msg.contains("503")
        || msg.contains("service unavailable")
        || msg.contains("504")
        || msg.contains("gateway timeout")
        // DNS
        || msg.contains("dns")
        || msg.contains("name resolution")
}

/// Parse a human-readable duration string like "1s", "500ms", "30s", "2m".
pub fn parse_duration(s: &str) -> Result<Duration> {
    let s = s.trim();
    if let Some(rest) = s.strip_suffix("ms") {
        let ms: u64 = rest
            .parse()
            .map_err(|_| anyhow::anyhow!("Invalid duration: {s}"))?;
        Ok(Duration::from_millis(ms))
    } else if let Some(rest) = s.strip_suffix('s') {
        let secs: f64 = rest
            .parse()
            .map_err(|_| anyhow::anyhow!("Invalid duration: {s}"))?;
        Ok(Duration::from_secs_f64(secs))
    } else if let Some(rest) = s.strip_suffix('m') {
        let mins: f64 = rest
            .parse()
            .map_err(|_| anyhow::anyhow!("Invalid duration: {s}"))?;
        Ok(Duration::from_secs_f64(mins * 60.0))
    } else {
        // Default: treat as seconds
        let secs: f64 = s.parse().map_err(|_| {
            anyhow::anyhow!("Invalid duration: {s}. Use format like '1s', '500ms', '2m'")
        })?;
        Ok(Duration::from_secs_f64(secs))
    }
}

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

    #[test]
    fn test_parse_duration_seconds() {
        assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
    }

    #[test]
    fn test_parse_duration_milliseconds() {
        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
    }

    #[test]
    fn test_parse_duration_minutes() {
        assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
    }

    #[test]
    fn test_parse_duration_bare_number() {
        assert_eq!(parse_duration("5").unwrap(), Duration::from_secs(5));
    }

    #[test]
    fn test_parse_duration_invalid() {
        assert!(parse_duration("abc").is_err());
    }

    #[test]
    fn test_compute_delay_exponential() {
        let config = RetryConfig {
            max_retries: 5,
            initial_delay: Duration::from_secs(1),
            max_delay: Duration::from_secs(30),
        };
        assert_eq!(compute_delay(&config, 1), Duration::from_secs(1));
        assert_eq!(compute_delay(&config, 2), Duration::from_secs(2));
        assert_eq!(compute_delay(&config, 3), Duration::from_secs(4));
        assert_eq!(compute_delay(&config, 4), Duration::from_secs(8));
    }

    #[test]
    fn test_compute_delay_capped() {
        let config = RetryConfig {
            max_retries: 10,
            initial_delay: Duration::from_secs(1),
            max_delay: Duration::from_secs(5),
        };
        assert_eq!(compute_delay(&config, 10), Duration::from_secs(5));
    }

    #[test]
    fn test_is_transient_timeout() {
        let err = anyhow::anyhow!("request timeout");
        assert!(is_transient(&err));
    }

    #[test]
    fn test_is_transient_429() {
        let err = anyhow::anyhow!("429 Too Many Requests");
        assert!(is_transient(&err));
    }

    #[test]
    fn test_is_transient_auth_not_transient() {
        let err = anyhow::anyhow!("Not authenticated");
        assert!(!is_transient(&err));
    }
}
