use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum BoardEvent {
    GameStart {
        game: Game,
    },
    GameFinish {
        game: Game,
    },
    Challenge {
        challenge: Challenge,
        compat: Option<Compat>,
    },
    ChallengeCanceled {
        challenge: Challenge,
    },
    ChallengeDeclined {
        challenge: ChallengeDeclined,
    },
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Game {
    pub full_id: String,
    pub game_id: String,
    pub fen: String,
    pub color: Color,
    pub last_move: String,
    pub source: Source,
    pub status: GameStatus,
    pub variant: Variant,
    pub speed: Speed,
    pub perf: String,
    pub rated: bool,
    pub has_moved: bool,
    pub opponent: Opponent,
    pub is_my_turn: bool,
    pub seconds_left: Option<i64>,
    pub winner: Option<Color>,
    #[serde(rename = "ratingDiff")]
    pub rating_diff: Option<i32>,
    pub compat: Option<Compat>,
    pub id: String,
    #[serde(rename = "tournamentId")]
    pub tournament_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Challenge {
    pub id: String,
    pub url: String,
    pub status: ChallengeStatus,
    pub challenger: Challenger,
    pub dest_user: Option<Challenger>,
    pub variant: Variant,
    pub rated: bool,
    pub speed: Speed,
    pub time_control: TimeControl,
    pub color: ChallengeColor,
    pub final_color: Option<Color>,
    pub perf: Perf,
    pub direction: Option<Direction>,
    pub initial_fen: Option<String>,
    pub rematch_of: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChallengeDeclined {
    #[serde(flatten)]
    pub challenge: Challenge,
    /// Human readable, possibly translated reason why the challenge was declined.
    pub decline_reason: String,
    /// Untranslated, computer-matchable reason why the challenge was declined.
    pub decline_reason_key: DeclineReasonKey,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum DeclineReasonKey {
    Generic,
    Later,
    Toofast,
    Tooslow,
    Timecontrol,
    Rated,
    Casual,
    Standard,
    Variant,
    Nobot,
    Onlybot,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameStatus {
    pub id: i32,
    pub name: GameStatusName,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Opponent {
    #[serde(rename_all = "camelCase")]
    Player {
        id: String,
        username: String,
        rating: i32,
        rating_diff: Option<i32>,
    },
    #[serde(rename_all = "camelCase")]
    Ai {
        id: Option<String>,
        username: String,
        /// AI level, from 1 to 8, where 1 is the weakest and 8 is the strongest.
        ai: i32,
    },
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Variant {
    pub key: VariantKey,
    pub name: String,
    pub short: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Compat {
    /// Compatible with Bot API
    pub bot: bool,
    /// Compatible with Board API
    pub board: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Challenger {
    pub id: String,
    pub name: String,
    pub rating: Option<i32>,
    /// only appears if the user is a titled player or a bot user
    pub title: Option<Title>,
    /// See [available flair list and images](https://github.com/lichess-org/lila/tree/master/public/flair)
    pub flair: Option<String>,
    /// Use patronColor value instead to determine if player is a patron.
    #[deprecated]
    pub patron: Option<bool>,
    /// Players can choose a color for their Patron wings.
    /// See [here for the color mappings](https://github.com/lichess-org/lila/blob/master/ui/lib/css/abstract/_patron-colors.scss).
    ///
    /// The presence of this field indicates the player is an active Patron.
    pub patron_color: Option<i32>,
    pub provisional: Option<bool>,
    pub online: Option<bool>,
    pub lag: Option<i32>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum TimeControl {
    Clock {
        limit: i64,
        increment: i64,
        show: String,
    },
    Correspondence {
        days_per_turn: i32,
    },
    Unlimited,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Perf {
    pub icon: String,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Color {
    White,
    Black,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ChallengeColor {
    White,
    Black,
    Random,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Source {
    Lobby,
    Friend,
    Ai,
    Api,
    Tournament,
    Position,
    Import,
    Importlive,
    Simul,
    Relay,
    Pool,
    Arena,
    Swiss,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum GameStatusName {
    Created,
    Started,
    Aborted,
    Mate,
    Resign,
    Stalemate,
    Timeout,
    Draw,
    Outoftime,
    Cheat,
    NoStart,
    UnknownFinish,
    InsufficientMaterialClaim,
    VariantEnd,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VariantKey {
    Standard,
    Chess960,
    Crazyhouse,
    Antichess,
    Atomic,
    Horde,
    KingOfTheHill,
    RacingKings,
    ThreeCheck,
    FromPosition,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Speed {
    UltraBullet,
    Bullet,
    Blitz,
    Rapid,
    Classical,
    Correspondence,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Title {
    GM,
    WGM,
    IM,
    WIM,
    FM,
    WFM,
    NM,
    CM,
    WCM,
    WNM,
    LM,
    BOT,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Direction {
    In,
    Out,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ChallengeStatus {
    Created,
    Offline,
    Canceled,
    Declined,
    Accepted,
}

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

    #[test]
    fn test_game_start_deserialization() {
        let json = include_str!("../../test_data/board/game_start.json");
        let event: BoardEvent =
            serde_json::from_str(json).expect("Failed to deserialize GameStart event");

        if let BoardEvent::GameStart { game } = event {
            assert_eq!(game.full_id, "MoWs8TanjdQO");
            assert_eq!(game.game_id, "MoWs8Tan");
            assert_eq!(game.color, Color::White);
            assert_eq!(game.source, Source::Friend);
            assert_eq!(game.rated, true);
            if let Opponent::Player { id, .. } = game.opponent {
                assert_eq!(id, "aaron");
            } else {
                panic!("Expected player opponent");
            }
        } else {
            panic!("Expected GameStart event");
        }
    }

    #[test]
    fn test_game_start_ai_deserialization() {
        let json = include_str!("../../test_data/board/game_start_ai.json");
        let event: BoardEvent =
            serde_json::from_str(json).expect("Failed to deserialize GameStart (AI) event");

        if let BoardEvent::GameStart { game } = event {
            assert_eq!(game.full_id, "hX5KbyLWtSFO");
            assert_eq!(game.source, Source::Ai);
            assert_eq!(game.rated, false);
            if let Opponent::Ai { ai, username, .. } = game.opponent {
                assert_eq!(ai, 1);
                assert_eq!(username, "Stockfish level 1");
            } else {
                panic!("Expected AI opponent");
            }
        } else {
            panic!("Expected GameStart event");
        }
    }

    #[test]
    fn test_game_finish_deserialization() {
        let json = include_str!("../../test_data/board/game_finish.json");
        let event: BoardEvent =
            serde_json::from_str(json).expect("Failed to deserialize GameFinish event");

        if let BoardEvent::GameFinish { game } = event {
            assert_eq!(game.full_id, "MoWs8TanjdQO");
            assert_eq!(game.status.name, GameStatusName::Resign);
            assert_eq!(game.winner, Some(Color::Black));
            assert_eq!(game.rating_diff, Some(-33));
            if let Opponent::Player { rating_diff, .. } = game.opponent {
                assert_eq!(rating_diff, Some(45));
            } else {
                panic!("Expected player opponent");
            }
        } else {
            panic!("Expected GameFinish event");
        }
    }

    #[test]
    fn test_game_finish_ai_deserialization() {
        let json = include_str!("../../test_data/board/game_finish_ai.json");
        let event: BoardEvent =
            serde_json::from_str(json).expect("Failed to deserialize GameFinish (AI) event");

        if let BoardEvent::GameFinish { game } = event {
            assert_eq!(game.full_id, "hX5KbyLWtSFO");
            assert_eq!(game.status.name, GameStatusName::Aborted);
            assert_eq!(game.winner, None);
        } else {
            panic!("Expected GameFinish event");
        }
    }

    #[test]
    fn test_challenge_deserialization() {
        let json = include_str!("../../test_data/board/challenge.json");
        let event: BoardEvent =
            serde_json::from_str(json).expect("Failed to deserialize Challenge event");

        if let BoardEvent::Challenge { challenge, .. } = event {
            assert_eq!(challenge.id, "f5gJngU0");
            assert_eq!(challenge.status, ChallengeStatus::Created);
            assert_eq!(challenge.challenger.name, "Adriana");
            assert_eq!(challenge.dest_user.as_ref().unwrap().name, "Gabriela");
            assert!(matches!(challenge.time_control, TimeControl::Unlimited));
        } else {
            panic!("Expected Challenge event");
        }
    }

    #[test]
    fn test_challenge_declined_deserialization() {
        let json = include_str!("../../test_data/board/challenge_declined.json");
        let event: BoardEvent =
            serde_json::from_str(json).expect("Failed to deserialize ChallengeDeclined event");

        if let BoardEvent::ChallengeDeclined { challenge } = event {
            assert_eq!(challenge.challenge.id, "f5gJngU0");
            assert_eq!(challenge.challenge.status, ChallengeStatus::Declined);
            assert_eq!(challenge.decline_reason_key, DeclineReasonKey::Generic);
        } else {
            panic!("Expected ChallengeDeclined event");
        }
    }

    #[test]
    fn test_challenge_canceled_deserialization() {
        let json = include_str!("../../test_data/board/challenge_canceled.json");
        let event: BoardEvent =
            serde_json::from_str(json).expect("Failed to deserialize ChallengeCanceled event");

        if let BoardEvent::ChallengeCanceled { challenge } = event {
            assert_eq!(challenge.id, "eSb8Y7bX");
            assert_eq!(challenge.status, ChallengeStatus::Canceled);
            assert_eq!(challenge.challenger.title, Some(Title::CM));
        } else {
            panic!("Expected ChallengeCanceled event");
        }
    }
}
