// 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 {GhostAccessControlled} from "./GhostAccessControlled.sol";
import {IAllocator} from "../interfaces/IAllocator.sol";
import {ITreasuryExtender} from "../interfaces/ITreasuryExtender.sol";

abstract contract BaseAllocator is GhostAccessControlled, IAllocator {
    using SafeERC20 for IERC20;

    ITreasuryExtender public immutable extender; // forge-lint: disable-line(screaming-snake-case-immutable)
    AllocatorStatus public status;

    uint256[] internal _ids;
    IERC20[] internal _tokens;

    mapping(uint256 => uint256) public tokenIds;

    constructor(AllocatorInitData memory data)
        GhostAccessControlled(data.authority)
    {
        _tokens = data.tokens;
        extender = data.extender;

        uint256 i;
        for (; i < data.tokens.length; ) {
            data.tokens[i].approve(
                address(data.extender),
                type(uint256).max
            );
            unchecked { ++i; }
        }

        emit AllocatorDeployed(
            address(data.authority),
            address(data.extender)
        );
    }

    modifier onlyExtender {
        _onlyExtender(msg.sender);
        _;
    }

    modifier onlyActivated {
        _onlyActivated(status);
        _;
    }

    modifier onlyOffline {
        _onlyOffline(status);
        _;
    }

    modifier notMigrating {
        _notMigrating(status);
        _;
    }

    modifier isMigrating {
        _isMigrating(status);
        _;
    }

    function _activate() internal virtual {}

    function _prepareMigration() internal virtual;
    function _deactivate(bool panic) internal virtual;
    function _update(uint256 id) internal virtual returns (uint128 gain, uint128 loss);

    function deallocate(uint256[] memory amounts) public virtual;
    function amountAllocated(uint256 id) public view virtual returns (uint256);
    function rewardTokens() public view virtual returns (IERC20[] memory);
    function utilityTokens() public view virtual returns (IERC20[] memory);
    function name() external view virtual returns (string memory);

    function update(uint256 id) external override onlyGuardian onlyActivated {
        (uint128 gain, uint128 loss) = _update(id);

        if (_lossLimitViolated(id, loss)) {
            deactivate(true);
            return;
        }

        if (gain + loss > 0) extender.report(id, gain, loss);
    }

    function prepareMigration() external override onlyGuardian notMigrating {
        _prepareMigration();
        status = AllocatorStatus.MIGRATING;
    }

    function migrate() external override onlyGuardian isMigrating {
        IERC20[] memory utilityTokensArray = utilityTokens();
        address newAllocator = extender.getAllocatorById(extender.getTotalAllocatorCount() - 1);
        uint256 idLength = _ids.length;
        uint256 utilLength = utilityTokensArray.length;

        for (uint256 i; i < idLength; i++) {
            IERC20 token = _tokens[i];
            token.safeTransfer(newAllocator, token.balanceOf(address(this)));
            extender.report(_ids[i], type(uint128).max, type(uint128).max);
        }

        for (uint256 i; i < utilLength; i++) {
            IERC20 utilityToken = utilityTokensArray[i];
            utilityToken.safeTransfer(newAllocator, utilityToken.balanceOf(address(this)));
        }

        deactivate(false);
        emit MigrationExecuted(newAllocator);
    }

    function activate() external override onlyGuardian onlyOffline {
        _activate();
        status = AllocatorStatus.ACTIVATED;
        emit AllocatorActivated();
    }

    function addId(uint256 id) external override onlyExtender {
        _ids.push(id);
        tokenIds[id] = _ids.length - 1;
    }

    function ids() external view override returns (uint256[] memory) {
        return _ids;
    }

    function tokens() external view override returns (IERC20[] memory) {
        return _tokens;
    }

    function deactivate(bool panic) public override onlyGuardian {
        _deactivate(panic);
        status = AllocatorStatus.OFFLINE;
        emit AllocatorDeactivated(panic);
    }

    function version() public pure override returns (string memory) {
        return "v2.0.0";
    }

    function _lossLimitViolated(uint256 id, uint128 loss) internal returns (bool) {
        uint128 lastLoss = extender.getAllocatorPerformance(id).loss;

        if ((loss + lastLoss) >= extender.getAllocatorLimits(id).loss) {
            emit LossLimitViolated(lastLoss, loss, amountAllocated(tokenIds[id]));
            return true;
        }

        return false;
    }

    function _onlyExtender(address sender) internal view {
        if (sender != address(extender)) revert OnlyExtender(sender);
    }

    function _onlyActivated(AllocatorStatus inputStatus) internal pure {
        if (inputStatus != AllocatorStatus.ACTIVATED) revert AllocatorNotActivated();
    }

    function _onlyOffline(AllocatorStatus inputStatus) internal pure {
        if (inputStatus != AllocatorStatus.OFFLINE) revert AllocatorNotOffline();
    }

    function _notMigrating(AllocatorStatus inputStatus) internal pure {
        if (inputStatus == AllocatorStatus.MIGRATING) revert Migrating();
    }

    function _isMigrating(AllocatorStatus inputStatus) internal pure {
        if (inputStatus != AllocatorStatus.MIGRATING) revert NotMigrating();
    }
}
