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

import "../BaseDignityBond.sol";
import "./IERC20Minimal.sol";
import "./VaultfireErrors.sol";

/**
 * @title BaseYieldPoolBondV3
 * @notice Base contract for bonds with yield pool funding + multi-asset staking
 * @dev Extends BaseDignityBond with yield pool management and ERC20 token support
 *
 * V3.1 Changes (2026-02 Partner-Ready — Base + Avalanche):
 * - NEW: Multi-asset staking — accept native token (ETH/AVAX) AND whitelisted ERC20s
 *   (WETH, WAVAX, USDC, etc.) so partners on any chain can plug right in
 * - NEW: Token whitelist with owner-managed approval
 * - NEW: Per-bond asset tracking (native vs ERC20, which token)
 * - Previous V3 features preserved: timelocked withdrawal, yield pool, multi-cycle
 *
 * @custom:security-enhancement V3.1 Multi-Asset Partner-Ready
 * @custom:chains Base (ETH native, WAVAX/WETH ERC20), Avalanche (AVAX native, WETH/WAVAX ERC20)
 *
 * Mission Alignment: Remove barriers. Any AI or human on any supported chain
 * can partner with VaultFire without swapping tokens first. Meet them where they are.
 */
abstract contract BaseYieldPoolBondV3 is BaseDignityBond {

    // ============ Multi-Asset Infrastructure ============

    /// @notice Sentinel value meaning "native token" (ETH on Base, AVAX on Avalanche)
    address public constant NATIVE_TOKEN = address(0);

    /// @notice Whitelisted ERC20 tokens accepted for staking
    mapping(address => bool) public acceptedTokens;

    /// @notice Human-readable name for each accepted token (for events/UX)
    mapping(address => string) public tokenNames;

    /// @notice All accepted token addresses (for enumeration)
    address[] public acceptedTokenList;

    /// @notice Per-bond: which token was staked (address(0) = native)
    mapping(uint256 => address) public bondStakeToken;

    /// @notice Per-bond: actual stake amount in that token
    mapping(uint256 => uint256) public bondStakeAmount;

    // ============ Yield Pool State ============

    /// @notice Pool of native-token funds available for bond appreciations
    uint256 public yieldPool;

    /// @notice ERC20 yield pools — one per accepted token
    mapping(address => uint256) public tokenYieldPool;

    /// @notice Minimum yield pool balance required (native token)
    uint256 public minimumYieldPoolBalance;

    /// @notice Total value of all active bonds (for reserve ratio calculation)
    uint256 public totalActiveBondValue;

    /// @notice Whether yield pool checks are enforced
    bool public yieldPoolEnforced;

    /// @notice Total pending distribution amounts across all bonds
    uint256 public totalPendingDistributions;

    /// @notice Pending distribution amount for each bond
    mapping(uint256 => uint256) public pendingDistributionAmounts;

    /// @notice Snapshotted appreciation at distribution request time
    mapping(uint256 => int256) public snapshotAppreciation;

    // ============ Locked Funds Withdrawal (V3 FIX: C-01, C-02) ============

    /// @notice Accumulated protocol funds (partnership fund, forfeited stakes, etc.)
    uint256 public protocolFunds;

    /// @notice Pending withdrawal request
    struct WithdrawalRequest {
        address destination;
        uint256 amount;
        uint256 requestedAt;
        bool active;
    }

    WithdrawalRequest public pendingWithdrawal;

    /// @notice Timelock for fund withdrawals (7 days)
    uint256 public constant FUND_WITHDRAWAL_TIMELOCK = 7 days;

    // ============ Constants ============

    uint256 public constant DEFAULT_MINIMUM_YIELD_POOL = 10 ether;
    uint256 public constant MINIMUM_RESERVE_RATIO = 5000;

    // ============ Events ============

    event YieldPoolFunded(address indexed funder, uint256 amount, uint256 newBalance);
    event YieldPoolFundedToken(address indexed funder, address indexed token, uint256 amount, uint256 newBalance);
    event YieldPoolWithdrawn(address indexed owner, uint256 amount, uint256 newBalance);
    event YieldPoolUsed(uint256 indexed bondId, uint256 amount, uint256 remainingBalance);
    event YieldPoolUsedToken(uint256 indexed bondId, address indexed token, uint256 amount, uint256 remainingBalance);
    event YieldPoolReplenished(uint256 indexed bondId, uint256 amount, uint256 newBalance);
    event MinimumYieldPoolUpdated(uint256 oldMinimum, uint256 newMinimum);
    event YieldPoolEnforcementChanged(bool enforced);
    event LowYieldPoolWarning(uint256 currentBalance, uint256 minimumRequired, uint256 deficit);
    event ReserveRatioWarning(uint256 yieldPool, uint256 activeBonds, uint256 ratio);

    // V3 events
    event ProtocolFundsAccrued(string reason, uint256 amount, uint256 newTotal);
    event FundWithdrawalRequested(address indexed destination, uint256 amount, uint256 availableAt);
    event FundWithdrawalExecuted(address indexed destination, uint256 amount);
    event FundWithdrawalCancelled();

    // V3.1 multi-asset events
    event TokenAccepted(address indexed token, string name);
    event TokenRemoved(address indexed token);
    event BondStakedNative(uint256 indexed bondId, uint256 amount);
    event BondStakedToken(uint256 indexed bondId, address indexed token, uint256 amount);

    // ============ Constructor ============

    constructor() {
        minimumYieldPoolBalance = DEFAULT_MINIMUM_YIELD_POOL;
        yieldPoolEnforced = true;
    }

    // ============ Multi-Asset Management ============

    /**
     * @notice Add an ERC20 token to the accepted whitelist
     * @param token ERC20 token contract address
     * @param name Human-readable name (e.g. "Wrapped AVAX", "USDC")
     *
     * Mission Alignment: Expanding the whitelist opens VaultFire to more partners.
     * Each new token = more humans and AIs who can participate without friction.
     */
    function acceptToken(address token, string calldata name) external onlyOwner {
        if (token == address(0)) revert VF_NativeOnly();
        if (acceptedTokens[token]) revert VF_TokenAlreadyAccepted();
        if (bytes(name).length == 0 || bytes(name).length > 50) revert VF_NameInvalid();

        acceptedTokens[token] = true;
        tokenNames[token] = name;
        acceptedTokenList.push(token);

        emit TokenAccepted(token, name);
    }

    /**
     * @notice Remove an ERC20 token from the whitelist
     * @dev Existing bonds using this token are unaffected — they can still distribute.
     *      Only prevents NEW bonds from using this token.
     */
    function removeToken(address token) external onlyOwner {
        if (!acceptedTokens[token]) revert VF_TokenNotAccepted();
        acceptedTokens[token] = false;
        emit TokenRemoved(token);
    }

    /**
     * @notice Get all accepted token addresses
     * @return tokens Array of accepted token addresses (may include removed tokens — check acceptedTokens mapping)
     */
    function getAcceptedTokens() external view returns (address[] memory) {
        return acceptedTokenList;
    }

    /**
     * @notice Check if a token is currently accepted for new bonds
     */
    function isTokenAccepted(address token) external view returns (bool) {
        if (token == NATIVE_TOKEN) return true; // Native always accepted
        return acceptedTokens[token];
    }

    // ============ Internal Multi-Asset Helpers ============

    /**
     * @notice Record a native-token stake for a bond
     * @dev Called by child contract createBond when msg.value > 0
     */
    function _recordNativeStake(uint256 bondId, uint256 amount) internal {
        bondStakeToken[bondId] = NATIVE_TOKEN;
        bondStakeAmount[bondId] = amount;
        emit BondStakedNative(bondId, amount);
    }

    /**
     * @notice Record an ERC20 stake for a bond and pull the tokens
     * @dev Called by child contract createBond when using ERC20
     * @param bondId Bond ID
     * @param token ERC20 token address
     * @param amount Amount to stake
     * @param staker Address staking the tokens (must have approved this contract)
     */
    function _recordTokenStake(uint256 bondId, address token, uint256 amount, address staker) internal {
        if (!acceptedTokens[token]) revert VF_TokenNotAccepted();

        bondStakeToken[bondId] = token;
        bondStakeAmount[bondId] = amount;

        // Pull tokens from staker (requires prior approval)
        bool success = IERC20Minimal(token).transferFrom(staker, address(this), amount);
        if (!success) revert VF_TokenTransferApproval();

        emit BondStakedToken(bondId, token, amount);
    }

    /**
     * @notice Transfer stake back to a recipient (for deactivation / distribution)
     * @dev Handles both native and ERC20 transparently
     */
    function _transferStake(uint256 bondId, address payable recipient, uint256 amount) internal {
        address token = bondStakeToken[bondId];

        if (token == NATIVE_TOKEN) {
            (bool success, ) = recipient.call{value: amount}("");
            if (!success) revert VF_NativeTransferFailed();
        } else {
            bool success = IERC20Minimal(token).transfer(recipient, amount);
            if (!success) revert VF_TokenTransferFailed();
        }
    }

    // ============ Yield Pool Management ============

    /**
     * @notice Fund the native-token yield pool
     */
    function fundYieldPool() external payable nonReentrant {
        if (msg.value == 0) revert VF_MustSendValue();
        yieldPool += msg.value;
        emit YieldPoolFunded(msg.sender, msg.value, yieldPool);
    }

    /**
     * @notice Fund an ERC20 yield pool
     * @param token Accepted ERC20 token address
     * @param amount Amount to fund (must have approved this contract)
     */
    function fundYieldPoolToken(address token, uint256 amount) external nonReentrant {
        if (!acceptedTokens[token]) revert VF_TokenNotAccepted();
        if (amount == 0) revert VF_AmountZero();

        bool success = IERC20Minimal(token).transferFrom(msg.sender, address(this), amount);
        if (!success) revert VF_TokenTransferApproval();

        tokenYieldPool[token] += amount;
        emit YieldPoolFundedToken(msg.sender, token, amount, tokenYieldPool[token]);
    }

    /**
     * @notice Owner withdraws excess native yield pool funds
     */
    function withdrawYieldPool(uint256 amount) external onlyOwner nonReentrant {
        if (amount == 0) revert VF_AmountZero();
        if (amount > yieldPool) revert VF_InsufficientYieldPool();
        if (yieldPool - amount < minimumYieldPoolBalance) revert VF_BelowMinimumBalance();

        yieldPool -= amount;

        (bool success, ) = payable(owner).call{value: amount}("");
        if (!success) revert VF_TransferFailed();

        emit YieldPoolWithdrawn(owner, amount, yieldPool);
    }

    function setMinimumYieldPoolBalance(uint256 newMinimum) external onlyOwner {
        if (newMinimum == 0) revert VF_MinimumMustBePositive();
        uint256 oldMinimum = minimumYieldPoolBalance;
        minimumYieldPoolBalance = newMinimum;
        emit MinimumYieldPoolUpdated(oldMinimum, newMinimum);
    }

    function setYieldPoolEnforcement(bool enforced) external onlyOwner {
        yieldPoolEnforced = enforced;
        emit YieldPoolEnforcementChanged(enforced);
    }

    // ============ V3: Timelocked Fund Withdrawal (fixes C-01, C-02) ============

    function requestFundWithdrawal(address destination, uint256 amount) external onlyOwner {
        _validateAddress(destination, "Destination");
        _validateNonZero(amount, "Withdrawal amount");
        if (amount > protocolFunds) revert VF_ExceedsProtocolFunds();
        if (pendingWithdrawal.active) revert VF_WithdrawalAlreadyPending();

        pendingWithdrawal = WithdrawalRequest({
            destination: destination,
            amount: amount,
            requestedAt: block.timestamp,
            active: true
        });

        emit FundWithdrawalRequested(destination, amount, block.timestamp + FUND_WITHDRAWAL_TIMELOCK);
    }

    function executeFundWithdrawal() external onlyOwner nonReentrant {
        if (!pendingWithdrawal.active) revert VF_NoWithdrawalPending();
        if (block.timestamp < pendingWithdrawal.requestedAt + FUND_WITHDRAWAL_TIMELOCK) revert VF_TimelockNotExpired();
        if (pendingWithdrawal.amount > protocolFunds) revert VF_InsufficientProtocolFunds();

        uint256 amount = pendingWithdrawal.amount;
        address destination = pendingWithdrawal.destination;

        protocolFunds -= amount;
        pendingWithdrawal.active = false;

        (bool success, ) = payable(destination).call{value: amount}("");
        if (!success) revert VF_FundWithdrawalFailed();

        emit FundWithdrawalExecuted(destination, amount);
    }

    function cancelFundWithdrawal() external onlyOwner {
        if (!pendingWithdrawal.active) revert VF_NoWithdrawalPending();
        pendingWithdrawal.active = false;
        emit FundWithdrawalCancelled();
    }

    // ============ Internal Functions ============

    function _accrueProtocolFunds(string memory reason, uint256 amount) internal {
        protocolFunds += amount;
        emit ProtocolFundsAccrued(reason, amount, protocolFunds);
    }

    /**
     * @notice Deduct appreciation from the correct yield pool (native or ERC20)
     * @dev V3.1: Routes to native yieldPool or tokenYieldPool based on bond's stake token
     */
    function _useYieldPool(uint256 bondId, uint256 amount) internal {
        address token = bondStakeToken[bondId];

        if (token == NATIVE_TOKEN) {
            if (yieldPool < amount) revert VF_InsufficientNativeYield();

            if (yieldPoolEnforced) {
                if (yieldPool < minimumYieldPoolBalance) revert VF_YieldPoolBelowMinimum();
            }

            yieldPool -= amount;

            emit YieldPoolUsed(bondId, amount, yieldPool);

            if (yieldPool < minimumYieldPoolBalance * 2) {
                uint256 deficit = yieldPool >= minimumYieldPoolBalance ? 0 : (minimumYieldPoolBalance - yieldPool);
                emit LowYieldPoolWarning(yieldPool, minimumYieldPoolBalance, deficit);
            }
        } else {
            if (tokenYieldPool[token] < amount) revert VF_InsufficientTokenYield();
            tokenYieldPool[token] -= amount;
            emit YieldPoolUsedToken(bondId, token, amount, tokenYieldPool[token]);
        }

        // Remove from pending distributions
        if (pendingDistributionAmounts[bondId] > 0) {
            totalPendingDistributions -= pendingDistributionAmounts[bondId];
            pendingDistributionAmounts[bondId] = 0;
        }
    }

    /**
     * @notice Track pending distribution request
     */
    function _trackPendingDistribution(uint256 bondId, int256 appreciationValue) internal {
        if (appreciationValue == 0) revert VF_NoAppreciationToDistribute();

        uint256 amount = appreciationValue > 0 ? uint256(appreciationValue) : uint256(-appreciationValue);
        address token = bondStakeToken[bondId];

        if (token == NATIVE_TOKEN) {
            if (yieldPool < totalPendingDistributions + amount) revert VF_InsufficientYieldPending();
        } else {
            if (tokenYieldPool[token] < amount) revert VF_InsufficientTokenYieldPend();
        }

        snapshotAppreciation[bondId] = appreciationValue;
        pendingDistributionAmounts[bondId] = amount;
        totalPendingDistributions += amount;
    }

    function _replenishYieldPool(uint256 bondId, uint256 amount) internal {
        address token = bondStakeToken[bondId];
        if (token == NATIVE_TOKEN) {
            yieldPool += amount;
            emit YieldPoolReplenished(bondId, amount, yieldPool);
        } else {
            tokenYieldPool[token] += amount;
        }
    }

    function canYieldPoolCover(uint256 amount) public view returns (bool canCover, bool wouldBeHealthy) {
        canCover = yieldPool >= amount;
        wouldBeHealthy = canCover && (yieldPool - amount) >= minimumYieldPoolBalance;
    }

    /// @notice Reserve ratio for native yield pool only.
    /// @dev [audit-v3] In mixed-asset deployments, totalActiveBondValue includes
    ///      both native and ERC20 bond values (different units). This ratio is
    ///      meaningful only for native-only deployments. For mixed-asset health
    ///      monitoring, use per-token yield pool balances directly.
    function getReserveRatio() public view returns (uint256 ratio) {
        if (totalActiveBondValue == 0) return 10000;
        ratio = (yieldPool * 10000) / totalActiveBondValue;
    }

    function getProtocolHealth() external view returns (
        bool isHealthy,
        bool yieldPoolOK,
        bool reserveRatioOK,
        uint256 currentRatio
    ) {
        yieldPoolOK = yieldPool >= minimumYieldPoolBalance;
        currentRatio = getReserveRatio();
        reserveRatioOK = currentRatio >= MINIMUM_RESERVE_RATIO;
        isHealthy = yieldPoolOK && reserveRatioOK;
    }

    function _updateTotalActiveBondValue(uint256 newValue) internal {
        totalActiveBondValue = newValue;

        uint256 ratio = getReserveRatio();
        if (ratio < MINIMUM_RESERVE_RATIO) {
            emit ReserveRatioWarning(yieldPool, totalActiveBondValue, ratio);
        }
    }

    // ============ View Functions ============

    function getYieldPoolBalance() external view returns (uint256) {
        return yieldPool;
    }

    function getTokenYieldPoolBalance(address token) external view returns (uint256) {
        return tokenYieldPool[token];
    }

    function getMinimumYieldPool() external view returns (uint256) {
        return minimumYieldPoolBalance;
    }

    function isYieldPoolEnforced() external view returns (bool) {
        return yieldPoolEnforced;
    }

    function getProtocolFunds() external view returns (uint256) {
        return protocolFunds;
    }

    function getPendingWithdrawal() external view returns (
        address destination,
        uint256 amount,
        uint256 requestedAt,
        bool active,
        uint256 availableAt
    ) {
        destination = pendingWithdrawal.destination;
        amount = pendingWithdrawal.amount;
        requestedAt = pendingWithdrawal.requestedAt;
        active = pendingWithdrawal.active;
        availableAt = active ? requestedAt + FUND_WITHDRAWAL_TIMELOCK : 0;
    }

    /**
     * @notice Get bond's staking details
     * @return token The staked token address (address(0) = native)
     * @return amount The staked amount
     * @return tokenName Human-readable token name ("Native" for ETH/AVAX)
     */
    function getBondStakeInfo(uint256 bondId) external view returns (
        address token,
        uint256 amount,
        string memory tokenName
    ) {
        token = bondStakeToken[bondId];
        amount = bondStakeAmount[bondId];
        tokenName = token == NATIVE_TOKEN ? "Native" : tokenNames[token];
    }
}
