# services/product_pricing_service.py

from __future__ import annotations

import asyncio
import logging
import sqlite3
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from pathlib import Path
from typing import Optional

from services.iran_usdt_rate_service import (
    IranUsdtAllProvidersFailed,
    IranUsdtRateService,
)


logger = logging.getLogger(__name__)


# ============================================================
# Products
# ============================================================

PRODUCT_LABELS: dict[str, str] = {
    "prem_1m": "Premium 1 Month",
    "prem_3m": "Premium 3 Months",
    "prem_6m": "Premium 6 Months",
    "prem_1y": "Premium 1 Year",

    "stars_50": "50 Stars",
    "stars_100": "100 Stars",
    "stars_250": "250 Stars",
    "stars_500": "500 Stars",
    "stars_1000": "1000 Stars",
    "stars_5000": "5000 Stars",

    # Used only for custom Stars orders.
    "stars_unit": "Custom Stars - price per 1 Star",
}

PREMIUM_PRODUCT_IDS: tuple[str, ...] = (
    "prem_1m",
    "prem_3m",
    "prem_6m",
    "prem_1y",
)

STARS_PRODUCT_IDS: tuple[str, ...] = (
    "stars_unit",
)


# ============================================================
# Errors / Models
# ============================================================

class ProductPricingError(Exception):
    pass


class ProductPriceNotConfigured(ProductPricingError):
    pass


class ProductRateUnavailable(ProductPricingError):
    pass


class InvalidProductPrice(ProductPricingError):
    pass


@dataclass(slots=True, frozen=True)
class ProductTomanQuote:
    package_id: str
    base_usdt: Decimal
    usdt_toman: Decimal
    final_toman: int
    rate_source: str


# ============================================================
# Service
# ============================================================

class ProductPricingService:
    """
    Stable shop pricing without MarketApp/API scraping.

    Source of product base price:
        admin-configured USDT values in SQLite.

    Source of Toman conversion:
        IranUsdtRateService:
            Tabdeal -> Wallex -> Bitpin -> Nobitex -> Exir -> Sarrafex

    Premium:
        exact configured package USDT price.

    Stars:
        only stars_unit is configured.
        Every fixed/custom Stars package uses:
            stars_unit * stars amount
    """

    def __init__(
        self,
        db_path: str | Path = "matrix_bot.db",
    ) -> None:
        self.db_path = str(db_path)

        if not self.db_path.strip():
            raise ValueError(
                "db_path cannot be empty."
            )

        self._schema_lock = asyncio.Lock()
        self._schema_ready = False

    # ========================================================
    # SQLite
    # ========================================================

    def _connect(
        self,
    ) -> sqlite3.Connection:
        conn = sqlite3.connect(
            self.db_path,
            timeout=30,
            check_same_thread=False,
        )

        conn.row_factory = sqlite3.Row

        try:
            conn.execute(
                "PRAGMA journal_mode=WAL"
            )
        except sqlite3.DatabaseError:
            pass

        conn.execute(
            "PRAGMA busy_timeout=30000"
        )

        return conn

    async def _run(
        self,
        func,
        *args,
    ):
        return await asyncio.to_thread(
            func,
            *args,
        )

    async def ensure_schema(
        self,
    ) -> None:
        if self._schema_ready:
            return

        async with self._schema_lock:
            if self._schema_ready:
                return

            await self._run(
                self._ensure_schema_sync
            )

            self._schema_ready = True

    def _ensure_schema_sync(
        self,
    ) -> None:
        conn = self._connect()

        try:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS product_base_prices (
                    product_id TEXT PRIMARY KEY,
                    price_usdt TEXT NOT NULL,
                    updated_by INTEGER,
                    updated_at TIMESTAMP
                        DEFAULT CURRENT_TIMESTAMP
                )
                """
            )

            conn.commit()

        finally:
            conn.close()

    # ========================================================
    # Validation
    # ========================================================

    @staticmethod
    def _decimal(
        value,
    ) -> Decimal:
        try:
            result = Decimal(
                str(value)
                .strip()
                .replace(",", ".")
            )

        except (
            InvalidOperation,
            TypeError,
            ValueError,
        ) as exc:
            raise InvalidProductPrice(
                "Invalid USDT product price."
            ) from exc

        if (
            not result.is_finite()
            or result <= 0
        ):
            raise InvalidProductPrice(
                "USDT product price must be positive."
            )

        # 8 decimals is plenty for per-Star pricing.
        return result.quantize(
            Decimal("0.00000001")
        )

    @staticmethod
    def _validate_product_id(
        product_id: str,
    ) -> str:
        product_id = str(
            product_id
            or ""
        ).strip().lower()

        if product_id not in PRODUCT_LABELS:
            raise ProductPricingError(
                f"Unknown product id: {product_id}"
            )

        return product_id

    # ========================================================
    # Admin CRUD
    # ========================================================

    async def set_base_usdt(
        self,
        product_id: str,
        price_usdt,
        *,
        updated_by: Optional[int] = None,
    ) -> Decimal:
        await self.ensure_schema()

        product_id = self._validate_product_id(
            product_id
        )

        value = self._decimal(
            price_usdt
        )

        await self._run(
            self._set_base_usdt_sync,
            product_id,
            str(value),
            (
                int(updated_by)
                if updated_by is not None
                else None
            ),
        )

        logger.info(
            "Product base price updated | "
            "product=%s price_usdt=%s",
            product_id,
            value,
        )

        return value

    def _set_base_usdt_sync(
        self,
        product_id: str,
        value: str,
        updated_by: Optional[int],
    ) -> None:
        conn = self._connect()

        try:
            conn.execute(
                """
                INSERT INTO product_base_prices (
                    product_id,
                    price_usdt,
                    updated_by,
                    updated_at
                )
                VALUES (?, ?, ?, CURRENT_TIMESTAMP)
                ON CONFLICT(product_id)
                DO UPDATE SET
                    price_usdt = excluded.price_usdt,
                    updated_by = excluded.updated_by,
                    updated_at = CURRENT_TIMESTAMP
                """,
                (
                    product_id,
                    value,
                    updated_by,
                ),
            )

            conn.commit()

        finally:
            conn.close()

    async def get_base_usdt(
        self,
        product_id: str,
    ) -> Decimal:
        await self.ensure_schema()

        product_id = self._validate_product_id(
            product_id
        )

        raw = await self._run(
            self._get_base_usdt_sync,
            product_id,
        )

        if raw is None:
            raise ProductPriceNotConfigured(
                f"Product price is not configured: {product_id}"
            )

        return self._decimal(
            raw
        )

    def _get_base_usdt_sync(
        self,
        product_id: str,
    ) -> Optional[str]:
        conn = self._connect()

        try:
            row = conn.execute(
                """
                SELECT price_usdt
                FROM product_base_prices
                WHERE product_id = ?
                LIMIT 1
                """,
                (
                    product_id,
                ),
            ).fetchone()

            return (
                str(
                    row["price_usdt"]
                )
                if row
                else None
            )

        finally:
            conn.close()

    async def get_all_base_usdt(
        self,
    ) -> dict[
        str,
        Optional[Decimal],
    ]:
        await self.ensure_schema()

        rows = await self._run(
            self._get_all_base_usdt_sync
        )

        result: dict[
            str,
            Optional[Decimal],
        ] = {
            key: None
            for key
            in PRODUCT_LABELS
        }

        for product_id, raw in rows:
            if product_id not in result:
                continue

            try:
                result[
                    product_id
                ] = self._decimal(
                    raw
                )
            except InvalidProductPrice:
                result[
                    product_id
                ] = None

        return result

    def _get_all_base_usdt_sync(
        self,
    ) -> list[
        tuple[
            str,
            str,
        ]
    ]:
        conn = self._connect()

        try:
            rows = conn.execute(
                """
                SELECT product_id, price_usdt
                FROM product_base_prices
                ORDER BY product_id
                """
            ).fetchall()

            return [
                (
                    str(
                        row["product_id"]
                    ),
                    str(
                        row["price_usdt"]
                    ),
                )
                for row
                in rows
            ]

        finally:
            conn.close()

    # ========================================================
    # Quote
    # ========================================================

    @staticmethod
    def _stars_amount_from_package_id(
        package_id: str,
    ) -> Optional[int]:
        package_id = str(
            package_id
            or ""
        ).strip().lower()

        if package_id == "stars_unit":
            return 1

        if package_id.startswith(
            "custom_stars_"
        ):
            raw = package_id[
                len(
                    "custom_stars_"
                ):
            ]

        elif package_id.startswith(
            "stars_"
        ):
            raw = package_id[
                len(
                    "stars_"
                ):
            ]

        else:
            return None

        try:
            amount = int(
                raw
            )

        except (
            TypeError,
            ValueError,
        ):
            return None

        return (
            amount
            if amount > 0
            else None
        )

    async def get_package_base_usdt(
        self,
        package_id: str,
    ) -> Decimal:
        """
        Premium:
            exact configured package USDT price.

        Stars:
            ONLY stars_unit is configured by admin.

            Every fixed/custom Stars package is calculated as:

                stars_unit_usdt * stars_amount
        """
        package_id = str(
            package_id
            or ""
        ).strip().lower()

        stars_amount = (
            self._stars_amount_from_package_id(
                package_id
            )
        )

        if stars_amount is not None:
            unit = await self.get_base_usdt(
                "stars_unit"
            )

            return (
                unit
                * Decimal(
                    stars_amount
                )
            ).quantize(
                Decimal("0.00000001")
            )

        if package_id in PREMIUM_PRODUCT_IDS:
            return await self.get_base_usdt(
                package_id
            )

        raise ProductPricingError(
            f"Unknown package: {package_id}"
        )

    async def quote_toman(
        self,
        package_id: str,
        *,
        force_refresh_rate: bool = False,
        allow_stale_rate: bool = True,
    ) -> ProductTomanQuote:
        base_usdt = await self.get_package_base_usdt(
            package_id
        )

        rates = IranUsdtRateService(
            timeout=4.0,
            cache_ttl=15.0,
            stale_ttl=300.0,
        )

        try:
            try:
                rate_quote = await rates.get_quote(
                    force_refresh=force_refresh_rate,
                    allow_stale=allow_stale_rate,
                )

            except IranUsdtAllProvidersFailed as exc:
                raise ProductRateUnavailable(
                    "USDT/Toman rate is unavailable."
                ) from exc

            toman = (
                base_usdt
                * rate_quote.rate_toman
            )

            final_toman = int(
                toman.quantize(
                    Decimal("1"),
                    rounding=ROUND_HALF_UP,
                )
            )

            if final_toman <= 0:
                raise ProductRateUnavailable(
                    "Converted Toman price is invalid."
                )

            logger.info(
                "Shop product quote | "
                "package=%s base_usdt=%s "
                "usdt_toman=%s source=%s final_toman=%s",
                package_id,
                base_usdt,
                rate_quote.rate_toman,
                rate_quote.source,
                final_toman,
            )

            return ProductTomanQuote(
                package_id=package_id,
                base_usdt=base_usdt,
                usdt_toman=rate_quote.rate_toman,
                final_toman=final_toman,
                rate_source=rate_quote.source,
            )

        finally:
            await rates.close()


__all__ = [
    "PRODUCT_LABELS",
    "PREMIUM_PRODUCT_IDS",
    "STARS_PRODUCT_IDS",
    "ProductPricingService",
    "ProductTomanQuote",
    "ProductPricingError",
    "ProductPriceNotConfigured",
    "ProductRateUnavailable",
    "InvalidProductPrice",
]