// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./FrontEndRewarder.sol";

import "../interfaces/IGHST.sol";
import "../interfaces/IStaking.sol";
import "../interfaces/ITreasury.sol";
import "../interfaces/INoteKeeper.sol";

import "@openzeppelin-contracts/utils/structs/EnumerableSet.sol";

abstract contract NoteKeeper is INoteKeeper, FrontEndRewarder {
    using EnumerableSet for EnumerableSet.UintSet;

    mapping(address => Note[]) public notes;
    mapping(address => mapping(uint256 => address)) private _noteTransfers;
    mapping(address => EnumerableSet.UintSet) private _pendingIndexes;

    IGHST internal immutable _ghst;
    IStaking internal immutable _staking;
    ITreasury internal _treasury;

    constructor(
        address _authority,
        address _ftsoAddress,
        address _ghstAddress,
        address _stakingAddress,
        address _treasuryAddress
    ) FrontEndRewarder(_authority, _ftsoAddress) {
        _ghst = IGHST(_ghstAddress);
        _staking = IStaking(_stakingAddress);
        _treasury = ITreasury(_treasuryAddress);
        _staking.toggleLock();
    }

    function updateTreasury() external {
        if (
            msg.sender != authority.governor() &&
            msg.sender != authority.guardian() &&
            msg.sender != authority.policy()
        ) revert OnlyAuthorized();
        _treasury = ITreasury(authority.vault());
    }

    function addNote(
        address user,
        uint256 payout,
        uint48 expiry,
        uint48 marketId,
        address referral
    ) internal returns (uint256 index) {
        index = notes[user].length;
        _pendingIndexes[user].add(index);
        notes[user].push(
            Note({
                payout: _ghst.balanceTo(payout),
                created: uint48(block.timestamp),
                matured: expiry,
                redeemed: 0,
                marketId: marketId
            })
        );

        uint256 rewards = _giveRewards(payout, referral);
        _treasury.mint(address(this), payout + rewards);
        _staking.stake(payout, address(this), false, true);
    }

    function redeem(
        address user,
        bool sendGhst,
        uint256[] memory indexes
    ) public override returns (uint256 payout) {
        _staking.claim(address(this), false);

        uint48 time = uint48(block.timestamp);
        uint256 i;

        for (; i < indexes.length; ) {
            (uint256 pay, bool matured) = pendingFor(user, indexes[i]);
            if (matured) {
                _pendingIndexes[user].remove(indexes[i]);
                notes[user][indexes[i]].redeemed = time;
                payout += pay;
            }
            unchecked { ++i; }
        }

        if (sendGhst) _ghst.transfer(user, payout);
        else _staking.unwrap(user, payout);
    }

    function redeemAll(
        address user,
        bool sendGhst
    ) external override returns (uint256) {
        return redeem(user, sendGhst, indexesFor(user));
    }

    function pushNote(address to, uint256 index) external override {
        if (notes[msg.sender][index].created == 0) revert NoteNotFound(msg.sender, index);
        _noteTransfers[msg.sender][index] = to;
    }

    function pullNote(
        address from,
        uint256 index
    ) external override returns (uint256 newIndex) {
        if (_noteTransfers[from][index] != msg.sender) revert TransferNotFound(from, index);
        if (!_pendingIndexes[from].contains(index)) revert AlreadyRedeemed(from, index);

        newIndex = notes[msg.sender].length;
        notes[msg.sender].push(notes[from][index]);
        _pendingIndexes[msg.sender].add(newIndex);

        delete notes[from][index];
        _pendingIndexes[from].remove(index);
    }

    function indexesFor(address user)
        public
        view
        override
        returns (uint256[] memory)
    {
        return _pendingIndexes[user].values();
    }

    function pendingFor(address user, uint256 index)
        public
        view
        override
        returns (
            uint256 payout,
            bool matured
        )
    {
        Note memory note = notes[user][index];
        payout = note.payout;
        matured = _pendingIndexes[user].contains(index) && note.matured <= block.timestamp && payout > 0;
    }
}
