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

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "./LiquidityPoolETH.sol";
import "./RouteTypesV2.sol";
import "./interfaces/IRouteExecutorAdapter.sol";
import "./interfaces/IWETH.sol";

contract EnhancedSwapRouterV2 is AccessControl, ReentrancyGuard {
    using SafeERC20 for IERC20;

    bytes32 public constant ROUTING_MANAGER_ROLE = keccak256("ROUTING_MANAGER_ROLE");
    bytes32 public constant COORDINATOR_ROLE = keccak256("COORDINATOR_ROLE");

    struct ProviderRouteConfig {
        address target;
        bytes providerData;
        bool enabled;
    }

    address public immutable weth;
    address public immutable usdt;
    address public immutable usdc;
    address public immutable dai;

    mapping(uint8 => address) public providerAdapters;
    mapping(uint8 => bool) public providerEnabled;
    mapping(address => mapping(address => mapping(uint8 => ProviderRouteConfig))) private providerRoutes;
    mapping(uint256 => RouteTypesV2.Provider[]) private sizeBasedRouting;

    event ProviderAdapterSet(RouteTypesV2.Provider indexed provider, address indexed adapter);
    event ProviderRouteSet(
        address indexed tokenIn,
        address indexed tokenOut,
        RouteTypesV2.Provider indexed provider,
        address target,
        bool enabled
    );
    event ProviderToggled(RouteTypesV2.Provider indexed provider, bool enabled);
    event RoutingConfigUpdated(uint256 indexed sizeCategory, RouteTypesV2.Provider[] providers);
    event RouteExecuted(
        bytes32 indexed routeHash,
        address indexed caller,
        address indexed recipient,
        address inputToken,
        address outputToken,
        uint256 amountIn,
        uint256 amountOut
    );

    error ZeroAddress();
    error ZeroAmount();
    error InvalidPlan();
    error InvalidStablecoin();
    error ProviderDisabled();
    error ProviderAdapterNotConfigured();
    error RouteNotConfigured();
    error RouteValidationFailed(string reason);
    error InsufficientOutput();

    constructor(address _weth, address _usdt, address _usdc, address _dai) {
        if (_weth == address(0) || _usdt == address(0) || _usdc == address(0) || _dai == address(0)) {
            revert ZeroAddress();
        }

        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ROUTING_MANAGER_ROLE, msg.sender);

        weth = _weth;
        usdt = _usdt;
        usdc = _usdc;
        dai = _dai;

        providerEnabled[uint8(RouteTypesV2.Provider.Dodo)] = true;
        providerEnabled[uint8(RouteTypesV2.Provider.UniswapV3)] = true;
        providerEnabled[uint8(RouteTypesV2.Provider.Balancer)] = true;
        providerEnabled[uint8(RouteTypesV2.Provider.Curve)] = true;
        providerEnabled[uint8(RouteTypesV2.Provider.OneInch)] = false;
        providerEnabled[uint8(RouteTypesV2.Provider.Partner)] = false;
        providerEnabled[uint8(RouteTypesV2.Provider.DodoV3)] = true;

        _initializeDefaultRouting();
    }

    function setProviderAdapter(RouteTypesV2.Provider provider, address adapter)
        external
        onlyRole(ROUTING_MANAGER_ROLE)
    {
        if (adapter == address(0)) revert ZeroAddress();
        providerAdapters[uint8(provider)] = adapter;
        emit ProviderAdapterSet(provider, adapter);
    }

    function setProviderRoute(
        address tokenIn,
        address tokenOut,
        RouteTypesV2.Provider provider,
        address target,
        bytes calldata providerData,
        bool enabled
    ) external onlyRole(ROUTING_MANAGER_ROLE) {
        if (tokenIn == address(0) || tokenOut == address(0) || target == address(0)) {
            revert ZeroAddress();
        }
        providerRoutes[tokenIn][tokenOut][uint8(provider)] =
            ProviderRouteConfig({target: target, providerData: providerData, enabled: enabled});
        emit ProviderRouteSet(tokenIn, tokenOut, provider, target, enabled);
    }

    function getProviderRoute(address tokenIn, address tokenOut, RouteTypesV2.Provider provider)
        external
        view
        returns (address target, bytes memory providerData, bool enabled)
    {
        ProviderRouteConfig storage config = providerRoutes[tokenIn][tokenOut][uint8(provider)];
        return (config.target, config.providerData, config.enabled);
    }

    function setProviderEnabled(RouteTypesV2.Provider provider, bool enabled) external onlyRole(ROUTING_MANAGER_ROLE) {
        providerEnabled[uint8(provider)] = enabled;
        emit ProviderToggled(provider, enabled);
    }

    function setRoutingConfig(uint256 sizeCategory, RouteTypesV2.Provider[] calldata providers)
        external
        onlyRole(ROUTING_MANAGER_ROLE)
    {
        require(sizeCategory < 3, "EnhancedSwapRouterV2: invalid size category");
        require(providers.length > 0, "EnhancedSwapRouterV2: empty providers");

        delete sizeBasedRouting[sizeCategory];
        for (uint256 i = 0; i < providers.length; i++) {
            sizeBasedRouting[sizeCategory].push(providers[i]);
        }

        emit RoutingConfigUpdated(sizeCategory, providers);
    }

    function executeRoute(RouteTypesV2.RoutePlan calldata plan)
        external
        payable
        nonReentrant
        returns (uint256 amountOut)
    {
        RouteTypesV2.RoutePlan memory memoryPlan = _copyPlan(plan);
        return _executeRoute(memoryPlan, msg.sender, msg.value);
    }

    function quoteConfiguredProviders(address tokenIn, address tokenOut, uint256 amountIn)
        external
        view
        returns (RouteTypesV2.ProviderQuote[] memory quotes)
    {
        RouteTypesV2.ProviderQuote[] memory temp =
            new RouteTypesV2.ProviderQuote[](uint256(type(RouteTypesV2.Provider).max) + 1);
        uint256 count = 0;

        for (uint8 providerId = 0; providerId <= uint8(type(RouteTypesV2.Provider).max); providerId++) {
            if (!providerEnabled[providerId]) {
                continue;
            }
            address adapter = providerAdapters[providerId];
            if (adapter == address(0)) {
                continue;
            }

            ProviderRouteConfig storage routeConfig = providerRoutes[tokenIn][tokenOut][providerId];
            if (!routeConfig.enabled || routeConfig.target == address(0)) {
                continue;
            }
            RouteTypesV2.ProviderQuote memory quote = _quoteConfiguredProvider(
                tokenIn, tokenOut, amountIn, RouteTypesV2.Provider(providerId), routeConfig, adapter
            );
            if (quote.executable) {
                temp[count] = quote;
                count++;
            }
        }

        quotes = new RouteTypesV2.ProviderQuote[](count);
        for (uint256 i = 0; i < count; i++) {
            quotes[i] = temp[i];
        }
    }

    function quoteConfiguredProvider(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        RouteTypesV2.Provider provider
    ) external view returns (RouteTypesV2.ProviderQuote memory quote) {
        uint8 providerId = uint8(provider);
        if (!providerEnabled[providerId]) revert ProviderDisabled();

        address adapter = providerAdapters[providerId];
        if (adapter == address(0)) revert ProviderAdapterNotConfigured();

        ProviderRouteConfig storage routeConfig = providerRoutes[tokenIn][tokenOut][providerId];
        if (!routeConfig.enabled || routeConfig.target == address(0)) revert RouteNotConfigured();

        return _quoteConfiguredProvider(tokenIn, tokenOut, amountIn, provider, routeConfig, adapter);
    }

    function _quoteConfiguredProvider(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        RouteTypesV2.Provider provider,
        ProviderRouteConfig storage routeConfig,
        address adapter
    ) internal view returns (RouteTypesV2.ProviderQuote memory quote) {
        RouteTypesV2.RouteLeg memory leg = RouteTypesV2.RouteLeg({
            provider: provider,
            tokenIn: tokenIn,
            tokenOut: tokenOut,
            amountSource: RouteTypesV2.AmountSource.UserInput,
            minAmountOut: 0,
            target: routeConfig.target,
            providerData: routeConfig.providerData
        });

        (bool ok,) = IRouteExecutorAdapter(adapter).validate(leg);
        if (!ok) {
            return RouteTypesV2.ProviderQuote({
                provider: provider,
                target: routeConfig.target,
                amountOut: 0,
                estimatedGas: 0,
                executable: false,
                providerData: routeConfig.providerData
            });
        }

        (uint256 amountOut, uint256 estimatedGas) = IRouteExecutorAdapter(adapter).quote(leg, amountIn);
        return RouteTypesV2.ProviderQuote({
            provider: provider,
            target: routeConfig.target,
            amountOut: amountOut,
            estimatedGas: estimatedGas,
            executable: amountOut > 0,
            providerData: routeConfig.providerData
        });
    }

    function swapTokenToToken(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin)
        external
        returns (uint256 amountOut)
    {
        return swapTokenToToken(tokenIn, tokenOut, amountIn, amountOutMin, _defaultPreferredProvider());
    }

    function swapTokenToToken(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        uint256 amountOutMin,
        RouteTypesV2.Provider preferredProvider
    ) public returns (uint256 amountOut) {
        if (amountIn == 0) revert ZeroAmount();
        ProviderRouteConfig memory routeConfig = _getRouteConfig(tokenIn, tokenOut, preferredProvider);
        RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
        legs[0] = RouteTypesV2.RouteLeg({
            provider: preferredProvider,
            tokenIn: tokenIn,
            tokenOut: tokenOut,
            amountSource: RouteTypesV2.AmountSource.UserInput,
            minAmountOut: amountOutMin,
            target: routeConfig.target,
            providerData: routeConfig.providerData
        });

        RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
            chainId: block.chainid,
            inputToken: tokenIn,
            outputToken: tokenOut,
            amountIn: amountIn,
            minAmountOut: amountOutMin,
            recipient: msg.sender,
            deadline: block.timestamp + 300,
            legs: legs
        });

        return _executeRoute(plan, msg.sender, 0);
    }

    function swapToStablecoin(
        LiquidityPoolETH.AssetType inputAsset,
        address stablecoinToken,
        uint256 amountIn,
        uint256 amountOutMin
    ) external payable returns (uint256 amountOut) {
        return swapToStablecoin(inputAsset, stablecoinToken, amountIn, amountOutMin, _defaultPreferredProvider());
    }

    function swapToStablecoin(
        LiquidityPoolETH.AssetType inputAsset,
        address stablecoinToken,
        uint256 amountIn,
        uint256 amountOutMin,
        RouteTypesV2.Provider preferredProvider
    ) public payable returns (uint256 amountOut) {
        if (amountIn == 0) revert ZeroAmount();
        if (!_isValidStablecoin(stablecoinToken)) revert InvalidStablecoin();

        address inputToken = inputAsset == LiquidityPoolETH.AssetType.ETH ? address(0) : weth;
        ProviderRouteConfig memory routeConfig = _getRouteConfig(weth, stablecoinToken, preferredProvider);
        RouteTypesV2.RouteLeg[] memory legs = new RouteTypesV2.RouteLeg[](1);
        legs[0] = RouteTypesV2.RouteLeg({
            provider: preferredProvider,
            tokenIn: weth,
            tokenOut: stablecoinToken,
            amountSource: RouteTypesV2.AmountSource.UserInput,
            minAmountOut: amountOutMin,
            target: routeConfig.target,
            providerData: routeConfig.providerData
        });

        RouteTypesV2.RoutePlan memory plan = RouteTypesV2.RoutePlan({
            chainId: block.chainid,
            inputToken: inputToken,
            outputToken: stablecoinToken,
            amountIn: amountIn,
            minAmountOut: amountOutMin,
            recipient: msg.sender,
            deadline: block.timestamp + 300,
            legs: legs
        });

        return _executeRoute(plan, msg.sender, msg.value);
    }

    function _executeRoute(RouteTypesV2.RoutePlan memory plan, address payer, uint256 nativeValue)
        internal
        returns (uint256 amountOut)
    {
        _validatePlan(plan);

        address currentToken = plan.inputToken;
        uint256 currentAmount = plan.amountIn;
        if (currentToken == address(0)) {
            require(nativeValue == currentAmount, "EnhancedSwapRouterV2: native amount mismatch");
            IWETH(weth).deposit{value: currentAmount}();
            currentToken = weth;
        } else {
            IERC20(currentToken).safeTransferFrom(payer, address(this), currentAmount);
        }

        for (uint256 i = 0; i < plan.legs.length; i++) {
            RouteTypesV2.RouteLeg memory leg = plan.legs[i];
            if (leg.tokenIn != currentToken) revert InvalidPlan();
            if (i == 0 && leg.amountSource != RouteTypesV2.AmountSource.UserInput) revert InvalidPlan();
            if (i > 0 && leg.amountSource != RouteTypesV2.AmountSource.PreviousLeg) revert InvalidPlan();

            uint8 providerId = uint8(leg.provider);
            if (!providerEnabled[providerId]) revert ProviderDisabled();

            address adapter = providerAdapters[providerId];
            if (adapter == address(0)) revert ProviderAdapterNotConfigured();

            (bool ok, string memory reason) = IRouteExecutorAdapter(adapter).validate(leg);
            if (!ok) revert RouteValidationFailed(reason);

            IERC20(currentToken).safeTransfer(adapter, currentAmount);
            uint256 balanceBefore = IERC20(leg.tokenOut).balanceOf(address(this));
            uint256 reportedAmountOut = IRouteExecutorAdapter(adapter).execute(leg, currentAmount);
            uint256 balanceAfter = IERC20(leg.tokenOut).balanceOf(address(this));
            uint256 actualAmountOut = balanceAfter - balanceBefore;
            if (actualAmountOut == 0) {
                actualAmountOut = reportedAmountOut;
            }
            if (actualAmountOut < leg.minAmountOut) revert InsufficientOutput();

            currentToken = leg.tokenOut;
            currentAmount = actualAmountOut;
        }

        if (currentToken != plan.outputToken) revert InvalidPlan();
        if (currentAmount < plan.minAmountOut) revert InsufficientOutput();

        address recipient = plan.recipient == address(0) ? payer : plan.recipient;
        IERC20(currentToken).safeTransfer(recipient, currentAmount);

        emit RouteExecuted(
            keccak256(abi.encode(plan.chainId, plan.inputToken, plan.outputToken, plan.amountIn, plan.deadline)),
            payer,
            recipient,
            plan.inputToken == address(0) ? weth : plan.inputToken,
            currentToken,
            plan.amountIn,
            currentAmount
        );

        return currentAmount;
    }

    function _getRouteConfig(address tokenIn, address tokenOut, RouteTypesV2.Provider preferredProvider)
        internal
        view
        returns (ProviderRouteConfig memory)
    {
        RouteTypesV2.Provider[] memory providers = _getRoutingProviders(preferredProvider);
        for (uint256 i = 0; i < providers.length; i++) {
            uint8 providerId = uint8(providers[i]);
            ProviderRouteConfig storage config = providerRoutes[tokenIn][tokenOut][providerId];
            if (config.enabled && config.target != address(0) && providerEnabled[providerId]) {
                return ProviderRouteConfig({
                    target: config.target, providerData: config.providerData, enabled: config.enabled
                });
            }
        }
        revert RouteNotConfigured();
    }

    function _getRoutingProviders(RouteTypesV2.Provider preferredProvider)
        internal
        view
        returns (RouteTypesV2.Provider[] memory providers)
    {
        if (providerEnabled[uint8(preferredProvider)]) {
            providers = new RouteTypesV2.Provider[](1);
            providers[0] = preferredProvider;
            return providers;
        }

        providers = sizeBasedRouting[0];
        if (providers.length == 0) {
            providers = new RouteTypesV2.Provider[](5);
            providers[0] = RouteTypesV2.Provider.Dodo;
            providers[1] = RouteTypesV2.Provider.UniswapV3;
            providers[2] = RouteTypesV2.Provider.Balancer;
            providers[3] = RouteTypesV2.Provider.Curve;
            providers[4] = RouteTypesV2.Provider.OneInch;
        }
    }

    function _validatePlan(RouteTypesV2.RoutePlan memory plan) internal view {
        if (plan.amountIn == 0 || plan.legs.length == 0) revert InvalidPlan();
        if (plan.outputToken == address(0)) revert InvalidPlan();
        if (plan.chainId != block.chainid) revert InvalidPlan();
        if (plan.deadline < block.timestamp) revert InvalidPlan();
    }

    function _initializeDefaultRouting() internal {
        sizeBasedRouting[0].push(RouteTypesV2.Provider.Dodo);
        sizeBasedRouting[0].push(RouteTypesV2.Provider.UniswapV3);

        sizeBasedRouting[1].push(RouteTypesV2.Provider.Dodo);
        sizeBasedRouting[1].push(RouteTypesV2.Provider.Balancer);
        sizeBasedRouting[1].push(RouteTypesV2.Provider.UniswapV3);

        sizeBasedRouting[2].push(RouteTypesV2.Provider.Dodo);
        sizeBasedRouting[2].push(RouteTypesV2.Provider.Curve);
        sizeBasedRouting[2].push(RouteTypesV2.Provider.Balancer);
    }

    function _copyPlan(RouteTypesV2.RoutePlan calldata plan)
        internal
        pure
        returns (RouteTypesV2.RoutePlan memory copied)
    {
        copied.chainId = plan.chainId;
        copied.inputToken = plan.inputToken;
        copied.outputToken = plan.outputToken;
        copied.amountIn = plan.amountIn;
        copied.minAmountOut = plan.minAmountOut;
        copied.recipient = plan.recipient;
        copied.deadline = plan.deadline;
        copied.legs = new RouteTypesV2.RouteLeg[](plan.legs.length);

        for (uint256 i = 0; i < plan.legs.length; i++) {
            copied.legs[i] = RouteTypesV2.RouteLeg({
                provider: plan.legs[i].provider,
                tokenIn: plan.legs[i].tokenIn,
                tokenOut: plan.legs[i].tokenOut,
                amountSource: plan.legs[i].amountSource,
                minAmountOut: plan.legs[i].minAmountOut,
                target: plan.legs[i].target,
                providerData: plan.legs[i].providerData
            });
        }
    }

    function _defaultPreferredProvider() internal pure returns (RouteTypesV2.Provider) {
        return RouteTypesV2.Provider.Dodo;
    }

    function _isValidStablecoin(address token) internal view returns (bool) {
        return token == usdt || token == usdc || token == dai;
    }

    receive() external payable {}
}
