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

/**
 * @title AereSwapRouter
 * @notice Periphery router for AereSwap constant-product AMM (factory + pairs
 *         already deployed). ABI-compatible with the canonical x*y=k router
 *         interface so existing wallets, aggregators, and SDKs work against
 *         AERE without code changes.
 *         Adds:
 *           - addLiquidity (with deadline + slippage min-amounts)
 *           - addLiquidityAERE (native AERE auto-wraps to WAERE)
 *           - removeLiquidity / removeLiquidityAERE
 *           - swapExactTokensForTokens
 *           - swapExactAEREForTokens / swapExactTokensForAERE (auto-wrap/unwrap)
 *           - quote / getAmountOut / getAmountIn / getAmountsOut / getAmountsIn helpers
 *
 *         Designed to plug into the already-deployed AereSwapFactory
 *         at 0xf0a8df7BDc25721892475B21271e52D77B0e84DC.
 */

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

interface IWAERE {
    function deposit() external payable;
    function withdraw(uint256) external;
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address) external view returns (uint256);
}

interface IAereSwapFactory {
    function getPair(address tokenA, address tokenB) external view returns (address pair);
    function createPair(address tokenA, address tokenB) external returns (address pair);
    function WAERE() external view returns (address);
}

interface IAereSwapPair {
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 ts);
    function mint(address to) external returns (uint256 liquidity);
    function burn(address to) external returns (uint256 amount0, uint256 amount1);
    function swap(uint256 amount0Out, uint256 amount1Out, address to) external;
    function totalSupply() external view returns (uint256);
    function balanceOf(address) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract AereSwapRouter {
    address public immutable factory;
    address public immutable WAERE;

    modifier ensure(uint256 deadline) {
        require(block.timestamp <= deadline, "AereRouter: EXPIRED");
        _;
    }

    constructor(address _factory, address _WAERE) {
        factory = _factory;
        WAERE = _WAERE;
    }

    // Allow this contract to receive native AERE from WAERE.withdraw().
    receive() external payable {
        require(msg.sender == WAERE, "AereRouter: only WAERE");
    }

    // ═════════════════════════════════════════════════════════════════════
    //                    LIQUIDITY
    // ═════════════════════════════════════════════════════════════════════

    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin
    ) internal returns (uint256 amountA, uint256 amountB) {
        if (IAereSwapFactory(factory).getPair(tokenA, tokenB) == address(0)) {
            IAereSwapFactory(factory).createPair(tokenA, tokenB);
        }
        (uint256 rA, uint256 rB) = getReserves(factory, tokenA, tokenB);
        if (rA == 0 && rB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint256 amountBOptimal = quote(amountADesired, rA, rB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, "AereRouter: INSUFFICIENT_B_AMOUNT");
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint256 amountAOptimal = quote(amountBDesired, rB, rA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, "AereRouter: INSUFFICIENT_A_AMOUNT");
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
        address pair = pairFor(factory, tokenA, tokenB);
        _safeTransferFrom(tokenA, msg.sender, pair, amountA);
        _safeTransferFrom(tokenB, msg.sender, pair, amountB);
        liquidity = IAereSwapPair(pair).mint(to);
    }

    function addLiquidityAERE(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountAEREMin,
        address to,
        uint256 deadline
    )
        external
        payable
        ensure(deadline)
        returns (uint256 amountToken, uint256 amountAERE, uint256 liquidity)
    {
        (amountToken, amountAERE) = _addLiquidity(
            token, WAERE, amountTokenDesired, msg.value, amountTokenMin, amountAEREMin
        );
        address pair = pairFor(factory, token, WAERE);
        _safeTransferFrom(token, msg.sender, pair, amountToken);
        IWAERE(WAERE).deposit{ value: amountAERE }();
        require(IWAERE(WAERE).transfer(pair, amountAERE), "AereRouter: WAERE_TRANSFER_FAIL");
        liquidity = IAereSwapPair(pair).mint(to);
        // refund any dust
        if (msg.value > amountAERE) _safeTransferAERE(msg.sender, msg.value - amountAERE);
    }

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) public ensure(deadline) returns (uint256 amountA, uint256 amountB) {
        address pair = pairFor(factory, tokenA, tokenB);
        require(IAereSwapPair(pair).transferFrom(msg.sender, pair, liquidity), "AereRouter: LP_TRANSFER_FAIL");
        (uint256 amount0, uint256 amount1) = IAereSwapPair(pair).burn(to);
        (address t0, ) = sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == t0 ? (amount0, amount1) : (amount1, amount0);
        require(amountA >= amountAMin, "AereRouter: INSUFFICIENT_A_AMOUNT");
        require(amountB >= amountBMin, "AereRouter: INSUFFICIENT_B_AMOUNT");
    }

    function removeLiquidityAERE(
        address token,
        uint256 liquidity,
        uint256 amountTokenMin,
        uint256 amountAEREMin,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256 amountToken, uint256 amountAERE) {
        (amountToken, amountAERE) = removeLiquidity(
            token, WAERE, liquidity, amountTokenMin, amountAEREMin, address(this), deadline
        );
        _safeTransfer(token, to, amountToken);
        IWAERE(WAERE).withdraw(amountAERE);
        _safeTransferAERE(to, amountAERE);
    }

    // ═════════════════════════════════════════════════════════════════════
    //                    SWAPS
    // ═════════════════════════════════════════════════════════════════════

    function _swap(uint256[] memory amounts, address[] memory path, address _to) internal {
        for (uint256 i = 0; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address t0, ) = sortTokens(input, output);
            uint256 amountOut = amounts[i + 1];
            (uint256 amount0Out, uint256 amount1Out) =
                input == t0 ? (uint256(0), amountOut) : (amountOut, uint256(0));
            address to = i < path.length - 2 ? pairFor(factory, output, path[i + 2]) : _to;
            IAereSwapPair(pairFor(factory, input, output)).swap(amount0Out, amount1Out, to);
        }
    }

    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256[] memory amounts) {
        amounts = getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, "AereRouter: INSUFFICIENT_OUTPUT_AMOUNT");
        _safeTransferFrom(path[0], msg.sender, pairFor(factory, path[0], path[1]), amounts[0]);
        _swap(amounts, path, to);
    }

    function swapExactAEREForTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable ensure(deadline) returns (uint256[] memory amounts) {
        require(path[0] == WAERE, "AereRouter: INVALID_PATH");
        amounts = getAmountsOut(factory, msg.value, path);
        require(amounts[amounts.length - 1] >= amountOutMin, "AereRouter: INSUFFICIENT_OUTPUT_AMOUNT");
        IWAERE(WAERE).deposit{ value: amounts[0] }();
        require(IWAERE(WAERE).transfer(pairFor(factory, path[0], path[1]), amounts[0]), "AereRouter: WAERE_FAIL");
        _swap(amounts, path, to);
    }

    function swapExactTokensForAERE(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256[] memory amounts) {
        require(path[path.length - 1] == WAERE, "AereRouter: INVALID_PATH");
        amounts = getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, "AereRouter: INSUFFICIENT_OUTPUT_AMOUNT");
        _safeTransferFrom(path[0], msg.sender, pairFor(factory, path[0], path[1]), amounts[0]);
        _swap(amounts, path, address(this));
        IWAERE(WAERE).withdraw(amounts[amounts.length - 1]);
        _safeTransferAERE(to, amounts[amounts.length - 1]);
    }

    // ═════════════════════════════════════════════════════════════════════
    //                    LIBRARY (constant-product AMM math, inlined)
    // ═════════════════════════════════════════════════════════════════════

    function sortTokens(address tokenA, address tokenB) public pure returns (address token0, address token1) {
        require(tokenA != tokenB, "AereRouter: IDENTICAL_ADDRESSES");
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), "AereRouter: ZERO_ADDRESS");
    }

    function pairFor(address _factory, address tokenA, address tokenB) public view returns (address pair) {
        pair = IAereSwapFactory(_factory).getPair(tokenA, tokenB);
        require(pair != address(0), "AereRouter: PAIR_NOT_EXISTS");
    }

    function getReserves(address _factory, address tokenA, address tokenB)
        public view returns (uint256 reserveA, uint256 reserveB)
    {
        (address t0, ) = sortTokens(tokenA, tokenB);
        (uint112 r0, uint112 r1, ) = IAereSwapPair(pairFor(_factory, tokenA, tokenB)).getReserves();
        (reserveA, reserveB) = tokenA == t0 ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0));
    }

    function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) public pure returns (uint256 amountB) {
        require(amountA > 0, "AereRouter: INSUFFICIENT_AMOUNT");
        require(reserveA > 0 && reserveB > 0, "AereRouter: INSUFFICIENT_LIQUIDITY");
        amountB = (amountA * reserveB) / reserveA;
    }

    // 0.3% fee — matches AereSwapPair.swap() math (1000-3 = 997 numerator).
    function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountOut) {
        require(amountIn > 0, "AereRouter: INSUFFICIENT_INPUT");
        require(reserveIn > 0 && reserveOut > 0, "AereRouter: INSUFFICIENT_LIQUIDITY");
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        amountOut = numerator / denominator;
    }

    function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountIn) {
        require(amountOut > 0, "AereRouter: INSUFFICIENT_OUTPUT");
        require(reserveIn > 0 && reserveOut > 0, "AereRouter: INSUFFICIENT_LIQUIDITY");
        uint256 numerator = reserveIn * amountOut * 1000;
        uint256 denominator = (reserveOut - amountOut) * 997;
        amountIn = (numerator / denominator) + 1;
    }

    function getAmountsOut(address _factory, uint256 amountIn, address[] memory path)
        public view returns (uint256[] memory amounts)
    {
        require(path.length >= 2, "AereRouter: INVALID_PATH");
        amounts = new uint256[](path.length);
        amounts[0] = amountIn;
        for (uint256 i = 0; i < path.length - 1; i++) {
            (uint256 rI, uint256 rO) = getReserves(_factory, path[i], path[i + 1]);
            amounts[i + 1] = getAmountOut(amounts[i], rI, rO);
        }
    }

    function getAmountsIn(address _factory, uint256 amountOut, address[] memory path)
        public view returns (uint256[] memory amounts)
    {
        require(path.length >= 2, "AereRouter: INVALID_PATH");
        amounts = new uint256[](path.length);
        amounts[amounts.length - 1] = amountOut;
        for (uint256 i = path.length - 1; i > 0; i--) {
            (uint256 rI, uint256 rO) = getReserves(_factory, path[i - 1], path[i]);
            amounts[i - 1] = getAmountIn(amounts[i], rI, rO);
        }
    }

    // ═════════════════════════════════════════════════════════════════════
    //                    SAFE TRANSFERS
    // ═════════════════════════════════════════════════════════════════════

    function _safeTransfer(address token, address to, uint256 value) private {
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
        require(ok && (data.length == 0 || abi.decode(data, (bool))), "AereRouter: TRANSFER_FAILED");
    }

    function _safeTransferFrom(address token, address from, address to, uint256 value) private {
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
        require(ok && (data.length == 0 || abi.decode(data, (bool))), "AereRouter: TRANSFER_FROM_FAILED");
    }

    function _safeTransferAERE(address to, uint256 value) private {
        (bool ok, ) = to.call{ value: value }("");
        require(ok, "AereRouter: AERE_TRANSFER_FAILED");
    }
}
