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

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

import {GhostWarmup} from "./Warmup.sol";
import {Gatekeeper} from "./Gatekeeper.sol";
import {GhostAccessControlled} from "./types/GhostAccessControlled.sol";

import {ISTNK} from "./interfaces/ISTNK.sol";
import {IGHST} from "./interfaces/IGHST.sol";
import {IStaking} from "./interfaces/IStaking.sol";
import {ITreasury} from "./interfaces/ITreasury.sol";
import {IGatekeeper} from "./interfaces/IGatekeeper.sol";
import {IDistributor} from "./interfaces/IDistributor.sol";
import {IGhostWarmup} from "./interfaces/IGhostWarmup.sol";
import {IStorageHistory} from "./interfaces/IStorageHistory.sol";
import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol";

contract GhostStaking is IStaking, GhostAccessControlled {
    using SafeERC20 for IERC20;
    using SafeERC20 for ISTNK;
    using SafeERC20 for IGHST;

    address public immutable ftso; // forge-lint: disable-line(screaming-snake-case-immutable)
    address public immutable stnk; // forge-lint: disable-line(screaming-snake-case-immutable)
    address public immutable ghst; // forge-lint: disable-line(screaming-snake-case-immutable)

    uint48 public warmupPeriod;

    Epoch public epoch;
    address public distributor;
    address public gatekeeper;
    address public warmup;

    uint256 private _lastRebaseBlock;

    mapping(address => bool) public locks;

    constructor(
        address _ftso,
        address _stnk,
        address _ghst,
        uint48 _epochLength,
        uint48 _firstEpochNumber,
        uint48 _firstEpochTime,
        address _authority,
        uint256 _existentialDeposit
    ) GhostAccessControlled(IGhostAuthority(_authority)) {
        ftso = _ftso;
        stnk = _stnk;
        ghst = _ghst;

        epoch = Epoch({
            length: _epochLength,
            number: _firstEpochNumber,
            end: _firstEpochTime,
            distribute: 0
        });

        GhostWarmup newWarmup = new GhostWarmup(_ghst);
        warmup = address(newWarmup);

        Gatekeeper newGatekeeper = new Gatekeeper(_existentialDeposit, address(0));
        gatekeeper = address(newGatekeeper);

        _lastRebaseBlock = 1;
    }

    function stake(
        uint256 amount,
        address to,
        bool isRebase,
        bool isClaim
    ) external override returns (uint256 returnAmount) {
        returnAmount = amount + rebase();
        IERC20(ftso).safeTransferFrom(msg.sender, address(this), amount);
        if (isClaim && warmupPeriod == 0) {
            returnAmount = _sendStnkBased(returnAmount, to, isRebase);
        } else {
            if (locks[to] && to != msg.sender) revert ExternalDepositsLocked();
            uint48 expiry = epoch.number + warmupPeriod;
            IGhostWarmup(warmup).addToWarmup(returnAmount, to, expiry);
        }
        emit Staked(msg.sender, to, amount, isRebase, isClaim);
    }

    function claim(address to, bool isRebase) public override returns (uint256) {
        if (locks[to] && to != msg.sender) revert ExternalDepositsLocked();
        uint256 claimedAmount = IGhostWarmup(warmup).claim(to, epoch.number);
        return _sendGhstBased(claimedAmount, to, isRebase);
    }

    function claimByAmount(
        address to,
        uint256 amount,
        bool isRebase
    ) public override returns (uint256) {
        if (locks[to] && to != msg.sender) revert ExternalDepositsLocked();
        uint256 claimedAmount = IGhostWarmup(warmup).claimByAmount(to, amount, epoch.number);
        return _sendGhstBased(claimedAmount, to, isRebase);
    }

    function breakout(
        bytes32 receiver,
        uint256 amount
    ) public override returns (uint256 claimedAmount) {
        claimedAmount = IGhostWarmup(warmup).breakout(msg.sender, amount);
        IGatekeeper(gatekeeper).ghost(receiver, claimedAmount);
    }

    function forfeit() external override returns (uint256 deposit) {
        deposit = IGhostWarmup(warmup).forfeit(msg.sender);
        IERC20(ftso).safeTransfer(msg.sender, deposit);
    }

    function toggleLock() external override {
        locks[msg.sender] = !locks[msg.sender];
    }

    function unstake(
        uint256 amount,
        address to,
        bool isTrigger,
        bool isRebase
    ) external override returns (uint256) {
        amount += isTrigger ? rebase() : 0;
        if (isRebase) {
            ISTNK(stnk).safeTransferFrom(msg.sender, address(this), amount);
        } else {
            IGHST(ghst).burn(msg.sender, amount);
            amount = IGHST(ghst).balanceFrom(amount);
        }

        if (amount > IERC20(ftso).balanceOf(address(this))) revert InsufficientBalance();
        IERC20(ftso).safeTransfer(to, amount);
        emit Unstaked(msg.sender, to, amount, isTrigger, isRebase);
        return amount;
    }

    function wrap(
        address to,
        uint256 amount
    ) external override returns (uint256 balance) {
        ISTNK(stnk).safeTransferFrom(msg.sender, address(this), amount);
        balance = IGHST(ghst).balanceTo(amount);
        IGHST(ghst).mint(to, balance);
    }

    function unwrap(
        address to,
        uint256 amount
    ) external override returns (uint256 balance) {
        IGHST(ghst).burn(msg.sender, amount);
        balance = IGHST(ghst).balanceFrom(amount);
        ISTNK(stnk).safeTransfer(to, balance);
    }

    function ghost(bytes32 receiver, uint256 amount) external override {
        IGHST(ghst).burn(msg.sender, amount);
        IGatekeeper(gatekeeper).ghost(receiver, amount);
    }

    function materialize(address receiver, uint256 amount) external override {
        if (gatekeeper != msg.sender) revert NotGatekeeper();
        IGHST(ghst).mint(receiver, amount);
    }

    function rebase() public override returns (uint256 bounty) {
        if (epoch.end <= block.timestamp && block.number > _lastRebaseBlock) {
            ISTNK(stnk).rebase(epoch.distribute, epoch.number);

            unchecked {
                epoch.end += epoch.length;
                ++epoch.number;
            }

            if (distributor != address(0)) {
                IDistributor(distributor).distribute();
                bounty = IDistributor(distributor).retrieveBounty();
            }

            uint256 balance = IERC20(ftso).balanceOf(address(this));
            uint256 extra = ISTNK(stnk).circulatingSupply() + bounty;

            _lastRebaseBlock = block.number;
            epoch.distribute = balance > extra ? balance - extra : 0;
        }
    }

    function setDistributor(address _distributor) external onlyGovernor {
        distributor = _distributor;
        emit DistributorSet(_distributor);
    }

    function setWarmupPeriod(uint256 _warmupPeriod) external onlyGovernor {
        // forge-lint: disable-next-line(unsafe-typecast)
        warmupPeriod = uint48(_warmupPeriod);
        emit WarmupSet(_warmupPeriod);
    }

    function setGatekeeperAddress(uint256 existentialDeposit) external onlyGovernor {
        Gatekeeper newGatekeeper = new Gatekeeper(existentialDeposit, gatekeeper);
        address storageHistory = IGatekeeper(gatekeeper).storageHistory();
        IStorageHistory(storageHistory).setOwner(address(newGatekeeper));

        gatekeeper = address(newGatekeeper);
        emit GatekeeperSet(address(newGatekeeper));
    }

    function index() public view override returns (uint256) {
        return ISTNK(stnk).index();
    }

    function supplyInWarmup() public view override returns (uint256) {
        return IGHST(ghst).balanceFrom(IGhostWarmup(warmup).ghstInWarmup());
    }

    function ghostedSupply() public view override returns (uint256 amount) {
        if (gatekeeper != address(0)) {
            amount = IGatekeeper(gatekeeper).ghostedSupply();
        }
    }

    function warmupInfo(address who) external view returns (uint256, uint256, uint48, bool) {
        bool lock = locks[who];
        (uint256 deposit, uint256 payout, uint48 expiry) = IGhostWarmup(warmup).warmupInfo(who);
        return (deposit, payout, expiry, lock);
    }

    function totalReserves() external override view returns (uint256) {
        address treasury = ISTNK(stnk).treasury();
        return ITreasury(treasury).totalReserves();
    }

    function baseSupply() external override view returns (uint256) {
        return IERC20(ftso).totalSupply();
    }

    function _sendStnkBased(
        uint256 amount,
        address to,
        bool isRebase
    ) internal returns (uint256) {
        if (isRebase) {
            ISTNK(stnk).safeTransfer(to, amount);
            return amount;
        } else {
            uint256 balanceTo = IGHST(ghst).balanceTo(amount);
            IGHST(ghst).mint(to, balanceTo);
            return balanceTo;
        }
    }

    function _sendGhstBased(
        uint256 amount,
        address to,
        bool isRebase
    ) internal returns (uint256) {
        if (isRebase) {
            uint256 convertedAmount = IGHST(ghst).balanceFrom(amount);
            ISTNK(stnk).safeTransfer(to, convertedAmount);
            return convertedAmount;
        } else {
            IGHST(ghst).mint(to, amount);
            return amount;
        }
    }
}
