"""TradeModule — high-level trading interface for swap, perps, and lending.

Each method encodes the adapter-specific calldata and routes it through
``TradeRouter.route(vault, adapter, data)``.
"""

from __future__ import annotations

from typing import Optional

from openhedge.constants import (
    AAVE_ACTION_SUPPLY,
    AAVE_ACTION_WITHDRAW,
    GAINS_ACTION_CLOSE,
    GAINS_ACTION_OPEN,
    GAINS_ACTION_PARTIAL_CLOSE,
    GAINS_ACTION_UPDATE_MARGIN,
    GAINS_ACTION_UPDATE_TP_SL,
    UNISWAP_DEFAULT_FEE,
    resolve_pair_index,
    resolve_token,
)
from openhedge.exceptions import ValidationError
from openhedge.models.trade import TradeResult, TradeType
from openhedge.modules.base import BaseModule


class TradeModule(BaseModule):
    """High-level trade execution through the TradeRouter."""

    # ------------------------------------------------------------------
    # Spot swap (Uniswap V3)
    # ------------------------------------------------------------------

    async def swap(
        self,
        vault: str,
        token_in: str,
        token_out: str,
        amount_in: int,
        *,
        min_amount_out: Optional[int] = None,
        slippage_bps: int = 50,
        fee: int = UNISWAP_DEFAULT_FEE,
        deadline_offset: int = 300,
    ) -> TradeResult:
        """Execute a spot swap through the Uniswap V3 adapter.

        Parameters
        ----------
        vault:
            Vault address that holds the tokens.
        token_in:
            Symbol (``'USDC'``) or address of the token to sell.
        token_out:
            Symbol or address of the token to buy.
        amount_in:
            Amount to sell in smallest units.
        min_amount_out:
            Explicit minimum output amount.  When provided this takes
            precedence over the ``slippage_bps`` fallback.  **Always
            supply this for cross-token swaps with different decimals.**
        slippage_bps:
            Maximum slippage tolerance (default 50 = 0.5%).  Only used
            as a fallback when *min_amount_out* is ``None`` and is only
            valid for same-decimal stablecoin pairs.
        fee:
            Uniswap pool fee tier (default 3000 = 0.3%).
        deadline_offset:
            Seconds from now until the swap deadline.
        """
        if slippage_bps < 0 or slippage_bps > 10000:
            raise ValidationError("slippage_bps must be 0-10000")

        token_in_addr = resolve_token(token_in, self._network)
        token_out_addr = resolve_token(token_out, self._network)

        # WARNING: For cross-token swaps with different decimals, always provide min_amount_out explicitly.
        if min_amount_out is not None:
            amount_out_min = min_amount_out
        else:
            # Fallback: only valid for same-decimal stablecoin pairs.
            # For cross-token swaps, always provide min_amount_out explicitly.
            amount_out_min = amount_in * (10000 - slippage_bps) // 10000

        # Get current block timestamp for deadline
        block = await self._rpc_call("eth_getBlockByNumber", ["latest", False])
        timestamp = int(block["timestamp"], 16)
        deadline = timestamp + deadline_offset

        # Encode adapter calldata:
        # abi.encode(address tokenIn, address tokenOut, uint24 fee,
        #            uint256 amountIn, uint256 amountOutMin, uint256 deadline)
        from eth_abi import encode

        adapter_data = encode(
            ["address", "address", "uint24", "uint256", "uint256", "uint256"],
            [token_in_addr, token_out_addr, fee, amount_in, amount_out_min, deadline],
        )

        # TradeRouter.route(address vault, address adapter, bytes data)
        uniswap_adapter = self._contracts["adapters"]["uniswap"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, uniswap_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.SWAP,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=uniswap_adapter,
            vault=vault,
        )

    # ------------------------------------------------------------------
    # Perpetuals (Gains / gTrade)
    # ------------------------------------------------------------------

    async def open_position(
        self,
        vault: str,
        pair: str,
        is_long: bool,
        collateral: int,
        leverage: int,
        *,
        take_profit: int = 0,
        stop_loss: int = 0,
    ) -> TradeResult:
        """Open a perpetual position through the Gains adapter.

        Parameters
        ----------
        vault:
            Vault address providing collateral.
        pair:
            Trading pair name (e.g. ``'BTC/USD'``).
        is_long:
            ``True`` for long, ``False`` for short.
        collateral:
            Collateral amount in underlying asset units.
        leverage:
            Leverage multiplier (e.g. 10 for 10x).
        take_profit:
            Take-profit price (0 = none).
        stop_loss:
            Stop-loss price (0 = none).
        """
        if leverage < 2:
            raise ValidationError("Leverage must be at least 2x")

        pair_index = resolve_pair_index(pair)

        from eth_abi import encode

        # action=0 (OPEN), pairIndex, isLong, collateral, leverage, tp, sl, orderType=0
        adapter_data = encode(
            ["uint8", "uint256", "bool", "uint256", "uint256", "uint256", "uint256", "uint8"],
            [GAINS_ACTION_OPEN, pair_index, is_long, collateral, leverage, take_profit, stop_loss, 0],
        )

        gains_adapter = self._contracts["adapters"]["gains"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, gains_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.PERP_OPEN,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=gains_adapter,
            vault=vault,
            details={"pair": pair, "is_long": is_long, "leverage": leverage},
        )

    async def close_trade(
        self,
        vault: str,
        trade_index: int,
    ) -> TradeResult:
        """Close an open perpetual position.

        Parameters
        ----------
        vault:
            Vault address that owns the position.
        trade_index:
            On-chain index of the trade to close.
        """
        from eth_abi import encode

        # action=1 (CLOSE), tradeIndex
        adapter_data = encode(
            ["uint8", "uint256"],
            [GAINS_ACTION_CLOSE, trade_index],
        )

        gains_adapter = self._contracts["adapters"]["gains"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, gains_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.PERP_CLOSE,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=gains_adapter,
            vault=vault,
            details={"trade_index": trade_index},
        )

    async def partial_close(
        self,
        vault: str,
        trade_index: int,
        percentage_bps: int,
    ) -> TradeResult:
        """Partially close a gTrade position.

        Parameters
        ----------
        vault:
            Vault address that owns the position.
        trade_index:
            On-chain index of the trade to partially close.
        percentage_bps:
            Percentage to close in basis points (e.g. 5000 = 50%).
        """
        if percentage_bps < 1 or percentage_bps > 10000:
            raise ValidationError("percentage_bps must be 1-10000")

        from eth_abi import encode

        # action=2 (PARTIAL_CLOSE), tradeIndex, percentageBps
        adapter_data = encode(
            ["uint8", "uint256", "uint256"],
            [GAINS_ACTION_PARTIAL_CLOSE, trade_index, percentage_bps],
        )

        gains_adapter = self._contracts["adapters"]["gains"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, gains_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.PERP_CLOSE,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=gains_adapter,
            vault=vault,
            details={"trade_index": trade_index, "percentage_bps": percentage_bps},
        )

    async def update_tp_sl(
        self,
        vault: str,
        trade_index: int,
        take_profit: int,
        stop_loss: int,
    ) -> TradeResult:
        """Update take profit and stop loss for a gTrade position.

        Parameters
        ----------
        vault:
            Vault address that owns the position.
        trade_index:
            On-chain index of the trade to update.
        take_profit:
            New take-profit price (integer, in contract units).
        stop_loss:
            New stop-loss price (integer, in contract units).
        """
        from eth_abi import encode

        # action=3 (UPDATE_TP_SL), tradeIndex, takeProfit, stopLoss
        adapter_data = encode(
            ["uint8", "uint256", "uint256", "uint256"],
            [GAINS_ACTION_UPDATE_TP_SL, trade_index, take_profit, stop_loss],
        )

        gains_adapter = self._contracts["adapters"]["gains"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, gains_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.UPDATE_TP_SL,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=gains_adapter,
            vault=vault,
            details={
                "trade_index": trade_index,
                "take_profit": take_profit,
                "stop_loss": stop_loss,
            },
        )

    async def update_margin(
        self,
        vault: str,
        trade_index: int,
        amount: int,
        is_add: bool,
    ) -> TradeResult:
        """Add or remove margin from a gTrade position.

        Parameters
        ----------
        vault:
            Vault address that owns the position.
        trade_index:
            On-chain index of the trade to update.
        amount:
            Margin amount in asset units.
        is_add:
            ``True`` to add margin, ``False`` to remove.
        """
        from eth_abi import encode

        # action=4 (UPDATE_MARGIN), tradeIndex, amount, isAdd
        adapter_data = encode(
            ["uint8", "uint256", "uint256", "bool"],
            [GAINS_ACTION_UPDATE_MARGIN, trade_index, amount, is_add],
        )

        gains_adapter = self._contracts["adapters"]["gains"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, gains_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.UPDATE_MARGIN,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=gains_adapter,
            vault=vault,
            details={
                "trade_index": trade_index,
                "amount": amount,
                "is_add": is_add,
            },
        )

    # ------------------------------------------------------------------
    # Lending (Aave)
    # ------------------------------------------------------------------

    async def lend(
        self,
        vault: str,
        asset: str,
        amount: int,
    ) -> TradeResult:
        """Supply assets to Aave through the lending adapter.

        Parameters
        ----------
        vault:
            Vault address providing the assets.
        asset:
            Token symbol or address to supply.
        amount:
            Amount in smallest units.
        """
        asset_addr = resolve_token(asset, self._network)

        from eth_abi import encode

        # action=0 (SUPPLY), asset, amount, recipient (vault)
        adapter_data = encode(
            ["uint8", "address", "uint256", "address"],
            [AAVE_ACTION_SUPPLY, asset_addr, amount, vault],
        )

        aave_adapter = self._contracts["adapters"]["aave"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, aave_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.LEND,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=aave_adapter,
            vault=vault,
            details={"asset": asset, "amount": amount},
        )

    async def withdraw_lending(
        self,
        vault: str,
        asset: str,
        amount: int,
    ) -> TradeResult:
        """Withdraw assets from Aave lending position.

        Parameters
        ----------
        vault:
            Vault address that holds the lending position.
        asset:
            Token symbol or address to withdraw.
        amount:
            Amount in smallest units (use ``2**256 - 1`` for max).
        """
        asset_addr = resolve_token(asset, self._network)

        from eth_abi import encode

        # action=1 (WITHDRAW), asset, amount, recipient (vault)
        adapter_data = encode(
            ["uint8", "address", "uint256", "address"],
            [AAVE_ACTION_WITHDRAW, asset_addr, amount, vault],
        )

        aave_adapter = self._contracts["adapters"]["aave"]
        calldata = self._encode_call(
            "route(address,address,bytes)",
            ["address", "address", "bytes"],
            [vault, aave_adapter, adapter_data],
        )

        trade_router = self._contracts["trade_router"]
        receipt = await self._send_tx(to=trade_router, data=calldata)

        return TradeResult(
            tx_hash=receipt.get("transactionHash", ""),
            trade_type=TradeType.WITHDRAW_LENDING,
            block_number=int(receipt.get("blockNumber", "0x0"), 16),
            gas_used=int(receipt.get("gasUsed", "0x0"), 16),
            status=int(receipt.get("status", "0x1"), 16),
            adapter=aave_adapter,
            vault=vault,
            details={"asset": asset, "amount": amount},
        )
