use std::fmt;
use std::io::{self, Write};

use serde::Serialize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorKind {
    InvalidInput,
    Network,
    Timeout,
    RateLimit,
    Upstream,
    Parse,
    Internal,
}

impl ErrorKind {
    pub fn retryable(self) -> bool {
        matches!(
            self,
            Self::Network | Self::Timeout | Self::RateLimit | Self::Upstream
        )
    }

    pub fn exit_code(self) -> i32 {
        match self {
            Self::InvalidInput => 3,
            Self::Network | Self::Timeout | Self::RateLimit | Self::Upstream => 4,
            Self::Parse | Self::Internal => 5,
        }
    }
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::InvalidInput => "invalid_input",
            Self::Network => "network",
            Self::Timeout => "timeout",
            Self::RateLimit => "rate_limit",
            Self::Upstream => "upstream",
            Self::Parse => "parse",
            Self::Internal => "internal",
        })
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct CliError {
    pub kind: ErrorKind,
    pub message: String,
    pub retryable: bool,
}

impl CliError {
    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            retryable: kind.retryable(),
        }
    }

    pub fn emit(&self) {
        let envelope = serde_json::json!({
            "error": {
                "kind": self.kind,
                "message": self.message,
                "retryable": self.retryable,
            }
        });
        let mut stderr = io::stderr().lock();
        let _ = writeln!(stderr, "{envelope}");
    }
}

pub fn from_reqwest(err: &reqwest::Error) -> CliError {
    let kind = if err.is_timeout() {
        ErrorKind::Timeout
    } else if err.is_connect() || err.is_request() {
        ErrorKind::Network
    } else if err.is_decode() {
        ErrorKind::Parse
    } else {
        ErrorKind::Upstream
    };
    CliError::new(kind, err.to_string())
}

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

    #[test]
    fn retryable_matches_spec() {
        assert!(!ErrorKind::InvalidInput.retryable());
        assert!(ErrorKind::Network.retryable());
        assert!(ErrorKind::Timeout.retryable());
        assert!(ErrorKind::RateLimit.retryable());
        assert!(ErrorKind::Upstream.retryable());
        assert!(!ErrorKind::Parse.retryable());
        assert!(!ErrorKind::Internal.retryable());
    }

    #[test]
    fn invalid_input_exits_3() {
        assert_eq!(ErrorKind::InvalidInput.exit_code(), 3);
    }

    #[test]
    fn network_errors_share_exit_code() {
        assert_eq!(ErrorKind::Network.exit_code(), 4);
        assert_eq!(ErrorKind::Timeout.exit_code(), 4);
        assert_eq!(ErrorKind::RateLimit.exit_code(), 4);
        assert_eq!(ErrorKind::Upstream.exit_code(), 4);
    }
}
