// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;

import { BEAR_ANCHOR_SHARE, BEAR_ANCHOR_WIDTH, BEAR_CAPITAL_INEFFICIENCY, BEAR_DISCOVERY_DEPTH, IOptimizer } from "./IOptimizer.sol";
import { Kraiken } from "./Kraiken.sol";
import { PriceOracle } from "./abstracts/PriceOracle.sol";
import { ThreePositionStrategy } from "./abstracts/ThreePositionStrategy.sol";
import { IWETH9 } from "./interfaces/IWETH9.sol";
import { CallbackValidation } from "@aperture/uni-v3-lib/CallbackValidation.sol";
import { PoolAddress, PoolKey } from "@aperture/uni-v3-lib/PoolAddress.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import { PositionKey } from "@uniswap-v3-periphery/libraries/PositionKey.sol";

/**
 * @title LiquidityManager
 * @notice Manages liquidity provisioning on Uniswap V3 using the three-position anti-arbitrage strategy
 * @dev Inherits from modular contracts for better separation of concerns and testability
 *
 * Key features:
 * - Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR)
 * - Dynamic parameter adjustment via Optimizer contract
 * - Asymmetric slippage profile prevents profitable arbitrage
 * - Exclusive minting rights for KRAIKEN token
 *
 * Price Validation:
 * - 30-second TWAP with 50-tick tolerance
 * - Prevents oracle manipulation attacks
 */
contract LiquidityManager is ThreePositionStrategy, PriceOracle {
    using SafeERC20 for IERC20;

    /// @notice Uniswap V3 fee tier (1%) - 10,000 basis points
    uint24 internal constant FEE = uint24(10_000);

    /// @notice Upper bound (inclusive) for scale-1 optimizer parameters: capitalInefficiency,
    ///         anchorShare, and discoveryDepth.  Values above this ceiling are silently clamped.
    uint256 internal constant MAX_PARAM_SCALE = 10 ** 18;

    /// @notice Immutable contract references
    address private immutable factory;
    IWETH9 private immutable weth;
    Kraiken private immutable kraiken;
    IOptimizer private immutable optimizer;
    IUniswapV3Pool private immutable pool;
    bool private immutable token0isWeth;
    PoolKey private poolKey;

    /// @notice Access control and fee management
    address private immutable deployer;
    address public feeDestination;
    bool public feeDestinationLocked;

    /// @notice Last recenter tick — used to determine net trade direction between recenters
    int24 public lastRecenterTick;

    /// @notice True once lastRecenterTick has been explicitly set by a recenter.
    /// @dev Prevents the else-branch direction check from comparing against the
    ///      Solidity-default 0 when no recenter has established a reference tick.
    ///      Without this guard, lastRecenterTick==0 is ambiguous: it could mean
    ///      "never recentered" or "previous recenter landed at tick 0 (price=1.0)".
    ///      See issue #609.
    bool private _hasRecenterTick;

    /// @notice Last recenter timestamp — rate limits recenters.
    uint256 public lastRecenterTime;
    /// @notice Minimum seconds between recenters
    uint256 internal constant MIN_RECENTER_INTERVAL = 60;
    /// @notice Target observation cardinality requested from the pool during construction
    uint16 internal constant ORACLE_CARDINALITY = 100;

    /// @notice Emitted on each successful recenter for monitoring and indexing
    event Recentered(int24 indexed currentTick, bool indexed isUp);

    /// @notice Emitted whenever feeDestination is updated
    event FeeDestinationSet(address indexed newDest);

    /// @notice Emitted when the fee destination lock is permanently engaged
    event FeeDestinationLocked(address indexed dest);

    /// @notice Custom errors
    error ZeroAddressInSetter();

    /// @notice Constructor initializes all contract references and pool configuration
    /// @param _factory The address of the Uniswap V3 factory
    /// @param _WETH9 The address of the WETH contract
    /// @param _kraiken The address of the Kraiken token contract
    /// @param _optimizer The address of the optimizer contract
    constructor(address _factory, address _WETH9, address _kraiken, address _optimizer) {
        deployer = msg.sender;
        factory = _factory;
        weth = IWETH9(_WETH9);
        poolKey = PoolAddress.getPoolKey(_WETH9, _kraiken, FEE);
        pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
        kraiken = Kraiken(_kraiken);
        token0isWeth = _WETH9 < _kraiken;
        optimizer = IOptimizer(_optimizer);
        // Increase observation cardinality so pool.observe() has sufficient history
        // for _isPriceStable() TWAP checks.
        pool.increaseObservationCardinalityNext(ORACLE_CARDINALITY);
    }

    /// @notice Callback function for Uniswap V3 mint operations
    /// @param amount0Owed Amount of token0 owed for the liquidity provision
    /// @param amount1Owed Amount of token1 owed for the liquidity provision
    function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
        CallbackValidation.verifyCallback(factory, poolKey);

        // Handle KRAIKEN minting - use existing balance first, then mint only the difference
        uint256 kraikenPulled = token0isWeth ? amount1Owed : amount0Owed;
        uint256 kraikenBalance = kraiken.balanceOf(address(this));
        if (kraikenBalance < kraikenPulled) {
            kraiken.mint(kraikenPulled - kraikenBalance);
        }

        // Handle WETH conversion
        uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
        if (weth.balanceOf(address(this)) < ethOwed) {
            weth.deposit{ value: address(this).balance }();
        }

        // Transfer tokens to pool
        if (amount0Owed > 0) IERC20(poolKey.token0).safeTransfer(msg.sender, amount0Owed);
        if (amount1Owed > 0) IERC20(poolKey.token1).safeTransfer(msg.sender, amount1Owed);
    }

    /// @notice Sets the fee destination address (deployer only).
    /// @dev Trapdoor lock: EOA addresses can be set repeatedly (enables staged deployment/migration)
    ///      until the lock is triggered. The lock fires in two ways:
    ///
    ///      1. Direct assignment: setting feeDestination to a contract address (code.length > 0)
    ///         immediately and permanently locks further changes. This is the expected production
    ///         path — set an EOA during development, upgrade to a treasury contract when ready.
    ///
    ///      2. Defensive CREATE2 guard: if the current feeDestination was an EOA when set but has
    ///         since acquired bytecode (e.g. via CREATE2 deployment), the next call to this
    ///         function permanently sets feeDestinationLocked = true and returns WITHOUT reverting.
    ///         Not reverting is intentional: only a successful transaction commits the lock to
    ///         storage, so the lock survives a subsequent SELFDESTRUCT that would otherwise clear
    ///         the bytecode evidence and re-open the assignment window.
    ///
    ///      Remaining limitation: an atomic CREATE2+SELFDESTRUCT within a single transaction
    ///      (still possible post-EIP-6780 for contracts that deploy and destroy in one call)
    ///      cannot be detected here because the bytecode is already absent when this function
    ///      executes. No on-chain guard can close that sub-transaction window.
    /// @param feeDestination_ The address that will receive trading fees
    function setFeeDestination(address feeDestination_) external {
        require(msg.sender == deployer, "only deployer");
        if (address(0) == feeDestination_) revert ZeroAddressInSetter();
        // Defensive CREATE2 guard: if the current destination has acquired bytecode since being
        // set as an EOA, lock permanently and return WITHOUT reverting so the write is committed
        // to storage. A subsequent SELFDESTRUCT clears the bytecode but cannot undo this write.
        if (!feeDestinationLocked && feeDestination != address(0) && feeDestination.code.length > 0) {
            feeDestinationLocked = true;
            emit FeeDestinationLocked(feeDestination);
            return;
        }
        require(!feeDestinationLocked, "fee destination locked");
        feeDestination = feeDestination_;
        emit FeeDestinationSet(feeDestination_);
        if (feeDestination_.code.length > 0) {
            feeDestinationLocked = true;
            emit FeeDestinationLocked(feeDestination_);
        }
    }

    /// @notice Adjusts liquidity positions in response to price movements.
    ///         Callable by anyone. Always enforces cooldown and TWAP price stability.
    ///         This function either completes a full recenter (removing all positions,
    ///         recording VWAP where applicable, and redeploying liquidity) or reverts —
    ///         it never returns silently without acting.
    ///
    /// @dev Revert conditions (no silent false return for failure):
    ///   - "recenter cooldown"          — MIN_RECENTER_INTERVAL has not elapsed since last recenter
    ///   - "price deviated from oracle" — price is outside TWAP bounds (manipulation guard)
    ///   - "amplitude not reached."     — anchor position exists but price has not moved far enough
    ///                                    from the anchor centre to warrant repositioning
    ///
    /// @return isUp True if the KRK price in ETH rose since the last recenter
    ///              (buy event / net ETH inflow), regardless of token0/token1 ordering.
    ///              False if the KRK price fell, or if no anchor position existed prior
    ///              to this recenter (bootstrap case, no directional reference point).
    ///              Both values indicate a successful, fully-executed recenter.
    function recenter() external returns (bool isUp) {
        (, int24 currentTick,,,,,) = pool.slot0();

        // Always enforce cooldown and TWAP price stability — no bypass path
        require(block.timestamp >= lastRecenterTime + MIN_RECENTER_INTERVAL, "recenter cooldown");
        require(_isPriceStable(currentTick), "price deviated from oracle");
        lastRecenterTime = block.timestamp;

        // Check if price movement is sufficient for recentering
        isUp = false;
        if (positions[Stage.ANCHOR].liquidity > 0) {
            int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
            int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper;
            int24 centerTick = anchorTickLower + (anchorTickUpper - anchorTickLower) / 2;

            bool isEnough;
            (isUp, isEnough) = _validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
            require(isEnough, "amplitude not reached.");
        }

        // Remove all existing positions and collect fees
        // Pass tick direction to determine if VWAP should record.
        // CRITICAL: record VWAP only when price FALLS (sells / ETH outflow), never when it rises.
        // If we recorded during buy events, an adversary could run N buy-recenter cycles to push
        // VWAP upward toward the inflated price.  When VWAP ≈ current tick, mirrorTick ≈ currentTick,
        // so the floor is placed near the inflated price — crystallising IL when the adversary sells.
        // Freezing VWAP during buy-only cycles keeps the floor anchored to the historical baseline.
        bool shouldRecordVWAP;
        if (!_hasRecenterTick || cumulativeVolume == 0) {
            // No reference tick yet, or no VWAP data — always bootstrap to prevent
            // vwapX96=0 fallback.  The _hasRecenterTick guard (issue #609) ensures we
            // never run the direction comparison against the Solidity-default 0 before
            // a recenter has established a real reference point.
            shouldRecordVWAP = true;
        } else {
            // token0isWeth: tick UP = price down in KRK terms = sells = ETH outflow
            // !token0isWeth: tick DOWN = price down in KRK terms = sells = ETH outflow
            // Only record when price falls — VWAP stays anchored to historical levels
            // during buy attacks.
            //
            // Edge case (issue #609): when lastRecenterTick==0 because a previous recenter
            // landed at tick 0 (price = 1.0 token ratio), the comparison below correctly
            // identifies direction — tick 0 is a valid reference like any other tick.
            shouldRecordVWAP = token0isWeth ? (currentTick > lastRecenterTick) : (currentTick < lastRecenterTick);
        }
        lastRecenterTick = currentTick;
        _hasRecenterTick = true;

        _scrapePositions(shouldRecordVWAP, currentTick);

        // Update total supply tracking if price moved up
        if (isUp) {
            kraiken.setPreviousTotalSupply(kraiken.totalSupply());
        }

        // Get optimizer parameters and set new positions
        try optimizer.getLiquidityParams() returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
            // Clamp parameters to valid ranges
            PositionParams memory params = PositionParams({
                capitalInefficiency: (capitalInefficiency > MAX_PARAM_SCALE) ? MAX_PARAM_SCALE : capitalInefficiency,
                anchorShare: (anchorShare > MAX_PARAM_SCALE) ? MAX_PARAM_SCALE : anchorShare,
                anchorWidth: (anchorWidth > MAX_ANCHOR_WIDTH) ? MAX_ANCHOR_WIDTH : anchorWidth,
                discoveryDepth: (discoveryDepth > MAX_PARAM_SCALE) ? MAX_PARAM_SCALE : discoveryDepth
            });

            _setPositions(currentTick, params);
        } catch {
            // Fallback to safe bear-mode defaults if optimizer fails.
            // Defence-in-depth: assert bear constants satisfy the same bounds
            // that the try-path clamps to.  These are compile-time constants so
            // the asserts can never fire at runtime, but they guard against a
            // future edit to IOptimizer.sol that pushes a value out of range.
            assert(BEAR_CAPITAL_INEFFICIENCY <= MAX_PARAM_SCALE);
            assert(BEAR_ANCHOR_SHARE <= MAX_PARAM_SCALE);
            assert(BEAR_ANCHOR_WIDTH <= MAX_ANCHOR_WIDTH);
            assert(BEAR_DISCOVERY_DEPTH <= MAX_PARAM_SCALE);

            PositionParams memory defaultParams = PositionParams({
                capitalInefficiency: BEAR_CAPITAL_INEFFICIENCY,
                anchorShare: BEAR_ANCHOR_SHARE,
                anchorWidth: BEAR_ANCHOR_WIDTH,
                discoveryDepth: BEAR_DISCOVERY_DEPTH
            });

            _setPositions(currentTick, defaultParams);
        }

        emit Recentered(currentTick, isUp);
    }

    /// @notice Removes all positions and collects fees
    /// @param recordVWAP Whether to record VWAP (only when net ETH outflow / price fell since last recenter, or at bootstrap)
    /// @param currentTick The current pool tick at time of recenter, used as the VWAP price sample
    function _scrapePositions(bool recordVWAP, int24 currentTick) internal {
        uint256 fee0 = 0;
        uint256 fee1 = 0;
        // Price at current tick: volume-weighted, sampled once per recenter.
        // token0isWeth: tick represents KRK/ETH — negate for price in ETH per KRK terms.
        uint256 currentPrice = _priceAtTick(token0isWeth ? -1 * currentTick : currentTick);

        for (uint256 i = uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
            TokenPosition storage position = positions[Stage(i)];
            if (position.liquidity > 0) {
                // Burn liquidity and collect tokens + fees
                (uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);

                (uint256 collected0, uint256 collected1) =
                    pool.collect(address(this), position.tickLower, position.tickUpper, type(uint128).max, type(uint128).max);

                // Calculate fees
                fee0 += collected0 - amount0;
                fee1 += collected1 - amount1;
            }
        }

        // Transfer fees and record volume for VWAP
        // VWAP is recorded only on sell events (price fell) or at bootstrap — see recenter().
        // Skip transfer when feeDestination is self or unset — fees accrue as deployable liquidity.
        if (feeDestination != address(this) && feeDestination != address(0)) {
            if (fee0 > 0) {
                if (token0isWeth) {
                    IERC20(address(weth)).safeTransfer(feeDestination, fee0);
                } else {
                    IERC20(address(kraiken)).safeTransfer(feeDestination, fee0);
                }
            }
            if (fee1 > 0) {
                if (token0isWeth) {
                    IERC20(address(kraiken)).safeTransfer(feeDestination, fee1);
                } else {
                    IERC20(address(weth)).safeTransfer(feeDestination, fee1);
                }
            }
        }

        // Always record VWAP regardless of fee destination
        if (recordVWAP) {
            uint256 ethFee = token0isWeth ? fee0 : fee1;
            if (ethFee > 0) _recordVolumeAndPrice(currentPrice, ethFee);
        }
    }

    /// @notice Allow contract to receive ETH
    receive() external payable { }

    // ========================================
    // ABSTRACT FUNCTION IMPLEMENTATIONS
    // ========================================

    /// @notice Implementation of abstract function from PriceOracle
    function _getPool() internal view override returns (IUniswapV3Pool) {
        return pool;
    }

    /// @notice Implementation of abstract function from ThreePositionStrategy
    function _getKraikenToken() internal view override returns (address) {
        return address(kraiken);
    }

    /// @notice Implementation of abstract function from ThreePositionStrategy
    function _getWethToken() internal view override returns (address) {
        return address(weth);
    }

    /// @notice Implementation of abstract function from ThreePositionStrategy
    function _isToken0Weth() internal view override returns (bool) {
        return token0isWeth;
    }

    /// @notice Implementation of abstract function from ThreePositionStrategy
    function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override {
        pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
        positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper });
    }

    /// @notice Implementation of abstract function from ThreePositionStrategy
    function _getEthBalance() internal view override returns (uint256) {
        return address(this).balance + weth.balanceOf(address(this));
    }

    /// @notice Implementation of abstract function from ThreePositionStrategy
    /// @dev Subtracts KRK at feeDestination (protocol revenue) and stakingPool (locked in staking)
    ///      since neither can be sold into the floor — only trader-held KRK matters for scarcity
    function _getOutstandingSupply() internal view override returns (uint256) {
        uint256 supply = kraiken.outstandingSupply();
        // Skip subtraction when feeDestination is self: outstandingSupply() already
        // excludes kraiken.balanceOf(address(this)), so subtracting again would double-count.
        if (feeDestination != address(0) && feeDestination != address(this)) {
            supply -= kraiken.balanceOf(feeDestination);
        }
        (, address stakingPoolAddr) = kraiken.peripheryContracts();
        // Guard against double-subtraction: if stakingPoolAddr == feeDestination,
        // that balance was already deducted above.
        if (stakingPoolAddr != address(0) && stakingPoolAddr != feeDestination) {
            supply -= kraiken.balanceOf(stakingPoolAddr);
        }
        return supply;
    }
}
