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

import { BacktestKraiken } from "./BacktestKraiken.sol";
import { BaselineStrategies } from "./BaselineStrategies.sol";
import { EventReplayer } from "./EventReplayer.sol";
import { KrAIkenDeployer, KrAIkenSystem } from "./KrAIkenDeployer.sol";
import { MockToken } from "./MockToken.sol";
import { Reporter } from "./Reporter.sol";
import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol";
import { StrategyExecutor } from "./StrategyExecutor.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import { Script } from "forge-std/Script.sol";
import { console2 } from "forge-std/console2.sol";

/**
 * @title BacktestRunner
 * @notice Entry point for backtesting. Deploys a UniswapV3 shadow pool that mirrors the
 *         AERO/WETH 1% pool configuration, initialised at the price from the event cache,
 *         deploys the KrAIken protocol on top of the shadow pool, then replays all
 *         Swap/Mint/Burn events from the cache while triggering recenter() at configurable
 *         block intervals.
 *
 * Token setup:
 *   Mock WETH (MockToken) and BacktestKraiken are the shadow pool token pair, sorted by
 *   address. Both are freely mintable via MockToken.mint(address,uint256), so EventReplayer
 *   can seed historical LP positions without access-control friction. LiquidityManager
 *   additionally uses BacktestKraiken.mint(uint256) — the Kraiken-compatible overload
 *   restricted to the LM address — for its own liquidity minting.
 *
 * Negligible-impact assumption:
 *   KrAIken positions sit in the same shadow pool as replayed historical events.
 *   Events are replayed as-is without adjusting swap amounts for KrAIken's liquidity.
 *   TODO(#319): Add a feedback loop that accounts for KrAIken's active tick ranges
 *               when computing the actual swap amounts seen by each LP.
 *
 * Usage:
 *   forge script script/backtesting/BacktestRunner.s.sol \
 *       --rpc-url http://localhost:8545 --broadcast \
 *       [BACKTEST_EVENTS_FILE=/path/to/events.jsonl] \
 *       [INITIAL_SQRT_PRICE_X96=<uint160>] \
 *       [RECENTER_INTERVAL=100] \
 *       [INITIAL_CAPITAL_WETH=10000000000000000000]
 *
 * Price resolution order (first that succeeds wins):
 *   1. Environment variable INITIAL_SQRT_PRICE_X96
 *   2. sqrtPriceX96 field from the first line of $BACKTEST_EVENTS_FILE
 *   3. Default: 2^96 (tick = 0, price 1:1)
 */
contract BacktestRunner is Script {
    /// @dev 2^96 — corresponds to tick 0 (price ratio 1:1).
    uint160 internal constant DEFAULT_SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336;

    /// @dev Default number of blocks between recenter attempts (~3 min on Base at 2s/block).
    uint256 internal constant DEFAULT_RECENTER_INTERVAL = 100;

    // -------------------------------------------------------------------------
    // Price resolution helpers
    // -------------------------------------------------------------------------

    /**
     * @notice Parse sqrtPriceX96 from the first line of a JSON Lines events file.
     * @dev Must be `external` so it can be called via `this.` inside a try/catch.
     *      fetch-events.ts serialises BigInt values as decimal strings, so we use
     *      parseJsonString then parseUint rather than parseJsonUint.
     */
    function _parseSqrtPriceFromFile(string memory eventsFile) external view returns (uint160) {
        string memory line = vm.readLine(eventsFile);
        string memory sqrtStr = vm.parseJsonString(line, ".sqrtPriceX96");
        return uint160(vm.parseUint(sqrtStr));
    }

    /**
     * @notice Resolve the initial sqrtPriceX96 without broadcasting any transaction.
     * @dev Call this BEFORE vm.startBroadcast() — the `this.` external call must not
     *      be included in the broadcast.
     */
    function _resolveSqrtPrice() internal view returns (uint160) {
        // 1. Explicit env override.
        try vm.envUint("INITIAL_SQRT_PRICE_X96") returns (uint256 val) {
            if (val != 0) return uint160(val);
        } catch { }

        // 2. First Swap event in the events cache.
        try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
            try this._parseSqrtPriceFromFile(eventsFile) returns (uint160 val) {
                if (val != 0) return val;
            } catch { }
        } catch { }

        // 3. Fallback default.
        return DEFAULT_SQRT_PRICE_X96;
    }

    // -------------------------------------------------------------------------
    // Main entry point
    // -------------------------------------------------------------------------

    function run() external {
        // Resolve configuration before broadcast so self-calls are not recorded.
        uint160 sqrtPriceX96 = _resolveSqrtPrice();

        uint256 recenterInterval = DEFAULT_RECENTER_INTERVAL;
        try vm.envUint("RECENTER_INTERVAL") returns (uint256 v) {
            if (v > 0) recenterInterval = v;
        } catch { }

        uint256 initialCapital = KrAIkenDeployer.DEFAULT_INITIAL_CAPITAL;
        try vm.envUint("INITIAL_CAPITAL_WETH") returns (uint256 v) {
            if (v > 0) initialCapital = v;
        } catch { }

        vm.startBroadcast();

        address sender = msg.sender;

        // ------------------------------------------------------------------
        // Token deployment
        //
        // BacktestKraiken extends MockToken, so EventReplayer can mint it
        // freely via MockToken.mint(address,uint256) for historical pool
        // positions. LiquidityManager calls the restricted mint(uint256)
        // overload (onlyLiquidityManager) wired by KrAIkenDeployer.deploy().
        // ------------------------------------------------------------------
        MockToken mockWeth = new MockToken("Mock WETH", "mWETH", 18);
        BacktestKraiken krk = new BacktestKraiken();

        // Seed the broadcaster with mock WETH for manual interactions.
        mockWeth.mint(sender, 1_000_000 ether);

        // Deploy factory + pool, sorted by address (token0 < token1).
        ShadowPool memory sp = ShadowPoolDeployer.deploy(address(mockWeth), address(krk), sqrtPriceX96);

        // ------------------------------------------------------------------
        // KrAIken system deployment (follows DeployLocal.sol pattern)
        //
        // 1. Deploy OptimizerV3Push3 (no proxy — getLiquidityParams() always reverts;
        //    LM's try/catch falls back to bear-mode defaults on every recenter).
        // 2. Deploy LiquidityManager pointing at the shadow pool.
        // 3. Wire BacktestKraiken.setLiquidityManager(lm).
        // 4. Set feeDestination = sender.
        // 5. Mint initialCapital mock WETH to LM.
        // ------------------------------------------------------------------
        KrAIkenSystem memory sys = KrAIkenDeployer.deploy(address(sp.factory), address(mockWeth), address(krk), sender, initialCapital);

        // Deploy StrategyExecutor — recenter() is now permissionless, so no
        // access grant is needed.  StrategyExecutor.maybeRecenter() calls
        // recenter() via try/catch and logs "SKIP" on cooldown/TWAP failures.
        // vm.warp in EventReplayer drives time so TWAP and cooldown pass.
        bool token0isWeth = sp.token0 == address(mockWeth);
        StrategyExecutor executor =
            new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval, sp.pool, token0isWeth);

        // Deploy baseline strategies and initialize with the same capital as KrAIken.
        BaselineStrategies baselines =
            new BaselineStrategies(sp.pool, MockToken(sp.token0), MockToken(sp.token1), token0isWeth, recenterInterval);
        baselines.initialize(initialCapital);

        vm.stopBroadcast();

        // Reporter uses Foundry cheatcodes (vm.writeFile) — must live outside the broadcast
        // block so it is never sent as a real transaction on a live fork.
        Reporter reporter = new Reporter();

        // ------------------------------------------------------------------
        // EventReplayer is instantiated outside the broadcast block because
        // it uses Foundry cheat codes (vm.readLine, vm.roll, vm.warp) that
        // only work in the forge simulation context and must not be sent as
        // real transactions.
        //
        // sp.token0 / sp.token1 are cast to MockToken — BacktestKraiken
        // inherits MockToken so the cast is valid; its mint(address,uint256)
        // selector matches, enabling EventReplayer's mint callback.
        // ------------------------------------------------------------------
        EventReplayer replayer = new EventReplayer(sp.pool, MockToken(sp.token0), MockToken(sp.token1));

        // Query pool state (view calls, no broadcast needed).
        (uint160 slot0SqrtPrice, int24 tick,,,,,) = sp.pool.slot0();
        uint128 liquidity = sp.pool.liquidity();
        int24 tickSpacing = sp.pool.tickSpacing();

        console2.log("=== Backtesting Shadow Pool Deployment ===");
        console2.log("Factory:        ", address(sp.factory));
        console2.log("Pool:           ", address(sp.pool));
        console2.log("Token0:         ", sp.token0);
        console2.log("Token1:         ", sp.token1);
        console2.log("Replayer:       ", address(replayer));
        console2.log("Fee tier:       ", uint256(ShadowPoolDeployer.SHADOW_FEE));
        console2.log("Tick spacing:   ", int256(tickSpacing));
        console2.log("sqrtPriceX96:   ", uint256(slot0SqrtPrice));
        console2.log("Initial tick:   ", int256(tick));
        console2.log("Liquidity:      ", uint256(liquidity));
        console2.log("\n=== KrAIken System ===");
        console2.log("BacktestKraiken:", address(krk));
        console2.log("Optimizer:      ", address(sys.optimizer));
        console2.log("LiquidityMgr:   ", address(sys.lm));
        console2.log("StrategyExec:   ", address(executor));
        console2.log("PositionTracker:", address(executor.tracker()));
        console2.log("BaselineStrats: ", address(baselines));
        console2.log("Reporter:       ", address(reporter));
        console2.log("Recenter intv:  ", recenterInterval, " blocks");
        console2.log("Initial capital:", initialCapital, " (mock WETH wei)");
        console2.log("token0isWeth:   ", token0isWeth);

        // -----------------------------------------------------------------------
        // Event replay (runs as local simulation — no broadcast required).
        // Each pool.mint / pool.swap / pool.burn call executes against the shadow
        // pool; vm.roll + vm.warp advance block state to match historical timing.
        // After each block advancement, StrategyExecutor.maybeRecenter() is called
        // to trigger LiquidityManager.recenter() at the configured interval.
        // -----------------------------------------------------------------------
        try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
            if (bytes(eventsFile).length > 0) {
                // Reset file position — _resolveSqrtPrice() may have consumed line 1.
                try vm.closeFile(eventsFile) { } catch { }

                // Pre-count events so replay() can show "[N/total]" progress lines.
                uint256 totalEvents = 0;
                {
                    string memory l = vm.readLine(eventsFile);
                    while (bytes(l).length > 0) {
                        totalEvents++;
                        l = vm.readLine(eventsFile);
                    }
                }
                // Reset before the replay pass; we know the file is open after the count loop.
                vm.closeFile(eventsFile);

                console2.log("\n=== Starting Event Replay ===");
                console2.log("Events file:    ", eventsFile);
                console2.log("Total events:   ", totalEvents);
                replayer.replay(eventsFile, totalEvents, executor, baselines);

                // Print final KrAIken strategy summary.
                executor.logSummary();

                // Materialize open-position fees for both LP baselines before summary/reporting.
                baselines.collectFinalFees();

                // Print baseline summaries (now have complete fee data).
                baselines.logFinalSummary();

                // Generate comparison report (Markdown + JSON).
                // firstUpdateBlock and lastNotifiedBlock are the actual block numbers from the
                // events file — use them directly for an accurate period estimate.
                uint256 startBlock = baselines.firstUpdateBlock();
                uint256 endBlock = executor.tracker().lastNotifiedBlock();
                uint256 periodDays = endBlock > startBlock ? ((endBlock - startBlock) * 2) / 86_400 : 0;

                reporter.generate(
                    executor,
                    baselines,
                    Reporter.Config({
                        poolAddress: address(sp.pool),
                        startBlock: startBlock,
                        endBlock: endBlock,
                        initialCapital: initialCapital,
                        recenterInterval: recenterInterval,
                        poolLabel: "AERO/WETH 1%",
                        periodDays: periodDays
                    })
                );
            }
        } catch { }
    }
}
