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

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

import {IUniswapV2Factory} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Factory.sol";
import {IUniswapV2Pair} from "@uniswap-v2-core-1.0.1/interfaces/IUniswapV2Pair.sol";
import {IUniswapV2Router02} from "@uniswap-v2-periphery-1.1.0-beta.0/interfaces/IUniswapV2Router02.sol";
import {IUniswapV2Router01} from "@uniswap-v2-periphery-1.1.0-beta.0/interfaces/IUniswapV2Router01.sol";

import {GhostAccessControlled} from "./types/GhostAccessControlled.sol";
import {Babylonian} from "./libraries/FixedPoint.sol";
import {FullMath} from "./libraries/FullMath.sol";

import {IFTSO} from "./interfaces/IFTSO.sol";
import {ISTNK} from "./interfaces/ISTNK.sol";
import {IBondingCalculator} from "./interfaces/IBondingCalculator.sol";
import {ITreasury} from "./interfaces/ITreasury.sol";
import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol";

contract GhostTreasury is GhostAccessControlled, ITreasury {
    using SafeERC20 for IERC20;

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

    uint256 public totalReserves;
    uint256 public totalDebt;
    uint256 public ftsoDebt;
    uint256 public onChainGovernanceTimelock;

    address public stnk;
    bool public timelockEnabled;

    Queue[] public permissionQueue;
    mapping(STATUS => address[]) public registry;
    mapping(STATUS => mapping(address => bool)) public permissions;
    mapping(address => address) public bondCalculator;
    mapping(address => uint256) public debtLimit;

    constructor(
        address _ftso,
        uint256 _timelock,
        address _authority
    ) GhostAccessControlled(IGhostAuthority(_authority)) {
        ftso = _ftso;
        blocksNeededForQueue = _timelock;
    }

    function deposit(
        address token,
        uint256 amount,
        uint256 profit
    ) external override returns (uint256 send) {
        if (permissions[STATUS.RESERVETOKEN][token]) {
            if (!permissions[STATUS.RESERVEDEPOSITOR][msg.sender]) revert NotApproved();
        } else if (permissions[STATUS.LIQUIDITYTOKEN][token]) {
            if (!permissions[STATUS.LIQUIDITYDEPOSITOR][msg.sender]) revert NotApproved();
        } else revert InvalidToken();

        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        uint256 value = tokenValue(token, amount);
        send = value - profit;
        IFTSO(ftso).mint(msg.sender, send);

        totalReserves = totalReserves + value;
        emit Deposit(token, amount, value);
    }

    function withdraw(address token, uint256 amount) external override {
        if (!permissions[STATUS.RESERVETOKEN][token]) revert NotAccepted();
        if (!permissions[STATUS.RESERVESPENDER][msg.sender]) revert NotApproved();

        uint256 value = tokenValue(token, amount);
        IFTSO(ftso).burnFrom(msg.sender, value);
        totalReserves = totalReserves - value;

        IERC20(token).safeTransfer(msg.sender, amount);
        emit Withdrawal(token, amount, value);
    }

    function manage(address token, uint256 amount) external override {
        if (permissions[STATUS.LIQUIDITYTOKEN][token]) {
            if (!permissions[STATUS.LIQUIDITYMANAGER][msg.sender]) revert NotApproved();
        } else {
            if (!permissions[STATUS.RESERVEMANAGER][msg.sender]) revert NotApproved();
        }

        if (permissions[STATUS.RESERVETOKEN][token] || permissions[STATUS.LIQUIDITYTOKEN][token]) {
            uint256 value = tokenValue(token, amount);
            if (value > excessReserves()) revert InsufficientReserves();
            totalReserves = totalReserves - value;
        }

        IERC20(token).safeTransfer(msg.sender, amount);
        emit Managed(token, amount);
    }

    function mint(address recipient, uint256 amount) external override {
        if (!permissions[STATUS.REWARDMANAGER][msg.sender]) revert NotApproved();
        if (amount > excessReserves()) revert InsufficientReserves();
        IFTSO(ftso).mint(recipient, amount);
        emit Minted(msg.sender, recipient, amount);
    }

    function incurDebt(address token, uint256 amount) external override {
        uint256 value;
        if (token == ftso) {
            if (!permissions[STATUS.FTSODEBTOR][msg.sender]) revert NotApproved();
            value = amount;
        } else {
            if (!permissions[STATUS.RESERVEDEBTOR][msg.sender]) revert NotApproved();
            if (!permissions[STATUS.RESERVETOKEN][token]) revert NotAccepted();
            value = tokenValue(token, amount);
        }
        if (value == 0) revert InvalidToken();

        ISTNK(stnk).changeDebt(value, msg.sender, true);
        if (ISTNK(stnk).debtBalances(msg.sender) > debtLimit[msg.sender]) revert TreasuryExceeds();
        totalDebt = totalDebt + value;

        if (token == ftso) {
            IFTSO(ftso).mint(msg.sender, value);
            ftsoDebt = ftsoDebt + value;
        } else {
            totalReserves = totalReserves - value;
            IERC20(token).safeTransfer(msg.sender, amount);
        }
        emit CreateDebt(msg.sender, token, amount, value);
    }

    function repayDebtWithReserves(address token, uint256 amount) external override {
        if (!permissions[STATUS.RESERVEDEBTOR][msg.sender]) revert NotApproved();
        if (!permissions[STATUS.RESERVETOKEN][token]) revert NotAccepted();
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        uint256 value = tokenValue(token, amount);
        ISTNK(stnk).changeDebt(value, msg.sender, false);
        totalDebt = totalDebt - value;
        totalReserves = totalReserves + value;
        emit RepayDebt(msg.sender, token, amount, value);
    }

    function repayDebtWithFtso(uint256 amount) external override {
        if (!permissions[STATUS.RESERVEDEBTOR][msg.sender]) revert NotApproved();
        if (!permissions[STATUS.FTSODEBTOR][msg.sender]) revert NotApproved();
        IFTSO(ftso).burnFrom(msg.sender, amount);
        ISTNK(stnk).changeDebt(amount, msg.sender, false);
        totalDebt = totalDebt - amount;
        emit RepayDebt(msg.sender, ftso, amount, amount);
    }

    function auditReserves() external {
        if (
            msg.sender != authority.governor() &&
            msg.sender != authority.guardian()
        ) revert NotApproved();

        uint256 reserves;
        address[] memory reserveTokens = registry[STATUS.RESERVETOKEN];

        uint256 i;
        for (; i < reserveTokens.length;) {
            address reserveToken = reserveTokens[i];
            if (permissions[STATUS.RESERVETOKEN][reserveToken]) {
                reserves = reserves + tokenValue(
                    reserveToken,
                    IERC20(reserveToken).balanceOf(address(this))
                );
            }
            unchecked { ++i; }
        }

        address[] memory liquidityTokens = registry[STATUS.LIQUIDITYTOKEN];
        i = 0;
        for (; i < liquidityTokens.length;) {
            address liquidityToken = liquidityTokens[i];
            if (permissions[STATUS.LIQUIDITYTOKEN][liquidityToken]) {
                reserves = reserves + tokenValue(
                    liquidityToken,
                    IERC20(liquidityToken).balanceOf(address(this))
                );
            }
            unchecked { ++i; }
        }

        totalReserves = reserves;
        emit ReservesAudited(reserves);
    }

    function setDebtLimit(address who, uint256 limit) external onlyGovernor {
        debtLimit[who] = limit;
    }

    function enable(
        STATUS status,
        address someAddress,
        address calculatorAddress
    ) external onlyGovernor {
        if (timelockEnabled) revert OnlyQueueTimelock();
        if (status == STATUS.STNK) {
            stnk = someAddress;
        } else {
            permissions[status][someAddress] = true;
            (bool registered, ) = indexInRegistry(someAddress, status);
            if (!registered && (status == STATUS.LIQUIDITYTOKEN || status == STATUS.RESERVETOKEN)) {
                assert(calculatorAddress != address(0));
                registry[status].push(someAddress);
                bondCalculator[someAddress] = calculatorAddress;
            }
        }
        emit Permissioned(someAddress, status, true);
    }

    function disable(STATUS status, address toDisable) external {
        if (
            msg.sender != authority.governor() &&
            msg.sender != authority.guardian()
        ) revert NotApproved();
        permissions[status][toDisable] = false;
        emit Permissioned(toDisable, status, false);
    }

    function _quantityToBeSwapped(uint256 xa, uint256 x1) internal pure returns (uint256) {
        uint256 y1 = x1 * 1994 / 1000;
        uint256 y2 = Babylonian.sqrt(y1**2 + 4 * xa * x1 * 997 / 1000);
        return (y2 - y1) * 1000 / 1994;
    }

    function forfeitReserves(
        address router,
        uint256 liquidity,
        bool destroyerMode
    ) external onlyGovernor {
        address weth = IUniswapV2Router01(router).WETH();
        address pair = IUniswapV2Factory(IUniswapV2Router01(router).factory()).getPair(ftso, weth);

        IERC20(pair).safeTransfer(pair, liquidity);
        (uint256 amount0, uint256 amount1) = IUniswapV2Pair(pair).burn(address(this));

        if (destroyerMode) {
            uint256 amountToDestroy;
            address token0 = IUniswapV2Pair(pair).token0();
            address token1 = IUniswapV2Pair(pair).token1();

            if (token0 == ftso) {
                amountToDestroy = amount0;
            }

            if (token1 == ftso) {
                amountToDestroy = amount1;
            }

            IFTSO(ftso).burn(amountToDestroy);
        }
    }

    function redeemReserve(
        address router, // could be an issue
        uint256 amount
    ) external onlyGovernor {
        address weth = IUniswapV2Router01(router).WETH();
        address pair = IUniswapV2Factory(IUniswapV2Router01(router).factory()).getPair(ftso, weth);

        IERC20(weth).approve(router, amount);

        (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pair).getReserves();
        address[] memory path = new address[](2);
        path[0] = weth;
        path[1] = ftso;

        if (ftso < weth) {
            reserve0 = reserve1 ^ reserve0;
            reserve1 = reserve1 ^ reserve0;
            reserve0 = reserve1 ^ reserve0;
        }

        uint256 amountIn = _quantityToBeSwapped(amount, reserve0);
        uint256[] memory amounts = IUniswapV2Router02(router).getAmountsOut(amountIn, path);
        IUniswapV2Router02(router).swapExactTokensForTokens(
            amountIn,
            amounts[1],
            path,
            address(this),
            block.timestamp
        );

        amountIn = amount - amountIn;

        IERC20(weth).safeTransfer(pair, amountIn);
        IERC20(ftso).safeTransfer(pair, amounts[1]);
        IUniswapV2Pair(pair).mint(address(this));
    }

    function indexInRegistry(
        address someAddress,
        STATUS status
    ) public view override returns (bool, uint256) {
        address[] memory entries = registry[status];
        uint256 i;
        for (; i < entries.length; ) {
            if (someAddress == entries[i]) {
                return (true, i);
            }
            unchecked { ++i; }
        }
        return (false, 0);
    }

    function queueTimelock(
        STATUS status,
        address someAddress,
        address calculatorAddress
    ) external onlyGovernor {
        if (someAddress == address(0)) revert EmptyAddress();
        if (!timelockEnabled) revert TimelockDisabled();

        uint256 timelock = block.number + blocksNeededForQueue;
        if (status == STATUS.RESERVEMANAGER || status == STATUS.LIQUIDITYMANAGER) {
            timelock = block.number + (blocksNeededForQueue * 2);
        }

        permissionQueue.push(
            Queue({
                managing: status,
                toPermit: someAddress,
                calculator: calculatorAddress,
                timelockEnd: timelock,
                nullify: false,
                executed: false
            })
        );
        emit PermissionQueued(status, someAddress);
    }

    function execute(uint256 idx) external {
        if (timelockEnabled) revert TimelockDisabled();

        Queue storage info = permissionQueue[idx];

        if (info.nullify) revert ActionNullified();
        if (info.executed) revert ActionExecuted();
        if (info.timelockEnd > block.number) revert TimelockNotComplete();

        if (info.managing == STATUS.STNK) {
            stnk = info.toPermit;
        } else {
            permissions[info.managing][info.toPermit] = true;
            (bool registered, ) = indexInRegistry(info.toPermit, info.managing);
            if (!registered && (info.managing == STATUS.LIQUIDITYTOKEN || info.managing == STATUS.RESERVETOKEN)) {
                assert(info.calculator != address(0));
                registry[info.managing].push(info.toPermit);
                bondCalculator[info.toPermit] = info.calculator;
            }
        }

        permissionQueue[idx].executed = true;
        emit Permissioned(info.toPermit, info.managing, true);
    }

    function nullify(uint256 idx) external onlyGovernor {
        permissionQueue[idx].nullify = true;
    }

    function toggleTimelock() external onlyGovernor {
        if (onChainGovernanceTimelock > 0 && onChainGovernanceTimelock <= block.number) {
            timelockEnabled = !timelockEnabled;
            onChainGovernanceTimelock = 0;
        } else {
            onChainGovernanceTimelock = block.number + (blocksNeededForQueue * 7);
        }
    }

    function originalCoefficient() external view returns (uint256) {
        address[] memory reserveTokens = registry[STATUS.RESERVETOKEN];
        return IBondingCalculator(bondCalculator[reserveTokens[0]]).fraction();
    }

    function excessReserves() public view override returns (uint256) {
        return totalReserves - (IFTSO(ftso).totalSupply() - totalDebt);
    }

    function tokenValue(
        address token,
        uint256 amount
    ) public view override returns (uint256 value) {
        if (permissions[STATUS.LIQUIDITYTOKEN][token]) {
            value = IBondingCalculator(bondCalculator[token]).valuation(token, amount);
        } else if (permissions[STATUS.RESERVETOKEN][token]) {
            value = FullMath.mulDiv(amount, 1e9, 10**IERC20Metadata(token).decimals());
            value = FullMath.mulDiv(value, IBondingCalculator(bondCalculator[token]).fraction(), 1e18);
        }
    }

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