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

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

import "./ConfiguratorStorage.sol";
import "./LendingFactory.sol";

contract Configurator is 
    ConfiguratorStorage, 
    UUPSUpgradeable,
    OwnableUpgradeable 
{
    event SetFactory(address indexed lendingProxy, address indexed oldFactory, address indexed newFactory);
    event SetConfiguration(address indexed lendingProxy, Configuration oldConfiguration, Configuration newConfiguration);
    event AddAsset(address indexed lendingProxy, AssetConfig assetConfig);
    event UpdateAsset(address indexed lendingProxy, AssetConfig oldAssetConfig, AssetConfig newAssetConfig);
    event LendingDeployed(address indexed lendingProxy, address indexed newLending);

    error AlreadyInitialized();
    error AssetDoesNotExist();
    error ConfigurationAlreadyExists();
    error InvalidAddress();

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize() external initializer {
        __UUPSUpgradeable_init();
        __Ownable_init(msg.sender);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

    function setFactory(address lendingProxy, address newFactory) external onlyOwner {
        if (newFactory == address(0)) revert InvalidAddress();
        
        address oldFactory = factory[lendingProxy];
        factory[lendingProxy] = newFactory;
        emit SetFactory(lendingProxy, oldFactory, newFactory);
    }

    function setConfiguration(address lendingProxy, Configuration calldata newConfiguration) 
        external 
        onlyOwner 
    {
        Configuration memory oldConfiguration = configuratorParams[lendingProxy];
        
        if (oldConfiguration.baseToken != address(0) &&
            (oldConfiguration.baseToken != newConfiguration.baseToken ||
             oldConfiguration.trackingIndexScale != newConfiguration.trackingIndexScale))
            revert ConfigurationAlreadyExists();

        delete configuratorParams[lendingProxy];
        
        configuratorParams[lendingProxy].baseToken = newConfiguration.baseToken;
        configuratorParams[lendingProxy].lendingPriceSource = newConfiguration.lendingPriceSource;
        configuratorParams[lendingProxy].supplyKink = newConfiguration.supplyKink;
        configuratorParams[lendingProxy].supplyPerYearInterestRateSlopeLow = newConfiguration.supplyPerYearInterestRateSlopeLow;
        configuratorParams[lendingProxy].supplyPerYearInterestRateSlopeHigh = newConfiguration.supplyPerYearInterestRateSlopeHigh;
        configuratorParams[lendingProxy].supplyPerYearInterestRateBase = newConfiguration.supplyPerYearInterestRateBase;
        configuratorParams[lendingProxy].borrowKink = newConfiguration.borrowKink;
        configuratorParams[lendingProxy].borrowPerYearInterestRateSlopeLow = newConfiguration.borrowPerYearInterestRateSlopeLow;
        configuratorParams[lendingProxy].borrowPerYearInterestRateSlopeHigh = newConfiguration.borrowPerYearInterestRateSlopeHigh;
        configuratorParams[lendingProxy].borrowPerYearInterestRateBase = newConfiguration.borrowPerYearInterestRateBase;
        configuratorParams[lendingProxy].storeFrontPriceFactor = newConfiguration.storeFrontPriceFactor;
        configuratorParams[lendingProxy].trackingIndexScale = newConfiguration.trackingIndexScale;
        configuratorParams[lendingProxy].baseBorrowMin = newConfiguration.baseBorrowMin;
        configuratorParams[lendingProxy].targetReserves = newConfiguration.targetReserves;
        
        for (uint i = 0; i < newConfiguration.assetConfigs.length; i++) {
            configuratorParams[lendingProxy].assetConfigs.push(newConfiguration.assetConfigs[i]);
        }
        
        emit SetConfiguration(lendingProxy, oldConfiguration, newConfiguration);
    }

    function addAsset(address lendingProxy, AssetConfig calldata assetConfig) 
        external 
        onlyOwner 
    {
        configuratorParams[lendingProxy].assetConfigs.push(assetConfig);
        emit AddAsset(lendingProxy, assetConfig);
    }

    function updateAsset(address lendingProxy, AssetConfig calldata newAssetConfig) 
        external 
        onlyOwner 
    {
        uint assetIndex = getAssetIndex(lendingProxy, newAssetConfig.asset);
        AssetConfig memory oldAssetConfig = configuratorParams[lendingProxy].assetConfigs[assetIndex];
        configuratorParams[lendingProxy].assetConfigs[assetIndex] = newAssetConfig;
        emit UpdateAsset(lendingProxy, oldAssetConfig, newAssetConfig);
    }

    function updateAssetBorrowCollateralFactor(
        address lendingProxy, 
        address asset, 
        uint64 newBorrowCF
    ) 
        external 
        onlyOwner 
    {
        uint assetIndex = getAssetIndex(lendingProxy, asset);
        configuratorParams[lendingProxy].assetConfigs[assetIndex].borrowCollateralFactor = newBorrowCF;
    }

    function updateAssetSupplyCap(
        address lendingProxy, 
        address asset, 
        uint128 newSupplyCap
    ) 
        external 
        onlyOwner 
    {
        uint assetIndex = getAssetIndex(lendingProxy, asset);
        configuratorParams[lendingProxy].assetConfigs[assetIndex].supplyCap = newSupplyCap;
    }

    function deploy(address lendingProxy) external onlyOwner returns (address) {
        address newLending = LendingFactory(factory[lendingProxy]).deploy();
        emit LendingDeployed(lendingProxy, newLending);
        return newLending;
    }

    function getAssetIndex(address lendingProxy, address asset) public view returns (uint) {
        AssetConfig[] memory assetConfigs = configuratorParams[lendingProxy].assetConfigs;
        uint numAssets = assetConfigs.length;
        for (uint i = 0; i < numAssets; ) {
            if (assetConfigs[i].asset == asset) {
                return i;
            }
            unchecked { i++; }
        }
        revert AssetDoesNotExist();
    }

    function getConfiguration(address lendingProxy) external view returns (Configuration memory) {
        return configuratorParams[lendingProxy];
    }

    uint256[50] private __gap;
}