// 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 {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/// @notice Engine X XAUt-backed USDC borrowing vault.
/// @dev This vault lends only pre-funded USDC. cWUSDC can be referenced as proof
///      provenance, but debt is always repaid in official USDC.
contract DBISEngineXXautUsdcBorrowVault is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 public constant BPS = 10_000;
    uint256 public constant MAX_LTV_BPS = 9_500;
    uint256 public constant MAX_LIQUIDATION_THRESHOLD_BPS = 9_800;
    uint256 public constant MAX_LIQUIDATION_BONUS_BPS = 2_000;

    IERC20 public immutable xaut;
    IERC20 public immutable usdc;
    IERC20 public immutable cWUSDC;

    uint256 public xautUsdPrice6;
    uint256 public lastPriceUpdate;
    bytes32 public lastPriceSourceHash;

    uint256 public ltvBps;
    uint256 public liquidationThresholdBps;
    uint256 public minHealthFactorBps;
    uint256 public liquidationBonusBps;
    uint256 public maxBorrowUsdc;
    bool public paused;

    uint256 public lenderUsdcAvailable;
    uint256 public totalCollateralXaut;
    uint256 public totalDebtUsdc;
    uint256 public totalCwusdcProofRepayUsdc;

    struct Position {
        uint256 collateralXaut;
        uint256 debtUsdc;
    }

    mapping(address => Position) public positions;

    event PriceUpdated(uint256 price6, bytes32 indexed sourceHash);
    event RiskParamsUpdated(
        uint256 ltvBps,
        uint256 liquidationThresholdBps,
        uint256 minHealthFactorBps,
        uint256 liquidationBonusBps,
        uint256 maxBorrowUsdc
    );
    event Paused(address indexed operator);
    event Unpaused(address indexed operator);
    event LenderFunded(address indexed funder, uint256 amount);
    event LenderUsdcWithdrawn(address indexed to, uint256 amount);
    event CollateralSupplied(address indexed account, uint256 amount);
    event CollateralWithdrawn(address indexed account, address indexed to, uint256 amount);
    event UsdcBorrowed(address indexed account, address indexed to, uint256 amount, uint256 debtAfter);
    event UsdcRepaid(address indexed account, address indexed payer, uint256 amount, uint256 debtAfter);
    event CwusdcSourcedRepay(
        address indexed account,
        address indexed payer,
        uint256 amount,
        bytes32 indexed publicSwapTxHash,
        bytes32 iso20022DocumentHash,
        bytes32 auditEnvelopeHash,
        bytes32 pegProofHash
    );
    event Liquidated(
        address indexed account,
        address indexed liquidator,
        uint256 repayUsdc,
        uint256 seizedXaut,
        uint256 debtAfter
    );
    event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount);

    modifier whenNotPaused() {
        require(!paused, "paused");
        _;
    }

    constructor(
        address xaut_,
        address usdc_,
        address cWUSDC_,
        address owner_,
        uint256 xautUsdPrice6_,
        uint256 ltvBps_,
        uint256 liquidationThresholdBps_,
        uint256 minHealthFactorBps_,
        uint256 liquidationBonusBps_,
        uint256 maxBorrowUsdc_,
        bytes32 priceSourceHash_
    ) Ownable(owner_) {
        require(xaut_ != address(0) && usdc_ != address(0) && cWUSDC_ != address(0), "zero token");
        require(owner_ != address(0), "zero owner");
        xaut = IERC20(xaut_);
        usdc = IERC20(usdc_);
        cWUSDC = IERC20(cWUSDC_);
        _setPrice(xautUsdPrice6_, priceSourceHash_);
        _setRiskParams(
            ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_
        );
    }

    function pause() external onlyOwner {
        paused = true;
        emit Paused(msg.sender);
    }

    function unpause() external onlyOwner {
        paused = false;
        emit Unpaused(msg.sender);
    }

    function setXautUsdPrice6(uint256 price6, bytes32 sourceHash) external onlyOwner {
        _setPrice(price6, sourceHash);
    }

    function setRiskParams(
        uint256 ltvBps_,
        uint256 liquidationThresholdBps_,
        uint256 minHealthFactorBps_,
        uint256 liquidationBonusBps_,
        uint256 maxBorrowUsdc_
    ) external onlyOwner {
        _setRiskParams(
            ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_
        );
    }

    function fundLender(uint256 amount) external nonReentrant whenNotPaused {
        require(amount > 0, "zero fund");
        usdc.safeTransferFrom(msg.sender, address(this), amount);
        lenderUsdcAvailable += amount;
        emit LenderFunded(msg.sender, amount);
    }

    function withdrawLenderUsdc(address to, uint256 amount) external onlyOwner nonReentrant {
        require(to != address(0), "zero to");
        require(amount > 0, "zero withdraw");
        require(amount <= lenderUsdcAvailable, "insufficient lender usdc");
        lenderUsdcAvailable -= amount;
        usdc.safeTransfer(to, amount);
        emit LenderUsdcWithdrawn(to, amount);
    }

    function supplyCollateral(uint256 amount) external nonReentrant whenNotPaused {
        require(amount > 0, "zero collateral");
        xaut.safeTransferFrom(msg.sender, address(this), amount);
        positions[msg.sender].collateralXaut += amount;
        totalCollateralXaut += amount;
        emit CollateralSupplied(msg.sender, amount);
    }

    function withdrawCollateral(uint256 amount, address to) external nonReentrant whenNotPaused {
        require(to != address(0), "zero to");
        require(amount > 0, "zero collateral");
        Position storage position = positions[msg.sender];
        require(amount <= position.collateralXaut, "insufficient collateral");
        position.collateralXaut -= amount;
        totalCollateralXaut -= amount;
        _requireHealthy(msg.sender);
        xaut.safeTransfer(to, amount);
        emit CollateralWithdrawn(msg.sender, to, amount);
    }

    function borrowUsdc(uint256 amount, address to) external nonReentrant whenNotPaused {
        require(to != address(0), "zero to");
        require(amount > 0, "zero borrow");
        require(amount <= lenderUsdcAvailable, "insufficient lender usdc");
        if (maxBorrowUsdc != 0) {
            require(totalDebtUsdc + amount <= maxBorrowUsdc, "max borrow exceeded");
        }

        Position storage position = positions[msg.sender];
        position.debtUsdc += amount;
        totalDebtUsdc += amount;
        lenderUsdcAvailable -= amount;
        _requireHealthy(msg.sender);

        usdc.safeTransfer(to, amount);
        emit UsdcBorrowed(msg.sender, to, amount, position.debtUsdc);
    }

    function repayUsdc(uint256 amount) external nonReentrant whenNotPaused returns (uint256 repaid) {
        repaid = _repay(msg.sender, msg.sender, amount);
    }

    function repayUsdcFor(address account, uint256 amount) external nonReentrant whenNotPaused returns (uint256 repaid) {
        require(account != address(0), "zero account");
        repaid = _repay(account, msg.sender, amount);
    }

    function repayUsdcFromCwusdcProof(
        uint256 amount,
        bytes32 publicSwapTxHash,
        bytes32 iso20022DocumentHash,
        bytes32 auditEnvelopeHash,
        bytes32 pegProofHash
    ) external nonReentrant whenNotPaused returns (uint256 repaid) {
        require(publicSwapTxHash != bytes32(0), "zero swap hash");
        require(iso20022DocumentHash != bytes32(0), "zero iso hash");
        require(auditEnvelopeHash != bytes32(0), "zero audit hash");
        require(pegProofHash != bytes32(0), "zero peg hash");

        repaid = _repay(msg.sender, msg.sender, amount);
        totalCwusdcProofRepayUsdc += repaid;
        emit CwusdcSourcedRepay(
            msg.sender, msg.sender, repaid, publicSwapTxHash, iso20022DocumentHash, auditEnvelopeHash, pegProofHash
        );
    }

    function liquidate(address account, uint256 repayAmount) external nonReentrant whenNotPaused returns (uint256 seized) {
        require(account != address(0), "zero account");
        require(repayAmount > 0, "zero repay");
        require(healthFactorBps(account) < minHealthFactorBps, "position healthy");

        Position storage position = positions[account];
        uint256 repaid = repayAmount > position.debtUsdc ? position.debtUsdc : repayAmount;
        require(repaid > 0, "zero debt");
        seized = _xautForUsdcWithBonus(repaid);
        require(seized <= position.collateralXaut, "insufficient collateral");

        usdc.safeTransferFrom(msg.sender, address(this), repaid);
        position.debtUsdc -= repaid;
        totalDebtUsdc -= repaid;
        lenderUsdcAvailable += repaid;
        position.collateralXaut -= seized;
        totalCollateralXaut -= seized;
        xaut.safeTransfer(msg.sender, seized);

        emit Liquidated(account, msg.sender, repaid, seized, position.debtUsdc);
    }

    function rescueUnaccountedToken(address token, address to, uint256 amount) external onlyOwner nonReentrant {
        require(to != address(0), "zero to");
        require(amount > 0, "zero withdraw");
        IERC20(token).safeTransfer(to, amount);
        _requireAccountingCollateralized();
        emit UnaccountedTokenWithdrawn(token, to, amount);
    }

    function collateralValueUsd6(address account) public view returns (uint256) {
        return collateralValueUsd6ForRaw(positions[account].collateralXaut);
    }

    function collateralValueUsd6ForRaw(uint256 xautRaw) public view returns (uint256) {
        return (xautRaw * xautUsdPrice6) / 1e6;
    }

    function maxDebtForCollateral(uint256 xautRaw) public view returns (uint256) {
        uint256 collateralUsd6 = collateralValueUsd6ForRaw(xautRaw);
        uint256 byLtv = (collateralUsd6 * ltvBps) / BPS;
        uint256 byHealth = (collateralUsd6 * liquidationThresholdBps) / minHealthFactorBps;
        return byLtv < byHealth ? byLtv : byHealth;
    }

    function maxAdditionalBorrow(address account) public view returns (uint256) {
        uint256 maxDebt = maxDebtForCollateral(positions[account].collateralXaut);
        if (positions[account].debtUsdc >= maxDebt) {
            return 0;
        }
        return maxDebt - positions[account].debtUsdc;
    }

    function healthFactorBps(address account) public view returns (uint256) {
        Position memory position = positions[account];
        if (position.debtUsdc == 0) {
            return type(uint256).max;
        }
        uint256 collateralUsd6 = collateralValueUsd6ForRaw(position.collateralXaut);
        return (collateralUsd6 * liquidationThresholdBps) / position.debtUsdc;
    }

    function _repay(address account, address payer, uint256 amount) internal returns (uint256 repaid) {
        require(amount > 0, "zero repay");
        Position storage position = positions[account];
        require(position.debtUsdc > 0, "zero debt");
        repaid = amount > position.debtUsdc ? position.debtUsdc : amount;
        usdc.safeTransferFrom(payer, address(this), repaid);
        position.debtUsdc -= repaid;
        totalDebtUsdc -= repaid;
        lenderUsdcAvailable += repaid;
        emit UsdcRepaid(account, payer, repaid, position.debtUsdc);
    }

    function _requireHealthy(address account) internal view {
        Position memory position = positions[account];
        if (position.debtUsdc == 0) {
            return;
        }
        require(position.debtUsdc <= maxDebtForCollateral(position.collateralXaut), "exceeds collateral");
        require(healthFactorBps(account) >= minHealthFactorBps, "health too low");
    }

    function _xautForUsdcWithBonus(uint256 usdcAmount) internal view returns (uint256) {
        uint256 numerator = usdcAmount * 1e6 * (BPS + liquidationBonusBps);
        uint256 denominator = xautUsdPrice6 * BPS;
        return (numerator + denominator - 1) / denominator;
    }

    function _setPrice(uint256 price6, bytes32 sourceHash) internal {
        require(price6 > 0, "zero price");
        require(sourceHash != bytes32(0), "zero source");
        xautUsdPrice6 = price6;
        lastPriceUpdate = block.timestamp;
        lastPriceSourceHash = sourceHash;
        emit PriceUpdated(price6, sourceHash);
    }

    function _setRiskParams(
        uint256 ltvBps_,
        uint256 liquidationThresholdBps_,
        uint256 minHealthFactorBps_,
        uint256 liquidationBonusBps_,
        uint256 maxBorrowUsdc_
    ) internal {
        require(ltvBps_ > 0 && ltvBps_ <= MAX_LTV_BPS, "bad ltv");
        require(
            liquidationThresholdBps_ >= ltvBps_ && liquidationThresholdBps_ <= MAX_LIQUIDATION_THRESHOLD_BPS,
            "bad threshold"
        );
        require(minHealthFactorBps_ >= BPS, "bad health");
        require(liquidationBonusBps_ <= MAX_LIQUIDATION_BONUS_BPS, "bad bonus");
        ltvBps = ltvBps_;
        liquidationThresholdBps = liquidationThresholdBps_;
        minHealthFactorBps = minHealthFactorBps_;
        liquidationBonusBps = liquidationBonusBps_;
        maxBorrowUsdc = maxBorrowUsdc_;
        emit RiskParamsUpdated(
            ltvBps_, liquidationThresholdBps_, minHealthFactorBps_, liquidationBonusBps_, maxBorrowUsdc_
        );
    }

    function _requireAccountingCollateralized() internal view {
        require(xaut.balanceOf(address(this)) >= totalCollateralXaut, "xaut undercollateralized");
        require(usdc.balanceOf(address(this)) >= lenderUsdcAvailable, "usdc undercollateralized");
    }
}
