// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;

import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import { Math } from "@openzeppelin/utils/math/Math.sol";

/**
 * @title stakeable ERC20 Token
 * @notice This contract implements an ERC20 token with mechanisms for minting and burning in which a single account (staking Pool) is proportionally receiving a share. Only the liquidity manager has permission to manage token supply.
 * @dev Key features:
 * - Controlled minting exclusively by LiquidityManager
 * - Tax collection and redistribution mechanism through staking pool
 * - 20% supply cap for staking (20,000 positions max)
 * - Staking pool receives proportional share of all mints/burns
 */
contract Kraiken is ERC20, ERC20Permit {
    using Math for uint256;

    /**
     * @notice Protocol version for data structure compatibility.
     * Increment when making breaking changes to TAX_RATES, events, or core data structures.
     * Indexers and frontends validate against this to ensure sync.
     *
     * Version History:
     * - v1: Initial deployment with 30-tier TAX_RATES
     * - v2: OptimizerV3, VWAP mirror floor, directional VWAP recording
     */
    uint256 public constant VERSION = 2;

    // Minimum fraction of the total supply required for staking to prevent fragmentation of staking positions
    uint256 private constant MIN_STAKE_FRACTION = 3000;
    // Address authorized to call one-time setters (prevents frontrunning)
    address private immutable deployer;
    // Address of the liquidity manager
    address private liquidityManager;
    // Address of the staking pool
    address private stakingPool;

    // Previous total supply for staking calculations
    uint256 public previousTotalSupply;

    // Custom errors
    error ZeroAddressInSetter();
    error AddressAlreadySet();
    error InvalidAddress();

    // Modifier to restrict access to the liquidity manager
    modifier onlyLiquidityManager() {
        require(msg.sender == address(liquidityManager), "only liquidity manager");
        _;
    }

    /**
     * @notice Constructor for the Kraiken token
     * @param name_ The name of the token
     * @param symbol_ The symbol of the token
     */
    constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) {
        deployer = msg.sender;
    }

    /**
     * @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract.
     * @dev Should be called only once right after the contract deployment to set the liquidity manager address.
     *      Throws AddressAlreadySet if called more than once.
     * @param liquidityManager_ The address of the liquidity manager.
     */
    function setLiquidityManager(address liquidityManager_) external {
        require(msg.sender == deployer, "only deployer");
        if (address(0) == liquidityManager_) revert ZeroAddressInSetter();
        if (liquidityManager_ == stakingPool) revert InvalidAddress();
        if (liquidityManager != address(0)) revert AddressAlreadySet();
        liquidityManager = liquidityManager_;
    }

    /**
     * @notice Sets the address for the stakingPool. Used once post-deployment to initialize the contract.
     * @dev Should be called only once right after the contract deployment to set the staking pool address.
     *      Throws AddressAlreadySet if called more than once.
     * @param stakingPool_ The address of the staking pool.
     */
    function setStakingPool(address stakingPool_) external {
        require(msg.sender == deployer, "only deployer");
        if (address(0) == stakingPool_) revert ZeroAddressInSetter();
        if (stakingPool_ == liquidityManager) revert InvalidAddress();
        if (stakingPool != address(0)) revert AddressAlreadySet();
        stakingPool = stakingPool_;
    }

    /**
     * @notice Returns the addresses of the periphery contracts
     * @return The addresses of the TWAB controller, liquidity manager, staking pool, and liquidity pool
     */
    function peripheryContracts() external view returns (address, address) {
        return (liquidityManager, stakingPool);
    }

    /**
     * @notice Calculates the minimum stake based on the previous total supply
     * @return The minimum stake amount
     */
    function minStake() external view returns (uint256) {
        return previousTotalSupply / MIN_STAKE_FRACTION;
    }

    /**
     * @notice Allows the liquidity manager to mint tokens for itself.
     * @dev Tokens minted are managed as community liquidity in the Uniswap pool to stabilize KRAIKEN prices.
     *      Only callable by the Liquidity Manager. Minting rules and limits are defined externally.
     * @param _amount The number of tokens to mint.
     */
    function mint(uint256 _amount) external onlyLiquidityManager {
        if (_amount > 0) {
            // make sure staking pool grows proportional to economy
            uint256 stakingPoolBalance = balanceOf(stakingPool);
            if (stakingPoolBalance > 0) {
                uint256 newStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
                _mint(stakingPool, newStake);
            }
            _mint(address(liquidityManager), _amount);
        }
        if (previousTotalSupply == 0) {
            previousTotalSupply = totalSupply();
        }
    }

    /**
     * @notice Allows the liquidity manager to burn tokens from its account, adjusting the staking pool accordingly.
     * @dev When tokens are burned, the total supply shrinks, making excess tokens in the staking pool unnecessary.
     *      These excess tokens are burned to maintain the guaranteed fixed percentage of the total supply for stakers.
     * @param _amount The number of tokens to burn.
     */
    function burn(uint256 _amount) external onlyLiquidityManager {
        if (_amount > 0) {
            // shrink staking pool proportional to economy
            uint256 stakingPoolBalance = balanceOf(stakingPool);
            if (stakingPoolBalance > 0) {
                uint256 excessStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
                _burn(stakingPool, excessStake);
            }
            _burn(address(liquidityManager), _amount);
        }
    }

    /**
     * @notice Sets the previous total supply
     * @param _ts The previous total supply value
     */
    function setPreviousTotalSupply(uint256 _ts) external onlyLiquidityManager {
        previousTotalSupply = _ts;
    }

    /**
     * @notice Returns the outstanding supply, excluding the balances of the liquidity pool and liquidity manager
     * @return The outstanding supply
     */
    function outstandingSupply() public view returns (uint256) {
        return totalSupply() - balanceOf(liquidityManager);
    }
}
