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

import { BaselineStrategies } from "./BaselineStrategies.sol";
import { FormatLib } from "./FormatLib.sol";
import { PositionTracker } from "./PositionTracker.sol";
import { StrategyExecutor } from "./StrategyExecutor.sol";
import { Vm } from "forge-std/Vm.sol";
import { console2 } from "forge-std/console2.sol";

/**
 * @title Reporter
 * @notice Generates a Markdown + JSON backtest report comparing KrAIken's 3-position strategy
 *         against three baseline strategies (HODL, Full-Range LP, Fixed-Width LP).
 *
 * Output files (relative to the forge project root):
 *   script/backtesting/reports/report-{endBlock}.md
 *   script/backtesting/reports/report-{endBlock}.json
 *
 * Requires Foundry cheatcodes (vm.writeFile) and read-write fs_permissions.
 *
 * WARNING — backtesting use only. Not a deployable production contract.
 */
contract Reporter {
    using FormatLib for uint256;
    using FormatLib for int256;

    // -------------------------------------------------------------------------
    // Foundry cheatcode handle (same pattern as EventReplayer)
    // -------------------------------------------------------------------------

    Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));

    // -------------------------------------------------------------------------
    // Config struct (passed to generate())
    // -------------------------------------------------------------------------

    struct Config {
        address poolAddress;
        uint256 startBlock;
        uint256 endBlock;
        uint256 initialCapital; // token0 units (wei)
        uint256 recenterInterval;
        string poolLabel; // e.g. "AERO/WETH 1%"
        uint256 periodDays;
    }

    // -------------------------------------------------------------------------
    // Row data (internal working struct)
    // -------------------------------------------------------------------------

    struct Row {
        string name;
        uint256 initialCapital;
        uint256 finalValue;
        uint256 feesToken0;
        uint256 rebalances;
        int256 ilToken0;
        int256 netPnLToken0;
        uint256 blocksInRange;
        uint256 totalBlocks;
        bool isHodl; // suppresses time-in-range column, shows $0 fees
    }

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

    /**
     * @notice Collect metrics from all strategies, write Markdown and JSON reports.
     * @param kraiken   StrategyExecutor for the KrAIken 3-position strategy.
     * @param baselines Baseline strategies contract.
     * @param cfg       Report configuration.
     */
    function generate(StrategyExecutor kraiken, BaselineStrategies baselines, Config calldata cfg) external {
        PositionTracker tracker = kraiken.tracker();
        PositionTracker.FinalData memory fd = tracker.getFinalData();

        // Build KrAIken row from PositionTracker data.
        Row memory kRow = Row({
            name: "KrAIken 3-Pos",
            initialCapital: cfg.initialCapital,
            finalValue: uint256(int256(cfg.initialCapital) + fd.finalNetPnL),
            feesToken0: fd.finalFeesToken0,
            rebalances: fd.rebalances,
            ilToken0: fd.finalIL,
            netPnLToken0: fd.finalNetPnL,
            blocksInRange: fd.blocksInRange,
            totalBlocks: fd.totalBlocks,
            isHodl: false
        });

        // Collect baseline results.
        (BaselineStrategies.StrategyResult memory hodlR, BaselineStrategies.StrategyResult memory frR, BaselineStrategies.StrategyResult memory fwR) =
            baselines.getResults();

        Row memory frRow = Row({
            name: "Full-Range LP",
            initialCapital: frR.initialCapitalToken0,
            finalValue: frR.finalValueToken0,
            feesToken0: frR.feesToken0,
            rebalances: frR.rebalances,
            ilToken0: frR.ilToken0,
            netPnLToken0: frR.netPnLToken0,
            blocksInRange: frR.blocksInRange,
            totalBlocks: frR.totalBlocks,
            isHodl: false
        });

        Row memory fwRow = Row({
            name: "Fixed-Width LP",
            initialCapital: fwR.initialCapitalToken0,
            finalValue: fwR.finalValueToken0,
            feesToken0: fwR.feesToken0,
            rebalances: fwR.rebalances,
            ilToken0: fwR.ilToken0,
            netPnLToken0: fwR.netPnLToken0,
            blocksInRange: fwR.blocksInRange,
            totalBlocks: fwR.totalBlocks,
            isHodl: false
        });

        Row memory hodlRow = Row({
            name: "HODL",
            initialCapital: hodlR.initialCapitalToken0,
            finalValue: hodlR.finalValueToken0,
            feesToken0: 0,
            rebalances: 0,
            ilToken0: 0,
            netPnLToken0: hodlR.netPnLToken0,
            blocksInRange: 0,
            totalBlocks: 0,
            isHodl: true
        });

        // Write files.
        string memory suffix = cfg.endBlock.str();
        string memory mdPath = string.concat("script/backtesting/reports/report-", suffix, ".md");
        string memory jsonPath = string.concat("script/backtesting/reports/report-", suffix, ".json");

        vm.writeFile(mdPath, _buildMarkdown(kRow, frRow, fwRow, hodlRow, cfg));
        console2.log(string.concat("[REPORTER] Markdown: ", mdPath));

        vm.writeFile(jsonPath, _buildJson(kRow, frRow, fwRow, hodlRow, cfg));
        console2.log(string.concat("[REPORTER] JSON:     ", jsonPath));
    }

    // -------------------------------------------------------------------------
    // Markdown generation
    // -------------------------------------------------------------------------

    function _buildMarkdown(Row memory k, Row memory fr, Row memory fw, Row memory hodl, Config calldata cfg)
        internal
        pure
        returns (string memory)
    {
        return string.concat(
            "# Backtest Report: ",
            cfg.poolLabel,
            ", ",
            cfg.periodDays.str(),
            " days\n\n",
            "| Strategy       | Final Value (token0) | Fees Earned (token0) | Rebalances |"
            " IL (%) | Net P&L (%) | Time in Range |\n",
            "|----------------|----------------------|----------------------|------------|"
            "---------|-------------|---------------|\n",
            _mdRow(k),
            _mdRow(fr),
            _mdRow(fw),
            _mdRow(hodl),
            "\n## Configuration\n\n",
            "- Pool: `",
            _addrStr(cfg.poolAddress),
            "`\n",
            "- Period: block ",
            cfg.startBlock.str(),
            " to block ",
            cfg.endBlock.str(),
            " (",
            cfg.periodDays.str(),
            " days)\n",
            "- Initial capital: ",
            _etherStr(cfg.initialCapital),
            " ETH equivalent\n",
            "- Recenter interval: ",
            cfg.recenterInterval.str(),
            " blocks\n"
        );
    }

    function _mdRow(Row memory r) internal pure returns (string memory) {
        string memory tir;
        if (r.isHodl) {
            tir = "N/A";
        } else if (r.totalBlocks == 0) {
            tir = "0.00%";
        } else {
            tir = _bpsStr((r.blocksInRange * 10_000) / r.totalBlocks);
        }

        string memory fees = r.isHodl ? "$0" : _etherStr(r.feesToken0);
        string memory il = r.isHodl ? "0" : _signedPctStr(r.ilToken0, r.initialCapital);

        return string.concat(
            "| ",
            _pad(r.name, 14),
            " | ",
            _etherStr(r.finalValue),
            " | ",
            fees,
            " | ",
            r.rebalances.str(),
            " | ",
            il,
            " | ",
            _signedPctStr(r.netPnLToken0, r.initialCapital),
            " | ",
            tir,
            " |\n"
        );
    }

    // -------------------------------------------------------------------------
    // JSON generation
    // -------------------------------------------------------------------------

    function _buildJson(Row memory k, Row memory fr, Row memory fw, Row memory hodl, Config calldata cfg)
        internal
        pure
        returns (string memory)
    {
        return string.concat(
            "{\n",
            '  "strategies": {\n',
            '    "kraiken": ',
            _jsonRow(k),
            ",\n",
            '    "fullRange": ',
            _jsonRow(fr),
            ",\n",
            '    "fixedWidth": ',
            _jsonRow(fw),
            ",\n",
            '    "hodl": ',
            _jsonRow(hodl),
            "\n",
            "  },\n",
            '  "config": {\n',
            '    "pool": "',
            _addrStr(cfg.poolAddress),
            '",\n',
            '    "poolLabel": "',
            cfg.poolLabel,
            '",\n',
            '    "startBlock": ',
            cfg.startBlock.str(),
            ",\n",
            '    "endBlock": ',
            cfg.endBlock.str(),
            ",\n",
            '    "periodDays": ',
            cfg.periodDays.str(),
            ",\n",
            '    "initialCapital": "',
            cfg.initialCapital.str(),
            '",\n',
            '    "recenterInterval": ',
            cfg.recenterInterval.str(),
            "\n",
            "  }\n",
            "}\n"
        );
    }

    function _jsonRow(Row memory r) internal pure returns (string memory) {
        string memory tir = r.isHodl ? "null" : r.totalBlocks.str();
        string memory inRange = r.isHodl ? "null" : r.blocksInRange.str();
        return string.concat(
            "{\n",
            '      "initialCapitalToken0": "',
            r.initialCapital.str(),
            '",\n',
            '      "finalValueToken0": "',
            r.finalValue.str(),
            '",\n',
            '      "feesToken0": "',
            r.feesToken0.str(),
            '",\n',
            '      "rebalances": ',
            r.rebalances.str(),
            ",\n",
            '      "ilToken0": "',
            r.ilToken0.istr(),
            '",\n',
            '      "netPnLToken0": "',
            r.netPnLToken0.istr(),
            '",\n',
            '      "blocksInRange": ',
            inRange,
            ",\n",
            '      "totalBlocks": ',
            tir,
            "\n",
            "    }"
        );
    }

    // -------------------------------------------------------------------------
    // Formatting helpers
    // -------------------------------------------------------------------------

    /**
     * @notice Format wei as "<integer>.<6 decimals>" (without "ETH" suffix — unit depends on context).
     */
    function _etherStr(uint256 wei_) internal pure returns (string memory) {
        uint256 whole = wei_ / 1e18;
        uint256 frac = (wei_ % 1e18) / 1e12; // 6 decimal places
        string memory fracStr = frac.str();
        uint256 len = bytes(fracStr).length;
        string memory pad = "";
        for (uint256 i = len; i < 6; i++) {
            pad = string.concat("0", pad);
        }
        return string.concat(whole.str(), ".", pad, fracStr);
    }

    /**
     * @notice Format a signed ratio (value/base) as a percentage with 2 decimals: "±X.XX%".
     */
    function _signedPctStr(int256 value, uint256 base) internal pure returns (string memory) {
        if (base == 0) return "0.00%";
        bool neg = value < 0;
        uint256 abs = neg ? uint256(-value) : uint256(value);
        uint256 bps = (abs * 10_000) / base;
        return string.concat(neg ? "-" : "", _bpsStr(bps));
    }

    /**
     * @notice Format bps (0..10000+) as "X.XX%".
     */
    function _bpsStr(uint256 bps) internal pure returns (string memory) {
        uint256 whole = bps / 100;
        uint256 dec = bps % 100;
        return string.concat(whole.str(), ".", dec < 10 ? string.concat("0", dec.str()) : dec.str(), "%");
    }

    /**
     * @notice Right-pad string to `width` with spaces.
     */
    function _pad(string memory s, uint256 width) internal pure returns (string memory) {
        uint256 len = bytes(s).length;
        if (len >= width) return s;
        string memory out = s;
        for (uint256 i = len; i < width; i++) {
            out = string.concat(out, " ");
        }
        return out;
    }

    /**
     * @notice Encode an address as a lowercase hex string "0x...".
     */
    function _addrStr(address addr) internal pure returns (string memory) {
        bytes memory b = abi.encodePacked(addr);
        bytes memory h = new bytes(42);
        h[0] = "0";
        h[1] = "x";
        for (uint256 i = 0; i < 20; i++) {
            h[2 + i * 2] = _hexChar(uint8(b[i]) >> 4);
            h[3 + i * 2] = _hexChar(uint8(b[i]) & 0x0f);
        }
        return string(h);
    }

    function _hexChar(uint8 v) internal pure returns (bytes1) {
        return v < 10 ? bytes1(48 + v) : bytes1(87 + v);
    }
}
