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

import {IERC3156FlashBorrower} from "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import {IERC3156FlashLender} from "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
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 maintained proof vault with virtual batch settlement.
/// @dev Use only for accounting proofs: it compresses identical maintained loops into one net settlement.
contract DBISEngineXVirtualBatchVault is IERC3156FlashLender, Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    bytes32 private constant _FLASH_RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");
    uint256 public constant MAX_FLASH_FEE_BPS = 1_000;

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

    uint256 public poolCwusdcReserve;
    uint256 public poolUsdcReserve;
    uint256 public lenderUsdcAvailable;
    uint256 public totalNeutralizedCwusdc;
    uint256 public totalVirtualLoops;
    uint256 public totalVirtualDebtUsdc;
    uint256 public totalVirtualCwusdcIn;
    uint256 public totalFlashFeesCollectedUsdc;
    uint256 public flashFeeBps = 5;
    uint256 public maxFlashLoanAmount;
    bool public paused;
    bool public flashBorrowerAllowlistEnabled;
    mapping(bytes32 => bool) public usedProofIds;
    mapping(address => bool) public approvedFlashBorrower;

    uint256 public immutable xautUsdPrice6;
    uint256 public immutable ltvBps;
    uint256 public immutable maxRoundTripLossBps;

    address public surplusReceiver;

    event PoolSeeded(uint256 cwusdcAmount, uint256 usdcAmount);
    event LenderFunded(uint256 usdcAmount);
    event SurplusReceiverUpdated(address indexed receiver);
    event VirtualProofClosed(
        bytes32 indexed proofId,
        uint256 virtualLoops,
        uint256 debtUsdcPerLoop,
        uint256 collateralXaut,
        uint256 cwusdcInPerLoop,
        uint256 cwusdcOutPerLoop,
        uint256 cwusdcLossPerLoop,
        uint256 totalCwusdcInAmount,
        uint256 totalCwusdcOutAmount,
        uint256 totalNeutralizedCwusdcAmount,
        uint256 poolCwusdcReserveAfter,
        uint256 poolUsdcReserveAfter
    );
    event VirtualProofAuditEvidence(
        bytes32 indexed proofId,
        address indexed operator,
        address indexed outputRecipient,
        uint256 exactOutputAmount,
        uint256 outputRoundingAmount,
        address roundingReceiver,
        bytes32 iso20022DocumentHash,
        bytes32 auditEnvelopeHash,
        bytes32 pegProofHash
    );
    event PoolLiquidityWithdrawn(address indexed to, uint256 cwusdcAmount, uint256 usdcAmount);
    event LenderUsdcWithdrawn(address indexed to, uint256 amount);
    event UnaccountedTokenWithdrawn(address indexed token, address indexed to, uint256 amount);
    event FlashFeeBpsUpdated(uint256 feeBps);
    event MaxFlashLoanAmountUpdated(uint256 amount);
    event Paused(address indexed operator);
    event Unpaused(address indexed operator);
    event FlashBorrowerAllowlistEnabledUpdated(bool enabled);
    event FlashBorrowerApprovalUpdated(address indexed borrower, bool approved);
    event EngineXFlashLoan(
        address indexed initiator,
        IERC3156FlashBorrower indexed receiver,
        address indexed token,
        uint256 amount,
        uint256 fee
    );

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

    constructor(
        address cWUSDC_,
        address usdc_,
        address xaut_,
        address owner_,
        address surplusReceiver_,
        uint256 xautUsdPrice6_,
        uint256 ltvBps_,
        uint256 maxRoundTripLossBps_
    ) Ownable(owner_) {
        require(cWUSDC_ != address(0) && usdc_ != address(0) && xaut_ != address(0), "zero token");
        require(owner_ != address(0), "zero owner");
        require(surplusReceiver_ != address(0), "zero receiver");
        require(xautUsdPrice6_ > 0, "zero price");
        require(ltvBps_ > 0 && ltvBps_ < 10_000, "bad ltv");
        require(maxRoundTripLossBps_ > 0 && maxRoundTripLossBps_ < 10_000, "bad loss");
        cWUSDC = IERC20(cWUSDC_);
        usdc = IERC20(usdc_);
        xaut = IERC20(xaut_);
        surplusReceiver = surplusReceiver_;
        xautUsdPrice6 = xautUsdPrice6_;
        ltvBps = ltvBps_;
        maxRoundTripLossBps = maxRoundTripLossBps_;
    }

    function seedPool(uint256 cwusdcAmount, uint256 usdcAmount) external onlyOwner nonReentrant whenNotPaused {
        require(cwusdcAmount > 0 && usdcAmount > 0, "zero seed");
        cWUSDC.safeTransferFrom(msg.sender, address(this), cwusdcAmount);
        usdc.safeTransferFrom(msg.sender, address(this), usdcAmount);
        poolCwusdcReserve += cwusdcAmount;
        poolUsdcReserve += usdcAmount;
        emit PoolSeeded(cwusdcAmount, usdcAmount);
    }

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

    function setSurplusReceiver(address receiver) external onlyOwner {
        require(receiver != address(0), "zero receiver");
        surplusReceiver = receiver;
        emit SurplusReceiverUpdated(receiver);
    }

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

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

    function setFlashFeeBps(uint256 newFlashFeeBps) external onlyOwner {
        require(newFlashFeeBps <= MAX_FLASH_FEE_BPS, "fee too high");
        flashFeeBps = newFlashFeeBps;
        emit FlashFeeBpsUpdated(newFlashFeeBps);
    }

    function setMaxFlashLoanAmount(uint256 amount) external onlyOwner {
        maxFlashLoanAmount = amount;
        emit MaxFlashLoanAmountUpdated(amount);
    }

    function setFlashBorrowerAllowlistEnabled(bool enabled) external onlyOwner {
        flashBorrowerAllowlistEnabled = enabled;
        emit FlashBorrowerAllowlistEnabledUpdated(enabled);
    }

    function setFlashBorrowerApproved(address borrower, bool approved) external onlyOwner {
        require(borrower != address(0), "zero borrower");
        approvedFlashBorrower[borrower] = approved;
        emit FlashBorrowerApprovalUpdated(borrower, approved);
    }

    function previewCwusdcInForExactUsdc(uint256 usdcOut) public view returns (uint256) {
        return _getAmountIn(usdcOut, poolCwusdcReserve, poolUsdcReserve);
    }

    function previewCwusdcOutForExactUsdcIn(uint256 usdcIn) public view returns (uint256) {
        return _getAmountOut(usdcIn, poolUsdcReserve, poolCwusdcReserve);
    }

    function currentSurplusCwusdc() public view returns (uint256) {
        return poolCwusdcReserve > poolUsdcReserve ? poolCwusdcReserve - poolUsdcReserve : 0;
    }

    function maxFlashLoan(address token) external view override returns (uint256) {
        if (paused || token != address(usdc)) {
            return 0;
        }
        if (maxFlashLoanAmount == 0 || maxFlashLoanAmount > lenderUsdcAvailable) {
            return lenderUsdcAvailable;
        }
        return maxFlashLoanAmount;
    }

    function flashFee(address token, uint256 amount) public view override returns (uint256) {
        require(token == address(usdc), "unsupported flash token");
        return (amount * flashFeeBps) / 10_000;
    }

    function minimumXautCollateral(uint256 debtUsdc) public view returns (uint256) {
        uint256 numerator = debtUsdc * 1e6 * 10_000;
        uint256 denominator = xautUsdPrice6 * ltvBps;
        return (numerator + denominator - 1) / denominator;
    }

    function previewVirtualProof(uint256 debtUsdcPerLoop, uint256 virtualLoops)
        public
        view
        returns (
            uint256 collateralXaut,
            uint256 cwusdcInPerLoop,
            uint256 cwusdcOutPerLoop,
            uint256 cwusdcLossPerLoop,
            uint256 totalCwusdcInAmount,
            uint256 totalCwusdcOutAmount,
            uint256 totalNeutralizedCwusdcAmount
        )
    {
        require(virtualLoops > 0, "zero loops");
        require(poolCwusdcReserve == poolUsdcReserve, "pool not maintained");
        require(lenderUsdcAvailable >= debtUsdcPerLoop, "insufficient lender usdc");
        collateralXaut = minimumXautCollateral(debtUsdcPerLoop);
        cwusdcInPerLoop = _getAmountIn(debtUsdcPerLoop, poolCwusdcReserve, poolUsdcReserve);

        uint256 cwusdcReserveAfterIn = poolCwusdcReserve + cwusdcInPerLoop;
        uint256 usdcReserveAfterOut = poolUsdcReserve - debtUsdcPerLoop;
        cwusdcOutPerLoop = _getAmountOut(debtUsdcPerLoop, usdcReserveAfterOut, cwusdcReserveAfterIn);
        cwusdcLossPerLoop = cwusdcInPerLoop - cwusdcOutPerLoop;
        require(cwusdcLossPerLoop * 10_000 <= cwusdcInPerLoop * maxRoundTripLossBps, "roundtrip loss too high");

        totalCwusdcInAmount = cwusdcInPerLoop * virtualLoops;
        totalCwusdcOutAmount = cwusdcOutPerLoop * virtualLoops;
        totalNeutralizedCwusdcAmount = cwusdcLossPerLoop * virtualLoops;
    }

    function runVirtualProof(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops)
        external
        nonReentrant
        whenNotPaused
    {
        _runVirtualProofFor(
            msg.sender, proofId, debtUsdcPerLoop, virtualLoops, 0, msg.sender, bytes32(0), bytes32(0), bytes32(0)
        );
    }

    function runVirtualProofTo(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 virtualLoops, address outputRecipient)
        external
        nonReentrant
        whenNotPaused
    {
        require(outputRecipient != address(0), "zero output");
        _runVirtualProofFor(
            outputRecipient, proofId, debtUsdcPerLoop, virtualLoops, 0, msg.sender, bytes32(0), bytes32(0), bytes32(0)
        );
    }

    function runVirtualProofExactOutTo(
        bytes32 proofId,
        uint256 debtUsdcPerLoop,
        uint256 virtualLoops,
        address outputRecipient,
        uint256 exactOutputAmount,
        address roundingReceiver,
        bytes32 iso20022DocumentHash,
        bytes32 auditEnvelopeHash,
        bytes32 pegProofHash
    ) external nonReentrant whenNotPaused {
        require(outputRecipient != address(0), "zero output");
        require(exactOutputAmount > 0, "zero exact output");
        require(roundingReceiver != address(0), "zero rounding");
        require(iso20022DocumentHash != bytes32(0), "zero iso hash");
        require(auditEnvelopeHash != bytes32(0), "zero audit hash");
        require(pegProofHash != bytes32(0), "zero peg hash");
        _runVirtualProofFor(
            outputRecipient,
            proofId,
            debtUsdcPerLoop,
            virtualLoops,
            exactOutputAmount,
            roundingReceiver,
            iso20022DocumentHash,
            auditEnvelopeHash,
            pegProofHash
        );
    }

    function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
        external
        override
        nonReentrant
        whenNotPaused
        returns (bool)
    {
        require(token == address(usdc), "unsupported flash token");
        require(amount > 0, "zero flash amount");
        require(amount <= lenderUsdcAvailable, "insufficient lender usdc");
        require(maxFlashLoanAmount == 0 || amount <= maxFlashLoanAmount, "flash amount too high");
        require(
            !flashBorrowerAllowlistEnabled || approvedFlashBorrower[address(receiver)], "flash borrower not approved"
        );

        uint256 fee = flashFee(token, amount);
        uint256 balanceBefore = usdc.balanceOf(address(this));
        require(balanceBefore >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized");

        lenderUsdcAvailable -= amount;
        usdc.safeTransfer(address(receiver), amount);

        bytes32 retval = receiver.onFlashLoan(msg.sender, token, amount, fee, data);
        require(retval == _FLASH_RETURN_VALUE, "invalid flash callback");

        uint256 expectedBalance = balanceBefore + fee;
        uint256 balanceAfterCallback = usdc.balanceOf(address(this));
        if (balanceAfterCallback < expectedBalance) {
            usdc.safeTransferFrom(address(receiver), address(this), expectedBalance - balanceAfterCallback);
        }
        require(usdc.balanceOf(address(this)) >= expectedBalance, "flash repayment failed");

        lenderUsdcAvailable += amount + fee;
        totalFlashFeesCollectedUsdc += fee;
        require(
            usdc.balanceOf(address(this)) >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized"
        );

        emit EngineXFlashLoan(msg.sender, receiver, token, amount, fee);
        return true;
    }

    function _runVirtualProofFor(
        address outputRecipient,
        bytes32 proofId,
        uint256 debtUsdcPerLoop,
        uint256 virtualLoops,
        uint256 exactOutputAmount,
        address roundingReceiver,
        bytes32 iso20022DocumentHash,
        bytes32 auditEnvelopeHash,
        bytes32 pegProofHash
    ) internal {
        require(proofId != bytes32(0), "zero proof");
        require(!usedProofIds[proofId], "proof used");
        usedProofIds[proofId] = true;
        (
            uint256 collateralXaut,
            uint256 cwusdcInPerLoop,
            uint256 cwusdcOutPerLoop,
            uint256 cwusdcLossPerLoop,
            uint256 totalCwusdcInAmount,
            uint256 totalCwusdcOutAmount,
            uint256 totalNeutralizedCwusdcAmount
        ) = previewVirtualProof(debtUsdcPerLoop, virtualLoops);
        require(exactOutputAmount <= totalCwusdcOutAmount, "exact output too high");

        uint256 outputAmount = exactOutputAmount == 0 ? totalCwusdcOutAmount : exactOutputAmount;
        uint256 outputRoundingAmount = totalCwusdcOutAmount - outputAmount;

        xaut.safeTransferFrom(msg.sender, address(this), collateralXaut);
        cWUSDC.safeTransferFrom(msg.sender, address(this), totalCwusdcInAmount);
        cWUSDC.safeTransfer(outputRecipient, outputAmount);
        if (outputRoundingAmount > 0) {
            cWUSDC.safeTransfer(roundingReceiver, outputRoundingAmount);
        }
        cWUSDC.safeTransfer(surplusReceiver, totalNeutralizedCwusdcAmount);
        xaut.safeTransfer(msg.sender, collateralXaut);

        totalNeutralizedCwusdc += totalNeutralizedCwusdcAmount;
        totalVirtualLoops += virtualLoops;
        totalVirtualDebtUsdc += debtUsdcPerLoop * virtualLoops;
        totalVirtualCwusdcIn += totalCwusdcInAmount;

        emit VirtualProofClosed(
            proofId,
            virtualLoops,
            debtUsdcPerLoop,
            collateralXaut,
            cwusdcInPerLoop,
            cwusdcOutPerLoop,
            cwusdcLossPerLoop,
            totalCwusdcInAmount,
            totalCwusdcOutAmount,
            totalNeutralizedCwusdcAmount,
            poolCwusdcReserve,
            poolUsdcReserve
        );
        if (iso20022DocumentHash != bytes32(0) || auditEnvelopeHash != bytes32(0) || pegProofHash != bytes32(0)) {
            emit VirtualProofAuditEvidence(
                proofId,
                msg.sender,
                outputRecipient,
                exactOutputAmount,
                outputRoundingAmount,
                roundingReceiver,
                iso20022DocumentHash,
                auditEnvelopeHash,
                pegProofHash
            );
        }
    }

    function withdrawPoolLiquidity(address to, uint256 cwusdcAmount, uint256 usdcAmount)
        external
        onlyOwner
        nonReentrant
    {
        require(to != address(0), "zero to");
        require(cwusdcAmount > 0 || usdcAmount > 0, "zero withdraw");
        require(cwusdcAmount <= poolCwusdcReserve, "insufficient pool cwusdc");
        require(usdcAmount <= poolUsdcReserve, "insufficient pool usdc");

        uint256 nextCwusdcReserve = poolCwusdcReserve - cwusdcAmount;
        uint256 nextUsdcReserve = poolUsdcReserve - usdcAmount;
        require(nextCwusdcReserve == nextUsdcReserve, "would break maintained pool");

        poolCwusdcReserve = nextCwusdcReserve;
        poolUsdcReserve = nextUsdcReserve;

        if (cwusdcAmount > 0) {
            cWUSDC.safeTransfer(to, cwusdcAmount);
        }
        if (usdcAmount > 0) {
            usdc.safeTransfer(to, usdcAmount);
        }

        emit PoolLiquidityWithdrawn(to, cwusdcAmount, usdcAmount);
    }

    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);
    }

    /// @notice Rescue or migrate only tokens that are not backing Engine X pool/lender accounting.
    function withdraw(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 _requireAccountingCollateralized() internal view {
        require(cWUSDC.balanceOf(address(this)) >= poolCwusdcReserve, "accounting undercollateralized");
        require(
            usdc.balanceOf(address(this)) >= poolUsdcReserve + lenderUsdcAvailable, "accounting undercollateralized"
        );
    }

    function _getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256) {
        require(amountOut > 0, "insufficient output");
        require(reserveIn > 0 && reserveOut > amountOut, "insufficient liquidity");
        uint256 numerator = reserveIn * amountOut * 1000;
        uint256 denominator = (reserveOut - amountOut) * 997;
        return (numerator / denominator) + 1;
    }

    function _getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256) {
        require(amountIn > 0, "insufficient input");
        require(reserveIn > 0 && reserveOut > 0, "insufficient liquidity");
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        return numerator / denominator;
    }
}
