// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.24;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./IAcceptAvailability.sol";

import "hardhat/console.sol";

struct Stake {
    bool inFavor;
    uint256 amount;
    address sender;
}

struct ValidationPool {
    mapping(uint => Stake) stakes;
    uint stakeCount;
    address author;
    uint256 fee;
    uint256 initialStakedFor;
    uint256 initialStakedAgainst;
    uint duration;
    uint endTime;
    bool resolved;
    bool outcome;
}

struct StakeData {
    uint poolIndex;
    bool inFavor;
}

/// This contract must manage validation pools and reputation,
/// because otherwise there's no way to enforce appropriate permissions on
/// transfer of value between reputation NFTs.
contract DAO is ERC20("Reputation", "REP") {
    mapping(uint => address) public members;
    uint public memberCount;
    mapping(address => bool) public isMember;
    mapping(uint => ValidationPool) public validationPools;
    uint public validationPoolCount;

    // ufixed8x1 constant mintingRatio = 1;
    // ufixed8x1 constant quorum = 0;
    // ufixed8x1 constant stakeForAuthor = 0.5;
    // ufixed8x1 constant winningRatio = 0.5;
    // TODO: Make parameters adjustable
    // TODO: Add forum parameters

    event ValidationPoolInitiated(uint poolIndex);
    event ValidationPoolResolved(uint poolIndex, bool votePasses);

    /// Accept fee to initiate a validation pool
    /// TODO: Rather than accept author as a parameter, accept a reference to a forum post
    /// TODO: Handle multiple authors
    /// TODO: Constrain duration to allowable range
    function initiateValidationPool(
        address author,
        uint duration
    ) public payable returns (uint poolIndex) {
        require(msg.value > 0, "Fee is required to initiate validation pool");
        poolIndex = validationPoolCount++;
        ValidationPool storage pool = validationPools[poolIndex];
        pool.author = author;
        pool.fee = msg.value;
        pool.duration = duration;
        pool.endTime = block.timestamp + duration;
        // Because we need to stake part of the mited value for the pool an part against,
        // we mint two new tokens.
        // Here we assume a minting ratio of 1, and a stakeForAuthor ratio of 0.5
        // Implementing this with adjustable parameters will require more advanced fixed point math.
        // TODO: Make minting ratio an adjustable parameter
        // TODO: Make stakeForAuthor an adjustable parameter
        _mint(author, msg.value);
        // TODO: We need a way to exclude this pending reputation from the total supply when computing fee distribution
        _stake(pool, author, msg.value / 2, true);
        _stake(pool, author, msg.value / 2, false);
        emit ValidationPoolInitiated(poolIndex);
    }

    /// Internal function to register a stake for/against a validation pool
    function _stake(
        ValidationPool storage pool,
        address sender,
        uint256 amount,
        bool inFavor
    ) internal {
        require(block.timestamp <= pool.endTime, "Pool end time has passed");
        _transfer(sender, address(this), amount);
        Stake storage s = pool.stakes[pool.stakeCount++];
        s.sender = sender;
        s.inFavor = inFavor;
        s.amount = amount;
    }

    /// Accept reputation stakes toward a validation pool
    function stake(uint poolIndex, uint256 amount, bool inFavor) public {
        ValidationPool storage pool = validationPools[poolIndex];
        _stake(pool, msg.sender, amount, inFavor);
    }

    /// Evaluate outcome of a validation pool
    function evaluateOutcome(uint poolIndex) public returns (bool votePasses) {
        ValidationPool storage pool = validationPools[poolIndex];
        require(
            block.timestamp > pool.endTime,
            "Pool end time has not yet arrived"
        );
        require(pool.resolved == false, "Pool is already resolved");
        uint256 stakedFor;
        uint256 stakedAgainst;
        Stake storage s;
        for (uint i = 0; i < pool.stakeCount; i++) {
            s = pool.stakes[i];
            if (s.inFavor) {
                stakedFor += s.amount;
            } else {
                stakedAgainst += s.amount;
            }
        }
        // Here we assume a quorum of 0
        // TODO: Make quorum an adjustable parameter
        // A tie is resolved in favor of the validation pool.
        // This is especially important so that the DAO's first pool can pass,
        // when no reputation has yet been minted.
        votePasses = stakedFor >= stakedAgainst;
        if (votePasses && !isMember[pool.author]) {
            members[memberCount++] = pool.author;
            isMember[pool.author] = true;
        }
        pool.resolved = true;
        emit ValidationPoolResolved(poolIndex, votePasses);
        // Value of losing stakes should be di stributed among winners, in proportion to their stakes
        uint256 amountFromWinners = votePasses ? stakedFor : stakedAgainst;
        uint256 amountFromLosers = votePasses ? stakedAgainst : stakedFor;
        uint256 totalRewards;
        for (uint i = 0; i < pool.stakeCount; i++) {
            s = pool.stakes[i];
            if (votePasses == s.inFavor) {
                uint256 reward = (amountFromLosers * s.amount) /
                    amountFromWinners;
                _transfer(address(this), s.sender, s.amount + reward);
                totalRewards += reward;
            }
        }
        // Due to rounding, there may be some reward left over. Include this as a reward to the author.
        uint256 remainder = amountFromLosers - totalRewards;
        if (remainder > 0) {
            _transfer(address(this), pool.author, remainder);
        }
        // Distribute fee proportionatly among all reputation holders
        for (uint i = 0; i < memberCount; i++) {
            address member = members[i];
            uint256 share = (pool.fee * balanceOf(member)) / totalSupply();
            // TODO: For efficiency this could be modified to hold the funds for recipients to withdraw
            payable(member).transfer(share);
        }
    }

    /// Transfer REP to a contract, and call that contract's receiveTransfer method
    function stakeAvailability(
        address to,
        uint256 value,
        uint duration
    ) external returns (bool transferred) {
        transferred = super.transfer(to, value);
        if (transferred)
            IAcceptAvailability(to).acceptAvailability(
                msg.sender,
                value,
                duration
            );
    }
}