import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional

from pydantic import BaseModel, Field, RootModel, ValidationError, model_validator

DEFAULT_TARIFF_CURRENCY = "rub"
STARS_TARIFF_CURRENCY = "stars"

Currency = str
BillingModel = Literal["period", "traffic"]


def normalize_currency_key(value: Any, default: str = DEFAULT_TARIFF_CURRENCY) -> str:
    text = str(value or "").strip().lower()
    if not text:
        return default
    aliases = {
        "rur": "rub",
        "xtr": STARS_TARIFF_CURRENCY,
        "star": STARS_TARIFF_CURRENCY,
        "stars": STARS_TARIFF_CURRENCY,
    }
    normalized = aliases.get(text, text)
    cleaned = "".join(ch for ch in normalized if ch.isalnum() or ch in {"_", "-"}).strip("_-")
    return cleaned or default


def payment_currency_code(currency: Any, default: str = "RUB") -> str:
    key = normalize_currency_key(currency, default=normalize_currency_key(default))
    if key == STARS_TARIFF_CURRENCY:
        return "XTR"
    return key.upper()


def default_currency_key_for_settings(settings: Any) -> str:
    try:
        config = getattr(settings, "tariffs_config", None)
    except Exception:
        config = None
    if config is not None and getattr(config, "default_currency", None):
        return normalize_currency_key(config.default_currency)
    return normalize_currency_key(getattr(settings, "DEFAULT_CURRENCY_SYMBOL", None))


def default_payment_currency_code_for_settings(settings: Any) -> str:
    return payment_currency_code(default_currency_key_for_settings(settings))


class TrafficPackage(BaseModel):
    gb: float
    price: float

    @model_validator(mode="after")
    def validate_values(self) -> "TrafficPackage":
        if self.gb <= 0:
            raise ValueError("package gb must be greater than zero")
        if self.price < 0:
            raise ValueError("package price must be non-negative")
        return self


class HwidDevicePackage(BaseModel):
    count: int
    price: float
    prices: Dict[str, float] = Field(default_factory=dict)
    min_price: Optional[float] = None

    @model_validator(mode="after")
    def validate_values(self) -> "HwidDevicePackage":
        if self.count <= 0:
            raise ValueError("device package count must be greater than zero")
        if self.price < 0:
            raise ValueError("device package price must be non-negative")
        normalized_prices: Dict[str, float] = {}
        for period, value in (self.prices or {}).items():
            period_key = str(period).strip()
            if not period_key:
                raise ValueError("device package price period must not be empty")
            try:
                period_months = int(period_key)
            except (TypeError, ValueError) as exc:
                raise ValueError("device package price period must be an integer") from exc
            if period_months <= 0:
                raise ValueError("device package price period must be positive")
            if float(value) < 0:
                raise ValueError("device package period price must be non-negative")
            normalized_prices[str(period_months)] = float(value)
        self.prices = normalized_prices
        if self.min_price is not None and self.min_price < 0:
            raise ValueError("device package min_price must be non-negative")
        return self

    def price_for_period(self, months: int) -> float:
        months_int = max(1, int(months or 1))
        value = self.prices.get(str(months_int))
        if value is not None:
            return float(value)
        return float(self.price) * months_int


class PackageSet(RootModel[Dict[str, List[TrafficPackage]]]):
    root: Dict[str, List[TrafficPackage]] = Field(default_factory=dict)

    @model_validator(mode="before")
    @classmethod
    def normalize_input(cls, data: Any) -> Any:
        if data is None:
            return {}
        if not isinstance(data, dict):
            return data
        normalized: Dict[str, Any] = {}
        for currency, packages in data.items():
            key = normalize_currency_key(currency, default="")
            if not key:
                raise ValueError("package currency must not be empty")
            normalized[key] = packages or []
        return normalized

    def for_currency(self, currency: Currency) -> List[TrafficPackage]:
        return list(self.root.get(normalize_currency_key(currency), []) or [])

    @property
    def rub(self) -> List[TrafficPackage]:
        return self.for_currency("rub")

    @property
    def stars(self) -> List[TrafficPackage]:
        return self.for_currency("stars")

    @property
    def non_stars_currencies(self) -> List[str]:
        return [
            currency for currency, packages in self.root.items() if currency != "stars" and packages
        ]

    def has_any(self) -> bool:
        return any(bool(packages) for packages in self.root.values())


class HwidDevicePackageSet(RootModel[Dict[str, List[HwidDevicePackage]]]):
    root: Dict[str, List[HwidDevicePackage]] = Field(default_factory=dict)

    @model_validator(mode="before")
    @classmethod
    def normalize_input(cls, data: Any) -> Any:
        if data is None:
            return {}
        if not isinstance(data, dict):
            return data
        normalized: Dict[str, Any] = {}
        for currency, packages in data.items():
            key = normalize_currency_key(currency, default="")
            if not key:
                raise ValueError("device package currency must not be empty")
            normalized[key] = packages or []
        return normalized

    def for_currency(self, currency: Currency) -> List[HwidDevicePackage]:
        return list(self.root.get(normalize_currency_key(currency), []) or [])

    @property
    def rub(self) -> List[HwidDevicePackage]:
        return self.for_currency("rub")

    @property
    def stars(self) -> List[HwidDevicePackage]:
        return self.for_currency("stars")

    def has_any(self) -> bool:
        return any(bool(packages) for packages in self.root.values())


class Tariff(BaseModel):
    key: str
    names: Dict[str, str] = Field(default_factory=dict)
    descriptions: Dict[str, str] = Field(default_factory=dict)
    premium_names: Dict[str, str] = Field(default_factory=dict)
    squad_uuids: List[str] = Field(default_factory=list)
    billing_model: BillingModel
    enabled: bool = True

    monthly_gb: Optional[float] = None
    prices: Dict[str, Dict[str, float]] = Field(default_factory=dict)
    prices_rub: Dict[str, float] = Field(default_factory=dict)
    prices_stars: Dict[str, float] = Field(default_factory=dict)
    referral_bonus_days_inviter: Dict[str, int] = Field(default_factory=dict)
    referral_bonus_days_referee: Dict[str, int] = Field(default_factory=dict)
    enabled_periods: List[int] = Field(default_factory=list)
    topup_packages: Optional[PackageSet] = None

    traffic_packages: Optional[PackageSet] = None
    conversion_rate_per_gb: Optional[float] = None
    conversion_rate_rub_per_gb: Optional[float] = None
    hwid_device_limit: Optional[int] = None
    hwid_device_packages: Optional[HwidDevicePackageSet] = None
    premium_squad_uuids: List[str] = Field(default_factory=list)
    premium_monthly_gb: Optional[float] = None
    premium_topup_packages: Optional[PackageSet] = None

    @model_validator(mode="after")
    def validate_tariff(self) -> "Tariff":
        if not self.key.strip():
            raise ValueError("tariff key must not be empty")
        self.key = self.key.strip()
        self.squad_uuids = [uuid.strip() for uuid in self.squad_uuids if uuid.strip()]
        self.premium_squad_uuids = [
            uuid.strip() for uuid in self.premium_squad_uuids if uuid.strip()
        ]
        if self.hwid_device_limit is not None and self.hwid_device_limit < 0:
            raise ValueError(f"tariff {self.key}: hwid_device_limit must be >= 0")
        if self.premium_monthly_gb is not None and self.premium_monthly_gb < 0:
            raise ValueError(f"tariff {self.key}: premium_monthly_gb must be >= 0")
        if self.premium_topup_packages and not self.premium_squad_uuids:
            raise ValueError(
                f"tariff {self.key}: premium_topup_packages require premium_squad_uuids"
            )
        if self.premium_monthly_gb and self.premium_monthly_gb > 0 and not self.premium_squad_uuids:
            raise ValueError(f"tariff {self.key}: premium_monthly_gb requires premium_squad_uuids")

        self.prices = self._normalize_prices_by_currency(self.prices)
        self.prices_rub = self._normalize_period_price_map(self.prices_rub, "prices_rub")
        self.prices_stars = self._normalize_period_price_map(
            self.prices_stars,
            "prices_stars",
        )
        if self.prices_rub:
            self.prices["rub"] = dict(self.prices_rub)
        elif self.prices.get("rub"):
            self.prices_rub = dict(self.prices["rub"])
        if self.prices_stars:
            self.prices["stars"] = dict(self.prices_stars)
        elif self.prices.get("stars"):
            self.prices_stars = dict(self.prices["stars"])

        if self.conversion_rate_per_gb is None and self.conversion_rate_rub_per_gb is not None:
            self.conversion_rate_per_gb = float(self.conversion_rate_rub_per_gb)
        if self.conversion_rate_per_gb is not None and self.conversion_rate_per_gb <= 0:
            raise ValueError(f"traffic tariff {self.key}: conversion_rate_per_gb must be > 0")

        if self.billing_model == "period":
            if self.monthly_gb is None or self.monthly_gb < 0:
                raise ValueError(f"period tariff {self.key}: monthly_gb must be >= 0")
            self.referral_bonus_days_inviter = self._normalize_referral_bonus_map(
                self.referral_bonus_days_inviter,
                "referral_bonus_days_inviter",
            )
            self.referral_bonus_days_referee = self._normalize_referral_bonus_map(
                self.referral_bonus_days_referee,
                "referral_bonus_days_referee",
            )
            if not self.enabled_periods:
                raise ValueError(f"period tariff {self.key}: enabled_periods is required")
            for months in self.enabled_periods:
                if months <= 0:
                    raise ValueError(f"period tariff {self.key}: enabled periods must be positive")
                period_prices = [
                    float(prices.get(str(months), 0) or 0) for prices in self.prices.values()
                ]
                if not any(price > 0 for price in period_prices):
                    raise ValueError(
                        f"period tariff {self.key}: period {months} needs a non-zero price"
                    )
            return self

        if not self.traffic_packages or not self.traffic_packages.has_any():
            raise ValueError(f"traffic tariff {self.key}: traffic_packages is required")
        if not self.traffic_packages.non_stars_currencies and self.conversion_rate_per_gb is None:
            raise ValueError(
                f"traffic tariff {self.key}: conversion_rate_per_gb is required without fiat packages"  # noqa: E501
            )
        return self

    def _normalize_period_price_map(
        self,
        values: Dict[str, float],
        field_name: str,
    ) -> Dict[str, float]:
        normalized: Dict[str, float] = {}
        for period, value in (values or {}).items():
            try:
                months = int(float(str(period).strip()))
                price = float(value)
            except (TypeError, ValueError) as exc:
                raise ValueError(f"tariff {self.key}: {field_name} contains invalid entry") from exc
            if months <= 0:
                raise ValueError(f"tariff {self.key}: {field_name} periods must be positive")
            if price < 0:
                raise ValueError(f"tariff {self.key}: {field_name} prices must be >= 0")
            normalized[str(months)] = price
        return normalized

    def _normalize_prices_by_currency(
        self,
        values: Dict[str, Dict[str, float]],
    ) -> Dict[str, Dict[str, float]]:
        normalized: Dict[str, Dict[str, float]] = {}
        for currency, price_map in (values or {}).items():
            key = normalize_currency_key(currency, default="")
            if not key:
                raise ValueError(f"tariff {self.key}: price currency must not be empty")
            normalized[key] = self._normalize_period_price_map(price_map or {}, f"prices.{key}")
        return normalized

    def _normalize_referral_bonus_map(
        self, values: Dict[str, int], field_name: str
    ) -> Dict[str, int]:
        normalized: Dict[str, int] = {}
        for period, days in (values or {}).items():
            try:
                months = int(float(str(period).strip()))
                bonus_days = int(float(days))
            except (TypeError, ValueError):
                raise ValueError(f"tariff {self.key}: {field_name} contains invalid entry")
            if months <= 0:
                raise ValueError(f"tariff {self.key}: {field_name} periods must be positive")
            if bonus_days < 0:
                raise ValueError(f"tariff {self.key}: {field_name} days must be >= 0")
            normalized[str(months)] = bonus_days
        return normalized

    def name(self, lang: str, fallback: str = "ru") -> str:
        return self.names.get(lang) or self.names.get(fallback) or self.key

    def description(self, lang: str, fallback: str = "ru") -> str:
        return self.descriptions.get(lang) or self.descriptions.get(fallback) or ""

    def premium_name(self, lang: str, fallback: str = "ru") -> str:
        default = "Premium-серверы" if (lang or fallback) == "ru" else "Premium servers"
        return self.premium_names.get(lang) or self.premium_names.get(fallback) or default

    @property
    def monthly_bytes(self) -> int:
        if self.monthly_gb is None or self.monthly_gb <= 0:
            return 0
        return int(float(self.monthly_gb) * (1024**3))

    def period_price(self, months: int, currency: Currency = "rub") -> Optional[float]:
        source = self.prices.get(normalize_currency_key(currency), {})
        value = source.get(str(months))
        return float(value) if value is not None else None

    def referral_inviter_bonus_days(self, months: int) -> Optional[int]:
        value = self.referral_bonus_days_inviter.get(str(int(months)))
        return int(value) if value is not None else None

    def referral_referee_bonus_days(self, months: int) -> Optional[int]:
        value = self.referral_bonus_days_referee.get(str(int(months)))
        return int(value) if value is not None else None

    def min_period_price(self, currency: Currency = "rub") -> Optional[float]:
        key = normalize_currency_key(currency)
        source = self.prices.get(key, {})
        prices = [
            float(source[str(months)])
            for months in self.enabled_periods
            if source.get(str(months), 0) and source.get(str(months), 0) > 0
        ]
        return min(prices) if prices else None

    def min_period_price_rub(self) -> Optional[float]:
        return self.min_period_price("rub")

    def min_traffic_package(self, currency: Currency = "rub") -> Optional[TrafficPackage]:
        packages = self.traffic_packages.for_currency(currency) if self.traffic_packages else []
        return min(packages, key=lambda pkg: pkg.price) if packages else None

    def min_traffic_package_rub(self) -> Optional[TrafficPackage]:
        return self.min_traffic_package("rub")

    def currency_per_gb_for_conversion(self, currency: Currency = "rub") -> float:
        if self.conversion_rate_per_gb:
            return float(self.conversion_rate_per_gb)
        packages = self.traffic_packages.for_currency(currency) if self.traffic_packages else []
        if not packages and self.traffic_packages:
            for key in self.traffic_packages.non_stars_currencies:
                packages = self.traffic_packages.for_currency(key)
                if packages:
                    break
        return min(float(pkg.price) / float(pkg.gb) for pkg in packages)

    def rub_per_gb_for_conversion(self) -> float:
        return self.currency_per_gb_for_conversion("rub")

    def has_hwid_device_packages(self) -> bool:
        return bool(self.hwid_device_packages and self.hwid_device_packages.has_any())

    @property
    def premium_monthly_bytes(self) -> int:
        if self.premium_monthly_gb is None or self.premium_monthly_gb <= 0:
            return 0
        return int(float(self.premium_monthly_gb) * (1024**3))

    def has_premium_squad_limit(self) -> bool:
        return bool(
            self.premium_squad_uuids
            and (self.premium_monthly_bytes > 0 or self.premium_topup_packages)
        )


class TariffsConfig(BaseModel):
    default_tariff: str
    default_currency: str = DEFAULT_TARIFF_CURRENCY
    topup_packages_default: Optional[PackageSet] = None
    tariffs: List[Tariff]

    @model_validator(mode="after")
    def validate_config(self) -> "TariffsConfig":
        self.default_currency = normalize_currency_key(self.default_currency)
        if self.default_currency == STARS_TARIFF_CURRENCY:
            raise ValueError("default_currency must be a non-Stars payment currency")
        keys = [tariff.key for tariff in self.tariffs]
        if len(keys) != len(set(keys)):
            raise ValueError("tariff keys must be unique")
        active = [tariff for tariff in self.tariffs if tariff.enabled]
        if not active:
            raise ValueError("at least one enabled tariff is required")
        active_keys = {tariff.key for tariff in active}
        if self.default_tariff not in active_keys:
            raise ValueError("default_tariff must reference an enabled tariff")
        return self

    @property
    def enabled_tariffs(self) -> List[Tariff]:
        return [tariff for tariff in self.tariffs if tariff.enabled]

    def get(self, key: str) -> Optional[Tariff]:
        return next((tariff for tariff in self.tariffs if tariff.key == key), None)

    def require(self, key: str) -> Tariff:
        tariff = self.get(key)
        if not tariff or not tariff.enabled:
            raise KeyError(f"Unknown or disabled tariff: {key}")
        return tariff

    @property
    def default(self) -> Tariff:
        return self.require(self.default_tariff)

    @property
    def default_payment_currency_code(self) -> str:
        return payment_currency_code(self.default_currency)

    def topup_packages_for(self, tariff: Tariff) -> Optional[PackageSet]:
        if tariff.billing_model == "traffic":
            return tariff.traffic_packages
        return tariff.topup_packages


def load_tariffs_config(path: str | Path) -> Optional[TariffsConfig]:
    config_path = Path(path)
    if not config_path.exists():
        return None
    try:
        data = json.loads(config_path.read_text(encoding="utf-8"))
        return TariffsConfig.model_validate(data)
    except (OSError, json.JSONDecodeError, ValidationError, ValueError) as exc:
        logging.critical("Failed to load tariffs config from %s: %s", config_path, exc)
        raise
