/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Error data model of RocketMQ rust client.

use std::error::Error;
use std::fmt;
use std::fmt::{Debug, Display, Formatter};

/// Error type using by [`ClientError`].
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum ErrorKind {
    #[error("Failed to parse config")]
    Config,

    #[error("Failed to create session")]
    Connect,

    #[error("Message is invalid")]
    InvalidMessage,

    #[error("Server error")]
    Server,

    #[error("No broker available to send message")]
    NoBrokerAvailable,

    #[error("Client internal error")]
    ClientInternal,

    #[error("Client is not running")]
    ClientIsNotRunning,

    #[error("Failed to send message via channel")]
    ChannelSend,

    #[error("Failed to receive message via channel")]
    ChannelReceive,

    #[error("Unknown error")]
    Unknown,
}

/// Error returned by producer or consumer.
pub struct ClientError {
    pub(crate) kind: ErrorKind,
    pub(crate) message: String,
    pub(crate) operation: &'static str,
    pub(crate) context: Vec<(&'static str, String)>,
    pub(crate) source: Option<anyhow::Error>,
}

impl Error for ClientError {}

impl ClientError {
    pub(crate) fn new(kind: ErrorKind, message: &str, operation: &'static str) -> Self {
        Self {
            kind,
            message: message.to_string(),
            operation,
            context: Vec::new(),
            source: None,
        }
    }

    /// Error type
    pub fn kind(&self) -> &ErrorKind {
        &self.kind
    }

    /// Error message
    pub fn message(&self) -> &str {
        &self.message
    }

    /// Name of operation that produced this error
    pub fn operation(&self) -> &str {
        self.operation
    }

    /// Error context, formatted in key-value pairs
    pub fn context(&self) -> &Vec<(&'static str, String)> {
        &self.context
    }

    /// Source error
    pub fn source(&self) -> Option<&anyhow::Error> {
        self.source.as_ref()
    }

    pub(crate) fn with_operation(mut self, operation: &'static str) -> Self {
        if !self.operation.is_empty() {
            self.context.push(("called", self.operation.to_string()));
        }

        self.operation = operation;
        self
    }

    pub(crate) fn with_context(mut self, key: &'static str, value: impl Into<String>) -> Self {
        self.context.push((key, value.into()));
        self
    }

    pub(crate) fn set_source(mut self, src: impl Into<anyhow::Error>) -> Self {
        debug_assert!(self.source.is_none(), "the source error has been set");

        self.source = Some(src.into());
        self
    }
}

impl Display for ClientError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{} at {}", self.kind, self.operation)?;

        if !self.context.is_empty() {
            write!(f, ", context: {{ ")?;
            write!(
                f,
                "{}",
                self.context
                    .iter()
                    .map(|(k, v)| format!("{k}: {v}"))
                    .collect::<Vec<_>>()
                    .join(", ")
            )?;
            write!(f, " }}")?;
        }

        if !self.message.is_empty() {
            write!(f, " => {}", self.message)?;
        }

        if let Some(source) = &self.source {
            write!(f, ", source: {source}")?;
        }

        Ok(())
    }
}

impl Debug for ClientError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        // If alternate has been specified, we will print like Debug.
        if f.alternate() {
            let mut debug = f.debug_struct("Error");
            debug.field("kind", &self.kind);
            debug.field("message", &self.message);
            debug.field("operation", &self.operation);
            debug.field("context", &self.context);
            debug.field("source", &self.source);
            return debug.finish();
        }

        write!(f, "{} at {}", self.kind, self.operation)?;
        if !self.message.is_empty() {
            write!(f, " => {}", self.message)?;
        }
        writeln!(f)?;

        if !self.context.is_empty() {
            writeln!(f)?;
            writeln!(f, "Context:")?;
            for (k, v) in self.context.iter() {
                writeln!(f, "    {k}: {v}")?;
            }
        }
        if let Some(source) = &self.source {
            writeln!(f)?;
            writeln!(f, "Source: {source:?}")?;
        }

        Ok(())
    }
}

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

    #[test]
    fn error_client_error() {
        let err = ClientError::new(ErrorKind::Config, "fake_message", "error_client_error")
            .with_operation("another_operation")
            .with_context("context_key", "context_value")
            .set_source(anyhow::anyhow!("fake_source_error"));
        assert_eq!(
            err.to_string(),
            "Failed to parse config at another_operation, context: { called: error_client_error, context_key: context_value } => fake_message, source: fake_source_error"
        );
        assert_eq!(format!("{:?}", err), "Failed to parse config at another_operation => fake_message\n\nContext:\n    called: error_client_error\n    context_key: context_value\n\nSource: fake_source_error\n");
        assert_eq!(format!("{:#?}", err), "Error {\n    kind: Config,\n    message: \"fake_message\",\n    operation: \"another_operation\",\n    context: [\n        (\n            \"called\",\n            \"error_client_error\",\n        ),\n        (\n            \"context_key\",\n            \"context_value\",\n        ),\n    ],\n    source: Some(\n        \"fake_source_error\",\n    ),\n}");
    }
}
