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

import { FormatLib } from "./FormatLib.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 { console2 } from "forge-std/console2.sol";

/**
 * @title PositionTracker
 * @notice Tracks KrAIken's three-position lifecycle and computes P&L metrics
 *         for backtesting analysis.
 *
 * For each position lifecycle (open → close on next recenter), records:
 *   - Tick range (tickLower, tickUpper) and liquidity
 *   - Entry/exit block and timestamp
 *   - Token amounts at entry vs exit (via Uniswap V3 LiquidityAmounts math)
 *   - Fees earned — attributed only to positions whose tick range contained the
 *     active tick at the time of close (the only positions that could have been
 *     accruing fees). Falls back to raw-liquidity weighting if no position is
 *     in range at close time (rare edge case).
 *   - Impermanent loss vs holding the initial token amounts
 *   - Net P&L = fees value + IL  (IL is negative when LP underperforms HODL)
 *
 * Aggregate metrics:
 *   - Total fees (token0 + token1 raw, and token0-equivalent)
 *   - Cumulative IL and net P&L (token0 units)
 *   - Rebalance count
 *   - Time in range: % of notified blocks where Anchor tick range contains current tick
 *   - Capital efficiency accumulators (feesToken0 / liqBlocks) for offline calculation
 *
 * All output lines carry a [TRACKER][TYPE] prefix for downstream parseability.
 * Cumulative P&L is logged every CUMULATIVE_LOG_INTERVAL blocks.
 *
 * Not a Script — no vm access. Uses console2 for output only.
 *
 * WARNING — backtesting use only: `recordRecenter` has no access control. Do not
 * reuse this contract in production contexts where untrusted callers exist.
 */
contract PositionTracker {
    using Math for uint256;
    using FormatLib for uint256;
    using FormatLib for int256;

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

    /**
     * @notice Snapshot of all three KrAIken positions at a point in time.
     * @dev Stage ordinals: 0 = FLOOR, 1 = ANCHOR, 2 = DISCOVERY.
     */
    struct PositionSnapshot {
        uint128 floorLiq;
        int24 floorLo;
        int24 floorHi;
        uint128 anchorLiq;
        int24 anchorLo;
        int24 anchorHi;
        uint128 discLiq;
        int24 discLo;
        int24 discHi;
    }

    struct OpenPosition {
        int24 tickLower;
        int24 tickUpper;
        uint128 liquidity;
        uint256 entryBlock;
        uint256 entryTimestamp;
        uint256 entryAmount0;
        uint256 entryAmount1;
        bool active;
    }

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

    uint256 internal constant Q96 = 2 ** 96;

    /// @notice Blocks between cumulative P&L log lines.
    uint256 public constant CUMULATIVE_LOG_INTERVAL = 500;

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

    IUniswapV3Pool public immutable pool;

    /// @notice True when pool token0 is WETH; affects fees0/fees1 mapping.
    bool public immutable token0isWeth;

    // -------------------------------------------------------------------------
    // Open position state (indexed 0=FLOOR, 1=ANCHOR, 2=DISCOVERY)
    // -------------------------------------------------------------------------

    OpenPosition[3] public openPositions;

    // -------------------------------------------------------------------------
    // Cumulative aggregate metrics
    // -------------------------------------------------------------------------

    uint256 public totalFees0;
    uint256 public totalFees1;
    uint256 public rebalanceCount;

    /// @notice Cumulative IL across all closed positions in token0 units.
    ///         Negative means LP underperformed HODL.
    int256 public totalILToken0;

    /// @notice Cumulative net P&L = IL + fees value (token0 units).
    int256 public totalNetPnLToken0;

    // Time-in-range (Anchor position).
    uint256 public blocksChecked;
    uint256 public blocksAnchorInRange;
    uint256 public lastNotifiedBlock;

    // Capital efficiency accumulators.
    /// @notice Cumulative fees expressed in token0 units.
    uint256 public totalFeesToken0;
    /// @notice Sum of (liquidity × blocksOpen) for all closed positions.
    uint256 public totalLiquidityBlocks;

    // -------------------------------------------------------------------------
    // Internal
    // -------------------------------------------------------------------------

    uint256 internal _lastCumulativeLogBlock;

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

    constructor(IUniswapV3Pool _pool, bool _token0isWeth) {
        pool = _pool;
        token0isWeth = _token0isWeth;
    }

    // -------------------------------------------------------------------------
    // Integration API (called by StrategyExecutor)
    // -------------------------------------------------------------------------

    /**
     * @notice Notify the tracker that a new block has been observed.
     *         Updates Anchor time-in-range and emits cumulative P&L lines at
     *         every CUMULATIVE_LOG_INTERVAL blocks.
     * @param blockNum Block number as advanced by EventReplayer.
     */
    function notifyBlock(uint256 blockNum) external {
        if (blockNum == lastNotifiedBlock) return;
        lastNotifiedBlock = blockNum;

        // Track Anchor time-in-range.
        OpenPosition storage anchor = openPositions[1]; // ANCHOR = 1
        if (anchor.active) {
            (, int24 tick,,,,,) = pool.slot0();
            blocksChecked++;
            if (tick >= anchor.tickLower && tick < anchor.tickUpper) {
                blocksAnchorInRange++;
            }
        }

        // Log cumulative P&L at regular intervals.
        if (_lastCumulativeLogBlock == 0) {
            _lastCumulativeLogBlock = blockNum;
        } else if (blockNum - _lastCumulativeLogBlock >= CUMULATIVE_LOG_INTERVAL) {
            _logCumulative(blockNum);
            _lastCumulativeLogBlock = blockNum;
        }
    }

    /**
     * @notice Record a successful recenter: close old positions, open new ones.
     *
     * Fee attribution: fees are distributed only to positions whose tick range
     * contained the active tick at the moment of close — the only positions that
     * could have been accruing fees in Uniswap V3. When no position is in range
     * at close time (rare), attribution falls back to raw liquidity weighting and
     * a warning is logged.
     *
     * @dev No access control — this contract is backtesting-only tooling.
     * @param oldPos     Pre-recenter snapshot (positions being burned).
     * @param newPos     Post-recenter snapshot (positions being minted).
     * @param feesWeth   Total WETH fees collected across all positions this recenter.
     * @param feesKrk    Total KRK fees collected across all positions this recenter.
     * @param blockNum   Block number of the recenter.
     * @param timestamp  Block timestamp of the recenter.
     */
    function recordRecenter(
        PositionSnapshot calldata oldPos,
        PositionSnapshot calldata newPos,
        uint256 feesWeth,
        uint256 feesKrk,
        uint256 blockNum,
        uint256 timestamp
    )
        external
    {
        (uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0();

        // Map WETH/KRK fees to pool token0/token1 based on pool ordering.
        uint256 fees0 = token0isWeth ? feesWeth : feesKrk;
        uint256 fees1 = token0isWeth ? feesKrk : feesWeth;

        totalFees0 += fees0;
        totalFees1 += fees1;
        totalFeesToken0 += _valueInToken0(fees0, fees1, sqrtPriceX96);
        rebalanceCount++;

        // Uniswap V3 only accrues fees to a position when the active tick is
        // inside its range. Weight fee attribution by in-range liquidity only.
        uint256 fFloorWeight = _inRangeLiq(oldPos.floorLiq, oldPos.floorLo, oldPos.floorHi, currentTick);
        uint256 fAnchorWeight = _inRangeLiq(oldPos.anchorLiq, oldPos.anchorLo, oldPos.anchorHi, currentTick);
        uint256 fDiscWeight = _inRangeLiq(oldPos.discLiq, oldPos.discLo, oldPos.discHi, currentTick);
        uint256 totalFeeWeight = fFloorWeight + fAnchorWeight + fDiscWeight;

        // Fallback to raw-liquidity weights when no position covers the current tick.
        if (totalFeeWeight == 0) {
            console2.log("[TRACKER][WARN] no position in range at close: fee attribution falling back to raw liquidity");
            fFloorWeight = oldPos.floorLiq;
            fAnchorWeight = oldPos.anchorLiq;
            fDiscWeight = oldPos.discLiq;
            totalFeeWeight = uint256(oldPos.floorLiq) + uint256(oldPos.anchorLiq) + uint256(oldPos.discLiq);
        }

        // Close old positions, passing per-position fee weights.
        _closePosition(0, oldPos.floorLiq, oldPos.floorLo, oldPos.floorHi, fees0, fees1, fFloorWeight, totalFeeWeight, sqrtPriceX96, blockNum);
        _closePosition(1, oldPos.anchorLiq, oldPos.anchorLo, oldPos.anchorHi, fees0, fees1, fAnchorWeight, totalFeeWeight, sqrtPriceX96, blockNum);
        _closePosition(2, oldPos.discLiq, oldPos.discLo, oldPos.discHi, fees0, fees1, fDiscWeight, totalFeeWeight, sqrtPriceX96, blockNum);

        // Open new positions.
        _openPosition(0, newPos.floorLiq, newPos.floorLo, newPos.floorHi, sqrtPriceX96, blockNum, timestamp);
        _openPosition(1, newPos.anchorLiq, newPos.anchorLo, newPos.anchorHi, sqrtPriceX96, blockNum, timestamp);
        _openPosition(2, newPos.discLiq, newPos.discLo, newPos.discHi, sqrtPriceX96, blockNum, timestamp);
    }

    // -------------------------------------------------------------------------
    // View: final computed metrics (consumed by Reporter)
    // -------------------------------------------------------------------------

    struct FinalData {
        int256 finalIL;
        int256 finalNetPnL;
        uint256 finalFeesToken0;
        uint256 rebalances;
        uint256 blocksInRange;
        uint256 totalBlocks;
    }

    /**
     * @notice Compute final aggregate metrics including open-position IL.
     *         Returns the same values that logFinalSummary() logs.
     */
    function getFinalData() external view returns (FinalData memory d) {
        (uint160 sqrtPriceX96,,,,,,) = pool.slot0();
        d.finalIL = totalILToken0;
        d.finalNetPnL = totalNetPnLToken0;
        d.finalFeesToken0 = totalFeesToken0;
        d.rebalances = rebalanceCount;
        d.blocksInRange = blocksAnchorInRange;
        d.totalBlocks = blocksChecked;

        for (uint8 i = 0; i < 3; i++) {
            OpenPosition storage pos = openPositions[i];
            if (!pos.active) continue;
            (uint256 exitAmt0, uint256 exitAmt1) = _positionAmounts(pos.liquidity, pos.tickLower, pos.tickUpper, sqrtPriceX96);
            int256 il = _computeIL(pos.entryAmount0, pos.entryAmount1, exitAmt0, exitAmt1, sqrtPriceX96);
            d.finalIL += il;
            d.finalNetPnL += il; // no fees for unclosed positions
        }
    }

    /**
     * @notice Log the final aggregate summary. Call once at the end of replay.
     * @param blockNum Final block number (for context in the summary line).
     */
    function logFinalSummary(uint256 blockNum) external view {
        // Compute incremental IL from still-open positions without mutating state.
        (uint160 sqrtPriceX96,,,,,,) = pool.slot0();
        int256 finalIL = totalILToken0;
        int256 finalNetPnL = totalNetPnLToken0;
        for (uint8 i = 0; i < 3; i++) {
            OpenPosition storage pos = openPositions[i];
            if (!pos.active) continue;
            (uint256 exitAmt0, uint256 exitAmt1) = _positionAmounts(pos.liquidity, pos.tickLower, pos.tickUpper, sqrtPriceX96);
            int256 il = _computeIL(pos.entryAmount0, pos.entryAmount1, exitAmt0, exitAmt1, sqrtPriceX96);
            finalIL += il;
            finalNetPnL += il; // no fees for unclosed positions
        }

        uint256 timeInRangeBps = blocksChecked > 0 ? (blocksAnchorInRange * 10_000) / blocksChecked : 0;

        console2.log("[TRACKER][SUMMARY] === Final P&L Summary ===");
        console2.log(string.concat("[TRACKER][SUMMARY] finalBlock=", blockNum.str()));
        console2.log(string.concat("[TRACKER][SUMMARY] rebalances=", rebalanceCount.str()));
        console2.log(
            string.concat("[TRACKER][SUMMARY] totalFees0=", totalFees0.str(), " totalFees1=", totalFees1.str(), " totalFeesToken0=", totalFeesToken0.str())
        );
        console2.log(string.concat("[TRACKER][SUMMARY] totalIL=", finalIL.istr(), " netPnL=", finalNetPnL.istr(), " (token0 units)"));
        console2.log(
            string.concat(
                "[TRACKER][SUMMARY] timeInRange=", timeInRangeBps.str(), " bps blocksAnchorInRange=", blocksAnchorInRange.str(), "/", blocksChecked.str()
            )
        );
        console2.log(string.concat("[TRACKER][SUMMARY] capitalEff: feesToken0=", totalFeesToken0.str(), " liqBlocks=", totalLiquidityBlocks.str()));
    }

    // -------------------------------------------------------------------------
    // Internal: position lifecycle
    // -------------------------------------------------------------------------

    function _openPosition(
        uint8 stageIdx,
        uint128 liquidity,
        int24 tickLower,
        int24 tickUpper,
        uint160 sqrtPriceX96,
        uint256 blockNum,
        uint256 timestamp
    )
        internal
    {
        if (liquidity == 0) return;
        (uint256 amt0, uint256 amt1) = _positionAmounts(liquidity, tickLower, tickUpper, sqrtPriceX96);
        openPositions[stageIdx] = OpenPosition({
            tickLower: tickLower,
            tickUpper: tickUpper,
            liquidity: liquidity,
            entryBlock: blockNum,
            entryTimestamp: timestamp,
            entryAmount0: amt0,
            entryAmount1: amt1,
            active: true
        });
        console2.log(
            string.concat(
                "[TRACKER][OPEN] stage=",
                _stageName(stageIdx),
                " block=",
                blockNum.str(),
                " ts=",
                timestamp.str(),
                " tick=[",
                int256(tickLower).istr(),
                ",",
                int256(tickUpper).istr(),
                "] liq=",
                uint256(liquidity).str(),
                " amt0=",
                amt0.str(),
                " amt1=",
                amt1.str()
            )
        );
    }

    function _closePosition(
        uint8 stageIdx,
        uint128 liquidity,
        int24 tickLower,
        int24 tickUpper,
        uint256 fees0Total,
        uint256 fees1Total,
        uint256 posWeight, // fee weight for this position (0 = out-of-range)
        uint256 totalWeight, // sum of fee weights across all positions
        uint160 sqrtPriceX96,
        uint256 blockNum
    )
        internal
    {
        if (liquidity == 0) return;

        OpenPosition storage pos = openPositions[stageIdx];
        if (!pos.active) {
            // First recenter: no prior open record exists (LM deployed with positions
            // already placed before tracking began). Log a warning so the gap is visible.
            console2.log(
                string.concat("[TRACKER][WARN] stage=", _stageName(stageIdx), " close skipped at block=", blockNum.str(), " (no open record: first recenter)")
            );
            return;
        }

        // Guard: the incoming snapshot must match the stored open record.
        // A mismatch would mean IL is computed across mismatched tick ranges.
        require(tickLower == pos.tickLower && tickUpper == pos.tickUpper, "PositionTracker: tick range mismatch");

        (uint256 exitAmt0, uint256 exitAmt1) = _positionAmounts(liquidity, tickLower, tickUpper, sqrtPriceX96);

        // Attribute fees proportional to this position's in-range weight.
        uint256 posLiq = uint256(liquidity);
        uint256 myFees0 = totalWeight > 0 ? fees0Total.mulDiv(posWeight, totalWeight) : 0;
        uint256 myFees1 = totalWeight > 0 ? fees1Total.mulDiv(posWeight, totalWeight) : 0;

        // IL = LP exit value (ex-fees) − HODL value at exit price (both in token0 units).
        int256 il = _computeIL(pos.entryAmount0, pos.entryAmount1, exitAmt0, exitAmt1, sqrtPriceX96);
        int256 feesToken0 = int256(_valueInToken0(myFees0, myFees1, sqrtPriceX96));
        int256 netPnL = il + feesToken0;

        totalILToken0 += il;
        totalNetPnLToken0 += netPnL;

        uint256 blocksOpen = blockNum > pos.entryBlock ? blockNum - pos.entryBlock : 0;
        totalLiquidityBlocks += posLiq * blocksOpen;

        console2.log(
            string.concat(
                "[TRACKER][CLOSE] stage=",
                _stageName(stageIdx),
                " block=",
                blockNum.str(),
                " entryBlock=",
                pos.entryBlock.str(),
                " tick=[",
                int256(tickLower).istr(),
                ",",
                int256(tickUpper).istr(),
                "] liq=",
                posLiq.str()
            )
        );
        console2.log(
            string.concat(
                "[TRACKER][CLOSE] entryAmt0=",
                pos.entryAmount0.str(),
                " entryAmt1=",
                pos.entryAmount1.str(),
                " exitAmt0=",
                exitAmt0.str(),
                " exitAmt1=",
                exitAmt1.str(),
                " fees0=",
                myFees0.str(),
                " fees1=",
                myFees1.str()
            )
        );
        console2.log(string.concat("[TRACKER][CLOSE] IL=", il.istr(), " netPnL=", netPnL.istr(), " (token0 units)"));

        delete openPositions[stageIdx];
    }

    function _logCumulative(uint256 blockNum) internal view {
        uint256 timeInRangeBps = blocksChecked > 0 ? (blocksAnchorInRange * 10_000) / blocksChecked : 0;
        console2.log(
            string.concat(
                "[TRACKER][CUMULATIVE] block=",
                blockNum.str(),
                " rebalances=",
                rebalanceCount.str(),
                " totalFees0=",
                totalFees0.str(),
                " totalFees1=",
                totalFees1.str(),
                " IL=",
                totalILToken0.istr(),
                " netPnL=",
                totalNetPnLToken0.istr(),
                " timeInRange=",
                timeInRangeBps.str(),
                " bps"
            )
        );
    }

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

    /**
     * @notice Return `liquidity` if currentTick is inside [tickLo, tickHi), else 0.
     *         Used to attribute fees only to positions that were potentially in range.
     */
    function _inRangeLiq(uint128 liquidity, int24 tickLo, int24 tickHi, int24 currentTick) internal pure returns (uint256) {
        if (liquidity == 0) return 0;
        return (currentTick >= tickLo && currentTick < tickHi) ? uint256(liquidity) : 0;
    }

    /**
     * @notice Compute (amount0, amount1) for a liquidity position at the given sqrt price.
     */
    function _positionAmounts(
        uint128 liquidity,
        int24 tickLower,
        int24 tickUpper,
        uint160 sqrtPriceX96
    )
        internal
        pure
        returns (uint256 amount0, uint256 amount1)
    {
        uint160 sqrtRatioLow = TickMath.getSqrtRatioAtTick(tickLower);
        uint160 sqrtRatioHigh = TickMath.getSqrtRatioAtTick(tickUpper);
        (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtRatioLow, sqrtRatioHigh, liquidity);
    }

    /**
     * @notice Impermanent loss for a position close (token0 units).
     *         IL = lpExitValue − hodlValue at the exit price.
     *         Negative when LP underperforms HODL (the typical case).
     */
    function _computeIL(uint256 entryAmt0, uint256 entryAmt1, uint256 exitAmt0, uint256 exitAmt1, uint160 sqrtPriceX96) internal pure returns (int256) {
        uint256 lpVal = _valueInToken0(exitAmt0, exitAmt1, sqrtPriceX96);
        uint256 hodlVal = _valueInToken0(entryAmt0, entryAmt1, sqrtPriceX96);
        return int256(lpVal) - int256(hodlVal);
    }

    /**
     * @notice Convert (amount0, amount1) to token0-equivalent units at the given sqrt price.
     * @dev    value = amount0 + amount1 / price
     *              = amount0 + amount1 × Q96² / sqrtPriceX96²
     *              = amount0 + mulDiv(mulDiv(amount1, Q96, sqrtPrice), Q96, sqrtPrice)
     */
    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;
    }

    // -------------------------------------------------------------------------
    // Formatting helper
    // -------------------------------------------------------------------------

    function _stageName(uint8 idx) internal pure returns (string memory) {
        if (idx == 0) return "FLOOR";
        if (idx == 1) return "ANCHOR";
        return "DISC";
    }
}
