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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "../../interfaces/IYTAssetVault.sol";
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract YTPriceFeed is Initializable, UUPSUpgradeable {

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

    error Forbidden();
    error MaxChangeTooHigh();
    error PriceChangeTooLarge();
    error SpreadTooHigh();
    error InvalidAddress();
    error InvalidChainlinkPrice();
    
    address public gov;
    
    uint256 public constant PRICE_PRECISION = 10 ** 30;
    uint256 public constant BASIS_POINTS_DIVISOR = 10000;
    uint256 public constant MAX_SPREAD_BASIS_POINTS = 200;
    
    address public usdcAddress;
    
    uint256 public maxPriceChangeBps;

    AggregatorV3Interface internal usdcPriceFeed;
    
    mapping(address => uint256) public spreadBasisPoints;
    
    mapping(address => uint256) public lastPrice;
    
    mapping(address => bool) public isKeeper;
    
    event PriceUpdate(address indexed token, uint256 oldPrice, uint256 newPrice, uint256 timestamp);
    event SpreadUpdate(address indexed token, uint256 spreadBps);
    event KeeperSet(address indexed keeper, bool isActive);
    
    modifier onlyGov() {
        if (msg.sender != gov) revert Forbidden();
        _;
    }
    
    modifier onlyKeeper() {
        if (!isKeeper[msg.sender] && msg.sender != gov) revert Forbidden();
        _;
    }
    
    function initialize(address _usdcAddress, address _usdcPriceFeed) external initializer {
        __UUPSUpgradeable_init();
        if (_usdcAddress == address(0)) revert InvalidAddress();
        usdcAddress = _usdcAddress;
        usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeed);
        gov = msg.sender;
        maxPriceChangeBps = 500;
    }

    function setUSDCAddress(address _usdcAddress) external onlyGov {
        if (_usdcAddress == address(0)) revert InvalidAddress();
        usdcAddress = _usdcAddress;
    }

    function setUSDCPriceFeed(address _usdcPriceFeed) external onlyGov {
        usdcPriceFeed = AggregatorV3Interface(_usdcPriceFeed);
    }
    
    function _authorizeUpgrade(address newImplementation) internal override onlyGov {}
    
    function setKeeper(address _keeper, bool _isActive) external onlyGov {
        isKeeper[_keeper] = _isActive;
        emit KeeperSet(_keeper, _isActive);
    }
    
    function setMaxPriceChangeBps(uint256 _maxPriceChangeBps) external onlyGov {
        if (_maxPriceChangeBps > 2000) revert MaxChangeTooHigh();
        maxPriceChangeBps = _maxPriceChangeBps;
    }
    
    function setSpreadBasisPoints(address _token, uint256 _spreadBasisPoints) external onlyGov {
        if (_spreadBasisPoints > MAX_SPREAD_BASIS_POINTS) revert SpreadTooHigh();
        spreadBasisPoints[_token] = _spreadBasisPoints;
        emit SpreadUpdate(_token, _spreadBasisPoints);
    }
    
    function setSpreadBasisPointsForMultiple(
        address[] calldata _tokens,
        uint256[] calldata _spreadBasisPoints
    ) external onlyGov {
        require(_tokens.length == _spreadBasisPoints.length, "length mismatch");
        for (uint256 i = 0; i < _tokens.length; i++) {
            if (_spreadBasisPoints[i] > MAX_SPREAD_BASIS_POINTS) revert SpreadTooHigh();
            spreadBasisPoints[_tokens[i]] = _spreadBasisPoints[i];
            emit SpreadUpdate(_tokens[i], _spreadBasisPoints[i]);
        }
    }

    function updatePrice(address _token) external onlyKeeper returns (uint256) {
        if (_token == usdcAddress) {
            return _getUSDCPrice();
        }
        
        uint256 oldPrice = lastPrice[_token];
        uint256 newPrice = _getRawPrice(_token);
        
        _validatePriceChange(_token, newPrice);
        
        lastPrice[_token] = newPrice;
        
        emit PriceUpdate(_token, oldPrice, newPrice, block.timestamp);
        
        return newPrice;
    }
    
    function forceUpdatePrice(address _token, uint256 _price) external onlyGov {
        uint256 oldPrice = lastPrice[_token];
        lastPrice[_token] = _price;
        emit PriceUpdate(_token, oldPrice, _price, block.timestamp);
    }
    

    function getPrice(address _token, bool _maximise) external view returns (uint256) {
        if (_token == usdcAddress) {
            return _getUSDCPrice();
        }
        
        uint256 basePrice = _getRawPrice(_token);
        
        _validatePriceChange(_token, basePrice);
        
        return _applySpread(_token, basePrice, _maximise);
    }
    
    function _getRawPrice(address _token) private view returns (uint256) {
        return IYTAssetVault(_token).ytPrice();
    }

    function _getUSDCPrice() internal view returns (uint256) {
        (
            /* uint80 roundId */,
            int256 price,
            /* uint256 startedAt */,
            /* uint256 updatedAt */,
            /* uint80 answeredInRound */
        ) = usdcPriceFeed.latestRoundData();
        
        if (price <= 0) revert InvalidChainlinkPrice();
        
        return uint256(price) * 1e22; // 1e22 = 10^(30-8)
    }

    function _applySpread(
        address _token,
        uint256 _basePrice,
        bool _maximise
    ) private view returns (uint256) {
        uint256 spread = spreadBasisPoints[_token];
        
        if (spread == 0) {
            return _basePrice;
        }
        
        if (_maximise) {
            return _basePrice * (BASIS_POINTS_DIVISOR + spread) / BASIS_POINTS_DIVISOR;
        } else {
            return _basePrice * (BASIS_POINTS_DIVISOR - spread) / BASIS_POINTS_DIVISOR;
        }
    }
    
    function _validatePriceChange(address _token, uint256 _newPrice) private view {
        uint256 oldPrice = lastPrice[_token];
        
        if (oldPrice == 0) {
            return;
        }
        
        uint256 priceDiff = _newPrice > oldPrice ? _newPrice - oldPrice : oldPrice - _newPrice;
        uint256 maxDiff = oldPrice * maxPriceChangeBps / BASIS_POINTS_DIVISOR;
        
        if (priceDiff > maxDiff) revert PriceChangeTooLarge();
    }
    
    function getPriceInfo(address _token) external view returns (
        uint256 currentPrice,
        uint256 cachedPrice,
        uint256 maxPrice,
        uint256 minPrice,
        uint256 spread
    ) {
        if (_token == usdcAddress) {
            uint256 usdcPrice = _getUSDCPrice();
            currentPrice = usdcPrice;
            cachedPrice = usdcPrice;
            maxPrice = usdcPrice;
            minPrice = usdcPrice;
            spread = 0;
        } else {
            currentPrice = _getRawPrice(_token);
            cachedPrice = lastPrice[_token];
            spread = spreadBasisPoints[_token];
            maxPrice = _applySpread(_token, currentPrice, true);
            minPrice = _applySpread(_token, currentPrice, false);
        }
    }

    function getMaxPrice(address _token) external view returns (uint256) {
        if (_token == usdcAddress) {
            return _getUSDCPrice();
        }
        uint256 basePrice = _getRawPrice(_token);
        _validatePriceChange(_token, basePrice);
        return _applySpread(_token, basePrice, true);
    }
    
    function getMinPrice(address _token) external view returns (uint256) {
        if (_token == usdcAddress) {
            return _getUSDCPrice();
        }
        uint256 basePrice = _getRawPrice(_token);
        _validatePriceChange(_token, basePrice);
        return _applySpread(_token, basePrice, false);
    }
    
    uint256[50] private __gap;
}