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

import { Optimizer } from "../src/Optimizer.sol";
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";

import { BearMarketOptimizer } from "../test/mocks/BearMarketOptimizer.sol";
import { BullMarketOptimizer } from "../test/mocks/BullMarketOptimizer.sol";
import { ConfigurableOptimizer } from "../test/mocks/ConfigurableOptimizer.sol";

import { ExtremeOptimizer } from "../test/mocks/ExtremeOptimizer.sol";
import { MaliciousOptimizer } from "../test/mocks/MaliciousOptimizer.sol";
import { NeutralMarketOptimizer } from "../test/mocks/NeutralMarketOptimizer.sol";
import { WhaleOptimizer } from "../test/mocks/WhaleOptimizer.sol";
import { FuzzingBase } from "./helpers/FuzzingBase.sol";
import "forge-std/console2.sol";

/// @title StreamlinedFuzzing
/// @notice Per-run CSV fuzzer: deploys a named optimizer, runs N randomized
///         trade sequences, and writes one CSV per run with full position snapshots.
///         Supports ConfigurableOptimizer (env-driven params), uncapped swaps,
///         and random stake/unstake actions interleaved with trades.
contract StreamlinedFuzzing is FuzzingBase {
    // CSV filename for current run
    string csvFilename;

    // Track cumulative fees
    uint256 totalFees0;
    uint256 totalFees1;

    // Track recentering
    uint256 lastRecenterBlock;
    uint256 recenterCount;

    // Staking position tracking (max 10 active positions per run)
    uint256[10] activePositions;
    uint256 numActivePositions;

    // Batch seed for multi-batch uniqueness
    uint256 bSeed;

    function run() public {
        uint256 numRuns = vm.envOr("FUZZING_RUNS", uint256(20));
        uint256 tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15));
        uint256 buyBias = vm.envOr("BUY_BIAS", uint256(50));
        string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
        bool uncapped = vm.envOr("UNCAPPED_SWAPS", false);
        bSeed = vm.envOr("BATCH_SEED", uint256(0));
        uint256 bgLpEthPerLayer = vm.envOr("BG_LP_ETH_PER_LAYER", uint256(0));
        uint256 stakingLevel = vm.envOr("STAKING_LEVEL", uint256(0)); // 0-100%
        uint256 stakingTaxRate = vm.envOr("STAKING_TAX_RATE", uint256(3)); // tax rate index 0-29

        console2.log("=== Streamlined Fuzzing Analysis ===");
        console2.log("Optimizer:", optimizerClass);
        console2.log("Uncapped:", uncapped);
        if (bgLpEthPerLayer > 0) {
            console2.log("Background LP: ", bgLpEthPerLayer / 1e18, "ETH/layer x5");
        }
        if (stakingLevel > 0) {
            console2.log("Staking level: ", stakingLevel, "% at tax rate index", stakingTaxRate);
        }

        _initInfrastructure();

        string memory scenarioCode = _generateScenarioId(bSeed);

        for (uint256 runIndex = 0; runIndex < numRuns; runIndex++) {
            string memory runId = string(abi.encodePacked(scenarioCode, "-", _padNumber(runIndex, 3)));
            console2.log("\nRun:", runId);

            csvFilename = string(abi.encodePacked("analysis/fuzz-", runId, ".csv"));
            string memory header =
                "action,amount,tick,floor_lower,floor_upper,floor_liq,anchor_lower,anchor_upper,anchor_liq,discovery_lower,discovery_upper,discovery_liq,eth_balance,kraiken_balance,vwap,fees_eth,fees_kraiken,recenter,fee_dest_weth,fee_dest_krk\n";
            vm.writeFile(csvFilename, header);

            // Deploy fresh environment per run
            address optimizer = _deployOptimizer(optimizerClass);
            _setupEnvironment(optimizer, runIndex % 2 == 0, uncapped);

            // Deploy background LP if configured
            if (bgLpEthPerLayer > 0) {
                _deployBackgroundLP(bgLpEthPerLayer);
            }

            // Pre-seed staking to set initial sentiment
            if (stakingLevel > 0) {
                _seedStaking(stakingLevel, uint32(stakingTaxRate));
            }

            // Reset tracking
            totalFees0 = 0;
            totalFees1 = 0;
            lastRecenterBlock = block.number;
            recenterCount = 0;
            numActivePositions = 0;

            // Fund trader
            uint256 traderFund = 500 ether + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, "trader"))) % 500 ether);
            vm.deal(trader, traderFund * 2);
            vm.prank(trader);
            weth.deposit{ value: traderFund }();

            _recordState("INIT", 0);

            for (uint256 i = 0; i < tradesPerRun; i++) {
                // Recenter on average every 3 trades
                uint256 recenterRand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i, "recenter"))) % 3;
                if (recenterRand == 0) {
                    if (_tryRecenter()) {
                        lastRecenterBlock = block.number;
                        recenterCount++;
                        // Rebalance background LP every 10 recenters to track market
                        // (every recenter is too expensive for 2000-trade runs)
                        if (bgLpEthPerLayer > 0 && recenterCount % 10 == 0) {
                            _rebalanceBackgroundLP(bgLpEthPerLayer);
                        }
                        _recordState("RECENTER", 0);
                    }
                }

                // 10% chance of staking action instead of trade
                uint256 stakeRand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i, "stake"))) % 100;
                if (stakeRand < 10) {
                    _runStakingAction(runIndex, i);
                    continue;
                }

                uint256 rand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i))) % 100;
                if (rand < buyBias) {
                    _runBuy(runIndex, i);
                } else {
                    _runSell(runIndex, i);
                }
            }

            // Exit all staking positions before liquidation
            _exitAllPositions();

            _liquidateTraderHoldings();
            _recordState("FINAL", 0);

            // Log fee revenue
            (uint256 feeWeth, uint256 feeKrk) = _getFeeRevenue();
            console2.log("Fee revenue: WETH=", feeWeth / 1e18, "KRK=", feeKrk / 1e18);
        }

        console2.log("\n=== Analysis Complete ===");
        console2.log("Generated", numRuns, "CSV files with prefix:", scenarioCode);
    }

    function _runBuy(uint256 runIndex, uint256 tradeIndex) internal {
        uint256 amount = _getTradeAmount(runIndex, tradeIndex, true);
        if (_executeBuy(amount)) {
            _recordState("BUY", amount);
        } else {
            _recordState("BUY_FAIL", amount);
        }
    }

    function _runSell(uint256 runIndex, uint256 tradeIndex) internal {
        uint256 amount = _getTradeAmount(runIndex, tradeIndex, false);
        if (_executeSell(amount)) {
            _recordState("SELL", amount);
        } else {
            _recordState("SELL_FAIL", amount);
        }
    }

    function _getTradeAmount(uint256 runIndex, uint256 tradeIndex, bool isBuy) internal view returns (uint256) {
        uint256 baseAmount = 10 ether + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex))) % 90 ether);
        return isBuy ? baseAmount : baseAmount * 1000;
    }

    // ── Staking Actions ──────────────────────────────────────────────────

    function _runStakingAction(uint256 runIndex, uint256 tradeIndex) internal {
        // If we have active positions, 50/50 stake vs unstake; otherwise always stake
        bool doStake = numActivePositions == 0 || uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "stakeOrUnstake"))) % 2 == 0;

        if (doStake) {
            _tryStake(runIndex, tradeIndex);
        } else {
            _tryUnstake(runIndex, tradeIndex);
        }
    }

    function _tryStake(uint256 runIndex, uint256 tradeIndex) internal {
        uint256 krkBalance = kraiken.balanceOf(trader);
        if (krkBalance == 0 || numActivePositions >= 10) {
            _recordState("STAKE_SKIP", 0);
            return;
        }

        // Stake 5-20% of KRK holdings
        uint256 pct = 5 + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "stakePct"))) % 16);
        uint256 stakeAmount = krkBalance * pct / 100;

        // Check minimum stake
        try kraiken.minStake() returns (uint256 minStake) {
            if (stakeAmount < minStake) stakeAmount = minStake;
            if (stakeAmount > krkBalance) {
                _recordState("STAKE_SKIP", 0);
                return;
            }
        } catch {
            _recordState("STAKE_SKIP", 0);
            return;
        }

        // Random tax rate index (0-29), bias toward low rates
        uint32 taxRate = uint32(uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "taxRate"))) % 10);

        vm.startPrank(trader);
        kraiken.approve(address(stake), stakeAmount);
        uint256[] memory empty = new uint256[](0);
        try stake.snatch(stakeAmount, trader, taxRate, empty) returns (uint256 positionId) {
            vm.stopPrank();
            activePositions[numActivePositions] = positionId;
            numActivePositions++;
            _recordState("STAKE", stakeAmount);
        } catch {
            vm.stopPrank();
            _recordState("STAKE_FAIL", stakeAmount);
        }
    }

    function _tryUnstake(uint256 runIndex, uint256 tradeIndex) internal {
        if (numActivePositions == 0) return;

        // Pick a random position to exit
        uint256 idx = uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "unstakeIdx"))) % numActivePositions;
        uint256 positionId = activePositions[idx];

        // Advance time so we pass the 3-day tax floor
        vm.warp(block.timestamp + 4 days);

        vm.prank(trader);
        try stake.exitPosition(positionId) {
            // Remove from tracking by swapping with last
            activePositions[idx] = activePositions[numActivePositions - 1];
            numActivePositions--;
            _recordState("UNSTAKE", positionId);
        } catch {
            _recordState("UNSTAKE_FAIL", positionId);
        }
    }

    function _exitAllPositions() internal {
        for (uint256 i = 0; i < numActivePositions; i++) {
            vm.warp(block.timestamp + 4 days);
            vm.prank(trader);
            try stake.exitPosition(activePositions[i]) { } catch { }
        }
        numActivePositions = 0;
    }

    function _generateScenarioId(uint256 seed) internal view returns (string memory) {
        uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, seed)));
        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);
    }

    // ── Optimizer Deployment ─────────────────────────────────────────────

    function _deployOptimizer(string memory optimizerClass) internal returns (address) {
        if (keccak256(bytes(optimizerClass)) == keccak256(bytes("ConfigurableOptimizer"))) {
            uint256 ci = vm.envOr("CI_VALUE", uint256(0));
            uint256 as_ = vm.envOr("AS_VALUE", uint256(100_000_000_000_000_000));
            uint256 aw = vm.envOr("AW_VALUE", uint256(20));
            uint256 dd = vm.envOr("DD_VALUE", uint256(500_000_000_000_000_000));
            return address(new ConfigurableOptimizer(ci, as_, uint24(aw > 100 ? 100 : aw), dd));
        } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BullMarketOptimizer"))) {
            return address(new BullMarketOptimizer());
        } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BearMarketOptimizer"))) {
            return address(new BearMarketOptimizer());
        } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("NeutralMarketOptimizer"))) {
            return address(new NeutralMarketOptimizer());
        } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("WhaleOptimizer"))) {
            return address(new WhaleOptimizer());
        } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("ExtremeOptimizer"))) {
            return address(new ExtremeOptimizer());
        } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("MaliciousOptimizer"))) {
            return address(new MaliciousOptimizer());
        } else {
            return address(new BullMarketOptimizer());
        }
    }

    // ── CSV Recording ─────────────────────────────────────────────────────

    function _recordState(string memory action, uint256 amount) internal {
        string memory row = _buildRowPart1(action, amount);
        row = string(abi.encodePacked(row, _buildRowPart2()));
        row = string(abi.encodePacked(row, _buildRowPart3()));
        vm.writeLine(csvFilename, row);
    }

    function _buildRowPart1(string memory action, uint256 amount) internal view returns (string memory) {
        (, int24 tick,,,,,) = pool.slot0();
        (uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);

        return string(
            abi.encodePacked(
                action,
                ",",
                vm.toString(amount),
                ",",
                vm.toString(tick),
                ",",
                vm.toString(floorLower),
                ",",
                vm.toString(floorUpper),
                ",",
                vm.toString(uint256(floorLiq)),
                ","
            )
        );
    }

    function _buildRowPart2() internal view returns (string memory) {
        (uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
        (uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);

        return string(
            abi.encodePacked(
                vm.toString(anchorLower),
                ",",
                vm.toString(anchorUpper),
                ",",
                vm.toString(uint256(anchorLiq)),
                ",",
                vm.toString(discoveryLower),
                ",",
                vm.toString(discoveryUpper),
                ",",
                vm.toString(uint256(discoveryLiq)),
                ","
            )
        );
    }

    function _buildRowPart3() internal view returns (string memory) {
        uint256 ethBalance = weth.balanceOf(trader);
        uint256 kraikenBalance = kraiken.balanceOf(trader);

        (uint128 fees0, uint128 fees1) = pool.protocolFees();
        uint256 deltaFees0 = fees0 > totalFees0 ? fees0 - totalFees0 : 0;
        uint256 deltaFees1 = fees1 > totalFees1 ? fees1 - totalFees1 : 0;

        // Fee destination balances (LM fee revenue)
        (uint256 feeDestWeth, uint256 feeDestKrk) = _getFeeRevenue();

        return string(
            abi.encodePacked(
                vm.toString(ethBalance),
                ",",
                vm.toString(kraikenBalance),
                ",",
                "0,",
                vm.toString(deltaFees0),
                ",",
                vm.toString(deltaFees1),
                ",",
                "0,",
                vm.toString(feeDestWeth),
                ",",
                vm.toString(feeDestKrk)
            )
        );
    }
}
