// File: src/PaymentManager.sol



pragma solidity 0.8.21;

abstract contract IERC20
{
    function transfer(address to, uint value) virtual external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) virtual external returns (bool);
}

struct PaymentTierSettings
{
    uint256 ratePerSecond;
    uint256 timeMaximum;
    uint256 timeMinimum;
    bool enabled;
}

struct PaymentEntry
{
    bool exists;
    uint endTimestamp;
}

contract PaymentManager
{
    address public admin;
    address public token;
    uint8 public constant version = 2;

    enum paymentTierTypes 
    {
        FREE,
        STANDARD,
        PRIORITY
    }
    uint constant numPaymentTiers = 3;
    PaymentTierSettings[numPaymentTiers] public tierSettings;

    mapping(address => PaymentEntry[numPaymentTiers]) public payments;

    event AccountPayment(address account, uint256 payment, uint256 endTimestamp);

    constructor(address token_)
    {
        admin = msg.sender;
        token = token_;
        for (uint i = 0 ; i < numPaymentTiers; i++)
        {
            tierSettings[i].enabled = true;
        }

        uint secondsPerDay = 1 days;

        tierSettings[uint(paymentTierTypes.FREE)].ratePerSecond     =    1000000000000000000 / secondsPerDay;
        tierSettings[uint(paymentTierTypes.STANDARD)].ratePerSecond =  100000000000000000000 / secondsPerDay;
        tierSettings[uint(paymentTierTypes.PRIORITY)].ratePerSecond = 1000000000000000000000 / secondsPerDay;

        tierSettings[uint(paymentTierTypes.FREE)].timeMaximum = 1 days;
        tierSettings[uint(paymentTierTypes.STANDARD)].timeMaximum = 60 days;
        tierSettings[uint(paymentTierTypes.PRIORITY)].timeMaximum = 60 days;

        tierSettings[uint(paymentTierTypes.FREE)].timeMinimum = 1 days;
        tierSettings[uint(paymentTierTypes.STANDARD)].timeMinimum = 3 days;
        tierSettings[uint(paymentTierTypes.PRIORITY)].timeMinimum = 3 days;
    }

    modifier onlyValidTier(uint tier) {
       require (tier < numPaymentTiers);
        _;
    }

    modifier onlyAdmin {
        require (msg.sender == admin, "ERR_ADMIN_PROTECT");
        _;
    }

    function setAdmin(address payable newAdmin) external onlyAdmin
    {
        admin = newAdmin;
    }

    function setPaymentsEnabled(uint tier, bool enabled) external onlyAdmin onlyValidTier(tier)
    {
        tierSettings[tier].enabled = enabled;
    }

    function setPaymentRatePerSecond(uint tier, uint256 newPaymentRatePerSecond) external onlyAdmin onlyValidTier(tier)
    {
        tierSettings[tier].ratePerSecond = newPaymentRatePerSecond;
    }

    function setMaximumPaymentTime(uint tier, uint256 newPaymentTimeMaximum) external onlyAdmin onlyValidTier(tier)
    {
        tierSettings[tier].timeMaximum = newPaymentTimeMaximum;
    }

    function setMinimumPaymentTime(uint tier, uint256 newPaymentTimeMinimum) external onlyAdmin onlyValidTier(tier)
    {
        tierSettings[tier].timeMinimum = newPaymentTimeMinimum;
    }

    function calculateMaxPayment(uint tier) public onlyValidTier(tier) view returns (uint256)
    {
        return calculateMaxPaymentForPayer(msg.sender, tier);
    }

    function calculateMinPayment(uint tier) public onlyValidTier(tier) view returns (uint256)
    {
        return tierSettings[tier].timeMinimum * tierSettings[tier].ratePerSecond;
    }

    function calculatePaymentByTierAndTime(uint tier, uint256 _seconds) public onlyValidTier(tier) view returns (uint256)
    {
        uint256 payment = _seconds * tierSettings[tier].ratePerSecond;

        require (payment <= calculateMaxPaymentForPayer(msg.sender, tier), "ERROR_PAYMENT_TOO_LARGE");
        require (payment >= tierSettings[tier].timeMinimum * tierSettings[tier].ratePerSecond, "ERROR_PAYMENT_TOO_SMALL");

        return payment;
    }

    function calculateMaxPaymentForPayer(address payer, uint tier) internal view returns (uint256)
    {
        uint256 maxPayment = 0;

        if (payments[payer][tier].exists && payments[payer][tier].endTimestamp > block.timestamp)
        {
            maxPayment = (tierSettings[tier].timeMaximum - (payments[payer][tier].endTimestamp - block.timestamp)) * tierSettings[tier].ratePerSecond;
        }
        else
        {
            maxPayment = tierSettings[tier].timeMaximum * tierSettings[tier].ratePerSecond;
        }

        return maxPayment;
    }

    function pay(uint tier, uint256 value) external onlyValidTier(tier)
    {
        require (tierSettings[tier].enabled == true, "ERROR_PAYMENTS_DISABLED");
        require (value <= calculateMaxPaymentForPayer(msg.sender, tier), "ERROR_PAYMENT_TOO_LARGE");
        require (value >= calculateMinPayment(tier), "ERROR_PAYMENT_TOO_SMALL");

        if (payments[msg.sender][tier].exists)
        {
            if (payments[msg.sender][tier].endTimestamp > block.timestamp)
            {
                // account has a payment active
                payments[msg.sender][tier].endTimestamp = payments[msg.sender][tier].endTimestamp + (value / tierSettings[tier].ratePerSecond);
            }
            else
            {
                // account exists, but payment expired
                payments[msg.sender][tier].endTimestamp = block.timestamp + (value / tierSettings[tier].ratePerSecond);
            }
        }
        else
        {
            // account does not exist yet
            payments[msg.sender][tier].exists = true;
            payments[msg.sender][tier].endTimestamp = block.timestamp + (value / tierSettings[tier].ratePerSecond);
        }

        emit AccountPayment(msg.sender, value, payments[msg.sender][tier].endTimestamp);
        IERC20(token).transferFrom(msg.sender, address(this), value);
    }

    function grant(address[] calldata addresses, uint256[] calldata timestamps, uint8[] calldata tiers) onlyAdmin external
    {
        for (uint i = 0; i < addresses.length; i++)
        {
            if (payments[addresses[i]][tiers[i]].exists == false)
            {
                payments[addresses[i]][tiers[i]].exists = true;
            }

            payments[addresses[i]][tiers[i]].endTimestamp = timestamps[i];
        }
    }

    function transferToken(address tokenAddress, uint256 amount) onlyAdmin external
    {
        IERC20(tokenAddress).transfer(msg.sender, amount);
    }
}