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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../interfaces/IAggregationRouter.sol";
import "../interfaces/IBalancerVault.sol";
import "../interfaces/ICurvePool.sol";
import "../interfaces/ISwapRouter.sol";

library Chain138PilotVenueMath {
    function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
        require(tokenA != address(0) && tokenB != address(0) && tokenA != tokenB, "Chain138PilotVenueMath: invalid tokens");
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    }

    function pairKey(address tokenA, address tokenB, uint24 fee) internal pure returns (bytes32) {
        (address token0, address token1) = sortTokens(tokenA, tokenB);
        return keccak256(abi.encodePacked(token0, token1, fee));
    }

    function pairKey(address tokenA, address tokenB) internal pure returns (bytes32) {
        (address token0, address token1) = sortTokens(tokenA, tokenB);
        return keccak256(abi.encodePacked(token0, token1));
    }

    function constantProductQuote(
        uint256 reserveIn,
        uint256 reserveOut,
        uint256 amountIn,
        uint256 feeBps
    ) internal pure returns (uint256 amountOut) {
        if (reserveIn == 0 || reserveOut == 0 || amountIn == 0 || feeBps >= 10_000) {
            return 0;
        }
        uint256 amountInWithFee = amountIn * (10_000 - feeBps);
        return (reserveOut * amountInWithFee) / (reserveIn * 10_000 + amountInWithFee);
    }
}

contract Chain138PilotOwned {
    address public owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    modifier onlyOwner() {
        require(msg.sender == owner, "Chain138PilotOwned: not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Chain138PilotOwned: zero owner");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

contract Chain138PilotUniswapV3Router is Chain138PilotOwned, ISwapRouter {
    using SafeERC20 for IERC20;

    struct Pool {
        address token0;
        address token1;
        uint24 fee;
        uint256 reserve0;
        uint256 reserve1;
        bool exists;
    }

    mapping(bytes32 => Pool) private pools;

    event PairSeeded(bytes32 indexed poolKey, address indexed token0, address indexed token1, uint24 fee, uint256 amount0, uint256 amount1);
    event PairFunded(bytes32 indexed poolKey, uint256 amount0, uint256 amount1);
    event Swapped(bytes32 indexed poolKey, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut);

    function seedPair(
        address tokenA,
        address tokenB,
        uint24 fee,
        uint256 amountA,
        uint256 amountB
    ) external onlyOwner returns (bytes32 poolKey) {
        require(amountA > 0 && amountB > 0, "Chain138PilotUniswapV3Router: zero seed");
        poolKey = Chain138PilotVenueMath.pairKey(tokenA, tokenB, fee);
        Pool storage pool = pools[poolKey];
        (address token0, address token1) = Chain138PilotVenueMath.sortTokens(tokenA, tokenB);

        if (!pool.exists) {
            pool.token0 = token0;
            pool.token1 = token1;
            pool.fee = fee;
            pool.exists = true;
            emit PairSeeded(
                poolKey,
                token0,
                token1,
                fee,
                tokenA == token0 ? amountA : amountB,
                tokenA == token0 ? amountB : amountA
            );
        }

        _fundPool(poolKey, tokenA, tokenB, amountA, amountB);
    }

    function fundPair(
        address tokenA,
        address tokenB,
        uint24 fee,
        uint256 amountA,
        uint256 amountB
    ) external onlyOwner {
        bytes32 poolKey = Chain138PilotVenueMath.pairKey(tokenA, tokenB, fee);
        require(pools[poolKey].exists, "Chain138PilotUniswapV3Router: pair missing");
        _fundPool(poolKey, tokenA, tokenB, amountA, amountB);
    }

    function getPairReserves(
        address tokenA,
        address tokenB,
        uint24 fee
    ) external view returns (uint256 reserveIn, uint256 reserveOut, bool exists) {
        bytes32 poolKey = Chain138PilotVenueMath.pairKey(tokenA, tokenB, fee);
        Pool storage pool = pools[poolKey];
        if (!pool.exists) {
            return (0, 0, false);
        }
        if (tokenA == pool.token0) {
            return (pool.reserve0, pool.reserve1, true);
        }
        return (pool.reserve1, pool.reserve0, true);
    }

    function quoteExactInputSingle(
        address tokenIn,
        address tokenOut,
        uint24 fee,
        uint256 amountIn,
        uint160
    ) external view returns (uint256 amountOut) {
        (uint256 reserveIn, uint256 reserveOut, bool exists) = this.getPairReserves(tokenIn, tokenOut, fee);
        require(exists, "Chain138PilotUniswapV3Router: pair missing");
        return Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, amountIn, fee / 100);
    }

    function quoteExactInput(bytes calldata path, uint256 amountIn) external view returns (uint256 amountOut) {
        (address tokenIn, address tokenOut, uint24 fee) = _decodeSingleHopPath(path);
        return this.quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0);
    }

    function exactInputSingle(
        ExactInputSingleParams calldata params
    ) external payable override returns (uint256 amountOut) {
        amountOut = _swap(
            params.tokenIn,
            params.tokenOut,
            params.fee,
            params.amountIn,
            params.amountOutMinimum,
            params.recipient
        );
    }

    function exactInput(
        ExactInputParams calldata params
    ) external payable override returns (uint256 amountOut) {
        (address tokenIn, address tokenOut, uint24 fee) = _decodeSingleHopPath(params.path);
        amountOut = _swap(
            tokenIn,
            tokenOut,
            fee,
            params.amountIn,
            params.amountOutMinimum,
            params.recipient
        );
    }

    function _fundPool(
        bytes32 poolKey,
        address tokenA,
        address tokenB,
        uint256 amountA,
        uint256 amountB
    ) internal {
        Pool storage pool = pools[poolKey];
        IERC20(tokenA).safeTransferFrom(msg.sender, address(this), amountA);
        IERC20(tokenB).safeTransferFrom(msg.sender, address(this), amountB);

        if (tokenA == pool.token0) {
            pool.reserve0 += amountA;
            pool.reserve1 += amountB;
            emit PairFunded(poolKey, amountA, amountB);
        } else {
            pool.reserve0 += amountB;
            pool.reserve1 += amountA;
            emit PairFunded(poolKey, amountB, amountA);
        }
    }

    function _swap(
        address tokenIn,
        address tokenOut,
        uint24 fee,
        uint256 amountIn,
        uint256 minAmountOut,
        address recipient
    ) internal returns (uint256 amountOut) {
        bytes32 poolKey = Chain138PilotVenueMath.pairKey(tokenIn, tokenOut, fee);
        Pool storage pool = pools[poolKey];
        require(pool.exists, "Chain138PilotUniswapV3Router: pair missing");

        bool inputIsToken0 = tokenIn == pool.token0;
        uint256 reserveIn = inputIsToken0 ? pool.reserve0 : pool.reserve1;
        uint256 reserveOut = inputIsToken0 ? pool.reserve1 : pool.reserve0;

        IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
        amountOut = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, amountIn, fee / 100);
        require(amountOut >= minAmountOut, "Chain138PilotUniswapV3Router: insufficient output");

        if (inputIsToken0) {
            pool.reserve0 += amountIn;
            pool.reserve1 -= amountOut;
        } else {
            pool.reserve1 += amountIn;
            pool.reserve0 -= amountOut;
        }

        IERC20(tokenOut).safeTransfer(recipient, amountOut);
        emit Swapped(poolKey, tokenIn, tokenOut, amountIn, amountOut);
    }

    function _decodeSingleHopPath(bytes memory path) internal pure returns (address tokenIn, address tokenOut, uint24 fee) {
        require(path.length == 43, "Chain138PilotUniswapV3Router: single hop only");
        assembly {
            tokenIn := shr(96, mload(add(path, 32)))
            fee := shr(232, mload(add(path, 52)))
            tokenOut := shr(96, mload(add(path, 55)))
        }
    }
}

contract Chain138PilotBalancerVault is Chain138PilotOwned, IBalancerVault {
    using SafeERC20 for IERC20;

    struct Pool {
        address token0;
        address token1;
        uint256 reserve0;
        uint256 reserve1;
        uint256 feeBps;
        bool exists;
    }

    mapping(bytes32 => Pool) private pools;

    event PoolSeeded(bytes32 indexed poolId, address indexed token0, address indexed token1, uint256 amount0, uint256 amount1, uint256 feeBps);
    event PoolFunded(bytes32 indexed poolId, uint256 amount0, uint256 amount1);
    event Swapped(bytes32 indexed poolId, address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut);

    function seedPool(
        bytes32 poolId,
        address token0,
        address token1,
        uint256 amount0,
        uint256 amount1,
        uint256 feeBps
    ) external onlyOwner {
        require(poolId != bytes32(0), "Chain138PilotBalancerVault: zero poolId");
        require(amount0 > 0 && amount1 > 0, "Chain138PilotBalancerVault: zero seed");
        Pool storage pool = pools[poolId];
        if (!pool.exists) {
            pool.token0 = token0;
            pool.token1 = token1;
            pool.feeBps = feeBps;
            pool.exists = true;
            emit PoolSeeded(poolId, token0, token1, amount0, amount1, feeBps);
        }
        _fund(poolId, amount0, amount1);
    }

    function fundPool(bytes32 poolId, uint256 amount0, uint256 amount1) external onlyOwner {
        require(pools[poolId].exists, "Chain138PilotBalancerVault: pool missing");
        _fund(poolId, amount0, amount1);
    }

    function swap(
        SingleSwap memory singleSwap,
        FundManagement memory funds,
        uint256 limit,
        uint256 deadline
    ) external payable override returns (uint256 amountCalculated) {
        require(deadline >= block.timestamp, "Chain138PilotBalancerVault: expired");
        Pool storage pool = pools[singleSwap.poolId];
        require(pool.exists, "Chain138PilotBalancerVault: pool missing");
        require(singleSwap.kind == SwapKind.GIVEN_IN, "Chain138PilotBalancerVault: GIVEN_IN only");

        bool inputIsToken0;
        if (singleSwap.assetIn == pool.token0 && singleSwap.assetOut == pool.token1) {
            inputIsToken0 = true;
        } else if (singleSwap.assetIn == pool.token1 && singleSwap.assetOut == pool.token0) {
            inputIsToken0 = false;
        } else {
            revert("Chain138PilotBalancerVault: bad assets");
        }

        uint256 reserveIn = inputIsToken0 ? pool.reserve0 : pool.reserve1;
        uint256 reserveOut = inputIsToken0 ? pool.reserve1 : pool.reserve0;
        IERC20(singleSwap.assetIn).safeTransferFrom(funds.sender, address(this), singleSwap.amount);
        amountCalculated = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, singleSwap.amount, pool.feeBps);
        require(amountCalculated >= limit, "Chain138PilotBalancerVault: below limit");

        if (inputIsToken0) {
            pool.reserve0 += singleSwap.amount;
            pool.reserve1 -= amountCalculated;
        } else {
            pool.reserve1 += singleSwap.amount;
            pool.reserve0 -= amountCalculated;
        }

        IERC20(singleSwap.assetOut).safeTransfer(funds.recipient, amountCalculated);
        emit Swapped(singleSwap.poolId, singleSwap.assetIn, singleSwap.assetOut, singleSwap.amount, amountCalculated);
    }

    function getPool(bytes32 poolId) external view override returns (address poolAddress, uint8 specialization) {
        require(pools[poolId].exists, "Chain138PilotBalancerVault: pool missing");
        return (address(this), 0);
    }

    function queryBatchSwap(
        SwapKind kind,
        SingleSwap[] memory swaps,
        address[] memory
    ) external view override returns (int256[] memory assetDeltas) {
        require(kind == SwapKind.GIVEN_IN, "Chain138PilotBalancerVault: GIVEN_IN only");
        assetDeltas = new int256[](swaps.length > 0 ? 2 : 0);
        if (swaps.length == 0) {
            return assetDeltas;
        }
        Pool storage pool = pools[swaps[0].poolId];
        bool inputIsToken0 = swaps[0].assetIn == pool.token0;
        uint256 reserveIn = inputIsToken0 ? pool.reserve0 : pool.reserve1;
        uint256 reserveOut = inputIsToken0 ? pool.reserve1 : pool.reserve0;
        uint256 amountOut = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, swaps[0].amount, pool.feeBps);
        assetDeltas[0] = int256(swaps[0].amount);
        assetDeltas[1] = -int256(amountOut);
    }

    function getPoolTokens(
        bytes32 poolId
    ) external view override returns (address[] memory tokens, uint256[] memory balances, uint256 lastChangeBlock) {
        Pool storage pool = pools[poolId];
        require(pool.exists, "Chain138PilotBalancerVault: pool missing");
        tokens = new address[](2);
        balances = new uint256[](2);
        tokens[0] = pool.token0;
        tokens[1] = pool.token1;
        balances[0] = pool.reserve0;
        balances[1] = pool.reserve1;
        return (tokens, balances, block.number);
    }

    function _fund(bytes32 poolId, uint256 amount0, uint256 amount1) internal {
        Pool storage pool = pools[poolId];
        IERC20(pool.token0).safeTransferFrom(msg.sender, address(this), amount0);
        IERC20(pool.token1).safeTransferFrom(msg.sender, address(this), amount1);
        pool.reserve0 += amount0;
        pool.reserve1 += amount1;
        emit PoolFunded(poolId, amount0, amount1);
    }
}

contract Chain138PilotCurve3Pool is Chain138PilotOwned, ICurvePool {
    using SafeERC20 for IERC20;

    address[3] private _coins;
    uint256[3] private _reserves;
    uint256 public immutable feeBps;

    event Funded(uint256 amount0, uint256 amount1, uint256 amount2);
    event Exchanged(int128 indexed i, int128 indexed j, uint256 dx, uint256 dy);

    constructor(address token0, address token1, address token2, uint256 feeBps_) {
        require(token0 != address(0) && token1 != address(0), "Chain138PilotCurve3Pool: invalid tokens");
        _coins[0] = token0;
        _coins[1] = token1;
        _coins[2] = token2;
        feeBps = feeBps_;
    }

    function fund(uint256 amount0, uint256 amount1, uint256 amount2) external onlyOwner {
        if (amount0 > 0) IERC20(_coins[0]).safeTransferFrom(msg.sender, address(this), amount0);
        if (amount1 > 0) IERC20(_coins[1]).safeTransferFrom(msg.sender, address(this), amount1);
        if (_coins[2] != address(0) && amount2 > 0) IERC20(_coins[2]).safeTransferFrom(msg.sender, address(this), amount2);
        _reserves[0] += amount0;
        _reserves[1] += amount1;
        _reserves[2] += amount2;
        emit Funded(amount0, amount1, amount2);
    }

    function reserves(uint256 index) external view returns (uint256) {
        return _reserves[index];
    }

    function exchange(
        int128 i,
        int128 j,
        uint256 dx,
        uint256 min_dy
    ) external payable override returns (uint256 dy) {
        dy = _exchange(i, j, dx, min_dy, false);
    }

    function exchange_underlying(
        int128 i,
        int128 j,
        uint256 dx,
        uint256 min_dy
    ) external payable override returns (uint256 dy) {
        dy = _exchange(i, j, dx, min_dy, true);
    }

    function get_dy(
        int128 i,
        int128 j,
        uint256 dx
    ) public view override returns (uint256 dy) {
        uint256 ui = uint256(uint128(i));
        uint256 uj = uint256(uint128(j));
        require(ui < 3 && uj < 3 && ui != uj, "Chain138PilotCurve3Pool: bad indexes");
        require(_coins[ui] != address(0) && _coins[uj] != address(0), "Chain138PilotCurve3Pool: token missing");

        uint256 reserveIn = _reserves[ui];
        uint256 reserveOut = _reserves[uj];
        require(reserveIn > 0 && reserveOut > 0, "Chain138PilotCurve3Pool: empty reserves");

        uint256 grossOut = (dx * reserveOut) / reserveIn;
        dy = (grossOut * (10_000 - feeBps)) / 10_000;
        if (dy > reserveOut) {
            dy = reserveOut;
        }
    }

    function coins(uint256 i) external view override returns (address) {
        return _coins[i];
    }

    function _exchange(
        int128 i,
        int128 j,
        uint256 dx,
        uint256 min_dy,
        bool
    ) internal returns (uint256 dy) {
        uint256 ui = uint256(uint128(i));
        uint256 uj = uint256(uint128(j));
        dy = get_dy(i, j, dx);
        require(dy >= min_dy, "Chain138PilotCurve3Pool: insufficient output");

        IERC20(_coins[ui]).safeTransferFrom(msg.sender, address(this), dx);
        _reserves[ui] += dx;
        _reserves[uj] -= dy;
        IERC20(_coins[uj]).safeTransfer(msg.sender, dy);
        emit Exchanged(i, j, dx, dy);
    }
}

contract Chain138PilotOneInchAggregationRouter is Chain138PilotOwned, IAggregationRouter {
    using SafeERC20 for IERC20;

    struct Route {
        address token0;
        address token1;
        uint256 reserve0;
        uint256 reserve1;
        uint256 feeBps;
        bool exists;
    }

    mapping(bytes32 => Route) private routes;

    event RouteSeeded(address indexed token0, address indexed token1, uint256 amount0, uint256 amount1, uint256 feeBps);
    event RouteFunded(address indexed token0, address indexed token1, uint256 amount0, uint256 amount1);
    event Swapped(address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, address indexed recipient);

    function seedRoute(
        address tokenA,
        address tokenB,
        uint256 amountA,
        uint256 amountB,
        uint256 feeBps
    ) external onlyOwner {
        require(amountA > 0 && amountB > 0, "Chain138PilotOneInchAggregationRouter: zero seed");
        bytes32 key = Chain138PilotVenueMath.pairKey(tokenA, tokenB);
        Route storage route = routes[key];
        (address token0, address token1) = Chain138PilotVenueMath.sortTokens(tokenA, tokenB);
        if (!route.exists) {
            route.token0 = token0;
            route.token1 = token1;
            route.feeBps = feeBps;
            route.exists = true;
            emit RouteSeeded(
                token0,
                token1,
                tokenA == token0 ? amountA : amountB,
                tokenA == token0 ? amountB : amountA,
                feeBps
            );
        }
        _fundRoute(key, tokenA, tokenB, amountA, amountB);
    }

    function fundRoute(address tokenA, address tokenB, uint256 amountA, uint256 amountB) external onlyOwner {
        bytes32 key = Chain138PilotVenueMath.pairKey(tokenA, tokenB);
        require(routes[key].exists, "Chain138PilotOneInchAggregationRouter: route missing");
        _fundRoute(key, tokenA, tokenB, amountA, amountB);
    }

    function getRouteReserves(
        address tokenA,
        address tokenB
    ) external view returns (uint256 reserveIn, uint256 reserveOut, bool exists) {
        bytes32 key = Chain138PilotVenueMath.pairKey(tokenA, tokenB);
        Route storage route = routes[key];
        if (!route.exists) {
            return (0, 0, false);
        }
        if (tokenA == route.token0) {
            return (route.reserve0, route.reserve1, true);
        }
        return (route.reserve1, route.reserve0, true);
    }

    function quote(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256 amountOut) {
        (uint256 reserveIn, uint256 reserveOut, bool exists) = this.getRouteReserves(tokenIn, tokenOut);
        require(exists, "Chain138PilotOneInchAggregationRouter: route missing");
        return Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, amountIn, routes[Chain138PilotVenueMath.pairKey(tokenIn, tokenOut)].feeBps);
    }

    function swap(
        address,
        SwapDescription calldata desc,
        bytes calldata,
        bytes calldata
    ) external payable override returns (uint256 returnAmount, uint256 spentAmount) {
        bytes32 key = Chain138PilotVenueMath.pairKey(desc.srcToken, desc.dstToken);
        Route storage route = routes[key];
        require(route.exists, "Chain138PilotOneInchAggregationRouter: route missing");

        bool inputIsToken0 = desc.srcToken == route.token0;
        uint256 reserveIn = inputIsToken0 ? route.reserve0 : route.reserve1;
        uint256 reserveOut = inputIsToken0 ? route.reserve1 : route.reserve0;

        IERC20(desc.srcToken).safeTransferFrom(msg.sender, address(this), desc.amount);
        returnAmount = Chain138PilotVenueMath.constantProductQuote(reserveIn, reserveOut, desc.amount, route.feeBps);
        require(returnAmount >= desc.minReturnAmount, "Chain138PilotOneInchAggregationRouter: insufficient output");

        if (inputIsToken0) {
            route.reserve0 += desc.amount;
            route.reserve1 -= returnAmount;
        } else {
            route.reserve1 += desc.amount;
            route.reserve0 -= returnAmount;
        }

        IERC20(desc.dstToken).safeTransfer(desc.dstReceiver, returnAmount);
        emit Swapped(desc.srcToken, desc.dstToken, desc.amount, returnAmount, desc.dstReceiver);
        return (returnAmount, desc.amount);
    }

    function _fundRoute(
        bytes32 key,
        address tokenA,
        address tokenB,
        uint256 amountA,
        uint256 amountB
    ) internal {
        Route storage route = routes[key];
        IERC20(tokenA).safeTransferFrom(msg.sender, address(this), amountA);
        IERC20(tokenB).safeTransferFrom(msg.sender, address(this), amountB);

        if (tokenA == route.token0) {
            route.reserve0 += amountA;
            route.reserve1 += amountB;
            emit RouteFunded(route.token0, route.token1, amountA, amountB);
        } else {
            route.reserve0 += amountB;
            route.reserve1 += amountA;
            emit RouteFunded(route.token0, route.token1, amountB, amountA);
        }
    }
}
