#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
    attr, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, GamesResponse, InstantiateMsg, QueryMsg};
use crate::state::{GameState, ADMIN, BLACKLIST, GAMES, LEADERBOARD};
use crate::{GameMove, GameResult};

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:rock-paper-scissors";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

//#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    _msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    ADMIN.set(deps, Some(info.sender.clone()))?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", info.sender))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    let api = deps.api;
    match msg {
        ExecuteMsg::StartGame {
            opponent,
            first_move,
        } => try_start(deps, info, &opponent, &first_move),
        ExecuteMsg::Respond {
            host,
            response_move,
        } => try_respond(deps, info, &host, &response_move),
        ExecuteMsg::UpdateAdmin { new_admin } => {
            Ok(ADMIN.execute_update_admin(deps, info, Some(api.addr_validate(&new_admin)?))?)
        }
        ExecuteMsg::AddHook { addr } => {
            Ok(BLACKLIST.execute_add_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?)
        }
        ExecuteMsg::RemoveHook { addr } => {
            Ok(BLACKLIST.execute_remove_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?)
        }
    }
}

fn try_start(
    deps: DepsMut,
    info: MessageInfo,
    opponent: &str,
    first_move_str: &str,
) -> Result<Response, ContractError> {
    let validation = deps.api.addr_validate(opponent);
    let opponent_addr: Addr;
    match validation {
        Ok(addr) => {
            opponent_addr = addr;
        }
        Err(_) => return Err(ContractError::InvalidOpponent {}),
    }

    // check if info.sender is in blacklist
    if BLACKLIST
        .query_hooks(deps.as_ref())?
        .hooks
        .iter()
        .any(|a| a == &info.sender)
    {
        return Err(ContractError::Blacklisted {});
    }

    // TODO: convert gamemove to a struct with a ::new method
    let first_move = GameMove::from(first_move_str)?;
    // load the games state for the current sender, opponent tuple
    // insert a GameState with the first_move
    let key = (&info.sender, &opponent_addr);
    GAMES.update(deps.storage, key, |state| match state {
        Some(_result) => Err(ContractError::GameInProgress {}),
        None => Ok(GameState {
            host: info.sender.clone(),
            opponent: opponent_addr.clone(),
            host_move: first_move,
            opponent_move: None,
            result: None,
        }),
    })?;

    Ok(Response::new().add_attribute("method", "try_start"))
}

fn try_respond(
    deps: DepsMut,
    info: MessageInfo,
    host_unchecked: &str,
    response_move: &str,
) -> Result<Response, ContractError> {
    let host = deps.api.addr_validate(host_unchecked)?;
    // check if move valid
    let game_move = GameMove::from(response_move)?;
    // check if Game present
    let mut game: GameState;
    let key = (&host, &info.sender);
    match GAMES.may_load(deps.storage, key)? {
        Some(state) => game = state,
        None => return Err(ContractError::NoGameFound {}),
    }

    let mut attributes = vec![attr("host", &host), attr("opponent", &info.sender)];
    let result = game.clone().host_move.duel(&game_move);
    let upsert_score = |stored| -> Result<u8, ContractError> {
        match stored {
            Some(score) => Ok(score + 1),
            None => Ok(1),
        }
    };
    match result {
        GameResult::HostWins => {
            attributes.push(attr("result", "HostWins"));
            LEADERBOARD.update(deps.storage, &host, upsert_score)?;
        }
        GameResult::OpponentWins => {
            attributes.push(attr("result", "OpponentWins"));
            LEADERBOARD.update(deps.storage, &info.sender, upsert_score)?;
        }
        GameResult::Tie => attributes.push(attr("result", "Tie")),
    }
    game.result = Some(result);
    game.opponent_move = Some(game_move);

    // Save GameState
    GAMES.save(deps.storage, key, &game)?;

    // Delete GameState
    GAMES.remove(deps.storage, key);

    Ok(Response::new().add_attributes(attributes))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::ByHost { host } => to_binary(&query_games(deps, &host, true)?),
        QueryMsg::ByOpponent { opponent } => to_binary(&query_games(deps, &opponent, false)?),
        QueryMsg::QueryAdmin {} => to_binary(&ADMIN.get(deps)?),
        QueryMsg::Leaderboard {} => to_binary::<Vec<(Addr, u8)>>(
            // Sorted by address; Sorting by score done client-side
            &LEADERBOARD
                .range(deps.storage, None, None, Order::Ascending)
                .collect::<StdResult<Vec<_>>>()?,
        ),
    }
}

fn query_games(deps: Deps, raw_query_address: &str, by_host: bool) -> StdResult<GamesResponse> {
    let validated = deps.api.addr_validate(raw_query_address)?;
    let games: Vec<_>;
    if by_host {
        // this may be gas-inefficient for calling the loop twice, can't figure this out rn...
        let games_by_host: Vec<(Addr, GameState)> = GAMES
            .prefix(&validated)
            .range(deps.storage, None, None, Order::Ascending)
            .collect::<StdResult<_>>()?;
        games = games_by_host.iter().map(|item| item.clone().1).collect();
    } else {
        let games_by_opponent: Vec<((Addr, Addr), GameState)> = GAMES
            .range(deps.storage, None, None, Order::Ascending)
            .filter(|r| r.as_ref().unwrap().0 .1 == validated)
            .collect::<StdResult<_>>()?;
        games = games_by_opponent
            .iter()
            .map(|item| item.clone().1)
            .collect();
    }
    Ok(GamesResponse { games })
}

#[cfg(test)]
mod tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies_with_balance, mock_env, mock_info};
    use cosmwasm_std::{coins, from_binary};

    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));

        let info = mock_info("creator", &coins(1000, "earth"));
        // we can just call .unwrap() to assert this was a success
        let res = instantiate(deps.as_mut(), mock_env(), info, InstantiateMsg {}).unwrap();
        assert_eq!(0, res.messages.len());
    }

    #[test]
    fn addr_validation() {
        // unsing same sender for all msgs
        let info = mock_info("creator", &coins(1000, "earth"));

        // Invalid address: addr_validate -> invalid for >256 byte address
        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));
        let mut invalid_address = String::new();
        for _i in 0..300 {
            invalid_address.push('a');
        }
        let msg = ExecuteMsg::StartGame {
            opponent: invalid_address,
            first_move: "rock".to_string(),
        };
        let res = execute(deps.as_mut(), mock_env(), info.clone(), msg);
        match res {
            Err(ContractError::InvalidOpponent {}) => {}
            _ => panic!("Should have recognized invalid opponent address (too long)"),
        }

        // Valid
        let valid_address = "opponent";
        let msg = ExecuteMsg::StartGame {
            opponent: valid_address.to_string(),
            first_move: "scissors".to_string(),
        };
        let res: Response = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap();
        assert_eq!(res.attributes.len(), 1);
    }

    #[test]
    fn gamemove_validation() {
        // unsing same sender for all msgs
        let info = mock_info("creator", &coins(1000, "earth"));

        // Invalid address: addr_validate
        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));
        // Valid Address but invalid GameMove
        let msg = ExecuteMsg::StartGame {
            opponent: "opponent".to_string(),
            first_move: "skyscraper".to_string(),
        };
        let res = execute(deps.as_mut(), mock_env(), info, msg);
        match res {
            Err(ContractError::InvalidGameMove {}) => {}
            _ => panic!("Should have recognized invalid game move"),
        }
    }

    #[test]
    fn no_multiple_games() {
        let info = mock_info("creator", &coins(1000, "earth"));
        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));
        let msg = ExecuteMsg::StartGame {
            opponent: "opponent".to_string(),
            first_move: "scissors".to_string(),
        };
        // calling again raises Error
        let _res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone());
        match execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()) {
            Err(ContractError::GameInProgress {}) => {}
            _ => panic!("Should have recognized that this player already has a game in progress"),
        }
    }

    #[test]
    fn correct_query() {
        // unsing same sender for all msgss
        let players: Vec<String> = (0..10).map(|i| format!("player{:?}", i)).collect();

        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));
        for i in 1..10 {
            // player0 starts a game with every other player
            let info = mock_info(&players[0], &coins(1000, "earth"));
            let msg = ExecuteMsg::StartGame {
                opponent: players[i].clone(),
                first_move: "scissors".to_string(),
            };
            let _res = execute(deps.as_mut(), mock_env(), info, msg);

            if i > 1 && i < 7 {
                // player1 starts a game with players 2-6 (5 games)
                let info = mock_info(&players[1], &coins(1000, "earth"));
                let msg = ExecuteMsg::StartGame {
                    opponent: players[i].clone(),
                    first_move: "scissors".to_string(),
                };
                let _res = execute(deps.as_mut(), mock_env(), info, msg);
            }
        }

        // -> player0 has 9 games
        // -> player1 has 5 games
        // -> player1, player7, player8, player9 are opponents in 1 game
        // -> player2-6 are opponents in 2 games
        let msg = QueryMsg::ByHost {
            host: players[0].clone(),
        };
        let res = query(deps.as_ref(), mock_env(), msg).unwrap();
        let response: GamesResponse = from_binary(&res).unwrap();
        assert_eq!(response.games.len(), 9);

        let msg = QueryMsg::ByHost {
            host: players[1].clone(),
        };
        let res = query(deps.as_ref(), mock_env(), msg).unwrap();
        let response: GamesResponse = from_binary(&res).unwrap();
        assert_eq!(response.games.len(), 5);

        for i in 7..10 {
            let msg = QueryMsg::ByOpponent {
                opponent: players[i].clone(),
            };
            let res = query(deps.as_ref(), mock_env(), msg).unwrap();
            let response: GamesResponse = from_binary(&res).unwrap();
            assert_eq!(response.games.len(), 1);
        }

        for i in 2..7 {
            let msg = QueryMsg::ByOpponent {
                opponent: players[i].clone(),
            };
            let res = query(deps.as_ref(), mock_env(), msg).unwrap();
            let response: GamesResponse = from_binary(&res).unwrap();
            assert_eq!(response.games.len(), 2);
        }
    }

    #[test]
    fn admin() {
        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));

        let admin_info = mock_info("admin", &coins(1000, "earth"));
        // we can just call .unwrap() to assert this was a success
        let _res = instantiate(
            deps.as_mut(),
            mock_env(),
            admin_info.clone(),
            InstantiateMsg {},
        )
        .unwrap();

        // query
        let query_msg = QueryMsg::QueryAdmin {};
        let res = query(deps.as_ref(), mock_env(), query_msg.clone()).unwrap();
        let response: Option<Addr> = from_binary(&res).unwrap();
        match response {
            Some(addr) => assert_eq!(admin_info.sender.clone(), addr),
            _ => panic!("admin should be admin"),
        }

        // imposter cannot set himself as admin
        let imposter_info = mock_info("imposter", &coins(1000, "earth"));
        let msg = ExecuteMsg::UpdateAdmin {
            new_admin: String::from(imposter_info.sender.clone()),
        };
        match execute(
            deps.as_mut(),
            mock_env(),
            imposter_info.clone(),
            msg.clone(),
        ) {
            Err(_) => {}
            Ok(_) => panic!("imposter should not be able to set himself as admin"),
        }

        // admin can set imposter as new admin
        let _res = execute(deps.as_mut(), mock_env(), admin_info, msg.clone());
        let res = query(deps.as_ref(), mock_env(), query_msg.clone()).unwrap();
        let response: Option<Addr> = from_binary(&res).unwrap();
        match response {
            Some(addr) => assert_eq!(imposter_info.sender, addr),
            _ => panic!("imposter should be admin"),
        }
    }

    #[test]
    fn blacklist() {
        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));

        let admin_info = mock_info("admin", &coins(1000, "earth"));
        // we can just call .unwrap() to assert this was a success
        let res = instantiate(
            deps.as_mut(),
            mock_env(),
            admin_info.clone(),
            InstantiateMsg {},
        )
        .unwrap();
        assert_eq!(0, res.messages.len());

        // Admin can add to blacklist
        let msg = ExecuteMsg::AddHook {
            addr: "someone".to_string(),
        };
        let response = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap();
        assert_eq!(3, response.attributes.len());

        // Noone else can add to the blacklist, no matter how rich
        let noone_info = mock_info("noone", &coins(1_000_000_000, "earth"));
        let msg = ExecuteMsg::AddHook {
            addr: "someone_else".to_string(),
        };
        match execute(deps.as_mut(), mock_env(), noone_info.clone(), msg) {
            Err(ContractError::HookError { .. }) => {}
            _ => panic!("Should have aborted with HookError"),
        }

        // Someone on the blacklist cannot start a game
        let someone_info = mock_info("someone", &coins(1, "earth"));
        let blacklisted_msg = ExecuteMsg::StartGame {
            opponent: "admin".to_string(),
            first_move: "rock".to_string(),
        };
        match execute(
            deps.as_mut(),
            mock_env(),
            someone_info.clone(),
            blacklisted_msg.clone(),
        ) {
            Err(ContractError::Blacklisted {}) => {}
            _ => panic!("Should be blacklisted..."),
        }
        // Noone else can remove from the blacklist
        let unblacklist_msg = ExecuteMsg::RemoveHook {
            addr: "someone".to_string(),
        };
        match execute(
            deps.as_mut(),
            mock_env(),
            noone_info.clone(),
            unblacklist_msg.clone(),
        ) {
            Err(ContractError::HookError { .. }) => {}
            _ => panic!("Noone except admin should be able to remove from blacklist"),
        }
        // Admin can remove from the blacklist
        let _res = execute(
            deps.as_mut(),
            mock_env(),
            admin_info.clone(),
            unblacklist_msg.clone(),
        )
        .unwrap();
        // Someone removed from the blacklist can start a game
        let _res = execute(
            deps.as_mut(),
            mock_env(),
            someone_info.clone(),
            blacklisted_msg.clone(),
        )
        .unwrap();
    }

    #[test]
    fn duel() {
        let mut deps = mock_dependencies_with_balance(&coins(2, "token"));
        let host_info = mock_info("host", &coins(1000, "earth"));
        let opponent_info = mock_info("opponent", &coins(1000, "earth"));

        let host_moves = vec!["rock", "paper", "paper", "rock"];
        let opponent_moves = vec!["scissors", "scissors", "rock", "rock"];
        let results = vec!["HostWins", "OpponentWins", "HostWins", "Tie"];

        for ((host_move, opponent_move), result) in host_moves
            .iter()
            .zip(opponent_moves.iter())
            .zip(results.iter())
        {
            // start game
            let start_msg = ExecuteMsg::StartGame {
                opponent: opponent_info.sender.to_string(),
                first_move: host_move.to_string(),
            };
            let _res = execute(deps.as_mut(), mock_env(), host_info.clone(), start_msg);
            let query_msg = QueryMsg::ByOpponent {
                opponent: opponent_info.sender.to_string(),
            };
            let query_res = query(deps.as_ref(), mock_env(), query_msg.clone()).unwrap();
            let response: GamesResponse = from_binary(&query_res).unwrap();
            assert_eq!(response.games.len(), 1);

            // respond
            let respond_msg = ExecuteMsg::Respond {
                host: host_info.sender.to_string(),
                response_move: opponent_move.to_string(),
            };
            let response = execute(
                deps.as_mut(),
                mock_env(),
                opponent_info.clone(),
                respond_msg,
            )
            .unwrap();

            // assert correct result
            assert_eq!(response.attributes[2].value, result.to_string());

            // assert deletion
            let query_res = query(deps.as_ref(), mock_env(), query_msg).unwrap();
            let response: GamesResponse = from_binary(&query_res).unwrap();
            assert_eq!(response.games.len(), 0);
        }

        let leaderboard_msg = QueryMsg::Leaderboard {};
        let leaderboard_res = query(deps.as_ref(), mock_env(), leaderboard_msg).unwrap();
        let leaderboard: Vec<(Addr, u8)> = from_binary(&leaderboard_res).unwrap();
        for (addr, score) in leaderboard.iter() {
            if &host_info.sender == addr {
                assert_eq!(score, &2)
            } else if &opponent_info.sender == addr {
                assert_eq!(score, &1)
            } else {
                panic!("There should be noone else in the leaderbord than opponent and host")
            }
        }
    }
}
