from datetime import datetime
from enum import Enum, auto
from functools import singledispatchmethod
from typing import Optional

from eth_typing import ChecksumAddress, HexAddress, HexStr
from pydantic import BaseModel

from .token import Token, TokenAmount
from .wallet import Wallet
from .network import Network
from ..utils.client import _force_get_global_client


class Liquidity(BaseModel):
    token0_balance: int
    token0_price: Optional[float] = None
    token1_balance: int
    token1_price: Optional[float] = None
    pair: "DexPair"

    @property
    def token0_value(self):
        # TODO: handle invalid prices
        if self.token0_price:
            return (
                self.token0_price
                * self.token0_balance
                / 10**self.pair.token0.decimals
            )
        return -0.5

    @property
    def token1_value(self):
        # TODO: handle invalid prices
        if self.token1_price:
            return (
                self.token1_price
                * self.token1_balance
                / 10**self.pair.token1.decimals
            )
        return -0.5

    @property
    def value(self):
        return round(self.token0_value * 2, 2)

    def __repr__(self):
        token0_symbol = self.pair.token0.symbol
        token1_symbol = self.pair.token1.symbol
        token0 = self.token0_balance / 10**self.pair.token0.decimals
        token1 = self.token1_balance / 10**self.pair.token1.decimals
        return f"<Liquidity: ${format(self.value, ',')} ({token0_symbol}: {token0}, {token1_symbol}: {token1})>"

    __str__ = __repr__


class DexRoute(BaseModel):
    path: list[ChecksumAddress]
    pair_addresses: list[ChecksumAddress]
    eth_price: TokenAmount
    usdc_price: TokenAmount
    factory: "DexFactory"

    def __repr__(self):
        return f"<DexRoute | path={self.path} | eth_price: {self.eth_price.format(8)}, usdc_price: ${self.usdc_price.format(6)}>"

    __str__ = __repr__


class DexFactory(Enum):
    UniswapV2 = auto()  # 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f

    @property
    def weth(self):
        # TODO: handle by chain_id
        return "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"

    async def get_taxes(
        self,
        token0_address,
        token1_address=None,
        chain_id=1,
    ):
        if not token1_address:
            token1_address = self.weth
        client = _force_get_global_client()
        taxes = await client.prices.get_taxes(
            token0_address,
            token1_address,
            chain_id=chain_id,
        )
        return taxes

    @singledispatchmethod
    async def get_price(
        self,
        token_address: ChecksumAddress,
    ):
        client = _force_get_global_client()
        routes = await client.prices.get_routes(token_address)
        return [
            DexRoute(
                path=row["path"],
                pair_addresses=row["pair_addresses"],
                eth_price=TokenAmount(
                    amount=int(row["eth_price"] * 1e18),
                ),
                usdc_price=TokenAmount(
                    amount=int(row["usdc_price"] * 1e6),
                    decimals=6,
                ),
                factory=self,
            )
            for row in routes
        ]

    @get_price.register(Token)
    async def _(
        self,
        token: Token,
    ):
        return await self.get_price(token.address)

    async def get_pair_info(
        self,
        pair_address: HexAddress,
        force_checksum: bool = True,
        chain_id: int = 1,
    ):
        client = _force_get_global_client()
        pair_info = await client.prices.get_pair_info(
            pair_address,
            force_checksum=force_checksum,
            chain_id=chain_id,
        )
        token0 = Token(**pair_info["token0"])
        token1 = Token(**pair_info["token1"])
        return DexPair(
            factory_address=pair_info["factoryAddress"],
            token0=token0,
            token1=token1,
            address=pair_info["pairAddress"],
            index=pair_info["index"],
            fee=pair_info["feePercentage"],
            network=Network(pair_info["chainId"]),
            block_number=pair_info["blockNumber"],
            transaction_hash=pair_info["transactionHash"],
            factory=self,
        )

    async def get_pairs(
        self,
        token: Token,
    ) -> list["DexPair"]:
        client = _force_get_global_client()
        pairs = await client.prices.get_token_pairs(
            token_address=token.address,
            chain_id=token.network.value,
        )
        return [
            DexPair(
                factory_address=row["factoryAddress"],
                token0=Token(**row["token0"]),
                token1=Token(**row["token1"]),
                address=row["pairAddress"],
                index=row["index"],
                fee=row["feePercentage"],
                network=Network(row["chainId"]),
                block_number=row["blockNumber"],
                transaction_hash=row["transactionHash"],
                factory=self,
            )
            for row in pairs
        ]

    async def swap(
        self,
        wallet: Wallet,
        network: Network,
        path: list[ChecksumAddress],
        amount_in: int,
        slippage_percent: float,
        priority_fee: int,
        is_private: bool,
    ):
        raise NotImplementedError()


class SwapInterval(BaseModel):
    start_time: datetime
    open: float
    close: float
    tx_count: int
    min: float
    max: float
    prev_close: float


class SwapHistory(BaseModel):
    pair: "DexPair"
    intervals: list[SwapInterval]

    @property
    def timestamps(self):
        return [s.start_time for s in self.intervals]

    @property
    def opens(self):
        return [s.prev_close for s in self.intervals]

    @property
    def closes(self):
        return [s.close for s in self.intervals]

    @property
    def mins(self):
        return [s.min for s in self.intervals]

    @property
    def maxs(self):
        return [s.max for s in self.intervals]

    def __repr__(self):
        return f"<SwapHistory: {self.pair.address}>"

    __str__ = __repr__


class DexPair(BaseModel):
    factory_address: ChecksumAddress
    token0: Token
    token1: Token
    address: ChecksumAddress
    index: int

    fee: Optional[float]

    network: Network
    block_number: int

    transaction_hash: HexStr
    factory: DexFactory

    async def get_liquidity(self, block_number: Optional[int] = None):
        client = _force_get_global_client()
        response = await client.prices.get_liquidity(
            token_address=self.address,
            chain_id=self.network.value,
            block_number=block_number,
        )
        return Liquidity(
            token0_balance=response["balances"]["token0"]["amount"],
            token0_price=response["balances"]["token0"]["price"],
            token1_balance=response["balances"]["token1"]["amount"],
            token1_price=response["balances"]["token1"]["price"],
            pair=self,
        )

    async def get_taxes(self):
        return await self.factory.get_taxes(
            self.token0.address,
            self.token1.address,
        )

    async def swap_history(
        self, use_token0: bool = True, start_time=None, end_time=None
    ):
        client = _force_get_global_client()
        feed = await client.prices.load_feed(
            self.address,
            use_token0=use_token0,
        )
        response = []
        for row in sorted(feed.split("\n"))[1:]:
            (
                interval,
                open,
                close,
                min,
                max,
                min_block,
                max_block,
                num_tx,
                prev_close,
            ) = row.split(",")
            response.append(
                SwapInterval(
                    start_time=datetime.strptime(interval, "%Y-%m-%d %H:%M:%S+00:00"),
                    open=float(open),
                    close=float(close),
                    min=float(min),
                    max=float(max),
                    tx_count=int(num_tx),
                    prev_close=float(prev_close if prev_close != "None" else open),
                )
            )
        return SwapHistory(pair=self, intervals=response)

    async def swap(
        self,
        wallet: Wallet,
        amount: int,
        priority_fee: int,
        is_private: bool,
        use_token0: bool,
    ):
        raise NotImplementedError()

    async def honeypot(self) -> bool:
        raise NotImplementedError()

    def __repr__(self):
        return f"<DexPair: {self.token0.symbol}, {self.token1.symbol}>"

    __str__ = __repr__
