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

import { Kraiken } from "../../src/Kraiken.sol";
import { LiquidityManager } from "../../src/LiquidityManager.sol";
import { Stake } from "../../src/Stake.sol";
import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol";
import { UniswapHelpers } from "../../src/helpers/UniswapHelpers.sol";
import { IWETH9 } from "../../src/interfaces/IWETH9.sol";
import { TestEnvironment } from "../../test/helpers/TestBase.sol";

import { BackgroundLP } from "./BackgroundLP.sol";
import { SwapExecutor } from "./SwapExecutor.sol";
import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "forge-std/Script.sol";
import "forge-std/console2.sol";

/// @title FuzzingBase
/// @notice Shared infrastructure for all fuzzing/analysis scripts.
/// @dev Provides environment setup, trade execution, liquidation, token recovery,
///      and CSV parsing. Concrete scripts inherit this and implement their own run() logic.
abstract contract FuzzingBase is Script {
    // ── Constants ──────────────────────────────────────────────────────────
    uint256 internal constant LM_FUNDING_ETH = 200 ether;
    uint256 internal constant LM_INITIAL_WETH = 100 ether;
    uint256 internal constant RECENTER_GAS_LIMIT = 50_000_000;
    uint256 internal constant RECENTER_TIME_ADVANCE = 1 hours;
    uint256 internal constant LIQUIDATION_MAX_ATTEMPTS = 20;
    uint256 internal constant LIQUIDATION_RECENTER_INTERVAL = 3;
    address internal constant DEAD_ADDRESS = address(0xdead);
    uint256 internal constant SELL_PCT_MIN = 20;
    uint256 internal constant SELL_PCT_RANGE = 60;

    // ── Environment ───────────────────────────────────────────────────────
    TestEnvironment internal testEnv;
    IUniswapV3Factory internal factory;
    IUniswapV3Pool internal pool;
    IWETH9 internal weth;
    Kraiken internal kraiken;
    Stake internal stake;
    LiquidityManager internal lm;
    SwapExecutor internal swapExecutor;
    bool internal token0isWeth;

    // ── Actors ────────────────────────────────────────────────────────────
    address internal trader = makeAddr("trader");
    address internal fees = makeAddr("fees");
    BackgroundLP internal backgroundLP;

    // ══════════════════════════════════════════════════════════════════════
    //  Environment Setup
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Deploy TestEnvironment and Uniswap factory (call once per script)
    function _initInfrastructure() internal {
        testEnv = new TestEnvironment(fees);
        factory = UniswapHelpers.deployUniswapFactory();
    }

    /// @notice Deploy a fresh pool/LM/optimizer environment
    /// @param optimizer Address of the optimizer contract to use
    /// @param wethIsToken0 Token ordering for the pool
    /// @param uncapped Whether to deploy SwapExecutor in uncapped mode
    function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped) internal {
        (factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);

        swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped);

        vm.deal(address(lm), LM_FUNDING_ETH);
        vm.prank(address(lm));
        weth.deposit{ value: LM_INITIAL_WETH }();

        vm.prank(fees);
        try lm.recenter() { } catch { }
    }

    /// @notice Overload: setup with custom LM funding
    function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped, uint256 lmFundingEth) internal {
        (factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);

        swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped);

        vm.deal(address(lm), lmFundingEth);
        vm.prank(address(lm));
        weth.deposit{ value: lmFundingEth / 2 }();

        vm.prank(fees);
        try lm.recenter() { } catch { }
    }

    // ══════════════════════════════════════════════════════════════════════
    //  Background Liquidity (Gaussian competition)
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Deploy background LP with Gaussian-approximating stacked positions.
    ///         KRK is acquired by buying from the pool (realistic — no free minting).
    /// @param ethPerLayer ETH per layer (5 layers total). Half goes to buying KRK,
    ///        half stays as WETH for the liquidity positions.
    function _deployBackgroundLP(uint256 ethPerLayer) internal {
        backgroundLP = new BackgroundLP(pool, weth, kraiken, token0isWeth);

        // Fund with ETH (2× total: half to buy KRK, half for LP positions)
        uint256 totalEth = ethPerLayer * 5;
        vm.deal(address(backgroundLP), totalEth * 4);
        vm.prank(address(backgroundLP));
        weth.deposit{ value: totalEth * 2 }();

        // Buy KRK from the pool — just like a real LP would.
        // Buy in small chunks with recenters to avoid overshooting thin anchors.
        address bgAddr = address(backgroundLP);
        uint256 chunkSize = totalEth / 5;
        for (uint256 i = 0; i < 5; i++) {
            uint256 buyAmount = chunkSize;
            uint256 wethBal = weth.balanceOf(bgAddr);
            if (buyAmount > wethBal / 2) buyAmount = wethBal / 2; // keep some WETH for LP
            if (buyAmount == 0) break;

            vm.startPrank(bgAddr);
            weth.transfer(address(swapExecutor), buyAmount);
            vm.stopPrank();
            vm.prank(bgAddr);
            try swapExecutor.executeBuy(buyAmount, bgAddr) { } catch { }

            // Recenter between chunks to rebuild positions
            vm.warp(block.timestamp + 1 hours);
            vm.roll(block.number + 1);
            vm.prank(fees);
            try lm.recenter() { } catch { }
        }

        backgroundLP.rebalance(ethPerLayer);
    }

    /// @notice Rebalance background LP positions to track current tick.
    ///         Uses existing token balances — no new ETH or KRK injected.
    function _rebalanceBackgroundLP(uint256 ethPerLayer) internal {
        if (address(backgroundLP) == address(0)) return;
        backgroundLP.rebalance(ethPerLayer);
    }

    /// @notice Get fee revenue collected by feeDestination
    /// @return feeWeth WETH fees, feeKrk KRK fees
    function _getFeeRevenue() internal view returns (uint256 feeWeth, uint256 feeKrk) {
        feeWeth = weth.balanceOf(fees);
        feeKrk = kraiken.balanceOf(fees);
    }

    // ══════════════════════════════════════════════════════════════════════
    //  Staking Setup
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Pre-seed staking to simulate a given sentiment level.
    ///         Buys KRK from the pool and stakes it at the given tax rate.
    /// @param stakingPct Target % of authorized stake to fill (0-100, in whole %)
    /// @param taxRateIdx Tax rate index (0-29, lower = cheaper = more bullish)
    function _seedStaking(uint256 stakingPct, uint32 taxRateIdx) internal {
        if (stakingPct == 0) return;

        address staker = makeAddr("staker");

        // Buy KRK for staking — use 50 ETH per attempt, loop until target met
        uint256 maxAttempts = 20;
        for (uint256 attempt = 0; attempt < maxAttempts; attempt++) {
            // Check current staking level
            uint256 currentPct = stake.getPercentageStaked() * 100 / 1e18;
            if (currentPct >= stakingPct) break;

            // Fund staker with ETH
            vm.deal(staker, 100 ether);
            vm.prank(staker);
            weth.deposit{ value: 50 ether }();

            // Buy KRK
            vm.startPrank(staker);
            weth.transfer(address(swapExecutor), 50 ether);
            vm.stopPrank();
            vm.prank(staker);
            try swapExecutor.executeBuy(50 ether, staker) { }
            catch {
                break;
            }

            // Recenter to rebuild positions
            vm.warp(block.timestamp + 1 hours);
            vm.roll(block.number + 1);
            vm.prank(fees);
            try lm.recenter() { } catch { }

            // Stake all KRK
            uint256 krkBal = kraiken.balanceOf(staker);
            if (krkBal == 0) break;

            vm.startPrank(staker);
            kraiken.approve(address(stake), krkBal);
            uint256[] memory empty = new uint256[](0);
            try stake.snatch(krkBal, staker, taxRateIdx, empty) { }
            catch {
                vm.stopPrank();
                break;
            }
            vm.stopPrank();
        }

        // Final recenter to pick up new optimizer params
        vm.warp(block.timestamp + 1 hours);
        vm.roll(block.number + 1);
        vm.prank(fees);
        try lm.recenter() { } catch { }
    }

    // ══════════════════════════════════════════════════════════════════════
    //  Trade Execution
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Execute a buy trade via the SwapExecutor
    /// @return success Whether the buy produced a non-zero result
    function _executeBuy(uint256 amount) internal returns (bool success) {
        if (amount == 0 || weth.balanceOf(trader) < amount) return false;

        vm.startPrank(trader);
        weth.transfer(address(swapExecutor), amount);
        try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) {
            vm.stopPrank();
            _recoverStuckTokens();
            return actualAmount > 0;
        } catch {
            vm.stopPrank();
            _recoverStuckTokens();
            return false;
        }
    }

    /// @notice Execute a sell trade via the SwapExecutor
    /// @return success Whether the sell produced a non-zero result
    function _executeSell(uint256 amount) internal returns (bool success) {
        if (amount == 0 || kraiken.balanceOf(trader) < amount) return false;

        vm.startPrank(trader);
        kraiken.transfer(address(swapExecutor), amount);
        try swapExecutor.executeSell(amount, trader) returns (uint256 actualAmount) {
            vm.stopPrank();
            _recoverStuckTokens();
            return actualAmount > 0;
        } catch {
            vm.stopPrank();
            _recoverStuckTokens();
            return false;
        }
    }

    // ══════════════════════════════════════════════════════════════════════
    //  Recenter
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Advance time and attempt a recenter
    function _tryRecenter() internal returns (bool) {
        vm.warp(block.timestamp + RECENTER_TIME_ADVANCE);
        vm.roll(block.number + 1);
        vm.prank(fees);
        try lm.recenter{ gas: RECENTER_GAS_LIMIT }() {
            return true;
        } catch {
            return false;
        }
    }

    // ══════════════════════════════════════════════════════════════════════
    //  Liquidation
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Sell all trader's KRAIKEN, retrying with recenters between attempts
    function _liquidateTraderHoldings() internal {
        uint256 remaining = kraiken.balanceOf(trader);
        uint256 attempts;

        while (remaining > 0 && attempts < LIQUIDATION_MAX_ATTEMPTS) {
            uint256 prev = remaining;
            uint256 wethBefore = weth.balanceOf(trader);

            vm.startPrank(trader);
            kraiken.transfer(address(swapExecutor), remaining);
            vm.stopPrank();

            try swapExecutor.executeSell(remaining, trader) {
                if (weth.balanceOf(trader) <= wethBefore) {
                    _recoverStuckTokens();
                    break;
                }
            } catch {
                _recoverStuckTokens();
                break;
            }

            _recoverStuckTokens();

            if (attempts % LIQUIDATION_RECENTER_INTERVAL == LIQUIDATION_RECENTER_INTERVAL - 1) {
                _tryRecenter();
            }

            remaining = kraiken.balanceOf(trader);
            if (remaining >= prev) break;
            unchecked {
                attempts++;
            }
        }

        _recoverStuckTokens();
    }

    // ══════════════════════════════════════════════════════════════════════
    //  Token Recovery & Cleanup
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Recover any tokens stuck in SwapExecutor back to the trader
    function _recoverStuckTokens() internal {
        uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor));
        if (stuckKraiken > 0) {
            vm.prank(address(swapExecutor));
            kraiken.transfer(trader, stuckKraiken);
        }
        uint256 stuckWeth = weth.balanceOf(address(swapExecutor));
        if (stuckWeth > 0) {
            vm.prank(address(swapExecutor));
            weth.transfer(trader, stuckWeth);
        }
    }

    /// @notice Burn all trader + SwapExecutor tokens to prevent PnL leakage between runs
    function _cleanupTraderTokens() internal {
        uint256 leftoverWeth = weth.balanceOf(trader);
        if (leftoverWeth > 0) {
            vm.prank(trader);
            weth.transfer(DEAD_ADDRESS, leftoverWeth);
        }
        uint256 leftoverKraiken = kraiken.balanceOf(trader);
        if (leftoverKraiken > 0) {
            vm.prank(trader);
            kraiken.transfer(DEAD_ADDRESS, leftoverKraiken);
        }
        uint256 stuckWeth = weth.balanceOf(address(swapExecutor));
        if (stuckWeth > 0) {
            vm.prank(address(swapExecutor));
            weth.transfer(DEAD_ADDRESS, stuckWeth);
        }
        uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor));
        if (stuckKraiken > 0) {
            vm.prank(address(swapExecutor));
            kraiken.transfer(DEAD_ADDRESS, stuckKraiken);
        }
    }

    // ══════════════════════════════════════════════════════════════════════
    //  LM ETH Measurement
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Approximate total ETH attributable to the LM (direct balance + pool WETH)
    /// @dev Pool WETH includes both LM position ETH and trader buy ETH in transit.
    ///      This is a rough proxy — trader PnL is the reliable safety metric.
    function _getLmSystemEth() internal view returns (uint256) {
        return address(lm).balance + weth.balanceOf(address(lm)) + weth.balanceOf(address(pool));
    }

    // ══════════════════════════════════════════════════════════════════════
    //  CSV Parsing Utilities
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Parse a comma-separated string of uint256 values
    function _parseUintArray(string memory csv) internal pure returns (uint256[] memory) {
        bytes memory b = bytes(csv);
        uint256 count = 1;
        for (uint256 i = 0; i < b.length; i++) {
            if (b[i] == ",") count++;
        }

        uint256[] memory result = new uint256[](count);
        uint256 idx;
        uint256 start;

        for (uint256 i = 0; i <= b.length; i++) {
            if (i == b.length || b[i] == ",") {
                uint256 num;
                for (uint256 j = start; j < i; j++) {
                    require(b[j] >= "0" && b[j] <= "9", "Invalid number in CSV");
                    num = num * 10 + (uint256(uint8(b[j])) - 48);
                }
                result[idx] = num;
                idx++;
                start = i + 1;
            }
        }

        return result;
    }

    // ══════════════════════════════════════════════════════════════════════
    //  String Helpers
    // ══════════════════════════════════════════════════════════════════════

    /// @notice Generate a random 4-character alphanumeric ID
    function _generateScenarioId() internal view returns (string memory) {
        uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)));
        bytes memory chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        bytes memory result = new bytes(4);

        for (uint256 i = 0; i < 4; i++) {
            result[i] = chars[rand % chars.length];
            rand = rand / chars.length;
        }

        return string(result);
    }

    /// @notice Pad a number with leading zeros
    function _padNumber(uint256 num, uint256 digits) internal pure returns (string memory) {
        string memory numStr = vm.toString(num);
        bytes memory numBytes = bytes(numStr);

        if (numBytes.length >= digits) return numStr;

        bytes memory result = new bytes(digits);
        uint256 padding = digits - numBytes.length;

        for (uint256 i = 0; i < padding; i++) {
            result[i] = "0";
        }
        for (uint256 i = 0; i < numBytes.length; i++) {
            result[padding + i] = numBytes[i];
        }

        return string(result);
    }
}
