// 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 "./types/GhostAccessControlled.sol";

import {ITreasury} from "./interfaces/ITreasury.sol";
import {IAllocator} from "./interfaces/IAllocator.sol";
import {ITreasuryExtender} from "./interfaces/ITreasuryExtender.sol";
import {IGhostAuthority} from "./interfaces/IGhostAuthority.sol";

contract TreasuryExtender is GhostAccessControlled, ITreasuryExtender {
    using SafeERC20 for IERC20;

    ITreasury public immutable treasury; // forge-lint: disable-line(screaming-snake-case-immutable)
    IAllocator[] public allocators;
    mapping(IAllocator => mapping(uint256 => AllocatorData)) public allocatorData;

    constructor(address treasuryAddress, address authorityAddress)
        GhostAccessControlled(IGhostAuthority(authorityAddress))
    {
        treasury = ITreasury(treasuryAddress);
        allocators.push(IAllocator(address(0)));
    }

    function _allocatorActivated(IAllocator.AllocatorStatus status) internal pure {
        if (IAllocator.AllocatorStatus.ACTIVATED != status) revert AllocatorNotActivated();
    }

    function _allocatorOffline(IAllocator.AllocatorStatus status) internal pure {
        if (IAllocator.AllocatorStatus.OFFLINE != status) revert AllocatorNotOffline();
    }

    function _onlyAllocator(
        IAllocator byStateId,
        address sender,
        uint256 id
    ) internal pure {
        if (IAllocator(sender) != byStateId) revert OnlyAllocator(id, sender);
    }

    function registerDeposit(address newAllocator) external override onlyGuardian {
        IAllocator allocator = IAllocator(newAllocator);
        uint256 id = allocators.length;
        allocators.push(allocator);
        allocator.addId(id);
        emit NewDepositRegistered(
            newAllocator,
            address(allocator.tokens()[allocator.tokenIds(id)]),
            id
        );
    }

    function setAllocatorLimits(
        uint256 id,
        AllocatorLimits calldata limits
    ) external override onlyGuardian {
        IAllocator allocator = allocators[id];
        _allocatorOffline(allocator.status());
        allocatorData[allocator][id].limits = limits;
        emit AllocatorLimitsChanged(id, limits.allocated, limits.loss);
    }

    function report(
        uint256 id,
        uint128 gain,
        uint128 loss
    ) external override {
        IAllocator allocator = allocators[id];
        AllocatorData storage data = allocatorData[allocator][id];
        AllocatorPerformance memory perf = data.performance;
        IAllocator.AllocatorStatus status = allocator.status();

        _onlyAllocator(allocator, msg.sender, id);
        if (status == IAllocator.AllocatorStatus.OFFLINE) revert AllocatorOffline();

        if (gain >= loss) {
            if (loss == type(uint128).max) {
                AllocatorData storage newAllocatorData =
                    allocatorData[allocators[allocators.length - 1]][id];

                newAllocatorData.holdings.allocated = data.holdings.allocated;
                newAllocatorData.performance.gain = data.performance.gain;
                data.holdings.allocated = 0;

                perf.gain = 0;
                perf.loss = 0;

                emit AllocatorReportedMigration(id);
            } else {
                perf.gain += gain;
                emit AllocatorReportedGain(id, gain);
            }
        } else {
            data.holdings.allocated -= loss;
            perf.loss += loss;
            emit AllocatorReportedLoss(id, loss);
        }
        data.performance = perf;
    }

    function requestFundsFromTreasury(
        uint256 id,
        uint256 amount
    ) external override onlyGuardian {
        IAllocator allocator = allocators[id];
        AllocatorData memory data = allocatorData[allocator][id];
        address token = address(allocator.tokens()[allocator.tokenIds(id)]);
        uint256 value = treasury.tokenValue(token, amount);

        _allocatorActivated(allocator.status());
        _allocatorBelowLimit(data, amount);

        treasury.manage(token, amount);
        allocatorData[allocator][id].holdings.allocated += amount;

        IERC20(token).safeTransfer(address(allocator), amount);
        emit AllocatorFunded(id, amount, value);
    }

    function returnFundsToTreasury(
        uint256 id,
        uint256 amount
    ) external override onlyGuardian {
        IAllocator allocator = allocators[id];
        uint256 allocated = allocatorData[allocator][id].holdings.allocated;
        uint128 gain = allocatorData[allocator][id].performance.gain;
        address token = address(allocator.tokens()[allocator.tokenIds(id)]);

        if (amount > allocated) {
            amount -= allocated;
            if (amount > gain) {
                amount = allocated + gain;
                gain = 0;
            } else {
                // forge-lint: disable-next-line(unsafe-typecast)
                gain -= uint128(amount);
                amount += allocated;
            }
            allocated = 0;
        } else {
            allocated -= amount;
        }

        uint256 value = treasury.tokenValue(token, amount);
        _allowTreasuryWithdrawal(IERC20(token));
        IERC20(token).safeTransferFrom(address(allocator), address(this), amount);

        allocatorData[allocator][id].holdings.allocated = allocated;
        if (allocated == 0) allocatorData[allocator][id].performance.gain = gain;

        assert(treasury.deposit(token, amount, value) == 0);
        emit AllocatedWithdrawal(id, amount, value);
    }

    function returnRewardsToTreasury(
        uint256 id,
        address token,
        uint256 amount
    ) external override {
        _returnRewardsToTreasury(allocators[id], IERC20(token), amount);
    }

    function returnRewardsToTreasury(
        address allocatorAddress,
        address token,
        uint256 amount
    ) external override {
        _returnRewardsToTreasury(IAllocator(allocatorAddress), IERC20(token), amount);
    }

    function getAllocatorById(uint256 id)
        external
        view
        override
        returns (address)
    {
        return address(allocators[id]);
    }

    function getTotalAllocatorCount()
        external
        view
        returns (uint256)
    {
        return allocators.length;
    }

    function getAllocatorLimits(uint256 id)
        external
        view
        override
        returns (AllocatorLimits memory)
    {
        return allocatorData[allocators[id]][id].limits;
    }

    function getAllocatorPerformance(uint256 id)
        external
        view
        override
        returns (AllocatorPerformance memory)
    {
        return allocatorData[allocators[id]][id].performance;
    }

    function getAllocatorAllocated(uint256 id)
        external
        view
        override
        returns (uint256)
    {
        return allocatorData[allocators[id]][id].holdings.allocated;
    }

    function _returnRewardsToTreasury(
        IAllocator allocator,
        IERC20 token,
        uint256 amount
    ) internal onlyGuardian {
        uint256 balance = token.balanceOf(address(allocator));
        amount = balance < amount ? balance : amount;
        uint256 value = treasury.tokenValue(address(token), amount);

        _allowTreasuryWithdrawal(token);

        token.safeTransferFrom(address(allocator), address(this), amount);
        assert(treasury.deposit(address(token), amount, value) == 0);
        emit AllocatorRewardsWithdrawal(address(allocator), amount, value);
    }

    function _allowTreasuryWithdrawal(IERC20 token) internal {
        if (token.allowance(address(this), address(treasury)) == 0) {
            token.approve(address(treasury), type(uint256).max);
        }
    }

    function _allocatorBelowLimit(
        AllocatorData memory data,
        uint256 amount
    ) internal pure {
        uint256 newAllocated = data.holdings.allocated + amount;
        if (newAllocated > data.limits.allocated) {
            revert AllocatorMaxAllocation(newAllocated, data.limits.allocated);
        }
    }
}
