// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.20;
pragma abicoder v2;
import "hardhat/console.sol";

import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IWETH9 is IERC20 {
    function withdraw(uint256) external;
}

contract WhaleSync {
    address constant PH_ETH_ADDRESS =
        0x0000000000000000000000000000000000000000;

    address public immutable owner;
    ISwapRouter public immutable swapRouter;
    IUniswapV2Router02 public immutable swapRouterV2;
    IWETH9 public immutable weth;

    mapping(address => mapping(address => uint256)) private balances_per_user;
    mapping(address => address[]) private tokens_per_user;

    modifier isOwner() {
        require(msg.sender == owner, "Caller is not owner");
        _;
    }

    constructor(
        ISwapRouter _swapRouter,
        IUniswapV2Router02 _swapRouterV2,
        address _wethContract
    ) {
        owner = msg.sender;
        swapRouter = _swapRouter;
        swapRouterV2 = _swapRouterV2;
        weth = IWETH9(_wethContract);
    }

    function getTokens() public view returns (address[] memory tokens) {
        return tokens_per_user[msg.sender];
    }

    function getBalance(address token) public view returns (uint256 balance) {
        return balances_per_user[msg.sender][token];
    }

    receive() external payable {
        if (
            msg.sender != address(weth) && msg.sender != address(swapRouterV2)
        ) {
            _add_token_balance(msg.sender, msg.value, PH_ETH_ADDRESS);
        }
    }

    fallback() external payable {}

    function withdrawToken(uint256 amount, address token) public {
        require(
            amount <= balances_per_user[msg.sender][token],
            "Insufficient balance"
        );

        TransferHelper.safeTransferFrom(
            token,
            address(this),
            msg.sender,
            amount
        );

        balances_per_user[msg.sender][token] -= amount;
    }

    function withdrawEth(uint256 amount) public payable {
        require(
            amount <= balances_per_user[msg.sender][PH_ETH_ADDRESS],
            "Insufficient balance"
        );
        require(amount <= address(this).balance, "C: Insufficient balance");

        (bool sent, ) = payable(msg.sender).call{value: amount}("");
        require(sent, "Failed to send Ether");

        balances_per_user[msg.sender][PH_ETH_ADDRESS] -= amount;
    }

    /*
     *  Private Functions ----------------------------------------------------
     */

    function _add_token_to_user(address user, address token) private {
        address[] memory tokens = tokens_per_user[user];

        for (uint256 i = 0; i < tokens.length; i++) {
            if (tokens[i] == token) {
                return;
            }
        }

        tokens_per_user[user].push(token);
    }

    function _add_token_balance(
        address user,
        uint256 amount,
        address token
    ) private {
        _add_token_to_user(user, token);

        balances_per_user[user][token] += amount;
    }

    /*
     *  Owner Functions ----------------------------------------------------
     */

    function buy(
        address user,
        address token,
        uint256 amount,
        uint256 fee,
        uint256 amountOutMinimum,
        uint256 timestamp,
        uint24 swapFee
    ) external payable isOwner {
        require(
            balances_per_user[user][PH_ETH_ADDRESS] >= amount + fee,
            "swap: Insufficent funds"
        );

        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
            .ExactInputSingleParams({
                tokenIn: address(weth),
                tokenOut: token,
                fee: swapFee,
                recipient: address(this),
                deadline: timestamp,
                amountIn: amount,
                amountOutMinimum: amountOutMinimum,
                sqrtPriceLimitX96: 0
            });

        uint256 amountOut = swapRouter.exactInputSingle{value: amount}(params);

        balances_per_user[user][PH_ETH_ADDRESS] -= (amount + fee);
        _add_token_balance(user, amountOut, token);

        (bool sent, ) = payable(owner).call{value: fee}("");
        require(sent, "Failed to send Ether");
    }

    function sell(
        address user,
        address token,
        uint256 amount,
        uint256 fee,
        uint256 amountOutMinimum,
        uint256 timestamp,
        uint24 swapFee
    ) external isOwner {
        require(
            balances_per_user[user][token] >= amount,
            "swap: Insufficent funds"
        );

        TransferHelper.safeApprove(token, address(swapRouter), amount);

        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
            .ExactInputSingleParams({
                tokenIn: token,
                tokenOut: address(weth),
                fee: swapFee,
                recipient: address(this),
                deadline: timestamp,
                amountIn: amount,
                amountOutMinimum: amountOutMinimum,
                sqrtPriceLimitX96: 0
            });

        uint256 amountOut = swapRouter.exactInputSingle(params);

        TransferHelper.safeApprove(address(weth), address(weth), amountOut);
        IWETH9(address(weth)).withdraw(amountOut);

        require(amountOut - fee > 0, "swap fee: Insufficent funds");

        balances_per_user[user][token] -= amount;
        balances_per_user[user][PH_ETH_ADDRESS] += (amountOut - fee);

        (bool sent, ) = payable(owner).call{value: fee}("");
        require(sent, "Failed to send Ether");
    }

    function buyV2(
        address user,
        address token,
        uint256 amount,
        uint256 fee,
        uint256 amountOutMinimum,
        uint256 timestamp
    ) external payable isOwner {
        require(
            balances_per_user[user][PH_ETH_ADDRESS] >= amount + fee,
            "swap: Insufficent funds"
        );

        address[] memory path = new address[](2);
        path[0] = swapRouterV2.WETH();
        path[1] = token;

        uint[] memory amountsOut = swapRouterV2.swapExactETHForTokens{
            value: amount
        }(amountOutMinimum, path, address(this), timestamp);

        balances_per_user[user][PH_ETH_ADDRESS] -= (amount + fee);
        _add_token_balance(user, amountsOut[1], token);

        (bool sent, ) = payable(owner).call{value: fee}("");
        require(sent, "Failed to send Ether");
    }

    function sellV2(
        address user,
        address token,
        uint256 amount,
        uint256 fee,
        uint256 amountOutMinimum,
        uint256 timestamp
    ) external isOwner {
        require(
            balances_per_user[user][token] >= amount,
            "swap: Insufficent funds"
        );

        address[] memory path = new address[](2);
        path[0] = token;
        path[1] = swapRouterV2.WETH();

        uint256 balance = IERC20(token).balanceOf(address(this));

        require(amount <= balance, "ERROR: balance");

        TransferHelper.safeApprove(token, address(swapRouterV2), amount);

        uint[] memory amountsOut = swapRouterV2.swapExactTokensForETH(
            balance,
            amountOutMinimum,
            path,
            address(this),
            timestamp
        );

        require(amountsOut[1] - fee > 0, "swap fee: Insufficent funds");

        balances_per_user[user][token] -= amount;
        balances_per_user[user][PH_ETH_ADDRESS] += (amountsOut[1] - fee);

        (bool sent, ) = payable(owner).call{value: fee}("");
        require(sent, "Failed to send Ether");
    }

    function ownerGetTokens(
        address user
    ) public view isOwner returns (address[] memory tokens) {
        return tokens_per_user[user];
    }

    function ownerGetBalance(
        address user,
        address token
    ) public view isOwner returns (uint256 balance) {
        return balances_per_user[user][token];
    }
}
[{"inputs":[{"internalType":"contract ISwapRouter","name":"_swapRouter","type":"address"},{"internalType":"contract IUniswapV2Router02","name":"_swapRouterV2","type":"address"},{"internalType":"address","name":"_wethContract","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint24","name":"swapFee","type":"uint24"}],"name":"buy","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"buyV2","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTokens","outputs":[{"internalType":"address[]","name":"tokens","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"address","name":"token","type":"address"}],"name":"ownerGetBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"ownerGetTokens","outputs":[{"internalType":"address[]","name":"tokens","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"uint24","name":"swapFee","type":"uint24"}],"name":"sell","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"},{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"sellV2","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"swapRouter","outputs":[{"internalType":"contract ISwapRouter","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"swapRouterV2","outputs":[{"internalType":"contract IUniswapV2Router02","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"weth","outputs":[{"internalType":"contract IWETH9","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawEth","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address","name":"token","type":"address"}],"name":"withdrawToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]