// 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 "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import "./LendingStorage.sol";
import "./LendingMath.sol";
import "../interfaces/ILending.sol";
import "../interfaces/IYTLendingPriceFeed.sol";

contract Lending is 
    ILending,
    LendingStorage,
    UUPSUpgradeable,
    OwnableUpgradeable,
    PausableUpgradeable,
    ReentrancyGuardUpgradeable 
{
    using SafeERC20 for IERC20;

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

    function initialize(Configuration calldata config) external initializer {
        __UUPSUpgradeable_init();
        __Ownable_init(msg.sender);
        __Pausable_init();
        __ReentrancyGuard_init();
        
        baseToken = config.baseToken;
        lendingPriceSource = config.lendingPriceSource;

        uint64 SECONDS_PER_YEAR = 365 * 24 * 60 * 60;
        
        supplyKink = config.supplyKink;
        supplyPerSecondInterestRateSlopeLow = uint64(config.supplyPerYearInterestRateSlopeLow / SECONDS_PER_YEAR);
        supplyPerSecondInterestRateSlopeHigh = uint64(config.supplyPerYearInterestRateSlopeHigh / SECONDS_PER_YEAR);
        supplyPerSecondInterestRateBase = uint64(config.supplyPerYearInterestRateBase / SECONDS_PER_YEAR);
        
        borrowKink = config.borrowKink;
        borrowPerSecondInterestRateSlopeLow = uint64(config.borrowPerYearInterestRateSlopeLow / SECONDS_PER_YEAR);
        borrowPerSecondInterestRateSlopeHigh = uint64(config.borrowPerYearInterestRateSlopeHigh / SECONDS_PER_YEAR);
        borrowPerSecondInterestRateBase = uint64(config.borrowPerYearInterestRateBase / SECONDS_PER_YEAR);
        
        storeFrontPriceFactor = config.storeFrontPriceFactor;
        trackingIndexScale = config.trackingIndexScale;
        baseBorrowMin = config.baseBorrowMin;
        targetReserves = config.targetReserves;
        
        supplyIndex = 1e18;
        borrowIndex = 1e18;
        lastAccrualTime = block.timestamp;
        
        for (uint i = 0; i < config.assetConfigs.length; i++) {
            AssetConfig memory assetConfig = config.assetConfigs[i];
            
            if(assetConfig.liquidationFactor >= 1e18) revert InvalidLiquidationFactor();
            if(assetConfig.borrowCollateralFactor >= 1e18) revert InvalidBorrowCollateralFactor();
            if(assetConfig.liquidateCollateralFactor >= 1e18) revert InvalidLiquidateCollateralFactor();
            
            assetConfigs[assetConfig.asset] = assetConfig;
            assetList.push(assetConfig.asset);
        }
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

    function pause() external onlyOwner {
        _pause();
    }
    
    function unpause() external onlyOwner {
        _unpause();
    }

    function accruedInterestIndices(uint256 timeElapsed) internal view returns (uint256, uint256) {
        uint256 newSupplyIndex = supplyIndex;
        uint256 newBorrowIndex = borrowIndex;
        
        if (timeElapsed > 0) {
            uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
            uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
            
            uint64 utilization = LendingMath.getUtilization(totalSupply, totalBorrow);
            
            uint64 supplyRate = LendingMath.getSupplyRate(
                utilization,
                supplyKink,
                supplyPerSecondInterestRateSlopeLow,
                supplyPerSecondInterestRateSlopeHigh,
                supplyPerSecondInterestRateBase
            );
            
            uint64 borrowRate = LendingMath.getBorrowRate(
                utilization,
                borrowKink,
                borrowPerSecondInterestRateSlopeLow,
                borrowPerSecondInterestRateSlopeHigh,
                borrowPerSecondInterestRateBase
            );
            
            newSupplyIndex = LendingMath.accrueInterest(supplyIndex, supplyRate, timeElapsed);
            newBorrowIndex = LendingMath.accrueInterest(borrowIndex, borrowRate, timeElapsed);
        }
        
        return (newSupplyIndex, newBorrowIndex);
    }

    function accrueInterest() public {
        uint256 timeElapsed = block.timestamp - lastAccrualTime;
        if (timeElapsed == 0) return;
        
        (supplyIndex, borrowIndex) = accruedInterestIndices(timeElapsed);
        
        lastAccrualTime = block.timestamp;
    }

    function supply(uint256 amount) external override nonReentrant whenNotPaused {
        accrueInterest();
        
        IERC20(baseToken).transferFrom(msg.sender, address(this), amount);
        
        UserBasic memory user = userBasic[msg.sender];
        int104 oldPrincipal = user.principal;
        
        uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
        int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
        
        int256 newBalance = oldBalance + int256(amount);
        
        uint256 newIndex = newBalance >= 0 ? supplyIndex : borrowIndex;
        int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, newIndex);
        
        (uint104 repayAmount, uint104 supplyAmount) = LendingMath.repayAndSupplyAmount(oldPrincipal, newPrincipal);
        
        totalBorrowBase -= repayAmount;
        totalSupplyBase += supplyAmount;
        
        userBasic[msg.sender].principal = newPrincipal;
        
        emit Supply(msg.sender, msg.sender, amount);
    }

    function withdraw(uint256 amount) external override nonReentrant whenNotPaused {
        accrueInterest();
        
        UserBasic memory user = userBasic[msg.sender];
        int104 oldPrincipal = user.principal;
        
        uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
        int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
        
        int256 newBalance = oldBalance - int256(amount);
        
        uint256 newIndex = newBalance >= 0 ? supplyIndex : borrowIndex;
        int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, newIndex);
        
        (uint104 withdrawAmount, uint104 borrowAmount) = LendingMath.withdrawAndBorrowAmount(oldPrincipal, newPrincipal);
        
        totalSupplyBase -= withdrawAmount;
        totalBorrowBase += borrowAmount;

        userBasic[msg.sender].principal = newPrincipal;
        
        if (newBalance < 0) {
            if (uint256(-newBalance) < baseBorrowMin) revert BorrowTooSmall();
            if (!_isSolvent(msg.sender)) revert InsufficientCollateral();
        }
        
        IERC20(baseToken).safeTransfer(msg.sender, amount);
        
        emit Withdraw(msg.sender, msg.sender, amount);
    }

    function supplyCollateral(address asset, uint256 amount) external override nonReentrant whenNotPaused {
        AssetConfig memory config = assetConfigs[asset];
        if (config.asset == address(0)) revert Unauthorized();
        
        uint256 newTotal = userCollateral[msg.sender][asset] + amount;
        if (newTotal > config.supplyCap) revert SupplyCapExceeded();
        
        IERC20(asset).transferFrom(msg.sender, address(this), amount);
        
        userCollateral[msg.sender][asset] += amount;
        
        emit SupplyCollateral(msg.sender, msg.sender, asset, amount);
    }

    function withdrawCollateral(address asset, uint256 amount) external override nonReentrant whenNotPaused {
        accrueInterest();
        
        if (userCollateral[msg.sender][asset] < amount) revert InsufficientBalance();
        
        userCollateral[msg.sender][asset] -= amount;
        
        int104 principal = userBasic[msg.sender].principal;
        if (principal < 0) {
            if (!_isSolvent(msg.sender)) revert InsufficientCollateral();
        }
        
        IERC20(asset).safeTransfer(msg.sender, amount);
        
        emit WithdrawCollateral(msg.sender, msg.sender, asset, amount);
    }

    function borrow(uint256 amount) external override nonReentrant whenNotPaused {
        accrueInterest();
        
        UserBasic memory user = userBasic[msg.sender];
        int104 oldPrincipal = user.principal;
        
        uint256 index = oldPrincipal >= 0 ? supplyIndex : borrowIndex;
        int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, index);
        
        int256 newBalance = oldBalance - int256(amount);
        
        if (newBalance < 0 && uint256(-newBalance) < baseBorrowMin) revert BorrowTooSmall();
        
        uint256 newIndex = newBalance >= 0 ? supplyIndex : borrowIndex;
        int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, newIndex);
        
        (uint104 withdrawAmount, uint104 borrowAmount) = LendingMath.withdrawAndBorrowAmount(oldPrincipal, newPrincipal);
        
        totalSupplyBase -= withdrawAmount;
        totalBorrowBase += borrowAmount;
        
        userBasic[msg.sender].principal = newPrincipal;
        
        if (!_isSolvent(msg.sender)) revert InsufficientCollateral();
        
        IERC20(baseToken).safeTransfer(msg.sender, amount);
        
        emit Withdraw(msg.sender, msg.sender, amount);
    }

    function _absorbInternal(address absorber, address borrower) internal {
        if (!isLiquidatable(borrower)) revert NotLiquidatable();
        
        UserBasic memory user = userBasic[borrower];
        int104 oldPrincipal = user.principal;
        
        int256 oldBalance = LendingMath.principalToBalance(oldPrincipal, borrowIndex);
        if (oldBalance >= 0) revert NotLiquidatable();
        
        uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken);
        uint256 totalCollateralValue = 0;
        
        for (uint i = 0; i < assetList.length; i++) {
            address asset = assetList[i];
            uint256 collateralAmount = userCollateral[borrower][asset];
            
            if (collateralAmount > 0) {
                AssetConfig memory assetConfig = assetConfigs[asset];
                uint256 assetPrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset);
                
                uint256 assetScale = 10 ** assetConfig.decimals;
                uint256 collateralValueUSD = (collateralAmount * assetPrice) / assetScale;
                
                uint256 discountedValue = (collateralAmount * assetPrice * assetConfig.liquidationFactor) / (assetScale * 1e18);
                totalCollateralValue += discountedValue;
                
                userCollateral[borrower][asset] = 0;
                collateralReserves[asset] += collateralAmount;
                
                emit AbsorbCollateral(absorber, borrower, asset, collateralAmount, collateralValueUSD);
            }
        }
        
        uint256 baseScale = 10 ** IERC20Metadata(baseToken).decimals();
        uint256 collateralInBase = (totalCollateralValue * baseScale) / basePrice;
        
        int256 newBalance = oldBalance + int256(collateralInBase);
        
        if (newBalance < 0) {
            newBalance = 0;
        }
        
        int104 newPrincipal = LendingMath.balanceToPrincipal(newBalance, supplyIndex);
        
        userBasic[borrower].principal = newPrincipal;
        
        (uint104 repayAmount, uint104 supplyAmount) = LendingMath.repayAndSupplyAmount(oldPrincipal, newPrincipal);
        
        totalSupplyBase += supplyAmount;
        totalBorrowBase -= repayAmount;
        
        uint256 basePaidOut = 0;
        if (int256(collateralInBase) < -oldBalance) {
            basePaidOut = uint256(-oldBalance) - collateralInBase;
        }
        
        uint256 valueOfBasePaidOut = (basePaidOut * basePrice) / baseScale;
        
        emit AbsorbDebt(absorber, borrower, basePaidOut, valueOfBasePaidOut);
    }
    
    function absorb(address borrower) external override nonReentrant whenNotPaused {
        accrueInterest();
        _absorbInternal(msg.sender, borrower);
    }
    
    function absorbMultiple(address absorber, address[] calldata accounts) external override nonReentrant whenNotPaused {
        accrueInterest();
        for (uint i = 0; i < accounts.length; ) {
            _absorbInternal(absorber, accounts[i]);
            unchecked { i++; }
        }
    }

    function buyCollateral(
        address asset,
        uint256 minAmount,
        uint256 baseAmount,
        address recipient
    ) external override nonReentrant whenNotPaused {
        if (collateralReserves[asset] == 0) revert InsufficientBalance();
        
        int256 currentReserves = getReserves();
        if (currentReserves >= 0 && uint256(currentReserves) >= targetReserves) {
            revert NotForSale();
        }
        
        uint256 collateralAmount = quoteCollateral(asset, baseAmount);
        
        if (collateralAmount < minAmount) revert InsufficientBalance();
        if (collateralAmount > collateralReserves[asset]) revert InsufficientBalance();
        
        IERC20(baseToken).transferFrom(msg.sender, address(this), baseAmount);
        
        collateralReserves[asset] -= collateralAmount;
        
        IERC20(asset).safeTransfer(recipient, collateralAmount);
        
        emit BuyCollateral(msg.sender, asset, baseAmount, collateralAmount);
    }
    
    function quoteCollateral(address asset, uint256 baseAmount) public view override returns (uint256) {
        AssetConfig memory assetConfig = assetConfigs[asset];
        
        uint256 assetPrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset);
        uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken);
        
        uint256 FACTOR_SCALE = 1e18;
        uint256 baseScale = 10 ** uint256(IERC20Metadata(baseToken).decimals());
        uint256 assetScale = 10 ** uint256(assetConfig.decimals);

        uint256 discountFactor = (storeFrontPriceFactor * (FACTOR_SCALE - assetConfig.liquidationFactor)) / FACTOR_SCALE;
        
        uint256 effectiveAssetPrice = (assetPrice * (FACTOR_SCALE - discountFactor)) / FACTOR_SCALE;
        
        if (baseScale == assetScale) {
            return (baseAmount * basePrice) / effectiveAssetPrice;
        } else {
            uint256 adjustedAmount = (baseAmount * assetScale) / baseScale;
            return (adjustedAmount * basePrice) / effectiveAssetPrice;
        }
    }

    function _isSolvent(address account) internal view returns (bool) {
        int104 principal = userBasic[account].principal;
        if (principal >= 0) return true;
        
        int256 balance = LendingMath.principalToBalance(principal, borrowIndex);
        uint256 debt = uint256(-balance);
        
        uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken);
        uint256 baseDecimals = IERC20Metadata(baseToken).decimals();
        uint256 debtValue = (debt * basePrice) / (10 ** baseDecimals);
        
        uint256 borrowCapacity = _getCollateralValue(account);
        
        return borrowCapacity >= debtValue;
    }

    function _getCollateralValue(address account) internal view returns (uint256) {
        uint256 totalValue = 0;
        
        for (uint i = 0; i < assetList.length; i++) {
            address asset = assetList[i];
            uint256 amount = userCollateral[account][asset];
            if (amount > 0) {
                AssetConfig memory config = assetConfigs[asset];
                uint256 price = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset);
                uint256 value = LendingMath.getCollateralValue(amount, price, config.decimals);
                totalValue += (value * config.borrowCollateralFactor) / 1e18;
            }
        }
        
        return totalValue;
    }

    function getBalance(address account) external view override returns (int256) {
        int104 principal = userBasic[account].principal;
        return LendingMath.principalToBalance(principal, supplyIndex);
    }
    
    function supplyBalanceOf(address account) external view override returns (uint256) {
        int104 principal = userBasic[account].principal;
        if (principal <= 0) return 0;
        return uint256(LendingMath.principalToBalance(principal, supplyIndex));
    }
    
    function borrowBalanceOf(address account) external view override returns (uint256) {
        int104 principal = userBasic[account].principal;
        if (principal >= 0) return 0;
        int256 balance = LendingMath.principalToBalance(principal, borrowIndex);
        return uint256(-balance);
    }

    function getCollateral(address account, address asset) external view override returns (uint256) {
        return userCollateral[account][asset];
    }

    function isLiquidatable(address account) public view override returns (bool) {
        int104 principal = userBasic[account].principal;
        if (principal >= 0) return false;
        
        int256 balance = LendingMath.principalToBalance(principal, borrowIndex);
        uint256 debt = uint256(-balance);
        
        uint256 basePrice = IYTLendingPriceFeed(lendingPriceSource).getPrice(baseToken);
        uint256 baseDecimals = IERC20Metadata(baseToken).decimals();
        uint256 debtValue = (debt * basePrice) / (10 ** baseDecimals);
        
        uint256 collateralValue = 0;
        for (uint i = 0; i < assetList.length; i++) {
            address asset = assetList[i];
            uint256 amount = userCollateral[account][asset];
            if (amount > 0) {
                AssetConfig memory config = assetConfigs[asset];
                uint256 price = IYTLendingPriceFeed(lendingPriceSource).getPrice(asset);
                uint256 value = LendingMath.getCollateralValue(amount, price, config.decimals);
                collateralValue += (value * config.liquidateCollateralFactor) / 1e18;
            }
        }
        
        return debtValue > collateralValue;
    }

    function getTotalSupply() external view returns (uint256) {
        return (uint256(totalSupplyBase) * supplyIndex) / 1e18;
    }
    
    function getTotalBorrow() external view returns (uint256) {
        return (uint256(totalBorrowBase) * borrowIndex) / 1e18;
    }
    
    function getCollateralReserves(address asset) external view override returns (uint256) {
        return collateralReserves[asset];
    }
    
    function getReserves() public view override returns (int256) {
        uint256 timeElapsed = block.timestamp - lastAccrualTime;
        (uint256 newSupplyIndex, uint256 newBorrowIndex) = accruedInterestIndices(timeElapsed);
        
        uint256 balance = IERC20(baseToken).balanceOf(address(this));
        uint256 totalSupply = (uint256(totalSupplyBase) * newSupplyIndex) / 1e18;
        uint256 totalBorrow = (uint256(totalBorrowBase) * newBorrowIndex) / 1e18;
        
        return int256(balance) - int256(totalSupply) + int256(totalBorrow);
    }
    
    function getUtilization() external view override returns (uint256) {
        uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
        uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
        return LendingMath.getUtilization(totalSupply, totalBorrow);
    }
    
    function getSupplyRate() external view override returns (uint64) {
        uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
        uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
        uint64 utilization = LendingMath.getUtilization(totalSupply, totalBorrow);
        uint64 perSecondRate = LendingMath.getSupplyRate(
            utilization,
            supplyKink,
            supplyPerSecondInterestRateSlopeLow,
            supplyPerSecondInterestRateSlopeHigh,
            supplyPerSecondInterestRateBase
        );
        return perSecondRate * 31536000;
    }

    function getBorrowRate() external view override returns (uint64) {
        uint256 totalSupply = (uint256(totalSupplyBase) * supplyIndex) / 1e18;
        uint256 totalBorrow = (uint256(totalBorrowBase) * borrowIndex) / 1e18;
        uint64 utilization = LendingMath.getUtilization(totalSupply, totalBorrow);
        uint64 perSecondRate = LendingMath.getBorrowRate(
            utilization,
            borrowKink,
            borrowPerSecondInterestRateSlopeLow,
            borrowPerSecondInterestRateSlopeHigh,
            borrowPerSecondInterestRateBase
        );
        return perSecondRate * 31536000;
    }

    function withdrawReserves(address to, uint256 amount) external override onlyOwner nonReentrant {
        int256 currentReserves = getReserves();
        
        if (currentReserves < 0 || amount > uint256(currentReserves)) {
            revert InsufficientReserves();
        }
        
        IERC20(baseToken).safeTransfer(to, amount);
        
        emit WithdrawReserves(to, amount);
    }

    uint256[50] private __gap;
}