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

import "./core/DAO.sol";
import "./interfaces/IOnValidate.sol";
import "./interfaces/IOnProposalAccepted.sol";

contract Proposals is DAOContract, IOnValidate {
    enum Stage {
        Proposal,
        Referendum0,
        Referendum1,
        Referendum100,
        Failed,
        Accepted
    }

    struct Pool {
        uint poolIndex;
        bool started;
        bool completed;
        uint stakedFor;
        uint stakedAgainst;
        bool votePasses;
        bool quorumMet;
    }

    struct Referendum {
        uint duration;
        // Each referendum may retry up to 3x
        Pool[3] pools;
        uint retryCount;
    }

    struct Proposal {
        address sender;
        uint fee;
        uint remainingFee;
        string postId;
        uint startTime;
        Stage stage;
        mapping(address => uint) attestations;
        uint attestationTotal;
        Referendum[3] referenda;
        bool callbackOnAccepted;
        bytes callbackData;
    }

    mapping(uint => Proposal) public proposals;
    uint public proposalCount;

    event NewProposal(uint proposalIndex);
    event Attestation(uint proposalIndex);
    event ReferendumStarted(uint proposalIndex, uint poolIndex);
    event ProposalFailed(uint proposalIndex, string reason);
    event ProposalAccepted(uint proposalIndex);

    constructor(DAO dao) DAOContract(dao) {}

    // TODO receive : we want to be able to accept refunds from validation pools

    /// Submit a post as a proposal. DAO.addPost should be called before this.
    function propose(
        string calldata postId,
        uint[3] calldata durations,
        bool callbackOnAccepted,
        bytes calldata callbackData
    ) external payable returns (uint proposalIndex) {
        proposalIndex = proposalCount++;
        Proposal storage proposal = proposals[proposalIndex];
        proposal.sender = msg.sender;
        proposal.postId = postId;
        proposal.startTime = block.timestamp;
        proposal.referenda[0].duration = durations[0];
        proposal.referenda[1].duration = durations[1];
        proposal.referenda[2].duration = durations[2];
        proposal.fee = msg.value;
        proposal.remainingFee = proposal.fee;
        proposal.callbackOnAccepted = callbackOnAccepted;
        proposal.callbackData = callbackData;
        emit NewProposal(proposalIndex);
    }

    /// Provides a summary of pools for a given proposal. Useful for displaying a summary.
    function getPools(
        uint proposalIndex
    ) public view returns (Pool[3][3] memory pools) {
        Proposal storage proposal = proposals[proposalIndex];
        pools[0] = proposal.referenda[0].pools;
        pools[1] = proposal.referenda[1].pools;
        pools[2] = proposal.referenda[2].pools;
    }

    // TODO: function getProposals()
    // Enumerate timing so clients can render it

    /// External function for reputation holders to attest toward a given proposal;
    /// This is non-binding and non-encumbering, so it does not transfer any reputation.
    function attest(uint proposalIndex, uint amount) external {
        // Since this is non-binding, non-encumbering, we only need to verify that
        // the sender actually has the rep they claim to stake.
        require(
            dao.balanceOf(msg.sender) >= amount,
            "Sender has insufficient REP balance"
        );
        Proposal storage proposal = proposals[proposalIndex];
        proposal.attestationTotal -= proposal.attestations[msg.sender];
        proposal.attestations[msg.sender] = amount;
        proposal.attestationTotal += amount;
        emit Attestation(proposalIndex);
    }

    // --- Sequences of validation pool parameters ---
    // Percentage that each referendum is binding
    uint[3] referendaBindingPercent = [0, 1, 100];
    // Whether to redistribute the binding portion of losing stakes in each referendum
    bool[3] referendaRedistributeLosingStakes = [false, false, true];
    // For each referendum, a numerator-denominator pair representing its quorum
    uint[2][3] referendaQuora = [[1, 10], [1, 10], [1, 10]];
    // Win ratios
    uint[2][3] referendaWinRatio = [[2, 3], [2, 3], [2, 3]];

    /// Internal convenience function to wrap our call to dao.initiateValidationPool
    /// and to emit an event
    function initiateValidationPool(
        uint proposalIndex,
        uint referendumIndex,
        uint fee
    ) internal {
        Proposal storage proposal = proposals[proposalIndex];
        proposal.remainingFee -= fee;
        uint poolIndex = dao.initiateValidationPool{value: fee}(
            proposal.postId,
            proposal.referenda[referendumIndex].duration,
            referendaQuora[referendumIndex],
            referendaWinRatio[referendumIndex],
            referendaBindingPercent[referendumIndex],
            referendaRedistributeLosingStakes[referendumIndex],
            true,
            abi.encode(proposalIndex, referendumIndex, fee)
        );
        Referendum storage referendum = proposal.referenda[referendumIndex];
        Pool storage pool = referendum.pools[referendum.retryCount];
        pool.poolIndex = poolIndex;
        pool.started = true;
        emit ReferendumStarted(proposalIndex, poolIndex);
    }

    /// Callback to be executed when referenda pools complete
    function onValidate(
        bool votePasses,
        bool quorumMet,
        uint stakedFor,
        uint stakedAgainst,
        bytes calldata callbackData
    ) external {
        require(
            msg.sender == address(dao),
            "onValidate may only be called by the DAO contract"
        );
        (uint proposalIndex, uint referendumIndex, uint fee) = abi.decode(
            callbackData,
            (uint, uint, uint)
        );
        Proposal storage proposal = proposals[proposalIndex];
        Referendum storage referendum = proposal.referenda[referendumIndex];
        Pool storage pool = referendum.pools[referendum.retryCount];

        // Make a record of this result
        pool.completed = true;
        pool.stakedFor = stakedFor;
        pool.stakedAgainst = stakedAgainst;
        pool.quorumMet = quorumMet;
        pool.votePasses = votePasses;

        if (!quorumMet) {
            proposal.stage = Stage.Failed;
            emit ProposalFailed(proposalIndex, "Quorum not met");
            proposal.remainingFee += fee;
            return;
        }

        // Participation threshold of 50%
        bool participationAboveThreshold = 2 * (stakedFor + stakedAgainst) >=
            dao.totalSupply();

        // Handle Referendum 0%
        if (proposal.stage == Stage.Referendum0) {
            require(referendumIndex == 0, "Stage 0 index mismatch");
            // If vote passes (2/3 majority) and has >= 50% participation
            if (votePasses && participationAboveThreshold) {
                proposal.stage = Stage.Referendum1;
            } else if (referendum.retryCount >= 2) {
                proposal.stage = Stage.Failed;
                emit ProposalFailed(proposalIndex, "Retry count exceeded");
            } else {
                referendum.retryCount += 1;
            }
            // Handle Referendum 1%
        } else if (proposal.stage == Stage.Referendum1) {
            require(referendumIndex == 1, "Stage 1 index mismatch");
            if (votePasses && participationAboveThreshold) {
                proposal.stage = Stage.Referendum100;
            } else if (referendum.retryCount >= 2) {
                proposal.stage = Stage.Failed;
                emit ProposalFailed(proposalIndex, "Retry count exceeded");
            } else {
                referendum.retryCount += 1;
            }
            // Handle Referendum 1000000%
        } else if (proposal.stage == Stage.Referendum100) {
            require(referendumIndex == 2, "Stage 2 index mismatch");
            if (votePasses && participationAboveThreshold) {
                // The proposal has passed all referenda and should become "law"
                proposal.stage = Stage.Accepted;
                // This is an opportunity for some actions to occur
                // Emit an event
                emit ProposalAccepted(proposalIndex);
                // Execute a callback, if requested
                if (proposal.callbackOnAccepted) {
                    IOnProposalAccepted(proposal.sender).onProposalAccepted(
                        stakedFor,
                        stakedAgainst,
                        proposal.callbackData
                    );
                }
            } else if (referendum.retryCount >= 2) {
                proposal.stage = Stage.Failed;
                emit ProposalFailed(proposalIndex, "Retry count exceeded");
            } else {
                referendum.retryCount += 1;
            }
        }
        if (proposal.stage == Stage.Referendum0) {
            initiateValidationPool(proposalIndex, 0, proposal.fee / 10);
        } else if (proposal.stage == Stage.Referendum1) {
            initiateValidationPool(proposalIndex, 1, proposal.fee / 10);
        } else if (proposal.stage == Stage.Referendum100) {
            initiateValidationPool(proposalIndex, 2, proposal.fee / 10);
        }
        return;
    }

    /// External function that will advance a proposal to the referendum process
    /// if attestation threshold has been reached
    function evaluateAttestation(uint proposalIndex) external returns (bool) {
        Proposal storage proposal = proposals[proposalIndex];
        require(
            proposal.stage == Stage.Proposal,
            "Attestation only pertains to Proposal stage"
        );
        bool meetsAttestation = 10 * proposal.attestationTotal >=
            dao.totalSupply();
        bool expired = block.timestamp > proposal.startTime + 365 days;
        if (!meetsAttestation) {
            if (expired) {
                // Expired without meeting attestation threshold
                proposal.stage = Stage.Failed;
                emit ProposalFailed(
                    proposalIndex,
                    "Expired without meeting attestation threshold"
                );
                return false;
            }
            // Not yet expired, but has not met attestation threshold
            return false;
        }

        // Attestation threshold is met.
        // Note that this may succeed even after expiry
        // It can only happen once because the stage advances, and we required it above.
        proposal.stage = Stage.Referendum0;
        // Initiate validation pool
        initiateValidationPool(proposalIndex, 0, proposal.fee / 10);
        return true;
    }

    /// External function to reclaim remaining fees after a proposal has completed all referenda
    function reclaimRemainingFee(uint proposalIndex) external {
        Proposal storage proposal = proposals[proposalIndex];
        require(
            proposal.stage == Stage.Failed || proposal.stage == Stage.Accepted,
            "Remaining fees can only be reclaimed when proposal has been accepted or failed"
        );
        uint amount = proposal.remainingFee;
        proposal.remainingFee = 0;
        payable(msg.sender).transfer(amount);
    }
}