// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IPriceOracle {
    function getPrice(address asset) external view returns (uint256);  // 1e18-normalised USD price
}

/**
 * @title RWATransferAdapter — permissioned RWA → cash conversion for
 *                              liquidators who can't legally hold BUIDL/USDY/OUSG
 * @notice The problem:
 *           - BUIDL is a Securitize-permissioned token. Holders MUST be on
 *             Securitize allowlist.
 *           - On AERE, BUIDL.e (Hyperlane synthetic) is the unrestricted
 *             representation, but to redeem on Ethereum the bridged receiver
 *             must be allowlisted.
 *           - A liquidator who wins BUIDL.e collateral via AereLendingMarket
 *             liquidation can hold it but cannot redeem → effectively stuck
 *             with an illiquid asset.
 *
 *         The fix:
 *           - This adapter accepts BUIDL.e (or any registered permissioned
 *             RWA) and pays out PAYOUT_TOKEN (USDC.e) at oracle price minus
 *             a fixed haircut.
 *           - The Foundation Seychelles IBC (which IS allowlisted on
 *             Securitize for BUIDL, completes Ondo institutional KYC for
 *             USDY/OUSG) periodically sweeps the accumulated RWA via
 *             `sweepRWA` and unwinds it through Securitize / Ondo
 *             off-chain. The USDC.e they get back replenishes this adapter.
 *
 *         IMMUTABILITY:
 *           - PAYOUT_TOKEN (USDC.e) and ORACLE are fixed at deploy
 *           - FOUNDATION (Securitize-allowlisted entity) is fixed at deploy
 *           - Per-asset haircutBps + rateLimitPerPeriod are immutable per
 *             registration (Foundation can register new params for a new
 *             asset but cannot change a registered one)
 *
 *         FOUNDATION CAN:
 *           - Register new assets (with haircut + rate limit)
 *           - Pause assets (emergency)
 *           - Sweep accumulated RWA via `sweepRWA(asset, amount, recipient)`
 *
 *         FOUNDATION CANNOT:
 *           - Touch the PAYOUT_TOKEN treasury — the only exit for USDC.e
 *             is `redeem` paying it to a liquidator.
 *           - Change any registered asset's haircut or rate limit
 *           - Withdraw RWA before it has fully accrued via `redeem`
 *             (the sweep only sees what redemptions have deposited)
 */
contract RWATransferAdapter is ReentrancyGuard {

    /* ------------------------------- immutable ------------------------------- */

    address public immutable PAYOUT_TOKEN;     // USDC.e
    address public immutable ORACLE;
    address public immutable FOUNDATION;
    uint8   public immutable PAYOUT_DECIMALS;

    /* --------------------------------- types --------------------------------- */

    struct AssetConfig {
        bool registered;
        bool paused;
        uint16 haircutBps;                 // e.g. 100 = 1% off oracle price (covers bridge fee + delay risk)
        uint256 rateLimitPerPeriod;        // max payout in PAYOUT_TOKEN units per period
        uint32 periodSeconds;              // e.g. 86400 = daily
        uint8 assetDecimals;
    }

    struct AssetState {
        uint256 periodRedeemed;            // sum of payouts paid in current period
        uint64 periodStart;
        uint256 totalAccumulated;          // sum of RWA pulled in (= what sweepRWA can remove)
    }

    /* --------------------------------- state -------------------------------- */

    mapping(address => AssetConfig) public configs;
    mapping(address => AssetState) public states;

    /* --------------------------------- events ------------------------------- */

    event AssetRegistered(address indexed asset, uint16 haircutBps, uint256 rateLimitPerPeriod, uint32 periodSeconds);
    event AssetPausedSet(address indexed asset, bool paused);
    event Redeemed(address indexed user, address indexed asset, uint256 rwaIn, uint256 payoutOut, uint256 priceUsd);
    event Swept(address indexed asset, address indexed recipient, uint256 amount);

    /* --------------------------------- errors ------------------------------- */

    error NotFoundation();
    error AlreadyRegistered();
    error UnknownAsset();
    error AssetIsPaused();
    error RateLimitExceeded(uint256 wouldBe, uint256 cap);
    error InsufficientPayoutLiquidity();
    error InsufficientSweepBalance();
    error TransferFailed();
    error ZeroAddress();
    error ZeroAmount();
    error BadHaircut();

    /* ------------------------------- modifiers ------------------------------- */

    modifier onlyFoundation() {
        if (msg.sender != FOUNDATION) revert NotFoundation();
        _;
    }

    /* ----------------------------- constructor ------------------------------ */

    constructor(address payoutToken, address oracle, address foundation, uint8 payoutDecimals) {
        if (payoutToken == address(0) || oracle == address(0) || foundation == address(0)) revert ZeroAddress();
        PAYOUT_TOKEN = payoutToken;
        ORACLE = oracle;
        FOUNDATION = foundation;
        PAYOUT_DECIMALS = payoutDecimals;
    }

    /* ----------------------------- registration ----------------------------- */

    function registerAsset(
        address asset,
        uint16 haircutBps,
        uint256 rateLimitPerPeriod,
        uint32 periodSeconds,
        uint8 assetDecimals
    ) external onlyFoundation {
        if (asset == address(0)) revert ZeroAddress();
        if (configs[asset].registered) revert AlreadyRegistered();
        if (haircutBps > 5_000) revert BadHaircut();   // sanity cap 50%
        configs[asset] = AssetConfig({
            registered: true,
            paused: false,
            haircutBps: haircutBps,
            rateLimitPerPeriod: rateLimitPerPeriod,
            periodSeconds: periodSeconds,
            assetDecimals: assetDecimals
        });
        states[asset] = AssetState({ periodRedeemed: 0, periodStart: uint64(block.timestamp), totalAccumulated: 0 });
        emit AssetRegistered(asset, haircutBps, rateLimitPerPeriod, periodSeconds);
    }

    function setAssetPaused(address asset, bool paused) external onlyFoundation {
        AssetConfig storage c = configs[asset];
        if (!c.registered) revert UnknownAsset();
        c.paused = paused;
        emit AssetPausedSet(asset, paused);
    }

    /* -------------------------------- redeem -------------------------------- */

    /// @notice Permissionless: caller deposits `rwaAmount` of `asset`, receives
    ///         PAYOUT_TOKEN at oracle price minus haircutBps, subject to the
    ///         per-period rate limit.
    function redeem(address asset, uint256 rwaAmount) external nonReentrant returns (uint256 payoutOut) {
        AssetConfig memory c = configs[asset];
        if (!c.registered) revert UnknownAsset();
        if (c.paused) revert AssetIsPaused();
        if (rwaAmount == 0) revert ZeroAmount();

        uint256 rwaPriceUsd = IPriceOracle(ORACLE).getPrice(asset);   // 1e18 USD
        uint256 payoutPriceUsd = IPriceOracle(ORACLE).getPrice(PAYOUT_TOKEN);

        // value in 18-dec USD
        uint256 valueUsd = _scaleUp(rwaAmount, c.assetDecimals) * rwaPriceUsd / 1e18;
        // haircut
        valueUsd = valueUsd * (10_000 - c.haircutBps) / 10_000;
        // convert to payout token amount
        payoutOut = _scaleDown(valueUsd * 1e18 / payoutPriceUsd, PAYOUT_DECIMALS);
        if (payoutOut == 0) revert ZeroAmount();

        // H3 fix: TRUE rolling budget via linear leaky-bucket decay.
        // The previous tumbling window allowed 2× cap drain at the boundary.
        // Now: drain = max(0, periodRedeemed - elapsed * cap / periodSeconds)
        // and we cap (drain + new) ≤ rateLimitPerPeriod, then advance the
        // notional period anchor to "now". A burst of new traffic immediately
        // following a full-cap burst will simply revert until enough time
        // elapsed for the bucket to leak below the new headroom.
        AssetState storage s = states[asset];
        uint256 elapsed = block.timestamp - uint256(s.periodStart);
        if (elapsed > c.periodSeconds) elapsed = c.periodSeconds;
        uint256 leaked = (uint256(c.rateLimitPerPeriod) * elapsed) / uint256(c.periodSeconds);
        uint256 outstanding = s.periodRedeemed > leaked ? s.periodRedeemed - leaked : 0;
        if (outstanding + payoutOut > c.rateLimitPerPeriod) {
            revert RateLimitExceeded(outstanding + payoutOut, c.rateLimitPerPeriod);
        }
        s.periodStart = uint64(block.timestamp);
        s.periodRedeemed = outstanding + payoutOut;
        if (IERC20(PAYOUT_TOKEN).balanceOf(address(this)) < payoutOut) revert InsufficientPayoutLiquidity();

        // Pull RWA, send payout. CEI ordering.
        if (!IERC20(asset).transferFrom(msg.sender, address(this), rwaAmount)) revert TransferFailed();
        s.totalAccumulated += rwaAmount;
        if (!IERC20(PAYOUT_TOKEN).transfer(msg.sender, payoutOut)) revert TransferFailed();
        emit Redeemed(msg.sender, asset, rwaAmount, payoutOut, rwaPriceUsd);
    }

    /* ------------------------------- sweep ---------------------------------- */

    /// @notice Foundation pulls accumulated RWA to its Securitize-allowlisted
    ///         wallet for off-chain redemption. Cannot pull more than has
    ///         been deposited via `redeem` — the contract balance is the
    ///         ceiling, and accounting is strict.
    function sweepRWA(address asset, uint256 amount, address recipient) external onlyFoundation nonReentrant {
        AssetConfig memory c = configs[asset];
        if (!c.registered) revert UnknownAsset();
        if (recipient == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        AssetState storage s = states[asset];
        if (amount > s.totalAccumulated) revert InsufficientSweepBalance();
        s.totalAccumulated -= amount;
        if (!IERC20(asset).transfer(recipient, amount)) revert TransferFailed();
        emit Swept(asset, recipient, amount);
    }

    /* ---------------------------------- views ------------------------------- */

    function periodRemaining(address asset) external view returns (uint256) {
        AssetConfig memory c = configs[asset];
        if (!c.registered) return 0;
        AssetState memory s = states[asset];
        uint256 elapsed = block.timestamp - uint256(s.periodStart);
        if (elapsed > c.periodSeconds) elapsed = c.periodSeconds;
        uint256 leaked = (uint256(c.rateLimitPerPeriod) * elapsed) / uint256(c.periodSeconds);
        uint256 outstanding = s.periodRedeemed > leaked ? s.periodRedeemed - leaked : 0;
        return c.rateLimitPerPeriod > outstanding ? c.rateLimitPerPeriod - outstanding : 0;
    }

    function payoutLiquidity() external view returns (uint256) {
        return IERC20(PAYOUT_TOKEN).balanceOf(address(this));
    }

    /* ------------------------------ internal -------------------------------- */

    function _scaleUp(uint256 amount, uint8 dec) internal pure returns (uint256) {
        if (dec == 18) return amount;
        if (dec < 18) return amount * (10 ** (18 - dec));
        return amount / (10 ** (dec - 18));
    }

    function _scaleDown(uint256 amount, uint8 dec) internal pure returns (uint256) {
        if (dec == 18) return amount;
        if (dec < 18) return amount / (10 ** (18 - dec));
        return amount * (10 ** (dec - 18));
    }
}
