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

import {FrontEndRewarder} from "./FrontEndRewarder.sol";

import {IGHST} from "../interfaces/IGHST.sol";
import {IStaking} from "../interfaces/IStaking.sol";
import {ITreasury} from "../interfaces/ITreasury.sol";
import {INoteKeeper} from "../interfaces/INoteKeeper.sol";

import {EnumerableSet} from "@openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol";

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

    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) {
        uint256 expected = _maturedPayout(msg.sender, indexes);
        uint256 balance = _GHST.balanceOf(address(this));

        if (balance < expected) {
            uint256 deficit = expected - balance;
            payout = balance + _STAKING.claimByAmount(address(this), deficit, false);
        } else payout = expected;

        if (payout < expected) revert IncompleteRedeemPayout();

        if (sendGhst) _GHST.safeTransfer(user, payout);
        else _STAKING.unwrap(user, payout);
    }

    function forceRedeem(
        bytes32 receiver,
        uint256[] memory indexes
    ) public override returns (uint256 expected) {
        uint256 payout = _maturedPayout(msg.sender, indexes);
        uint256 balance = _GHST.balanceOf(address(this));
        expected = payout;

        if (balance > 0) {
            uint256 toGhost = payout > balance ? balance : payout;
            _STAKING.ghost(receiver, toGhost);
            payout -= toGhost;
        }

        if (payout > 0) {
            payout -= _STAKING.breakout(receiver, payout);
        }

        if (payout > 0) revert IncompleteRedeemPayout();
    }

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

    function forceRedeemAll(bytes32 receiver) external override returns (uint256) {
        return forceRedeem(receiver, indexesFor(msg.sender));
    }

    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;
    }

    function _maturedPayout(address user, uint256[] memory indexes) private returns (uint256 payout) {
        uint48 time = uint48(block.timestamp);
        uint256 i = 0;
        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; }
        }
    }
}
