from __future__ import annotations

import asyncio
import logging
import time
from dataclasses import dataclass
from decimal import (
    Decimal,
    InvalidOperation,
)
from typing import Optional

from services.crypto_deposit_service import (
    TON,
    TRX,
    USDT,
    normalize_currency,
)

from services.iran_usdt_rate_service import (
    IranUsdtRateService,
)

from services.tabdeal_rate_service import (
    TabdealRateService,
)


logger = logging.getLogger(
    __name__
)


# ============================================================
# Exceptions
# ============================================================

class FastRateUnavailable(
    Exception
):
    pass


# ============================================================
# Rate Quote
# ============================================================

@dataclass(
    slots=True,
    frozen=True,
)
class FastRateQuote:
    currency: str

    rate_toman: Decimal

    source: str

    fetched_at: float

    @property
    def age_seconds(
        self,
    ) -> float:

        return max(
            0.0,
            (
                time.time()
                - self.fetched_at
            ),
        )


# ============================================================
# Fast Crypto Rate Cache
# ============================================================

class FastCryptoRateCache:
    """
    نرخ سریع برای تأیید واریز ارزی.

    حالت عادی:
        نرخ از RAM خوانده می‌شود.

    تنظیم پیش‌فرض:
        Refresh Background:
            هر 10 ثانیه

        Fresh Rate:
            حداکثر 30 ثانیه

        Max Stale:
            حداکثر 60 ثانیه

        Emergency Refresh:
            حداکثر 2 ثانیه

    یعنی هنگام تأیید مالک معمولاً
    هیچ درخواست اینترنتی انجام نمی‌شود
    و نرخ مستقیماً از RAM برداشته می‌شود.
    """

    def __init__(
        self,
        refresh_interval: float = 10.0,
        fresh_ttl: float = 30.0,
        max_stale_ttl: float = 60.0,
        emergency_timeout: float = 2.0,
    ) -> None:

        self.refresh_interval = max(
            5.0,
            float(
                refresh_interval
            ),
        )

        self.fresh_ttl = max(
            self.refresh_interval,
            float(
                fresh_ttl
            ),
        )

        self.max_stale_ttl = max(
            self.fresh_ttl,
            float(
                max_stale_ttl
            ),
        )

        self.emergency_timeout = max(
            0.8,
            float(
                emergency_timeout
            ),
        )

        # ----------------------------------------------------
        # RAM Cache
        # ----------------------------------------------------

        self._quotes: dict[
            str,
            FastRateQuote,
        ] = {}

        self._quotes_lock = (
            asyncio.Lock()
        )

        self._refresh_lock = (
            asyncio.Lock()
        )

        self._start_lock = (
            asyncio.Lock()
        )

        self._task: Optional[
            asyncio.Task
        ] = None


    # ========================================================
    # Decimal Validator
    # ========================================================

    @staticmethod
    def _d(
        value,
    ) -> Decimal:

        try:

            result = Decimal(
                str(
                    value
                )
            )

        except (
            InvalidOperation,
            TypeError,
            ValueError,
        ) as exc:

            raise FastRateUnavailable(
                "نرخ نامعتبر است."
            ) from exc

        if (
            not result.is_finite()
            or result <= 0
        ):

            raise FastRateUnavailable(
                "نرخ نامعتبر است."
            )

        return result


    # ========================================================
    # Start Background Worker
    # ========================================================

    async def ensure_started(
        self,
    ) -> None:

        if (
            self._task
            and not self._task.done()
        ):
            return

        async with self._start_lock:

            if (
                self._task
                and not self._task.done()
            ):
                return

            self._task = (
                asyncio.create_task(
                    self._loop(),
                    name=(
                        "fast-crypto-rates"
                    ),
                )
            )


    # ========================================================
    # Background Loop
    # ========================================================

    async def _loop(
        self,
    ) -> None:

        while True:

            try:

                await self.refresh_all()

            except asyncio.CancelledError:

                raise

            except Exception:

                logger.warning(
                    (
                        "Fast crypto rate "
                        "refresh failed."
                    ),
                    exc_info=True,
                )

            await asyncio.sleep(
                self.refresh_interval
            )


    # ========================================================
    # Save Quote To RAM
    # ========================================================

    async def _save(
        self,
        quote: FastRateQuote,
    ) -> None:

        if (
            not quote.rate_toman.is_finite()
            or quote.rate_toman <= 0
        ):
            return

        async with self._quotes_lock:

            self._quotes[
                quote.currency
            ] = quote


    # ========================================================
    # Get Cached Quote
    # ========================================================

    async def cached(
        self,
        currency: str,
    ) -> Optional[
        FastRateQuote
    ]:

        currency = normalize_currency(
            currency
        )

        async with self._quotes_lock:

            return self._quotes.get(
                currency
            )


    # ========================================================
    # USDT / Toman
    # ========================================================

    async def _refresh_usdt(
        self,
    ) -> FastRateQuote:
        """
        نرخ USDT/Toman از سرویس چند صرافی ایرانی.

        سرویس اصلی:
            IranUsdtRateService

        timeout کوتاه نگه داشته شده
        چون هدف این فایل سرعت بالاست.
        """

        client = (
            IranUsdtRateService(
                timeout=2.0,
                cache_ttl=10.0,
                stale_ttl=60.0,
            )
        )

        try:

            quote = (
                await client.get_quote(
                    force_refresh=True,
                    allow_stale=True,
                )
            )

            return FastRateQuote(

                currency=USDT,

                rate_toman=(
                    self._d(
                        quote.rate_toman
                    )
                ),

                source=(
                    f"iran:"
                    f"{quote.source}"
                ),

                fetched_at=(
                    time.time()
                ),
            )

        finally:

            await client.close()


    # ========================================================
    # Get First Valid Bid From Tabdeal
    # ========================================================

    async def _first_bid(
        self,
        client: TabdealRateService,
        symbols: tuple[
            str,
            ...
        ],
    ) -> tuple[
        Decimal,
        str,
    ]:

        errors: list[
            str
        ] = []

        for symbol in symbols:

            try:

                value = (
                    await client.best_bid(
                        symbol,
                        force_refresh=True,
                    )
                )

                return (
                    self._d(
                        value
                    ),
                    symbol,
                )

            except Exception as exc:

                errors.append(
                    (
                        f"{symbol}:"
                        f"{type(exc).__name__}"
                    )
                )

        raise FastRateUnavailable(
            (
                "No market: "
                + ",".join(
                    errors
                )
            )
        )


    # ========================================================
    # TON / TRX Rate
    # ========================================================

    async def _asset(
        self,
        currency: str,
        direct: tuple[
            str,
            ...
        ],
        usdt_symbols: tuple[
            str,
            ...
        ],
        usdt_toman: Decimal,
    ) -> FastRateQuote:
        """
        اول تلاش می‌کند نرخ مستقیم به تومان بگیرد.

        مثال:
            TRXIRT

        اگر موجود نبود:

            TRXUSDT
                ×
            USDT/Toman

        برای TON هم:
            GRAMIRT / TONIRT

        و fallback:
            GRAMUSDT / TONUSDT
        """

        client = (
            TabdealRateService(
                timeout=2.0,
                cache_ttl=10.0,
                retries=1,
            )
        )

        try:

            # =================================================
            # Direct Asset / IRT
            # =================================================

            try:

                rate, symbol = (
                    await self._first_bid(
                        client,
                        direct,
                    )
                )

                return FastRateQuote(

                    currency=(
                        currency
                    ),

                    rate_toman=(
                        rate
                    ),

                    source=(
                        f"tabdeal:"
                        f"{symbol}:bid"
                    ),

                    fetched_at=(
                        time.time()
                    ),
                )

            except FastRateUnavailable:

                pass


            # =================================================
            # Asset / USDT
            # =================================================

            (
                asset_usdt,
                symbol,
            ) = await self._first_bid(
                client,
                usdt_symbols,
            )


            # =================================================
            # Convert To Toman
            # =================================================

            rate_toman = (
                asset_usdt
                * usdt_toman
            )


            return FastRateQuote(

                currency=(
                    currency
                ),

                rate_toman=(
                    self._d(
                        rate_toman
                    )
                ),

                source=(
                    f"tabdeal:"
                    f"{symbol}:bid"
                    "*USDT/Toman"
                ),

                fetched_at=(
                    time.time()
                ),
            )

        finally:

            await client.close()


    # ========================================================
    # Refresh All Rates
    # ========================================================

    async def refresh_all(
        self,
    ) -> dict[
        str,
        FastRateQuote,
    ]:

        async with self._refresh_lock:

            updated: dict[
                str,
                FastRateQuote,
            ] = {}


            # =================================================
            # USDT
            # =================================================

            try:

                usdt_quote = (
                    await self._refresh_usdt()
                )

                await self._save(
                    usdt_quote
                )

                updated[
                    USDT
                ] = usdt_quote

            except Exception:

                logger.warning(
                    (
                        "USDT/Toman "
                        "refresh failed."
                    ),
                    exc_info=True,
                )

                usdt_quote = (
                    await self.cached(
                        USDT
                    )
                )


            # =================================================
            # Need USDT/Toman For Fallback
            # =================================================

            if (
                not usdt_quote
                or (
                    usdt_quote.age_seconds
                    > self.max_stale_ttl
                )
            ):

                return updated


            # =================================================
            # TON / TRX Concurrent Refresh
            # =================================================

            tasks = {

                TON:
                    self._asset(
                        TON,

                        (
                            "GRAMIRT",
                            "TONIRT",
                        ),

                        (
                            "GRAMUSDT",
                            "TONUSDT",
                        ),

                        usdt_quote.rate_toman,
                    ),

                TRX:
                    self._asset(
                        TRX,

                        (
                            "TRXIRT",
                        ),

                        (
                            "TRXUSDT",
                        ),

                        usdt_quote.rate_toman,
                    ),
            }


            results = (
                await asyncio.gather(
                    *tasks.values(),
                    return_exceptions=True,
                )
            )


            for (
                currency,
                result,
            ) in zip(
                tasks,
                results,
            ):

                if isinstance(
                    result,
                    Exception,
                ):

                    logger.warning(
                        (
                            "%s/Toman "
                            "refresh failed: %s"
                        ),
                        currency,
                        result,
                    )

                    continue


                await self._save(
                    result
                )

                updated[
                    currency
                ] = result


            return updated


    # ========================================================
    # Public Fast Rate
    # ========================================================

    async def get_rate(
        self,
        currency: str,
    ) -> FastRateQuote:
        """
        مسیر سریع:

            Cache RAM
                ↓
            اگر <= 30 ثانیه
                ↓
            Return فوری

        اگر Cache قدیمی بود:

            Refresh سریع
                ↓
            حداکثر حدود 2 ثانیه

        اگر هنوز نرخ معتبر نبود:

            FastRateUnavailable

        در این حالت wallet.py نباید
        موجودی کاربر را تغییر دهد.
        """

        currency = normalize_currency(
            currency
        )


        # Background rate updater
        await self.ensure_started()


        # ====================================================
        # Fast RAM Path
        # ====================================================

        quote = (
            await self.cached(
                currency
            )
        )

        if (
            quote
            and (
                quote.age_seconds
                <= self.fresh_ttl
            )
        ):

            return quote


        # ====================================================
        # Emergency Short Refresh
        # ====================================================

        try:

            await asyncio.wait_for(
                self.refresh_all(),
                timeout=(
                    self.emergency_timeout
                ),
            )

        except asyncio.TimeoutError:

            logger.warning(
                (
                    "Emergency crypto rate "
                    "refresh timed out."
                )
            )

        except Exception:

            logger.warning(
                (
                    "Emergency crypto rate "
                    "refresh failed."
                ),
                exc_info=True,
            )


        # ====================================================
        # Check Cache Again
        # ====================================================

        quote = (
            await self.cached(
                currency
            )
        )

        if (
            quote
            and (
                quote.age_seconds
                <= self.max_stale_ttl
            )
        ):

            return quote


        # ====================================================
        # Fail Closed
        # ====================================================

        raise FastRateUnavailable(
            (
                f"نرخ معتبر "
                f"{currency}/Toman "
                "در دسترس نیست."
            )
        )


# ============================================================
# Singleton
# ============================================================

fast_crypto_rates = (
    FastCryptoRateCache()
)


# ============================================================
# Public Exports
# ============================================================

__all__ = [
    "FastCryptoRateCache",
    "FastRateQuote",
    "FastRateUnavailable",
    "fast_crypto_rates",
]