import logging

from django.db.models import QuerySet
from django.utils import timezone

import apps.currencies.exchange_rates.providers as providers
from apps.currencies.models import ExchangeRateService, ExchangeRate, Currency

logger = logging.getLogger(__name__)


# Map service types to provider classes
PROVIDER_MAPPING = {
    "coingecko_free": providers.CoinGeckoFreeProvider,
    "coingecko_pro": providers.CoinGeckoProProvider,
    "transitive": providers.TransitiveRateProvider,
    "frankfurter": providers.FrankfurterProvider,
    "twelvedata": providers.TwelveDataProvider,
    "twelvedatamarkets": providers.TwelveDataMarketsProvider,
    "yfinance": providers.YFinanceMarketsProvider,
}


class ExchangeRateFetcher:
    def _should_fetch_at_hour(service: ExchangeRateService, current_hour: int) -> bool:
        """Check if service should fetch rates at given hour based on interval type."""
        try:
            if service.interval_type == ExchangeRateService.IntervalType.NOT_ON:
                blocked_hours = ExchangeRateService._parse_hour_ranges(
                    service.fetch_interval
                )
                should_fetch = current_hour not in blocked_hours
                logger.info(
                    f"NOT_ON check for {service.name}: "
                    f"current_hour={current_hour}, "
                    f"blocked_hours={blocked_hours}, "
                    f"should_fetch={should_fetch}"
                )
                return should_fetch

            if service.interval_type == ExchangeRateService.IntervalType.ON:
                allowed_hours = ExchangeRateService._parse_hour_ranges(
                    service.fetch_interval
                )

                should_fetch = current_hour in allowed_hours

                logger.info(
                    f"ON check for {service.name}: "
                    f"current_hour={current_hour}, "
                    f"allowed_hours={allowed_hours}, "
                    f"should_fetch={should_fetch}"
                )

                return should_fetch

            if service.interval_type == ExchangeRateService.IntervalType.EVERY:
                try:
                    interval_hours = int(service.fetch_interval)

                    if service.last_fetch is None:
                        return True

                    # Round down to nearest hour
                    now = timezone.now().replace(minute=0, second=0, microsecond=0)
                    last_fetch = service.last_fetch.replace(
                        minute=0, second=0, microsecond=0
                    )

                    hours_since_last = (now - last_fetch).total_seconds() / 3600
                    should_fetch = hours_since_last >= interval_hours

                    logger.info(
                        f"EVERY check for {service.name}: "
                        f"hours_since_last={hours_since_last:.1f}, "
                        f"interval={interval_hours}, "
                        f"should_fetch={should_fetch}"
                    )
                    return should_fetch
                except ValueError:
                    logger.error(
                        f"Invalid EVERY interval format for {service.name}: "
                        f"expected single number, got '{service.fetch_interval}'"
                    )
                    return False

            return False

        except ValueError as e:
            logger.error(f"Error parsing fetch_interval for {service.name}: {e}")
            return False

    @staticmethod
    def fetch_due_rates(force: bool = False) -> None:
        """
        Fetch rates for all services that are due for update.
        Args:
            force (bool): If True, fetches all active services regardless of their schedule.
        """
        services = ExchangeRateService.objects.filter(is_active=True)
        current_time = timezone.now().astimezone()
        current_hour = current_time.hour

        for service in services:
            try:
                if force:
                    logger.info(f"Force fetching rates for {service.name}")
                    ExchangeRateFetcher._fetch_service_rates(service)
                    continue

                # Check if service should fetch based on interval type
                if ExchangeRateFetcher._should_fetch_at_hour(service, current_hour):
                    logger.info(
                        f"Fetching rates for {service.name}. "
                        f"Last fetch: {service.last_fetch}, "
                        f"Interval type: {service.interval_type}, "
                        f"Current hour: {current_hour}"
                    )
                    ExchangeRateFetcher._fetch_service_rates(service)
                else:
                    logger.debug(
                        f"Skipping {service.name}. "
                        f"Current hour: {current_hour}, "
                        f"Interval type: {service.interval_type}, "
                        f"Fetch interval: {service.fetch_interval}"
                    )

            except Exception as e:
                logger.error(f"Error checking fetch schedule for {service.name}: {e}")

    @staticmethod
    def _get_unique_currency_pairs(
        service: ExchangeRateService,
    ) -> tuple[QuerySet, set]:
        """
        Get unique currency pairs from both target_currencies and target_accounts
        Returns a tuple of (target_currencies QuerySet, exchange_currencies set)
        """
        # Get currencies from target_currencies
        target_currencies = set(service.target_currencies.all())

        # Add currencies from target_accounts
        for account in service.target_accounts.all():
            if account.currency and account.exchange_currency:
                target_currencies.add(account.currency)

        # Convert back to QuerySet for compatibility with existing code
        target_currencies_qs = Currency.objects.filter(
            id__in=[curr.id for curr in target_currencies]
        )

        # Get unique exchange currencies
        exchange_currencies = set()

        # From target_currencies
        for currency in target_currencies:
            if currency.exchange_currency:
                exchange_currencies.add(currency.exchange_currency)

        # From target_accounts
        for account in service.target_accounts.all():
            if account.exchange_currency:
                exchange_currencies.add(account.exchange_currency)

        return target_currencies_qs, exchange_currencies

    @staticmethod
    def _fetch_service_rates(service: ExchangeRateService) -> None:
        """Fetch rates for a specific service"""
        try:
            provider = service.get_provider()

            # Check if API key is required but missing
            if provider.requires_api_key() and not service.api_key:
                logger.error(f"API key required but not provided for {service.name}")
                return

            # Get unique currency pairs from both sources
            target_currencies, exchange_currencies = (
                ExchangeRateFetcher._get_unique_currency_pairs(service)
            )

            # Skip if no currencies to process
            if not target_currencies or not exchange_currencies:
                logger.info(f"No currency pairs to process for service {service.name}")
                return

            rates = provider.get_rates(target_currencies, exchange_currencies)

            # Track processed currency pairs to avoid duplicates
            processed_pairs = set()

            for from_currency, to_currency, rate in rates:
                # Create a unique identifier for this currency pair
                pair_key = (from_currency.id, to_currency.id)
                if pair_key in processed_pairs:
                    continue

                if provider.rates_inverted:
                    # If rates are inverted, we need to swap currencies
                    if service.singleton:
                        # Try to get the last automatically created exchange rate
                        exchange_rate = (
                            ExchangeRate.objects.filter(
                                automatic=True,
                                from_currency=to_currency,
                                to_currency=from_currency,
                            )
                            .order_by("-date")
                            .first()
                        )
                    else:
                        exchange_rate = None

                    if not exchange_rate:
                        ExchangeRate.objects.create(
                            automatic=True,
                            from_currency=to_currency,
                            to_currency=from_currency,
                            rate=rate,
                            date=timezone.now(),
                        )
                    else:
                        exchange_rate.rate = rate
                        exchange_rate.date = timezone.now()
                        exchange_rate.save()

                    processed_pairs.add((to_currency.id, from_currency.id))
                else:
                    # If rates are not inverted, we can use them as is
                    if service.singleton:
                        # Try to get the last automatically created exchange rate
                        exchange_rate = (
                            ExchangeRate.objects.filter(
                                automatic=True,
                                from_currency=from_currency,
                                to_currency=to_currency,
                            )
                            .order_by("-date")
                            .first()
                        )
                    else:
                        exchange_rate = None

                    if not exchange_rate:
                        ExchangeRate.objects.create(
                            automatic=True,
                            from_currency=from_currency,
                            to_currency=to_currency,
                            rate=rate,
                            date=timezone.now(),
                        )
                    else:
                        exchange_rate.rate = rate
                        exchange_rate.date = timezone.now()
                        exchange_rate.save()

                    processed_pairs.add((from_currency.id, to_currency.id))

            service.last_fetch = timezone.now()
            service.failure_count = 0
            service.save()

        except Exception as e:
            logger.error(f"Error fetching rates for {service.name}: {e}")
            service.failure_count += 1
            service.save()
