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

import { FormatLib } from "./FormatLib.sol";
import { MockToken } from "./MockToken.sol";
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
import { Math } from "@openzeppelin/utils/math/Math.sol";
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
import { console2 } from "forge-std/console2.sol";

/**
 * @title BaselineStrategies
 * @notice Manages three comparison strategies run alongside KrAIken during event replay:
 *
 *   HODL        — Hold initial token amounts, no LP. Zero fees, zero IL by definition.
 *   Full-Range  — Single position from aligned MIN_TICK to MAX_TICK. Set once, never rebalanced.
 *                 Always 100% time-in-range.
 *   Fixed-Width — ±FIXED_WIDTH_TICKS around current price. Rebalanced at the same block-interval
 *                 as KrAIken when the active tick leaves the range.
 *
 * Call order:
 *   1. initialize(initialCapital)    — during vm.startBroadcast()
 *   2. maybeUpdate(blockNum)         — called by EventReplayer on every block during replay
 *   3. collectFinalFees()            — after replay, before logFinalSummary / getResults
 *   4. logFinalSummary() / getResults() — view, rely on collectFinalFees() having run
 *
 * Capital:
 *   All strategies receive the same initial capital split 50/50 by value.
 *   HODL holds the same token amounts that Full-Range LP actually deployed, so the
 *   HODL vs LP comparison is perfectly apples-to-apples.
 *
 * Fee accounting:
 *   Fixed-Width fees are accumulated at each rebalance's contemporaneous sqrtPriceX96 into
 *   fwTotalFeesToken0.  Full-Range fees are collected once (in collectFinalFees) at the final
 *   price into frTotalFeesToken0.  Both accumulators are used directly by getResults().
 *
 * WARNING — backtesting use only. No access control.
 */
contract BaselineStrategies is IUniswapV3MintCallback {
    using Math for uint256;
    using FormatLib for uint256;
    using FormatLib for int256;

    // -------------------------------------------------------------------------
    // Types
    // -------------------------------------------------------------------------

    /// @notice Packed result for a single strategy, consumed by Reporter.
    struct StrategyResult {
        uint256 initialCapitalToken0;
        uint256 finalValueToken0;
        uint256 feesToken0;
        uint256 rebalances;
        int256 ilToken0;
        int256 netPnLToken0;
        uint256 blocksInRange;
        uint256 totalBlocks;
    }

    // -------------------------------------------------------------------------
    // Constants
    // -------------------------------------------------------------------------

    uint256 internal constant Q96 = 2 ** 96;

    /// @notice Half-width of the fixed-width LP range in ticks (total width = 2×).
    int24 public constant FIXED_WIDTH_TICKS = 2000;

    // -------------------------------------------------------------------------
    // Immutables
    // -------------------------------------------------------------------------

    IUniswapV3Pool public immutable pool;
    MockToken public immutable token0;
    MockToken public immutable token1;
    bool public immutable token0isWeth;
    uint256 public immutable recenterInterval;
    int24 public immutable tickSpacing;

    // -------------------------------------------------------------------------
    // HODL state
    // -------------------------------------------------------------------------

    uint256 public hodlEntryToken0;
    uint256 public hodlEntryToken1;
    uint256 public hodlInitialValueToken0;

    // -------------------------------------------------------------------------
    // Full-Range LP state
    // -------------------------------------------------------------------------

    uint128 public fullRangeLiquidity;
    int24 public frLo;
    int24 public frHi;
    /// @notice Token amounts deployed into the Full-Range position at entry.
    uint256 public frEntryToken0;
    uint256 public frEntryToken1;
    uint256 public frInitialValueToken0;
    /// @notice Raw fee totals (populated by collectFinalFees).
    uint256 public frFees0;
    uint256 public frFees1;
    /// @notice Fees in token0-equivalent units at collection price (set by collectFinalFees).
    uint256 public frTotalFeesToken0;
    uint256 public frTotalBlocks;
    uint256 public frBlocksInRange;

    // -------------------------------------------------------------------------
    // Fixed-Width LP state
    // -------------------------------------------------------------------------

    uint128 public fwLiquidity;
    int24 public fwLo;
    int24 public fwHi;
    /// @notice Entry amounts for the CURRENT open period (reset on each rebalance).
    uint256 public fwPeriodEntryToken0;
    uint256 public fwPeriodEntryToken1;
    uint256 public fwInitialValueToken0;
    /// @notice Raw fee totals accumulated across all periods.
    uint256 public fwFees0;
    uint256 public fwFees1;
    /// @notice Fees accumulated at contemporaneous prices across all closed periods,
    ///         plus the current period fees added by collectFinalFees().
    uint256 public fwTotalFeesToken0;
    uint256 public fwRebalances;
    /// @notice Cumulative IL from closed periods (token0 units, negative = LP underperformed HODL).
    int256 public fwCumulativeIL;
    uint256 public fwTotalBlocks;
    uint256 public fwBlocksInRange;
    uint256 public fwLastCheckBlock;

    // -------------------------------------------------------------------------
    // Common state
    // -------------------------------------------------------------------------

    bool public initialized;
    uint256 public lastUpdateBlock;
    /// @notice First block number observed during replay (set on first maybeUpdate call).
    uint256 public firstUpdateBlock;

    // -------------------------------------------------------------------------
    // Constructor
    // -------------------------------------------------------------------------

    constructor(IUniswapV3Pool _pool, MockToken _token0, MockToken _token1, bool _token0isWeth, uint256 _recenterInterval) {
        pool = _pool;
        token0 = _token0;
        token1 = _token1;
        token0isWeth = _token0isWeth;
        recenterInterval = _recenterInterval;
        tickSpacing = _pool.tickSpacing();
    }

    // -------------------------------------------------------------------------
    // Initialization
    // -------------------------------------------------------------------------

    /**
     * @notice Deploy all three baseline strategies with `initialCapital` token0 equivalent.
     *         Capital is split 50/50 by value: half in token0, the equivalent in token1.
     *         Full-Range LP is deployed first; HODL holds the same token amounts that
     *         Full-Range actually consumed so both strategies start with identical capital.
     */
    function initialize(uint256 initialCapital) external {
        require(!initialized, "BaselineStrategies: already initialized");
        initialized = true;

        (uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0();

        uint256 half0 = initialCapital / 2;
        uint256 half1 = _valueInToken1(half0, sqrtPriceX96);

        // ----- Full-Range LP (deployed first so HODL can mirror its amounts) -----
        {
            int24 lo = _fullRangeLo();
            int24 hi = _fullRangeHi();

            uint128 liq = LiquidityAmounts.getLiquidityForAmounts(
                sqrtPriceX96, TickMath.getSqrtRatioAtTick(lo), TickMath.getSqrtRatioAtTick(hi), half0, half1
            );

            if (liq > 0) {
                (uint256 used0, uint256 used1) = pool.mint(address(this), lo, hi, liq, "");
                fullRangeLiquidity = liq;
                frLo = lo;
                frHi = hi;
                frEntryToken0 = used0;
                frEntryToken1 = used1;
                frInitialValueToken0 = _valueInToken0(used0, used1, sqrtPriceX96);
            }
        }

        // ----- HODL: hold the same amounts Full-Range actually deployed -----
        // Falls back to half0/half1 if FullRange mint produced zero (edge case).
        if (frEntryToken0 > 0 || frEntryToken1 > 0) {
            hodlEntryToken0 = frEntryToken0;
            hodlEntryToken1 = frEntryToken1;
            hodlInitialValueToken0 = frInitialValueToken0;
        } else {
            hodlEntryToken0 = half0;
            hodlEntryToken1 = half1;
            hodlInitialValueToken0 = _valueInToken0(half0, half1, sqrtPriceX96);
        }

        // ----- Fixed-Width LP -----
        {
            (int24 lo, int24 hi) = _computeFixedRange(currentTick);

            uint128 liq = LiquidityAmounts.getLiquidityForAmounts(
                sqrtPriceX96, TickMath.getSqrtRatioAtTick(lo), TickMath.getSqrtRatioAtTick(hi), half0, half1
            );

            if (liq > 0) {
                (uint256 used0, uint256 used1) = pool.mint(address(this), lo, hi, liq, "");
                fwLiquidity = liq;
                fwLo = lo;
                fwHi = hi;
                fwPeriodEntryToken0 = used0;
                fwPeriodEntryToken1 = used1;
                fwInitialValueToken0 = _valueInToken0(used0, used1, sqrtPriceX96);
            }
            fwLastCheckBlock = block.number;
        }

        console2.log("=== Baseline Strategies Initialized ===");
        console2.log(
            string.concat(
                "HODL:       entry=[",
                hodlEntryToken0.str(),
                ",",
                hodlEntryToken1.str(),
                "] initialValue=",
                hodlInitialValueToken0.str()
            )
        );
        console2.log(
            string.concat(
                "FullRange:  liq=",
                uint256(fullRangeLiquidity).str(),
                " ticks=[",
                int256(frLo).istr(),
                ",",
                int256(frHi).istr(),
                "] entry=[",
                frEntryToken0.str(),
                ",",
                frEntryToken1.str(),
                "]"
            )
        );
        console2.log(
            string.concat(
                "FixedWidth: liq=",
                uint256(fwLiquidity).str(),
                " ticks=[",
                int256(fwLo).istr(),
                ",",
                int256(fwHi).istr(),
                "] entry=[",
                fwPeriodEntryToken0.str(),
                ",",
                fwPeriodEntryToken1.str(),
                "]"
            )
        );
    }

    // -------------------------------------------------------------------------
    // Per-block update (called by EventReplayer after each block advance)
    // -------------------------------------------------------------------------

    /**
     * @notice Update time-in-range counters and trigger FixedWidth rebalancing when needed.
     * @param blockNum Current block number as advanced by EventReplayer.
     */
    function maybeUpdate(uint256 blockNum) external {
        if (!initialized) return;
        if (blockNum == lastUpdateBlock) return;
        lastUpdateBlock = blockNum;

        // Record the first block seen during replay for accurate period reporting.
        if (firstUpdateBlock == 0) firstUpdateBlock = blockNum;

        (, int24 currentTick,,,,,) = pool.slot0();

        // Full-Range: always in range.
        frTotalBlocks++;
        frBlocksInRange++;

        // Fixed-Width: track in-range blocks.
        fwTotalBlocks++;
        if (fwLiquidity > 0 && currentTick >= fwLo && currentTick < fwHi) {
            fwBlocksInRange++;
        }

        // Fixed-Width rebalance: check every recenterInterval blocks.
        if (fwLiquidity > 0 && blockNum >= fwLastCheckBlock + recenterInterval) {
            fwLastCheckBlock = blockNum;
            if (currentTick < fwLo || currentTick >= fwHi) {
                _rebalanceFixedWidth(currentTick);
            }
        }
    }

    // -------------------------------------------------------------------------
    // Post-replay fee collection (MUST be called before logFinalSummary / getResults)
    // -------------------------------------------------------------------------

    /**
     * @notice Materialize accrued fees for the still-open Full-Range and Fixed-Width positions.
     *         Uses pool.burn(lo, hi, 0) to trigger fee accrual without removing liquidity,
     *         then pool.collect() to sweep fees to this contract.
     *
     *         Call once after replay ends and before any summary/reporting functions.
     *         Idempotent if positions have zero pending fees.
     */
    function collectFinalFees() external {
        (uint160 sqrtPriceX96,,,,,,) = pool.slot0();

        // ----- Full-Range: single collection at end-of-replay price -----
        if (fullRangeLiquidity > 0) {
            pool.burn(frLo, frHi, 0); // trigger fee accrual; no liquidity removed
            (uint128 f0, uint128 f1) = pool.collect(address(this), frLo, frHi, type(uint128).max, type(uint128).max);
            frFees0 += uint256(f0);
            frFees1 += uint256(f1);
            // Value all Full-Range fees at current price (only one collection, so no
            // historical repricing concern for this strategy).
            frTotalFeesToken0 = _valueInToken0(frFees0, frFees1, sqrtPriceX96);
        }

        // ----- Fixed-Width: current open period fees (historical fees already in fwTotalFeesToken0) -----
        if (fwLiquidity > 0) {
            pool.burn(fwLo, fwHi, 0); // trigger fee accrual; no liquidity removed
            (uint128 f0, uint128 f1) = pool.collect(address(this), fwLo, fwHi, type(uint128).max, type(uint128).max);
            fwFees0 += uint256(f0);
            fwFees1 += uint256(f1);
            // Add current-period fees at current price to the historical-price accumulator.
            fwTotalFeesToken0 += _valueInToken0(uint256(f0), uint256(f1), sqrtPriceX96);
        }
    }

    // -------------------------------------------------------------------------
    // Final summary (view — requires collectFinalFees() to have been called first)
    // -------------------------------------------------------------------------

    /**
     * @notice Log final metrics for all three strategies. Call once after collectFinalFees().
     */
    function logFinalSummary() external view {
        (uint160 sqrtPriceX96,,,,,,) = pool.slot0();

        // ----- HODL -----
        uint256 hodlFinalValue = _valueInToken0(hodlEntryToken0, hodlEntryToken1, sqrtPriceX96);
        int256 hodlNetPnL = int256(hodlFinalValue) - int256(hodlInitialValueToken0);
        console2.log("[BASELINE][HODL][SUMMARY] === HODL ===");
        console2.log(
            string.concat(
                "[BASELINE][HODL][SUMMARY] initialValue=",
                hodlInitialValueToken0.str(),
                " finalValue=",
                hodlFinalValue.str(),
                " netPnL=",
                hodlNetPnL.istr()
            )
        );

        // ----- Full-Range LP -----
        {
            (uint256 fr0, uint256 fr1) = _positionAmounts(fullRangeLiquidity, frLo, frHi, sqrtPriceX96);
            uint256 frFinalPos = _valueInToken0(fr0, fr1, sqrtPriceX96);
            // frTotalFeesToken0 populated by collectFinalFees().
            uint256 frHodlVal = _valueInToken0(frEntryToken0, frEntryToken1, sqrtPriceX96);
            int256 frIL = int256(frFinalPos) - int256(frHodlVal);
            int256 frNetPnL = frIL + int256(frTotalFeesToken0);
            uint256 frFinalValue = frFinalPos + frTotalFeesToken0;
            uint256 frTIR = frTotalBlocks > 0 ? (frBlocksInRange * 10_000) / frTotalBlocks : 10_000;
            console2.log("[BASELINE][FR][SUMMARY] === Full-Range LP ===");
            console2.log(
                string.concat(
                    "[BASELINE][FR][SUMMARY] initialValue=",
                    frInitialValueToken0.str(),
                    " finalValue=",
                    frFinalValue.str(),
                    " feesToken0=",
                    frTotalFeesToken0.str(),
                    " IL=",
                    frIL.istr(),
                    " netPnL=",
                    frNetPnL.istr(),
                    " timeInRange=",
                    frTIR.str(),
                    " bps"
                )
            );
        }

        // ----- Fixed-Width LP -----
        {
            (uint256 fw0, uint256 fw1) = _positionAmounts(fwLiquidity, fwLo, fwHi, sqrtPriceX96);
            uint256 fwFinalPos = _valueInToken0(fw0, fw1, sqrtPriceX96);
            // fwTotalFeesToken0: historical periods at contemporaneous prices + current period at current
            // price (added by collectFinalFees()).
            uint256 fwFinalValue = fwFinalPos + fwTotalFeesToken0;
            // Current period IL (open position vs holding current period entry amounts).
            uint256 fwPeriodHodlVal = _valueInToken0(fwPeriodEntryToken0, fwPeriodEntryToken1, sqrtPriceX96);
            int256 fwPeriodIL = int256(fwFinalPos) - int256(fwPeriodHodlVal);
            int256 fwTotalIL = fwCumulativeIL + fwPeriodIL;
            int256 fwNetPnL = fwTotalIL + int256(fwTotalFeesToken0);
            uint256 fwTIR = fwTotalBlocks > 0 ? (fwBlocksInRange * 10_000) / fwTotalBlocks : 0;
            console2.log("[BASELINE][FW][SUMMARY] === Fixed-Width LP ===");
            console2.log(
                string.concat(
                    "[BASELINE][FW][SUMMARY] initialValue=",
                    fwInitialValueToken0.str(),
                    " finalValue=",
                    fwFinalValue.str(),
                    " feesToken0=",
                    fwTotalFeesToken0.str(),
                    " cumulativeIL=",
                    fwCumulativeIL.istr(),
                    " currentPeriodIL=",
                    fwPeriodIL.istr(),
                    " totalIL=",
                    fwTotalIL.istr(),
                    " netPnL=",
                    fwNetPnL.istr(),
                    " rebalances=",
                    fwRebalances.str(),
                    " timeInRange=",
                    fwTIR.str(),
                    " bps"
                )
            );
        }
    }

    /**
     * @notice Return final computed metrics for each strategy. Used by Reporter.
     * @dev Requires collectFinalFees() to have been called first so fee accumulators are complete.
     *      finalValueToken0 = positionValue + feesToken0 (both non-negative, no underflow risk).
     */
    function getResults()
        external
        view
        returns (StrategyResult memory hodlResult, StrategyResult memory frResult, StrategyResult memory fwResult)
    {
        (uint160 sqrtPriceX96,,,,,,) = pool.slot0();

        // ----- HODL -----
        uint256 hodlFinalValue = _valueInToken0(hodlEntryToken0, hodlEntryToken1, sqrtPriceX96);
        hodlResult = StrategyResult({
            initialCapitalToken0: hodlInitialValueToken0,
            finalValueToken0: hodlFinalValue,
            feesToken0: 0,
            rebalances: 0,
            ilToken0: 0,
            netPnLToken0: int256(hodlFinalValue) - int256(hodlInitialValueToken0),
            blocksInRange: 0,
            totalBlocks: 0
        });

        // ----- Full-Range LP -----
        {
            (uint256 fr0, uint256 fr1) = _positionAmounts(fullRangeLiquidity, frLo, frHi, sqrtPriceX96);
            uint256 frPosValue = _valueInToken0(fr0, fr1, sqrtPriceX96);
            // finalValue = position value + fees (always non-negative).
            uint256 frFinalValue = frPosValue + frTotalFeesToken0;
            uint256 frHodlVal = _valueInToken0(frEntryToken0, frEntryToken1, sqrtPriceX96);
            int256 frIL = int256(frPosValue) - int256(frHodlVal);
            int256 frNetPnL = int256(frFinalValue) - int256(frInitialValueToken0);
            frResult = StrategyResult({
                initialCapitalToken0: frInitialValueToken0,
                finalValueToken0: frFinalValue,
                feesToken0: frTotalFeesToken0,
                rebalances: 0,
                ilToken0: frIL,
                netPnLToken0: frNetPnL,
                blocksInRange: frBlocksInRange,
                totalBlocks: frTotalBlocks
            });
        }

        // ----- Fixed-Width LP -----
        {
            (uint256 fw0, uint256 fw1) = _positionAmounts(fwLiquidity, fwLo, fwHi, sqrtPriceX96);
            uint256 fwPosValue = _valueInToken0(fw0, fw1, sqrtPriceX96);
            // finalValue = position value + all fees (always non-negative).
            uint256 fwFinalValue = fwPosValue + fwTotalFeesToken0;
            // Current period IL.
            uint256 fwPeriodHodlVal = _valueInToken0(fwPeriodEntryToken0, fwPeriodEntryToken1, sqrtPriceX96);
            int256 fwPeriodIL = int256(fwPosValue) - int256(fwPeriodHodlVal);
            int256 fwTotalIL = fwCumulativeIL + fwPeriodIL;
            int256 fwNetPnL = int256(fwFinalValue) - int256(fwInitialValueToken0);
            fwResult = StrategyResult({
                initialCapitalToken0: fwInitialValueToken0,
                finalValueToken0: fwFinalValue,
                feesToken0: fwTotalFeesToken0,
                rebalances: fwRebalances,
                ilToken0: fwTotalIL,
                netPnLToken0: fwNetPnL,
                blocksInRange: fwBlocksInRange,
                totalBlocks: fwTotalBlocks
            });
        }
    }

    // -------------------------------------------------------------------------
    // Uniswap V3 mint callback
    // -------------------------------------------------------------------------

    /// @inheritdoc IUniswapV3MintCallback
    function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external override {
        require(msg.sender == address(pool), "BaselineStrategies: bad mint callback");
        if (amount0Owed > 0) {
            uint256 have = token0.balanceOf(address(this));
            if (have < amount0Owed) token0.mint(address(this), amount0Owed - have);
            token0.transfer(address(pool), amount0Owed);
        }
        if (amount1Owed > 0) {
            uint256 have = token1.balanceOf(address(this));
            if (have < amount1Owed) token1.mint(address(this), amount1Owed - have);
            token1.transfer(address(pool), amount1Owed);
        }
    }

    // -------------------------------------------------------------------------
    // Internal: FixedWidth rebalancing
    // -------------------------------------------------------------------------

    function _rebalanceFixedWidth(int24 currentTick) internal {
        (uint160 sqrtPriceX96,,,,,,) = pool.slot0();

        // Capture old range before any mutation — used in the log line below.
        int24 oldLo = fwLo;
        int24 oldHi = fwHi;

        // Burn current position and collect principal + fees.
        (uint256 p0, uint256 p1) = pool.burn(fwLo, fwHi, fwLiquidity);
        (uint128 c0, uint128 c1) = pool.collect(address(this), fwLo, fwHi, type(uint128).max, type(uint128).max);

        // fees = collected - principal
        uint256 f0 = uint256(c0) > p0 ? uint256(c0) - p0 : 0;
        uint256 f1 = uint256(c1) > p1 ? uint256(c1) - p1 : 0;
        fwFees0 += f0;
        fwFees1 += f1;
        // Accumulate at contemporaneous price (more accurate than repricing at end).
        fwTotalFeesToken0 += _valueInToken0(f0, f1, sqrtPriceX96);

        // IL for this closed period: LP exit value vs holding entry amounts at exit price.
        uint256 exitVal = _valueInToken0(p0, p1, sqrtPriceX96);
        uint256 hodlVal = _valueInToken0(fwPeriodEntryToken0, fwPeriodEntryToken1, sqrtPriceX96);
        fwCumulativeIL += int256(exitVal) - int256(hodlVal);

        fwRebalances++;
        fwLiquidity = 0;

        // Deploy new position centered around current tick.
        (int24 newLo, int24 newHi) = _computeFixedRange(currentTick);
        uint256 avail0 = uint256(c0);
        uint256 avail1 = uint256(c1);

        uint128 newLiq = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96, TickMath.getSqrtRatioAtTick(newLo), TickMath.getSqrtRatioAtTick(newHi), avail0, avail1
        );

        if (newLiq > 0) {
            (uint256 used0, uint256 used1) = pool.mint(address(this), newLo, newHi, newLiq, "");
            fwLiquidity = newLiq;
            fwLo = newLo;
            fwHi = newHi;
            fwPeriodEntryToken0 = used0;
            fwPeriodEntryToken1 = used1;
        }

        // Log uses oldLo/oldHi captured before fwLo/fwHi were overwritten.
        console2.log(
            string.concat(
                "[BASELINE][FW][REBALANCE] #",
                fwRebalances.str(),
                " tick=",
                int256(currentTick).istr(),
                " oldRange=[",
                int256(oldLo).istr(),
                ",",
                int256(oldHi).istr(),
                "] newRange=[",
                int256(newLo).istr(),
                ",",
                int256(newHi).istr(),
                "] fees0=",
                f0.str(),
                " fees1=",
                f1.str()
            )
        );
    }

    // -------------------------------------------------------------------------
    // Internal: Uniswap V3 math helpers
    // -------------------------------------------------------------------------

    function _positionAmounts(
        uint128 liquidity,
        int24 tickLower,
        int24 tickUpper,
        uint160 sqrtPriceX96
    )
        internal
        pure
        returns (uint256 amount0, uint256 amount1)
    {
        if (liquidity == 0) return (0, 0);
        uint160 sqrtLow = TickMath.getSqrtRatioAtTick(tickLower);
        uint160 sqrtHigh = TickMath.getSqrtRatioAtTick(tickUpper);
        (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtLow, sqrtHigh, liquidity);
    }

    /**
     * @notice Convert (amount0, amount1) to token0-equivalent units.
     *         value = amount0 + amount1 × Q96² / sqrtPriceX96²
     */
    function _valueInToken0(uint256 amount0, uint256 amount1, uint160 sqrtPriceX96) internal pure returns (uint256) {
        if (sqrtPriceX96 == 0 || amount1 == 0) return amount0;
        uint256 amt1InT0 = Math.mulDiv(Math.mulDiv(amount1, Q96, uint256(sqrtPriceX96)), Q96, uint256(sqrtPriceX96));
        return amount0 + amt1InT0;
    }

    /**
     * @notice Convert a token0 amount to its token1 equivalent at the current price.
     *         amount1 = amount0 × sqrtPriceX96² / Q96²
     */
    function _valueInToken1(uint256 amount0, uint160 sqrtPriceX96) internal pure returns (uint256) {
        if (sqrtPriceX96 == 0 || amount0 == 0) return 0;
        return Math.mulDiv(Math.mulDiv(amount0, uint256(sqrtPriceX96), Q96), uint256(sqrtPriceX96), Q96);
    }

    // -------------------------------------------------------------------------
    // Internal: tick range helpers
    // -------------------------------------------------------------------------

    /// @notice Compute the full-range lower tick (smallest valid tick aligned to tickSpacing).
    function _fullRangeLo() internal view returns (int24) {
        // Solidity truncation toward zero = ceiling for negatives → smallest valid multiple >= MIN_TICK.
        return (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
    }

    /// @notice Compute the full-range upper tick (largest valid tick aligned to tickSpacing).
    function _fullRangeHi() internal view returns (int24) {
        return (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
    }

    /**
     * @notice Compute ±FIXED_WIDTH_TICKS range centered on `currentTick`, aligned to tickSpacing.
     *         lo is aligned DOWN (floor); hi is aligned UP (ceiling).
     *         Result is clamped to the full-range bounds.
     */
    function _computeFixedRange(int24 currentTick) internal view returns (int24 lo, int24 hi) {
        int24 rawLo = currentTick - FIXED_WIDTH_TICKS;
        int24 rawHi = currentTick + FIXED_WIDTH_TICKS;

        // Floor for lo (align down toward more-negative).
        lo = (rawLo / tickSpacing) * tickSpacing;
        if (rawLo < 0 && rawLo % tickSpacing != 0) lo -= tickSpacing;

        // Ceiling for hi (align up toward more-positive).
        hi = (rawHi / tickSpacing) * tickSpacing;
        if (rawHi > 0 && rawHi % tickSpacing != 0) hi += tickSpacing;

        // Clamp to valid pool range.
        int24 minValid = _fullRangeLo();
        int24 maxValid = _fullRangeHi();
        if (lo < minValid) lo = minValid;
        if (hi > maxValid) hi = maxValid;
        if (lo >= hi) {
            lo = minValid;
            hi = maxValid;
        }
    }
}
