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

interface IBurnVaultV2 {
    function burn() external payable;
}

interface IAereSinkV2 {
    function flush(address token, uint256 amount) external;
}

interface IWAERE {
    function deposit() external payable;
    function approve(address guy, uint256 wad) external returns (bool);
}

/**
 * @title AereCoinbaseSplitterV2
 * @notice V2 of the validator coinbase splitter. Replaces V1's 2-way split
 *         (burn / validator-rebate) with a 3-way split that ADDS the AereSink
 *         bucket so a portion of every validator reward feeds the sAERE
 *         flywheel.
 *
 *         Three buckets, all in basis points, owner-configurable within hard
 *         caps:
 *           burnBps      — direct burn at AereFeeBurnVault (default 3750 = 37.5%)
 *           sinkBps      — routed through AereSink (default 1500 = 15.0%)
 *           rebateBps    — back to validator (default 4750 = 47.5%, derived)
 *
 *         The 3-way default lifts the protocol-level deflation rate from 37.5%
 *         to ~52.5% net (burn bucket plus most of the sink — see saere_architecture
 *         for AereSink's internal 15/40/45 split which makes 55% of sink output
 *         a burn).
 *
 * @dev    Owner can adjust bps within these limits:
 *         - burnBps   ∈ [0, 5000]
 *         - sinkBps   ∈ [0, 3000]
 *         - rebateBps = 10000 - burnBps - sinkBps (auto-derived; revert if < 0)
 *         The sink address itself is owner-settable but rate-limited: a
 *         setSink call has a 7-day timelock to give validators warning before
 *         a sink swap.
 *
 *         This is the "in-place upgrade" that Week-1 references. The V1
 *         contract stays deployed and any AERE still flowing through it
 *         continues to honor the V1 burn behaviour. Validators migrate to
 *         V2 by switching the forwarder daemon's target address (off-chain
 *         config). Once migration is complete, V1 burnBps can be set to 0
 *         and the V1 contract retired.
 *
 *         Funds-safety: V2 cannot custody AERE across calls; every
 *         splitAndDistribute / splitToSelf fully dispatches msg.value.
 *         The sink bucket is wrapped via WAERE → IAereSink.flush() in one
 *         transaction; if the sink call reverts, the whole tx reverts and
 *         the validator can retry.
 */
contract AereCoinbaseSplitterV2 is Ownable, ReentrancyGuard {

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

    address payable public burnVault;
    address public sink;            // AereSink
    address public immutable WAERE_ADDR;

    uint256 public burnBps   = 3750; // 37.5%
    uint256 public sinkBps   = 1500; // 15.0%
    // rebateBps = 10000 - burnBps - sinkBps  (computed live)

    uint256 public constant BPS = 10_000;
    uint256 public constant SINK_CHANGE_TIMELOCK = 7 days;
    uint256 public constant MAX_BURN_BPS = 5000;
    uint256 public constant MAX_SINK_BPS = 3000;

    // Cumulative lifetime accounting.
    uint256 public totalBurned;
    uint256 public totalSentToSink;
    uint256 public totalRebated;

    mapping(address => uint256) public burnedBy;
    mapping(address => uint256) public sentToSinkBy;
    mapping(address => uint256) public rebatedTo;

    // Sink rotation timelock.
    address public pendingSink;
    uint256 public pendingSinkEarliestSetTs;

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

    event Burned(address indexed contributor, uint256 amount, uint256 newTotalBurned);
    event SinkRouted(address indexed contributor, uint256 amount, uint256 newTotalSentToSink);
    event Rebated(address indexed validator, uint256 amount, uint256 newTotalRebated);
    event BpsChanged(uint256 oldBurnBps, uint256 oldSinkBps, uint256 newBurnBps, uint256 newSinkBps);
    event BurnVaultChanged(address oldVault, address newVault);
    event PendingSinkProposed(address indexed newSink, uint256 earliestSet);
    event SinkChanged(address oldSink, address newSink);
    event PendingSinkCancelled();

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

    error ZeroAddress();
    error BpsOutOfRange();
    error TimelockNotElapsed(uint256 nowTs, uint256 earliest);
    error NoPendingProposal();
    error DistributeFailed();
    error ZeroAmount();

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

    constructor(address payable _burnVault, address _waere) {
        if (_burnVault == address(0) || _waere == address(0)) revert ZeroAddress();
        burnVault = _burnVault;
        WAERE_ADDR = _waere;
        // sink starts unset — until owner calls proposeSink+acceptSink, V2
        // behaves like V1 (no sink bucket; the 15% sinkBps default would
        // also be skipped, with that 15% rebated instead).
    }

    /* ------------------------------- admin (rate) ----------------------------- */

    /// Owner can adjust burn/sink bps within caps. rebateBps is derived.
    function setBps(uint256 _burnBps, uint256 _sinkBps) external onlyOwner {
        if (_burnBps > MAX_BURN_BPS) revert BpsOutOfRange();
        if (_sinkBps > MAX_SINK_BPS) revert BpsOutOfRange();
        if (_burnBps + _sinkBps > BPS) revert BpsOutOfRange();
        emit BpsChanged(burnBps, sinkBps, _burnBps, _sinkBps);
        burnBps = _burnBps;
        sinkBps = _sinkBps;
    }

    function setBurnVault(address payable _vault) external onlyOwner {
        if (_vault == address(0)) revert ZeroAddress();
        emit BurnVaultChanged(burnVault, _vault);
        burnVault = _vault;
    }

    /* ------------------------------- admin (sink) ----------------------------- */

    function proposeSink(address _sink) external onlyOwner {
        if (_sink == address(0)) revert ZeroAddress();
        pendingSink = _sink;
        pendingSinkEarliestSetTs = block.timestamp + SINK_CHANGE_TIMELOCK;
        emit PendingSinkProposed(_sink, pendingSinkEarliestSetTs);
    }

    function acceptSink() external onlyOwner {
        if (pendingSink == address(0)) revert NoPendingProposal();
        if (block.timestamp < pendingSinkEarliestSetTs) {
            revert TimelockNotElapsed(block.timestamp, pendingSinkEarliestSetTs);
        }
        address old = sink;
        sink = pendingSink;
        delete pendingSink;
        delete pendingSinkEarliestSetTs;
        emit SinkChanged(old, sink);
    }

    function cancelPendingSink() external onlyOwner {
        if (pendingSink == address(0)) revert NoPendingProposal();
        delete pendingSink;
        delete pendingSinkEarliestSetTs;
        emit PendingSinkCancelled();
    }

    /* ----------------------------------- core --------------------------------- */

    /**
     * @notice Split msg.value 3-ways. Anyone can call. Sink bucket is skipped
     *         (added to rebate) when sink is unset.
     */
    function splitAndDistribute(address payable validator) external payable nonReentrant {
        if (msg.value == 0) revert ZeroAmount();
        if (validator == address(0)) revert ZeroAddress();

        uint256 burnAmt = (msg.value * burnBps) / BPS;
        uint256 sinkAmt = (sink == address(0)) ? 0 : (msg.value * sinkBps) / BPS;
        uint256 rebateAmt = msg.value - burnAmt - sinkAmt;

        if (burnAmt > 0) {
            IBurnVaultV2(burnVault).burn{value: burnAmt}();
            totalBurned += burnAmt;
            burnedBy[msg.sender] += burnAmt;
            emit Burned(msg.sender, burnAmt, totalBurned);
        }

        if (sinkAmt > 0) {
            // Wrap msg.value's sink share into WAERE, then approve+flush.
            IWAERE(WAERE_ADDR).deposit{value: sinkAmt}();
            IWAERE(WAERE_ADDR).approve(sink, sinkAmt);
            IAereSinkV2(sink).flush(WAERE_ADDR, sinkAmt);
            totalSentToSink += sinkAmt;
            sentToSinkBy[msg.sender] += sinkAmt;
            emit SinkRouted(msg.sender, sinkAmt, totalSentToSink);
        }

        if (rebateAmt > 0) {
            (bool ok, ) = validator.call{value: rebateAmt}("");
            if (!ok) revert DistributeFailed();
            totalRebated += rebateAmt;
            rebatedTo[validator] += rebateAmt;
            emit Rebated(validator, rebateAmt, totalRebated);
        }
    }

    function splitToSelf() external payable nonReentrant {
        _splitToValidator(payable(msg.sender));
    }

    function _splitToValidator(address payable validator) internal {
        if (msg.value == 0) revert ZeroAmount();

        uint256 burnAmt = (msg.value * burnBps) / BPS;
        uint256 sinkAmt = (sink == address(0)) ? 0 : (msg.value * sinkBps) / BPS;
        uint256 rebateAmt = msg.value - burnAmt - sinkAmt;

        if (burnAmt > 0) {
            IBurnVaultV2(burnVault).burn{value: burnAmt}();
            totalBurned += burnAmt;
            burnedBy[msg.sender] += burnAmt;
            emit Burned(msg.sender, burnAmt, totalBurned);
        }

        if (sinkAmt > 0) {
            IWAERE(WAERE_ADDR).deposit{value: sinkAmt}();
            IWAERE(WAERE_ADDR).approve(sink, sinkAmt);
            IAereSinkV2(sink).flush(WAERE_ADDR, sinkAmt);
            totalSentToSink += sinkAmt;
            sentToSinkBy[msg.sender] += sinkAmt;
            emit SinkRouted(msg.sender, sinkAmt, totalSentToSink);
        }

        if (rebateAmt > 0) {
            (bool ok, ) = validator.call{value: rebateAmt}("");
            if (!ok) revert DistributeFailed();
            totalRebated += rebateAmt;
            rebatedTo[validator] += rebateAmt;
            emit Rebated(validator, rebateAmt, totalRebated);
        }
    }

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

    function rebateBps() external view returns (uint256) {
        return BPS - burnBps - sinkBps;
    }

    function splitStats() external view returns (
        uint256 lifetimeBurned,
        uint256 lifetimeSentToSink,
        uint256 lifetimeRebated,
        uint256 currentBurnBps,
        uint256 currentSinkBps,
        uint256 currentRebateBps,
        address currentSink
    ) {
        return (
            totalBurned,
            totalSentToSink,
            totalRebated,
            burnBps,
            sinkBps,
            BPS - burnBps - sinkBps,
            sink
        );
    }
}
