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

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

/**
 * @title AereLendingMarket — isolated single-pair money market
 * @notice One deployment per (collateral, debt) pair. Foundation sets
 *         immutable risk params at deploy; suppliers earn yield; borrowers
 *         pay interest; AereSink receives a configurable cut of interest
 *         (NOT principal) on every accrual via `pendingSinkFee`.
 *
 *         WAVE 1 PAIRS planned:
 *           Pair                    | LTV  | LiqThr | LiqBonus | Borrow fee bps
 *           BUIDL → USDC.e          | 90%  | 92%    | 1%       | 10
 *           USDY  → USDC.e          | 88%  | 91%    | 1%       | 10
 *           OUSG  → USDC.e          | 88%  | 91%    | 1%       | 10
 *           LBTC  → USDC.e          | 70%  | 78%    | 5%       | 30
 *           SolvBTC → USDC.e        | 65%  | 73%    | 6%       | 35
 *           pumpBTC → USDC.e        | 60%  | 70%    | 7%       | 40
 *
 *         ACCOUNTING (scaled-principal with global accrual indices):
 *           - `debtOf[user]` is the SCALED principal (in ray, 1e27).
 *             Real debt = debtOf[user] * borrowIndex / 1e27.
 *           - `supplierScaled[user]` is the SCALED supply. Real supply =
 *             supplierScaled[user] * supplyIndex / 1e27.
 *           - `borrowIndex` grows at `borrowRatePerSecond * dt` per accrue.
 *           - `supplyIndex` grows proportionally so suppliers receive
 *             (1 - feeBps) of every interest tick. Fee accumulates as
 *             `pendingSinkFee` (in DEBT_TOKEN units) and anyone can flush
 *             it to AereSink via `flushSinkFee()`.
 *
 *         INSOLVENT LIQUIDATIONS:
 *           - When the borrower's collateral can't cover the requested pay
 *             plus bonus, we PROPORTIONALLY REDUCE `pay` so the liquidator
 *             only purchases what they receive. The residual `unpaid`
 *             stays on `debtOf[borrower]` and is emitted as `BadDebt`. A
 *             future AereInsuranceFund (or socialised write-down) can
 *             clear it; the market never silently masks bad debt.
 *
 *         SAFETY:
 *           - Per-asset immutable risk params (constructor).
 *           - Oracle deviation + staleness guarded by AereLendingOracle.
 *           - ReentrancyGuard on all mutating fns.
 *           - Pause-borrow / pause-deposit toggles for emergency.
 *             LIQUIDATIONS NEVER PAUSE.
 *           - No admin withdrawal: Foundation cannot rug supplier deposits.
 *
 * @dev    Single-market isolation (one contract = one collateral/debt pair)
 *         is chosen over a unified Comptroller pattern for clean blast-radius:
 *         a SolvBTC oracle compromise can only hurt the SolvBTC market, not
 *         BUIDL.
 */
contract AereLendingMarket is Ownable, ReentrancyGuard {

    uint256 internal constant RAY = 1e27;

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

    IERC20  public immutable COLLATERAL;
    IERC20  public immutable DEBT_TOKEN;
    address public immutable SINK;
    address public immutable ORACLE;

    uint16 public immutable LTV_BPS;
    uint16 public immutable LIQ_THRESHOLD_BPS;
    uint16 public immutable LIQ_BONUS_BPS;
    uint16 public immutable BORROW_FEE_BPS;

    uint256 public immutable MARKET_DEBT_CAP;
    uint8   public immutable COLLATERAL_DECIMALS;
    uint8   public immutable DEBT_DECIMALS;

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

    /// @notice user → collateral deposited (in COLLATERAL token units)
    mapping(address => uint256) public collateralOf;
    /// @notice user → SCALED debt principal (multiply by borrowIndex/RAY for real)
    mapping(address => uint256) public debtOf;
    /// @notice user → SCALED supplier balance (multiply by supplyIndex/RAY for real)
    mapping(address => uint256) public supplierScaled;

    /// @notice cumulative indices (ray, 1e27)
    uint256 public borrowIndex = RAY;
    uint256 public supplyIndex = RAY;
    /// @notice last block.timestamp `accrueInterest` ran
    uint256 public lastAccrual;
    /// @notice total scaled debt principal (sum of debtOf)
    uint256 public totalBorrowsScaled;
    /// @notice total scaled supplier principal (sum of supplierScaled)
    uint256 public totalSuppliedScaled;

    /// @notice DEBT_TOKEN sitting in the contract earmarked for AereSink
    uint256 public pendingSinkFee;

    /// @notice borrow rate in ray (1e27) per second. Default ~5% APY:
    ///         0.05 * 1e27 / (365 * 86400) ≈ 1.585e18.
    uint256 public borrowRatePerSecond = 1_585_489_599_188_229_300;

    bool public borrowPaused;
    bool public depositPaused;

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

    event Deposit(address indexed user, uint256 amount);
    event Withdraw(address indexed user, uint256 amount);
    event Supply(address indexed user, uint256 amount, uint256 scaled);
    event Redeem(address indexed user, uint256 amount, uint256 scaled);
    event Borrow(address indexed user, uint256 amount, uint256 scaled);
    event Repay(address indexed user, uint256 amount, uint256 scaled);
    event Liquidated(address indexed liquidator, address indexed borrower, uint256 repaid, uint256 collateralSeized);
    event BadDebt(address indexed borrower, uint256 unpaidDebt);
    event InterestAccrued(uint256 newBorrowIndex, uint256 newSupplyIndex, uint256 interest, uint256 feeToSink);
    event SinkFeeFlushed(uint256 amount);
    event BorrowRateUpdated(uint256 newRatePerSecond);
    event PausedSet(bool borrowPaused, bool depositPaused);

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

    error ZeroAmount();
    error InsufficientCollateral();
    error InsufficientDebtLiquidity();
    error PositionHealthy();
    error PositionUnhealthy();
    error MarketCapReached();
    error TransferFailed();
    error BorrowPausedErr();
    error DepositPausedErr();
    error InsufficientSupplierBalance();

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

    struct Config {
        address collateral;
        address debtToken;
        address sink;
        address oracle;
        uint16 ltvBps;
        uint16 liqThresholdBps;
        uint16 liqBonusBps;
        uint16 borrowFeeBps;
        uint256 marketDebtCap;
        uint8 collateralDecimals;
        uint8 debtDecimals;
    }

    constructor(Config memory c) {
        require(c.collateral != address(0) && c.debtToken != address(0), "zero token");
        require(c.sink != address(0) && c.oracle != address(0), "zero infra");
        require(c.ltvBps <= c.liqThresholdBps && c.liqThresholdBps < 10_000, "thresholds");
        require(c.liqBonusBps < 5_000, "bonus huge");
        require(c.borrowFeeBps <= 5_000, "fee huge");

        COLLATERAL = IERC20(c.collateral);
        DEBT_TOKEN = IERC20(c.debtToken);
        SINK = c.sink;
        ORACLE = c.oracle;
        LTV_BPS = c.ltvBps;
        LIQ_THRESHOLD_BPS = c.liqThresholdBps;
        LIQ_BONUS_BPS = c.liqBonusBps;
        BORROW_FEE_BPS = c.borrowFeeBps;
        MARKET_DEBT_CAP = c.marketDebtCap;
        COLLATERAL_DECIMALS = c.collateralDecimals;
        DEBT_DECIMALS = c.debtDecimals;
        lastAccrual = block.timestamp;
    }

    /* ----------------------------- interest accrual ------------------------- */

    /// @notice Grow borrowIndex + supplyIndex over elapsed seconds, accumulate sink fee.
    function accrueInterest() public {
        uint256 dt = block.timestamp - lastAccrual;
        if (dt == 0) return;
        lastAccrual = block.timestamp;
        if (totalBorrowsScaled == 0) return;

        uint256 borrowsBefore = (totalBorrowsScaled * borrowIndex) / RAY;
        // borrowIndex grows as (1 + rate * dt) — linear approximation safe at 0.5s blocks.
        uint256 indexFactor = (borrowRatePerSecond * dt);          // ray
        uint256 borrowsAfter = borrowsBefore + (borrowsBefore * indexFactor) / RAY;
        uint256 interest = borrowsAfter - borrowsBefore;
        if (interest == 0) {
            borrowIndex += (borrowIndex * indexFactor) / RAY;
            emit InterestAccrued(borrowIndex, supplyIndex, 0, 0);
            return;
        }
        borrowIndex += (borrowIndex * indexFactor) / RAY;

        uint256 fee = (interest * BORROW_FEE_BPS) / 10_000;
        uint256 toSuppliers = interest - fee;

        // Grow supplyIndex proportionally so suppliers' real balance
        // increases by `toSuppliers` in aggregate.
        uint256 suppliedReal = (totalSuppliedScaled * supplyIndex) / RAY;
        if (suppliedReal > 0 && toSuppliers > 0) {
            // newSupplyIndex / oldSupplyIndex = (suppliedReal + toSuppliers) / suppliedReal
            supplyIndex = (supplyIndex * (suppliedReal + toSuppliers)) / suppliedReal;
            pendingSinkFee += fee;
        } else {
            // ROUND-3 FIX: borrows outstanding with no live supplier-claims
            // (e.g. all suppliers withdrew via socialised path) would silently
            // leak `toSuppliers` into raw contract balance — uncredited. Route
            // 100% of the interest tick to AereSink instead.
            pendingSinkFee += interest;
        }

        emit InterestAccrued(borrowIndex, supplyIndex, interest, fee);
    }

    /// @notice Permissionless: push accumulated fee to AereSink.
    function flushSinkFee() external nonReentrant {
        uint256 amount = pendingSinkFee;
        if (amount == 0) return;
        // Reset before external call (CEI).
        pendingSinkFee = 0;
        // Use forceApprove pattern: zero then set, to support USDT-like tokens.
        DEBT_TOKEN.approve(SINK, 0);
        DEBT_TOKEN.approve(SINK, amount);
        (bool ok, ) = SINK.call(abi.encodeWithSignature("flush(address,uint256)", address(DEBT_TOKEN), amount));
        if (!ok) {
            // Restore so a later call can retry.
            pendingSinkFee = amount;
            DEBT_TOKEN.approve(SINK, 0);
            revert TransferFailed();
        }
        emit SinkFeeFlushed(amount);
    }

    /* ------------------------------ supply side ----------------------------- */

    function supply(uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        accrueInterest();
        if (!DEBT_TOKEN.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
        uint256 scaled = (amount * RAY) / supplyIndex;
        supplierScaled[msg.sender] += scaled;
        totalSuppliedScaled += scaled;
        emit Supply(msg.sender, amount, scaled);
    }

    function redeem(uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        accrueInterest();
        uint256 userReal = (supplierScaled[msg.sender] * supplyIndex) / RAY;
        if (userReal < amount) revert InsufficientSupplierBalance();
        if (_availableLiquidity() < amount + pendingSinkFee) revert InsufficientDebtLiquidity();
        uint256 scaled = (amount * RAY) / supplyIndex;
        // Round-up protection: never let scaled exceed user's holding.
        if (scaled > supplierScaled[msg.sender]) scaled = supplierScaled[msg.sender];
        supplierScaled[msg.sender] -= scaled;
        totalSuppliedScaled -= scaled;
        if (!DEBT_TOKEN.transfer(msg.sender, amount)) revert TransferFailed();
        emit Redeem(msg.sender, amount, scaled);
    }

    function supplierBalance(address u) external view returns (uint256) {
        // Lazy-view: doesn't accrue. UI should call accrueInterest first for live yield.
        return (supplierScaled[u] * supplyIndex) / RAY;
    }

    function supplierPrincipal(address u) external view returns (uint256) {
        // Legacy compat — same as scaled balance pre-interest.
        return supplierScaled[u];
    }

    /* ----------------------------- borrower side ---------------------------- */

    function depositCollateral(uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        if (depositPaused) revert DepositPausedErr();
        accrueInterest();
        if (!COLLATERAL.transferFrom(msg.sender, address(this), amount)) revert TransferFailed();
        collateralOf[msg.sender] += amount;
        emit Deposit(msg.sender, amount);
    }

    function withdrawCollateral(uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        accrueInterest();
        if (collateralOf[msg.sender] < amount) revert InsufficientCollateral();
        uint256 newCol = collateralOf[msg.sender] - amount;
        uint256 debtReal = _realDebt(msg.sender);
        if (debtReal > 0 && !_healthyAt(newCol, debtReal)) revert PositionUnhealthy();
        collateralOf[msg.sender] = newCol;
        if (!COLLATERAL.transfer(msg.sender, amount)) revert TransferFailed();
        emit Withdraw(msg.sender, amount);
    }

    function borrow(uint256 amount) external nonReentrant {
        if (amount == 0) revert ZeroAmount();
        if (borrowPaused) revert BorrowPausedErr();
        accrueInterest();
        uint256 currentTotalReal = (totalBorrowsScaled * borrowIndex) / RAY;
        if (currentTotalReal + amount > MARKET_DEBT_CAP) revert MarketCapReached();
        // Must leave pendingSinkFee untouched for sink claim.
        if (_availableLiquidity() < amount + pendingSinkFee) revert InsufficientDebtLiquidity();

        uint256 borrowedScaled = (amount * RAY) / borrowIndex;
        // Round-up to avoid undercharging on dust.
        if ((borrowedScaled * borrowIndex) / RAY < amount) borrowedScaled += 1;
        debtOf[msg.sender] += borrowedScaled;
        totalBorrowsScaled += borrowedScaled;

        uint256 newDebtReal = _realDebt(msg.sender);
        if (!_healthyAt(collateralOf[msg.sender], newDebtReal)) revert PositionUnhealthy();
        if (!DEBT_TOKEN.transfer(msg.sender, amount)) revert TransferFailed();
        emit Borrow(msg.sender, amount, borrowedScaled);
    }

    function repay(uint256 amount) external nonReentrant {
        _repayInternal(msg.sender, msg.sender, amount);
    }

    /// @notice Permissionless: anyone (typically AereInsuranceFund or a
    ///         charitable donor) can pay down `account`'s debt. msg.sender
    ///         provides the tokens; `account`'s debt and totalBorrowsScaled
    ///         are reduced. No bonus, no liquidation — just pure repayment
    ///         at face value. Useful for clearing BadDebt residuals after
    ///         insolvent liquidations.
    function repayOnBehalfOf(address account, uint256 amount) external nonReentrant {
        _repayInternal(msg.sender, account, amount);
    }

    function _repayInternal(address payer, address account, uint256 amount) internal {
        if (amount == 0) revert ZeroAmount();
        accrueInterest();
        uint256 debtReal = _realDebt(account);
        if (debtReal == 0) return;
        uint256 pay = amount > debtReal ? debtReal : amount;
        if (!DEBT_TOKEN.transferFrom(payer, address(this), pay)) revert TransferFailed();

        uint256 repaidScaled = (pay * RAY) / borrowIndex;
        if (pay == debtReal) {
            repaidScaled = debtOf[account];
        }
        debtOf[account] -= repaidScaled;
        totalBorrowsScaled -= repaidScaled;
        emit Repay(account, pay, repaidScaled);
    }

    /* ------------------------------ liquidation ----------------------------- */

    /// @notice Liquidate `borrower`. Liquidator pays up to `repayAmount` of
    ///         the debt and receives proportional collateral + bonus.
    ///         Insolvent case (collateral ≤ value-needed): we proportionally
    ///         reduce the actual `pay` charged so the liquidator buys only
    ///         what they receive. Residual debt stays on the borrower and
    ///         is emitted as `BadDebt`.
    function liquidate(address borrower, uint256 repayAmount) external nonReentrant {
        if (repayAmount == 0) revert ZeroAmount();
        accrueInterest();

        uint256 collateral = collateralOf[borrower];
        uint256 debtReal = _realDebt(borrower);
        if (debtReal == 0) revert PositionHealthy();
        if (_healthyAtLiqThreshold(collateral, debtReal)) revert PositionHealthy();

        uint256 pay = repayAmount > debtReal ? debtReal : repayAmount;

        // LIQUIDATIONS NEVER PAUSE: use the non-freezing liquidation price so a
        // paused/deviating/stale feed can never block clearing bad debt.
        uint256 debtPrice = _priceLiq(address(DEBT_TOKEN));
        uint256 colPrice = _priceLiq(address(COLLATERAL));

        // value in 18-dec USD
        uint256 valueRepaid = _scaleUp(pay, DEBT_DECIMALS) * debtPrice / 1e18;
        uint256 valueToSeize = valueRepaid + (valueRepaid * LIQ_BONUS_BPS) / 10_000;
        uint256 collateralToSeize = _scaleDown(valueToSeize * 1e18 / colPrice, COLLATERAL_DECIMALS);

        // If we'd seize more than borrower has, clamp seize AND scale pay down
        // proportionally so liquidator only pays for what they receive.
        if (collateralToSeize > collateral) {
            collateralToSeize = collateral;
            uint256 valueOfClamped = _scaleUp(collateral, COLLATERAL_DECIMALS) * colPrice / 1e18;
            // valueOfClamped = valueRepaidReduced * (1 + LIQ_BONUS_BPS/10_000)
            uint256 valueRepaidReduced = (valueOfClamped * 10_000) / (10_000 + LIQ_BONUS_BPS);
            pay = _scaleDown(valueRepaidReduced * 1e18 / debtPrice, DEBT_DECIMALS);
            if (pay > debtReal) pay = debtReal;
        }

        if (!DEBT_TOKEN.transferFrom(msg.sender, address(this), pay)) revert TransferFailed();

        // Update borrower state (use scaled units to stay consistent with index).
        uint256 repaidScaled = (pay * RAY) / borrowIndex;
        if (repaidScaled > debtOf[borrower]) repaidScaled = debtOf[borrower];
        debtOf[borrower] -= repaidScaled;
        totalBorrowsScaled -= repaidScaled;
        collateralOf[borrower] = collateral - collateralToSeize;

        // Emit any residual debt that remains on a now-zero-collateral borrower.
        uint256 remainingCol = collateralOf[borrower];
        uint256 remainingDebt = _realDebt(borrower);
        if (remainingCol == 0 && remainingDebt > 0) {
            emit BadDebt(borrower, remainingDebt);
        }

        if (!COLLATERAL.transfer(msg.sender, collateralToSeize)) revert TransferFailed();
        emit Liquidated(msg.sender, borrower, pay, collateralToSeize);
    }

    /* ----------------------------- admin / governance ----------------------- */

    function setBorrowRate(uint256 newRatePerSecondRay) external onlyOwner {
        // Cap at 1000% APY (10x) in ray/sec — bounds extreme misconfiguration.
        // 10 * 1e27 / (365 * 86400) ≈ 3.17e20.
        require(newRatePerSecondRay <= 317_097_919_837_645_865_000, "rate too high");
        accrueInterest();
        borrowRatePerSecond = newRatePerSecondRay;
        emit BorrowRateUpdated(newRatePerSecondRay);
    }

    function setPaused(bool _borrowPaused, bool _depositPaused) external onlyOwner {
        borrowPaused = _borrowPaused;
        depositPaused = _depositPaused;
        emit PausedSet(_borrowPaused, _depositPaused);
    }

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

    /// @notice Returns position health as a ratio scaled by 10_000 (1.0 = 10_000).
    ///         Above 10_000 = healthy. Below 10_000 = liquidatable.
    function healthFactor(address user) external view returns (uint256) {
        uint256 debt = _realDebt(user);
        if (debt == 0) return type(uint256).max;
        uint256 col = collateralOf[user];
        uint256 colVal = _scaleUp(col, COLLATERAL_DECIMALS) * _price(address(COLLATERAL)) / 1e18;
        uint256 debtVal = _scaleUp(debt, DEBT_DECIMALS) * _price(address(DEBT_TOKEN)) / 1e18;
        return (colVal * LIQ_THRESHOLD_BPS) / debtVal;
    }

    function debtBalance(address user) external view returns (uint256) {
        return _realDebt(user);
    }

    function totalBorrowsCurrent() external view returns (uint256) {
        return (totalBorrowsScaled * borrowIndex) / RAY;
    }

    function totalSuppliedCurrent() external view returns (uint256) {
        return (totalSuppliedScaled * supplyIndex) / RAY;
    }

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

    function _realDebt(address user) internal view returns (uint256) {
        if (debtOf[user] == 0) return 0;
        return (debtOf[user] * borrowIndex) / RAY;
    }

    function _healthyAt(uint256 col, uint256 debt) internal view returns (bool) {
        if (debt == 0) return true;
        uint256 colVal = _scaleUp(col, COLLATERAL_DECIMALS) * _price(address(COLLATERAL)) / 1e18;
        uint256 debtVal = _scaleUp(debt, DEBT_DECIMALS) * _price(address(DEBT_TOKEN)) / 1e18;
        return colVal * LTV_BPS / 10_000 >= debtVal;
    }

    /// @dev Liquidation-eligibility check. Uses the non-freezing liquidation
    ///      price so a paused/deviating/stale feed cannot block a liquidation
    ///      that the "LIQUIDATIONS NEVER PAUSE" invariant promises must proceed.
    function _healthyAtLiqThreshold(uint256 col, uint256 debt) internal view returns (bool) {
        if (debt == 0) return true;
        uint256 colVal = _scaleUp(col, COLLATERAL_DECIMALS) * _priceLiq(address(COLLATERAL)) / 1e18;
        uint256 debtVal = _scaleUp(debt, DEBT_DECIMALS) * _priceLiq(address(DEBT_TOKEN)) / 1e18;
        return colVal * LIQ_THRESHOLD_BPS / 10_000 >= debtVal;
    }

    function _price(address asset) internal view returns (uint256) {
        return IAereOracleAdapterLending(ORACLE).getPrice(asset);
    }

    /// @dev Non-freezing price for the liquidation path only (never pauses, never
    ///      applies the deviation cap or staleness revert, falls back to the last
    ///      good price). Borrow-side checks keep the strict _price/getPrice so the
    ///      circuit-breaker still gates NEW borrows. See AereLendingOracle.
    function _priceLiq(address asset) internal view returns (uint256) {
        return IAereOracleAdapterLending(ORACLE).getPriceForLiquidation(asset);
    }

    function _availableLiquidity() internal view returns (uint256) {
        return DEBT_TOKEN.balanceOf(address(this));
    }

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

interface IAereOracleAdapterLending {
    function getPrice(address asset) external view returns (uint256);
    function getPriceForLiquidation(address asset) external view returns (uint256);
}
