// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

interface IERC20 {
    function balanceOf(address owner) external view returns (uint);
}

contract Betting {
    event Withdrawal(address indexed user, string matchId, uint256 amount);
    event NewBet(string matchId, address indexed better, uint betAmount, string teamName);
    event MatchResultChanged(string matchId, MatchResult newResult);
    event NewMatch(string matchId, string teamAName, string teamBName);

    enum MatchResult { NotDecided, TeamAWon, TeamBWon }
    mapping(string => MatchResult) public matchResults;

    struct Team {
        string name;
        uint256 totalBetAmount;
        uint256 totalBettors;
    }
    
    struct Match {
        string id;
        Team teamA;
        Team teamB;
        bool isBettingOpen;
        mapping(address => string) bettors;
    }
    
    struct Bet {
        address payable better;
        uint256 amount;
    }

    bool internal locked;
    uint256 private feeRate       = 50; // 5% fee
    uint256 private holderFeeRate = 10;  // 1% fee
    uint256 private minHolderAmount = 3500000 * 10 ** 9;

    address governanceToken = address(0xF5aAD0376Cf00aFC8E295d4DB6DB7570c2137Bd7);
    address feeReceiver = address(0xDA36593e7606Bc28a0cb9FcE92C6549Bb3EE2Ef2);

    string public name = "SoccerBet: Betting Contract";
    string[] private matchIds;
    mapping(string => Match) public matches;
    mapping(string => mapping(address => Bet)) public bets;
    mapping(address => bool) public administrators;
    mapping(string => string) teamNamesToMatchId;
    mapping(address => string[]) public bettorMatchIds;
    mapping(address => mapping(string => bool)) hasBettedOnMatch;

    modifier onlyAdministrator() {
        require(administrators[msg.sender], "Only administrators can call this function");
        _;
    }

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }

    constructor() {
        administrators[msg.sender] = true;
    }

    function addAdministrator(address admin) external onlyAdministrator() {
        administrators[admin] = true;
    }

    function addMatch(string memory id, string memory teamAName, string memory teamBName) external onlyAdministrator {
        require(administrators[msg.sender], "Only administrators can add matches");
        require(bytes(matches[id].id).length == 0, "Match already exists");
        require(keccak256(abi.encodePacked(teamAName)) != keccak256(abi.encodePacked(teamBName)), "Team names must be different");

        matches[id].id = id;

        matches[id].teamA.name = teamAName;
        matches[id].teamA.totalBetAmount = 0;
        matches[id].teamA.totalBettors = 0;

        matches[id].teamB.name = teamBName;
        matches[id].teamB.totalBetAmount = 0;
        matches[id].teamB.totalBettors = 0;

        matches[id].isBettingOpen = true;

        teamNamesToMatchId[getTeamNamesKey(teamAName, teamBName)] = id;
        matchIds.push(id);

        emit NewMatch(id, teamAName, teamBName);
    }

    function placeBet(string memory matchId, string memory teamName) external payable noReentrant {
        require(msg.value > 0, "Bet amount must be greater than 0");
        require(bytes(matches[matchId].id).length != 0, "Match doesn't exists");
        require(matches[matchId].isBettingOpen, "Betting is closed for this match");
        require(keccak256(abi.encodePacked(matches[matchId].bettors[msg.sender])) == keccak256(abi.encodePacked("")) || 
                keccak256(abi.encodePacked(matches[matchId].bettors[msg.sender])) == keccak256(abi.encodePacked(teamName)), "You already betted on this match with another team");
        
        bool isNewBetter = bets[matchId][msg.sender].amount == 0;

        if (keccak256(abi.encodePacked(teamName)) == keccak256(abi.encodePacked(matches[matchId].teamA.name))) {
            matches[matchId].teamA.totalBetAmount += msg.value;
            if (isNewBetter) matches[matchId].teamA.totalBettors++;
        } else if (keccak256(abi.encodePacked(teamName)) == keccak256(abi.encodePacked(matches[matchId].teamB.name))) {
            matches[matchId].teamB.totalBetAmount += msg.value;
            if (isNewBetter) matches[matchId].teamB.totalBettors++;
        } else {
            revert("Team doesn't exists");
        }

        matches[matchId].bettors[msg.sender] = teamName;
        bets[matchId][msg.sender].better = payable(msg.sender);
        bets[matchId][msg.sender].amount += msg.value;


        if (!hasBettedOnMatch[msg.sender][matchId]) {
            hasBettedOnMatch[msg.sender][matchId] = true;
            bettorMatchIds[msg.sender].push(matchId);
        }

        emit NewBet(matchId, msg.sender, msg.value, teamName);
    }

    function openBetting(string memory matchId) external onlyAdministrator {
        require(bytes(matches[matchId].id).length != 0, "Match doesn't exists");
        matches[matchId].isBettingOpen = true;
    }

    function closeBetting(string memory matchId) external onlyAdministrator {
        require(bytes(matches[matchId].id).length != 0, "Match doesn't exists");
        matches[matchId].isBettingOpen = false;
    }

    function getBettorMatchIds(address bettor) external view returns (string[] memory) {
        return bettorMatchIds[bettor];
    }

    function getBetDetailsByMatchId(string memory matchId) external view returns (
        string memory,
        string memory,
        string memory,
        uint256,
        MatchResult
        ) {
        require(bytes(matches[matchId].id).length != 0, "Match doesn't exists");

        return (
            matches[matchId].teamA.name,
            matches[matchId].teamB.name,
            matches[matchId].bettors[tx.origin],
            bets[matchId][tx.origin].amount,
            matchResults[matchId]
        );
    }

    function getMatchIds(bool filterIsBettingOpen) external view returns (string[] memory) {
        string[] memory found_matchIds = new string[](0);
    
        for(uint i = 0; i < matchIds.length; i++) {
            if (!filterIsBettingOpen || matches[matchIds[i]].isBettingOpen) {
                string[] memory temp = new string[](found_matchIds.length + 1);
                for(uint j = 0; j < found_matchIds.length; j++) {
                    temp[j] = found_matchIds[j];
                }
                temp[found_matchIds.length] = matchIds[i];
                found_matchIds = temp;
            }
        }
    
        return found_matchIds;
    }

    function setMatchResult(string memory matchId, MatchResult result) external onlyAdministrator {
        require(result != MatchResult.NotDecided, "Match result must be decided");
        matchResults[matchId] = result;

        if (result != MatchResult.NotDecided) {
            emit MatchResultChanged(matchId, result);
        }
    }

    function withdraw(string memory matchId) external noReentrant {
        require(matchResults[matchId] != MatchResult.NotDecided, "Match result is not decided yet");
        require(keccak256(abi.encodePacked(matches[matchId].bettors[msg.sender])) != keccak256(abi.encodePacked("")), "Better didn't bet on this match");

        MatchResult result = matchResults[matchId];
        string memory winnerTeam = result == MatchResult.TeamAWon ? matches[matchId].teamA.name : matches[matchId].teamB.name;
        uint loserTotalBetAmount = result == MatchResult.TeamAWon ? matches[matchId].teamB.totalBetAmount : matches[matchId].teamA.totalBetAmount;
        uint winerTotalBetAmount = result == MatchResult.TeamAWon ? matches[matchId].teamA.totalBetAmount : matches[matchId].teamB.totalBetAmount;

        require(keccak256(abi.encodePacked(matches[matchId].bettors[msg.sender])) == keccak256(abi.encodePacked(winnerTeam)), "Better didn't bet on the winning team");

        uint userBetAmount = bets[matchId][msg.sender].amount;
        uint winnings_without_fees = (userBetAmount * loserTotalBetAmount / winerTotalBetAmount) + userBetAmount;

        uint applicableFeeRate = IERC20(governanceToken).balanceOf(msg.sender) >= minHolderAmount ? holderFeeRate : feeRate;

        uint fees = winnings_without_fees * applicableFeeRate / 1000;
        uint winnings = winnings_without_fees - fees;

        require(userBetAmount > 0, "Better didn't bet on this match or already withdrew the winnings");
        require(winnings_without_fees <= address(this).balance, "Contract doesn't have enough balance to pay the winnings");

        bets[matchId][msg.sender].amount = 0;
        payable(msg.sender).transfer(winnings);
        payable(feeReceiver).transfer(fees);

        delete matches[matchId].bettors[msg.sender];

        emit Withdrawal(msg.sender, matchId, winnings);
    }

    function setFeeRating(uint256 newFee, uint256 newHolderFee) external onlyAdministrator {
        require(newFee >= 0 && newFee <= 50, "Fee cannot be greater than 5%");
        require(newHolderFee >= 0 && newHolderFee <= 50, "Holder fee cannot be greater than 2%");
        feeRate = newFee;
        holderFeeRate = newHolderFee;
    }

    function setFeeReceiver(address newReceiver) external onlyAdministrator {
        require(newReceiver != address(0), "Fee receiver cannot be zero address");
        feeReceiver = newReceiver;
    }

    function getFeeRating() external view returns (uint256, uint256) {
        return (feeRate, holderFeeRate);
    }

    function setMinHolderAmount(uint256 newMinHolder) external onlyAdministrator {
        minHolderAmount = newMinHolder;
    }

    function setGovernanceToken(address newToken) external onlyAdministrator {
        require(newToken != address(0), "Governance token cannot be zero address");
        governanceToken = newToken;
    }

    function getTotalBetAndBettors(string memory matchId) external view returns (uint256, uint256, uint256, uint256) {
        require(bytes(matches[matchId].id).length != 0, "Match doesn't exists");
        return (
            matches[matchId].teamA.totalBetAmount,
            matches[matchId].teamA.totalBettors,
            matches[matchId].teamB.totalBetAmount,
            matches[matchId].teamB.totalBettors
        );
    }

    function getMatchId(string memory teamAName, string memory teamBName) external view returns (string memory) {
        string memory matchId = teamNamesToMatchId[getTeamNamesKey(teamAName, teamBName)];
        require(bytes(matchId).length != 0, "Match not found");
        return matchId;
    }


    function getTeamNamesKey(string memory teamAName, string memory teamBName) internal pure returns (string memory) {
        return string(abi.encodePacked(teamAName, "_", teamBName));
    }
}