// 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";

/// @notice Mainnet proof-of-concept vault for the Engine X XAU/USD secured loop.
/// @dev This is intentionally small and deterministic: it is not a production lender.
contract DBISEngineXProofVault is Ownable {
    using SafeERC20 for IERC20;

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

    uint256 public poolCwusdcReserve;
    uint256 public poolUsdcReserve;
    uint256 public lenderUsdcAvailable;

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

    event PoolSeeded(uint256 cwusdcAmount, uint256 usdcAmount);
    event LenderFunded(uint256 usdcAmount);
    event ProofStep(
        bytes32 indexed proofId,
        uint256 indexed step,
        uint256 debtUsdc,
        uint256 collateralXaut,
        uint256 cwusdcIn,
        uint256 cwusdcOut,
        uint256 cwusdcLoss,
        uint256 poolCwusdcReserveAfter,
        uint256 poolUsdcReserveAfter
    );
    event ProofClosed(bytes32 indexed proofId, uint256 loops, uint256 totalDebtUsdc, uint256 totalCwusdcIn);
    event OwnerWithdraw(address indexed token, address indexed to, uint256 amount);

    constructor(
        address cWUSDC_,
        address usdc_,
        address xaut_,
        address owner_,
        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(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_);
        xautUsdPrice6 = xautUsdPrice6_;
        ltvBps = ltvBps_;
        maxRoundTripLossBps = maxRoundTripLossBps_;
    }

    function seedPool(uint256 cwusdcAmount, uint256 usdcAmount) external onlyOwner {
        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 {
        require(usdcAmount > 0, "zero fund");
        usdc.safeTransferFrom(msg.sender, address(this), usdcAmount);
        lenderUsdcAvailable += usdcAmount;
        emit LenderFunded(usdcAmount);
    }

    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 minimumXautCollateral(uint256 debtUsdc) public view returns (uint256) {
        uint256 numerator = debtUsdc * 1e6 * 10_000;
        uint256 denominator = xautUsdPrice6 * ltvBps;
        return (numerator + denominator - 1) / denominator;
    }

    function runProof(bytes32 proofId, uint256 debtUsdcPerLoop, uint256 loops) external {
        require(proofId != bytes32(0), "zero proof");
        require(debtUsdcPerLoop > 0, "zero debt");
        require(loops > 0 && loops <= 32, "bad loops");

        uint256 collateralPerLoop = minimumXautCollateral(debtUsdcPerLoop);
        uint256 totalCollateral = collateralPerLoop * loops;
        uint256 totalDebt;
        uint256 totalCwusdcIn;

        xaut.safeTransferFrom(msg.sender, address(this), totalCollateral);

        for (uint256 i = 0; i < loops; i++) {
            require(lenderUsdcAvailable >= debtUsdcPerLoop, "insufficient lender usdc");

            uint256 poolCwusdcBefore = poolCwusdcReserve;
            uint256 poolUsdcBefore = poolUsdcReserve;
            lenderUsdcAvailable -= debtUsdcPerLoop;
            uint256 cwusdcIn = _getAmountIn(debtUsdcPerLoop, poolCwusdcReserve, poolUsdcReserve);
            cWUSDC.safeTransferFrom(msg.sender, address(this), cwusdcIn);

            poolCwusdcReserve += cwusdcIn;
            poolUsdcReserve -= debtUsdcPerLoop;
            lenderUsdcAvailable += debtUsdcPerLoop;

            uint256 cwusdcOut = _getAmountOut(debtUsdcPerLoop, poolUsdcReserve, poolCwusdcReserve);
            poolUsdcReserve += debtUsdcPerLoop;
            poolCwusdcReserve -= cwusdcOut;
            cWUSDC.safeTransfer(msg.sender, cwusdcOut);

            uint256 cwusdcLoss = cwusdcIn - cwusdcOut;
            require(cwusdcLoss * 10_000 <= cwusdcIn * maxRoundTripLossBps, "roundtrip loss too high");
            require(poolUsdcReserve == poolUsdcBefore, "usdc reserve drift");
            require(poolCwusdcReserve >= poolCwusdcBefore, "cw reserve drift");

            totalDebt += debtUsdcPerLoop;
            totalCwusdcIn += cwusdcIn;

            emit ProofStep(
                proofId,
                i + 1,
                debtUsdcPerLoop,
                collateralPerLoop,
                cwusdcIn,
                cwusdcOut,
                cwusdcLoss,
                poolCwusdcReserve,
                poolUsdcReserve
            );
        }

        xaut.safeTransfer(msg.sender, totalCollateral);
        emit ProofClosed(proofId, loops, totalDebt, totalCwusdcIn);
    }

    function runProofBatch(bytes32[] calldata proofIds, uint256[] calldata debtsUsdcPerLoop, uint256[] calldata loops_)
        external
    {
        require(proofIds.length == debtsUsdcPerLoop.length && proofIds.length == loops_.length, "length mismatch");
        for (uint256 i = 0; i < proofIds.length; i++) {
            _runProofFor(msg.sender, proofIds[i], debtsUsdcPerLoop[i], loops_[i]);
        }
    }

    function _runProofFor(address account, bytes32 proofId, uint256 debtUsdcPerLoop, uint256 loops) internal {
        require(proofId != bytes32(0), "zero proof");
        require(debtUsdcPerLoop > 0, "zero debt");
        require(loops > 0 && loops <= 32, "bad loops");

        uint256 collateralPerLoop = minimumXautCollateral(debtUsdcPerLoop);
        uint256 totalCollateral = collateralPerLoop * loops;
        uint256 totalDebt;
        uint256 totalCwusdcIn;

        xaut.safeTransferFrom(account, address(this), totalCollateral);

        for (uint256 i = 0; i < loops; i++) {
            require(lenderUsdcAvailable >= debtUsdcPerLoop, "insufficient lender usdc");

            uint256 poolCwusdcBefore = poolCwusdcReserve;
            uint256 poolUsdcBefore = poolUsdcReserve;
            lenderUsdcAvailable -= debtUsdcPerLoop;
            uint256 cwusdcIn = _getAmountIn(debtUsdcPerLoop, poolCwusdcReserve, poolUsdcReserve);
            cWUSDC.safeTransferFrom(account, address(this), cwusdcIn);

            poolCwusdcReserve += cwusdcIn;
            poolUsdcReserve -= debtUsdcPerLoop;
            lenderUsdcAvailable += debtUsdcPerLoop;

            uint256 cwusdcOut = _getAmountOut(debtUsdcPerLoop, poolUsdcReserve, poolCwusdcReserve);
            poolUsdcReserve += debtUsdcPerLoop;
            poolCwusdcReserve -= cwusdcOut;
            cWUSDC.safeTransfer(account, cwusdcOut);

            uint256 cwusdcLoss = cwusdcIn - cwusdcOut;
            require(cwusdcLoss * 10_000 <= cwusdcIn * maxRoundTripLossBps, "roundtrip loss too high");
            require(poolUsdcReserve == poolUsdcBefore, "usdc reserve drift");
            require(poolCwusdcReserve >= poolCwusdcBefore, "cw reserve drift");

            totalDebt += debtUsdcPerLoop;
            totalCwusdcIn += cwusdcIn;

            emit ProofStep(
                proofId,
                i + 1,
                debtUsdcPerLoop,
                collateralPerLoop,
                cwusdcIn,
                cwusdcOut,
                cwusdcLoss,
                poolCwusdcReserve,
                poolUsdcReserve
            );
        }

        xaut.safeTransfer(account, totalCollateral);
        emit ProofClosed(proofId, loops, totalDebt, totalCwusdcIn);
    }

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

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