use std::{borrow::Cow, fmt, time::Duration};

use js_int::UInt;
use ruma_common::serde::JsonObject;
use serde::{
    de::{self, Deserialize, Deserializer, MapAccess, Visitor},
    ser::{self, Serialize, SerializeMap, Serializer},
};
use serde_json::{from_value as from_json_value, map::Entry};

use super::{
    BadStatusErrorData, CustomErrorKind, ErrorCode, ErrorKind, IncompatibleRoomVersionErrorData,
    LimitExceededErrorData, ResourceLimitExceededErrorData, RetryAfter, UnknownTokenErrorData,
    UserLimitExceededErrorData, WrongRoomKeysVersionErrorData,
};
#[cfg(feature = "unstable-msc4406")]
use crate::{OwnedUserId, api::error::SenderIgnoredErrorData};

enum Field<'de> {
    ErrorCode,
    SoftLogout,
    RetryAfterMs,
    RoomVersion,
    AdminContact,
    Status,
    Body,
    CurrentVersion,
    InfoUri,
    CanUpgrade,
    #[cfg(feature = "unstable-msc4406")]
    Sender,
    Other(Cow<'de, str>),
}

impl<'de> Field<'de> {
    fn new(s: Cow<'de, str>) -> Field<'de> {
        match s.as_ref() {
            "errcode" => Self::ErrorCode,
            "soft_logout" => Self::SoftLogout,
            "retry_after_ms" => Self::RetryAfterMs,
            "room_version" => Self::RoomVersion,
            "admin_contact" => Self::AdminContact,
            "status" => Self::Status,
            "body" => Self::Body,
            "current_version" => Self::CurrentVersion,
            "info_uri" => Self::InfoUri,
            "can_upgrade" => Self::CanUpgrade,
            #[cfg(feature = "unstable-msc4406")]
            "sender" => Self::Sender,
            _ => Self::Other(s),
        }
    }
}

impl<'de> Deserialize<'de> for Field<'de> {
    fn deserialize<D>(deserializer: D) -> Result<Field<'de>, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct FieldVisitor;

        impl<'de> Visitor<'de> for FieldVisitor {
            type Value = Field<'de>;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("any struct field")
            }

            fn visit_str<E>(self, value: &str) -> Result<Field<'de>, E>
            where
                E: de::Error,
            {
                Ok(Field::new(Cow::Owned(value.to_owned())))
            }

            fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Field<'de>, E>
            where
                E: de::Error,
            {
                Ok(Field::new(Cow::Borrowed(value)))
            }

            fn visit_string<E>(self, value: String) -> Result<Field<'de>, E>
            where
                E: de::Error,
            {
                Ok(Field::new(Cow::Owned(value)))
            }
        }

        deserializer.deserialize_identifier(FieldVisitor)
    }
}

struct ErrorKindVisitor;

impl<'de> Visitor<'de> for ErrorKindVisitor {
    type Value = ErrorKind;

    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("enum ErrorKind")
    }

    fn visit_map<V>(self, mut map: V) -> Result<ErrorKind, V::Error>
    where
        V: MapAccess<'de>,
    {
        let mut errcode = None;
        let mut soft_logout = None;
        let mut retry_after_ms = None;
        let mut room_version = None;
        let mut admin_contact = None;
        let mut status = None;
        let mut body = None;
        let mut current_version = None;
        let mut info_uri = None;
        let mut can_upgrade = None;
        #[cfg(feature = "unstable-msc4406")]
        let mut sender = None;
        let mut data = JsonObject::new();

        macro_rules! set_field {
            (errcode) => {
                set_field!(@inner errcode)
            };
            ($field:ident) => {
                match errcode {
                    Some(set_field!(@variant_containing $field)) | None => {
                        set_field!(@inner $field)
                    }
                    // if we already know we're deserializing a different variant to the one
                    // containing this field, ignore its value.
                    Some(_) => {
                        let _ = map.next_value::<de::IgnoredAny>()?;
                    },
                }
            };
            (@variant_containing soft_logout) => { ErrorCode::UnknownToken };
            (@variant_containing retry_after_ms) => { ErrorCode::LimitExceeded };
            (@variant_containing room_version) => { ErrorCode::IncompatibleRoomVersion };
            (@variant_containing admin_contact) => { ErrorCode::ResourceLimitExceeded };
            (@variant_containing status) => { ErrorCode::BadStatus };
            (@variant_containing body) => { ErrorCode::BadStatus };
            (@variant_containing current_version) => { ErrorCode::WrongRoomKeysVersion };
            (@variant_containing info_uri) => { ErrorCode::UserLimitExceeded };
            (@variant_containing can_upgrade) => { ErrorCode::UserLimitExceeded };
            (@variant_containing sender) => { ErrorCode::SenderIgnored };
            (@inner $field:ident) => {
                {
                    if $field.is_some() {
                        return Err(de::Error::duplicate_field(stringify!($field)));
                    }
                    $field = Some(map.next_value()?);
                }
            };
        }

        while let Some(key) = map.next_key()? {
            match key {
                Field::ErrorCode => set_field!(errcode),
                Field::SoftLogout => set_field!(soft_logout),
                Field::RetryAfterMs => set_field!(retry_after_ms),
                Field::RoomVersion => set_field!(room_version),
                Field::AdminContact => set_field!(admin_contact),
                Field::Status => set_field!(status),
                Field::Body => set_field!(body),
                Field::CurrentVersion => set_field!(current_version),
                Field::InfoUri => set_field!(info_uri),
                Field::CanUpgrade => set_field!(can_upgrade),
                #[cfg(feature = "unstable-msc4406")]
                Field::Sender => set_field!(sender),
                Field::Other(other) => match data.entry(other.into_owned()) {
                    Entry::Vacant(v) => {
                        v.insert(map.next_value()?);
                    }
                    Entry::Occupied(o) => {
                        return Err(de::Error::custom(format!("duplicate field `{}`", o.key())));
                    }
                },
            }
        }

        let errcode = errcode.ok_or_else(|| de::Error::missing_field("errcode"))?;

        Ok(match errcode {
            ErrorCode::AppserviceLoginUnsupported => ErrorKind::AppserviceLoginUnsupported,
            ErrorCode::BadAlias => ErrorKind::BadAlias,
            ErrorCode::BadJson => ErrorKind::BadJson,
            ErrorCode::BadState => ErrorKind::BadState,
            ErrorCode::BadStatus => ErrorKind::BadStatus(BadStatusErrorData {
                status: status
                    .map(|s| {
                        from_json_value::<u16>(s)
                            .map_err(de::Error::custom)?
                            .try_into()
                            .map_err(de::Error::custom)
                    })
                    .transpose()?,
                body: body.map(from_json_value).transpose().map_err(de::Error::custom)?,
            }),
            ErrorCode::CannotLeaveServerNoticeRoom => ErrorKind::CannotLeaveServerNoticeRoom,
            ErrorCode::CannotOverwriteMedia => ErrorKind::CannotOverwriteMedia,
            ErrorCode::CaptchaInvalid => ErrorKind::CaptchaInvalid,
            ErrorCode::CaptchaNeeded => ErrorKind::CaptchaNeeded,
            #[cfg(feature = "unstable-msc4306")]
            ErrorCode::ConflictingUnsubscription => ErrorKind::ConflictingUnsubscription,
            ErrorCode::ConnectionFailed => ErrorKind::ConnectionFailed,
            ErrorCode::ConnectionTimeout => ErrorKind::ConnectionTimeout,
            ErrorCode::DuplicateAnnotation => ErrorKind::DuplicateAnnotation,
            ErrorCode::Exclusive => ErrorKind::Exclusive,
            ErrorCode::Forbidden => ErrorKind::Forbidden,
            ErrorCode::GuestAccessForbidden => ErrorKind::GuestAccessForbidden,
            ErrorCode::IncompatibleRoomVersion => {
                ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
                    room_version: from_json_value(
                        room_version.ok_or_else(|| de::Error::missing_field("room_version"))?,
                    )
                    .map_err(de::Error::custom)?,
                })
            }
            ErrorCode::InvalidParam => ErrorKind::InvalidParam,
            ErrorCode::InvalidRoomState => ErrorKind::InvalidRoomState,
            ErrorCode::InvalidUsername => ErrorKind::InvalidUsername,
            ErrorCode::InviteBlocked => ErrorKind::InviteBlocked,
            ErrorCode::LimitExceeded => ErrorKind::LimitExceeded(LimitExceededErrorData {
                retry_after: retry_after_ms
                    .map(from_json_value::<UInt>)
                    .transpose()
                    .map_err(de::Error::custom)?
                    .map(Into::into)
                    .map(Duration::from_millis)
                    .map(RetryAfter::Delay),
            }),
            ErrorCode::MissingParam => ErrorKind::MissingParam,
            ErrorCode::MissingToken => ErrorKind::MissingToken,
            ErrorCode::NotFound => ErrorKind::NotFound,
            #[cfg(feature = "unstable-msc4306")]
            ErrorCode::NotInThread => ErrorKind::NotInThread,
            ErrorCode::NotJson => ErrorKind::NotJson,
            ErrorCode::NotYetUploaded => ErrorKind::NotYetUploaded,
            ErrorCode::ResourceLimitExceeded => {
                ErrorKind::ResourceLimitExceeded(ResourceLimitExceededErrorData {
                    admin_contact: from_json_value(
                        admin_contact.ok_or_else(|| de::Error::missing_field("admin_contact"))?,
                    )
                    .map_err(de::Error::custom)?,
                })
            }
            ErrorCode::RoomInUse => ErrorKind::RoomInUse,
            #[cfg(feature = "unstable-msc4406")]
            ErrorCode::SenderIgnored => ErrorKind::SenderIgnored(SenderIgnoredErrorData {
                sender: sender
                    .map(from_json_value::<Option<OwnedUserId>>)
                    .transpose()
                    .map_err(de::Error::custom)?
                    .flatten(),
            }),
            ErrorCode::ServerNotTrusted => ErrorKind::ServerNotTrusted,
            ErrorCode::ThreepidAuthFailed => ErrorKind::ThreepidAuthFailed,
            ErrorCode::ThreepidDenied => ErrorKind::ThreepidDenied,
            ErrorCode::ThreepidInUse => ErrorKind::ThreepidInUse,
            ErrorCode::ThreepidMediumNotSupported => ErrorKind::ThreepidMediumNotSupported,
            ErrorCode::ThreepidNotFound => ErrorKind::ThreepidNotFound,
            ErrorCode::TokenIncorrect => ErrorKind::TokenIncorrect,
            ErrorCode::TooLarge => ErrorKind::TooLarge,
            ErrorCode::UnableToAuthorizeJoin => ErrorKind::UnableToAuthorizeJoin,
            ErrorCode::UnableToGrantJoin => ErrorKind::UnableToGrantJoin,
            #[cfg(feature = "unstable-msc3843")]
            ErrorCode::Unactionable => ErrorKind::Unactionable,
            ErrorCode::Unauthorized => ErrorKind::Unauthorized,
            ErrorCode::Unknown => ErrorKind::Unknown,
            #[cfg(feature = "unstable-msc4186")]
            ErrorCode::UnknownPos => ErrorKind::UnknownPos,
            ErrorCode::UnknownToken => ErrorKind::UnknownToken(UnknownTokenErrorData {
                soft_logout: soft_logout
                    .map(from_json_value)
                    .transpose()
                    .map_err(de::Error::custom)?
                    .unwrap_or_default(),
            }),
            ErrorCode::Unrecognized => ErrorKind::Unrecognized,
            ErrorCode::UnsupportedRoomVersion => ErrorKind::UnsupportedRoomVersion,
            ErrorCode::UrlNotSet => ErrorKind::UrlNotSet,
            ErrorCode::UserDeactivated => ErrorKind::UserDeactivated,
            ErrorCode::UserInUse => ErrorKind::UserInUse,
            ErrorCode::UserLimitExceeded => {
                ErrorKind::UserLimitExceeded(UserLimitExceededErrorData {
                    info_uri: from_json_value(
                        info_uri.ok_or_else(|| de::Error::missing_field("info_uri"))?,
                    )
                    .map_err(de::Error::custom)?,
                    can_upgrade: can_upgrade
                        .map(from_json_value)
                        .transpose()
                        .map_err(de::Error::custom)?
                        .unwrap_or_default(),
                })
            }
            ErrorCode::UserLocked => ErrorKind::UserLocked,
            ErrorCode::UserSuspended => ErrorKind::UserSuspended,
            ErrorCode::WeakPassword => ErrorKind::WeakPassword,
            ErrorCode::WrongRoomKeysVersion => {
                ErrorKind::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData {
                    current_version: from_json_value(
                        current_version
                            .ok_or_else(|| de::Error::missing_field("current_version"))?,
                    )
                    .map_err(de::Error::custom)?,
                })
            }
            ErrorCode::_Custom(errcode) => {
                ErrorKind::_Custom(CustomErrorKind { errcode: errcode.0.into(), data })
            }
        })
    }
}

impl<'de> Deserialize<'de> for ErrorKind {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_map(ErrorKindVisitor)
    }
}

impl Serialize for ErrorKind {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut st = serializer.serialize_map(None)?;
        st.serialize_entry("errcode", &self.errcode())?;
        match self {
            Self::BadStatus(BadStatusErrorData { status, body }) => {
                if let Some(status) = status {
                    st.serialize_entry("status", &status.as_u16())?;
                }
                if let Some(body) = body {
                    st.serialize_entry("body", body)?;
                }
            }
            Self::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData { room_version }) => {
                st.serialize_entry("room_version", room_version)?;
            }
            Self::LimitExceeded(LimitExceededErrorData {
                retry_after: Some(RetryAfter::Delay(duration)),
            }) => {
                st.serialize_entry(
                    "retry_after_ms",
                    &UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
                )?;
            }
            Self::ResourceLimitExceeded(ResourceLimitExceededErrorData { admin_contact }) => {
                st.serialize_entry("admin_contact", admin_contact)?;
            }
            Self::UnknownToken(UnknownTokenErrorData { soft_logout: true }) | Self::UserLocked => {
                st.serialize_entry("soft_logout", &true)?;
            }
            Self::UserLimitExceeded(UserLimitExceededErrorData { info_uri, can_upgrade }) => {
                st.serialize_entry("info_uri", info_uri)?;

                if *can_upgrade {
                    st.serialize_entry("can_upgrade", can_upgrade)?;
                }
            }
            Self::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData { current_version }) => {
                st.serialize_entry("current_version", current_version)?;
            }
            #[cfg(feature = "unstable-msc4406")]
            Self::SenderIgnored(SenderIgnoredErrorData { sender }) => {
                if let Some(sender) = sender {
                    st.serialize_entry("sender", sender)?;
                }
            }
            Self::_Custom(CustomErrorKind { data, .. }) => {
                for (k, v) in data {
                    st.serialize_entry(k, v)?;
                }
            }
            Self::AppserviceLoginUnsupported
            | Self::BadAlias
            | Self::BadJson
            | Self::BadState
            | Self::CannotLeaveServerNoticeRoom
            | Self::CannotOverwriteMedia
            | Self::CaptchaInvalid
            | Self::CaptchaNeeded
            | Self::ConnectionFailed
            | Self::ConnectionTimeout
            | Self::DuplicateAnnotation
            | Self::Exclusive
            | Self::Forbidden
            | Self::GuestAccessForbidden
            | Self::InvalidParam
            | Self::InvalidRoomState
            | Self::InvalidUsername
            | Self::InviteBlocked
            | Self::LimitExceeded(LimitExceededErrorData {
                retry_after: None | Some(RetryAfter::DateTime(_)),
            })
            | Self::MissingParam
            | Self::MissingToken
            | Self::NotFound
            | Self::NotJson
            | Self::NotYetUploaded
            | Self::RoomInUse
            | Self::ServerNotTrusted
            | Self::ThreepidAuthFailed
            | Self::ThreepidDenied
            | Self::ThreepidInUse
            | Self::ThreepidMediumNotSupported
            | Self::ThreepidNotFound
            | Self::TokenIncorrect
            | Self::TooLarge
            | Self::UnableToAuthorizeJoin
            | Self::UnableToGrantJoin
            | Self::Unauthorized
            | Self::Unknown
            | Self::UnknownToken(UnknownTokenErrorData { soft_logout: false })
            | Self::Unrecognized
            | Self::UnsupportedRoomVersion
            | Self::UrlNotSet
            | Self::UserDeactivated
            | Self::UserInUse
            | Self::UserSuspended
            | Self::WeakPassword => {}
            #[cfg(feature = "unstable-msc4306")]
            Self::ConflictingUnsubscription => {}
            #[cfg(feature = "unstable-msc4306")]
            Self::NotInThread => {}
            #[cfg(feature = "unstable-msc3843")]
            Self::Unactionable => {}
            #[cfg(feature = "unstable-msc4186")]
            Self::UnknownPos => {}
        }
        st.end()
    }
}

#[cfg(test)]
mod tests {
    use ruma_common::room_version_id;
    use serde_json::{from_value as from_json_value, json};

    use super::{ErrorKind, IncompatibleRoomVersionErrorData};

    #[test]
    fn deserialize_forbidden() {
        let deserialized: ErrorKind = from_json_value(json!({ "errcode": "M_FORBIDDEN" })).unwrap();
        assert_eq!(deserialized, ErrorKind::Forbidden);
    }

    #[test]
    fn deserialize_forbidden_with_extra_fields() {
        let deserialized: ErrorKind = from_json_value(json!({
            "errcode": "M_FORBIDDEN",
            "error": "…",
        }))
        .unwrap();

        assert_eq!(deserialized, ErrorKind::Forbidden);
    }

    #[test]
    fn deserialize_incompatible_room_version() {
        let deserialized: ErrorKind = from_json_value(json!({
            "errcode": "M_INCOMPATIBLE_ROOM_VERSION",
            "room_version": "7",
        }))
        .unwrap();

        assert_eq!(
            deserialized,
            ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
                room_version: room_version_id!("7")
            })
        );
    }
}
