# Standard Library
from collections import defaultdict
from typing import TYPE_CHECKING

# Django
from django.db import models, transaction
from django.db.models import (
    DecimalField,
    ExpressionWrapper,
    F,
    Q,
    Sum,
    Value,
)
from django.db.models.functions import Coalesce, Round
from django.utils import timezone

# Alliance Auth
from allianceauth.services.hooks import get_extension_logger

# AA Ledger
from ledger import __title__
from ledger.app_settings import LEDGER_BULK_BATCH_SIZE, LEDGER_PRICE_PERCENTAGE
from ledger.decorators import log_timing
from ledger.models.helpers.update_manager import CharacterUpdateSection
from ledger.providers import AppLogger, esi

if TYPE_CHECKING:
    # Alliance Auth
    from esi.stubs import CharactersCharacterIdMiningGetItem

    # AA Ledger
    from ledger.models.characteraudit import (
        CharacterMiningLedger as MiningLedgerContext,
    )
    from ledger.models.characteraudit import CharacterOwner
    from ledger.models.general import UpdateSectionResult

logger = AppLogger(get_extension_logger(__name__), __title__)


def require_valid_price_percentage(func):
    def wrapper(*args, **kwargs):
        if not isinstance(LEDGER_PRICE_PERCENTAGE, (int, float)):
            raise ValueError("LEDGER_PRICE_PERCENTAGE must be a number")
        if LEDGER_PRICE_PERCENTAGE <= 0:
            raise ValueError("LEDGER_PRICE_PERCENTAGE must be positive")
        return func(*args, **kwargs)

    return wrapper


class CharacterMiningLedgerEntryQueryset(models.QuerySet["MiningLedgerContext"]):
    @require_valid_price_percentage
    def annotate_pricing(self) -> models.QuerySet:
        """Annotate price and total columns."""
        return self.annotate(price=F("price_per_unit")).annotate(
            total=ExpressionWrapper(
                (F("price_per_unit") * F("quantity")) * LEDGER_PRICE_PERCENTAGE,
                output_field=models.DecimalField(),
            ),
        )

    def annotate_mining(self, with_period: bool = False) -> models.QuerySet:
        """Annotate mining columns."""
        qs = self.annotate_pricing()
        values_fields = [
            "character__eve_character__character_id",
            "character__eve_character__character_name",
        ]
        if with_period:
            values_fields.append("period")

        return qs.values(*values_fields).annotate(
            mining_income=Round(
                Coalesce(
                    Sum(F("total")),
                    Value(0),
                    output_field=DecimalField(),
                ),
                precision=2,
            )
        )

    def aggregate_mining(self):
        """Aggregate mining amounts."""
        qs = self.annotate_pricing()
        return qs.aggregate(
            total_amount=Round(
                Coalesce(
                    Sum(F("total")),
                    Value(0),
                    output_field=DecimalField(),
                ),
                precision=2,
            )
        )["total_amount"]

    def aggregate_amounts_information_modal(
        self, amounts: defaultdict, chars_list: list, filter_date: timezone.datetime
    ) -> dict:
        """Generate data template for the ledger character information view."""
        qs = self.filter(Q(character__eve_character__character_id__in=chars_list))
        qs = qs.annotate_pricing()
        qs = qs.aggregate(
            total_amount=Round(
                Coalesce(
                    Sum(F("total"), filter=Q(date__year=filter_date.year)),
                    Value(0),
                    output_field=DecimalField(),
                ),
                precision=2,
            ),
            total_amount_day=Round(
                Coalesce(
                    Sum(F("total"), filter=Q(date__day=filter_date.day)),
                    Value(0),
                    output_field=DecimalField(),
                ),
                precision=2,
            ),
        )

        amounts["mining"]["total_amount"] = qs["total_amount"]
        amounts["mining"]["total_amount_day"] = qs["total_amount_day"]

        return amounts

    def annotate_billboard(self, chars_list: list) -> models.QuerySet:
        """Annotate billboard columns."""
        qs = self.filter(Q(character__eve_character__character_id__in=chars_list))
        return qs.annotate(
            total_amount=Round(
                Coalesce(
                    Sum(F("total")),
                    Value(0),
                    output_field=DecimalField(),
                ),
                precision=2,
            )
        )


class CharacterMiningLedgerEntryManager(models.Manager["MiningLedgerContext"]):
    def get_queryset(self) -> CharacterMiningLedgerEntryQueryset:
        return CharacterMiningLedgerEntryQueryset(self.model, using=self._db)

    @require_valid_price_percentage
    def annotate_pricing(self) -> models.QuerySet:
        """Annotate price and total columns."""
        return self.get_queryset().annotate_pricing()

    def annotate_mining(self, with_period: bool = False) -> models.QuerySet:
        """Annotate mining columns."""
        return self.get_queryset().annotate_mining(with_period=with_period)

    def aggregate_mining(self):
        """Aggregate mining amounts."""
        return self.get_queryset().aggregate_mining()

    def aggregate_amounts_information_modal(
        self, amounts: defaultdict, chars_list: list, filter_date: timezone.datetime
    ) -> dict:
        """Generate data template for the ledger character information view."""
        return self.get_queryset().aggregate_amounts_information_modal(
            amounts, chars_list, filter_date
        )

    def annotate_billboard(self, chars_list: list) -> models.QuerySet:
        """Annotate billboard columns."""
        return self.get_queryset().annotate_billboard(chars_list)

    @log_timing(logger)
    def update_or_create_esi(
        self, owner: "CharacterOwner", force_refresh: bool = False
    ) -> "UpdateSectionResult":
        """Update or Create a mining ledger entry from ESI data."""
        return owner.update_manager.update_section_if_changed(
            section=CharacterUpdateSection.MINING_LEDGER,
            fetch_func=self._fetch_esi_data,
            force_refresh=force_refresh,
        )

    def _fetch_esi_data(
        self, owner: "CharacterOwner", force_refresh: bool = False
    ) -> None:
        """Fetch mining ledger entries from ESI data."""
        req_scopes = ["esi-industry.read_character_mining.v1"]
        token = owner.get_token(scopes=req_scopes)

        # Make the ESI request
        operation = esi.client.Industry.GetCharactersCharacterIdMining(
            character_id=owner.eve_character.character_id,
            token=token,
        )

        mining_items = operation.results(force_refresh=force_refresh)

        # Process and update or create mining ledger entries
        self._update_or_create_objs(owner=owner, objs=mining_items)

    @transaction.atomic()
    def _update_or_create_objs(
        self,
        owner: "CharacterOwner",
        objs: list["CharactersCharacterIdMiningGetItem"],
    ) -> None:
        """Update or Create mining ledger entries from objs data."""
        existings_pks = set(
            self.filter(
                character=owner,
                date__gte=timezone.now() - timezone.timedelta(days=30),
            ).values_list("id", flat=True)
        )
        type_ids = set()
        system_ids = set()
        new_events = []
        old_events = []

        for entry in objs:
            type_ids.add(entry.type_id)
            system_ids.add(entry.solar_system_id)
            pk = self.model.create_primary_key(owner.pk, entry)

            # Convert to time zone supported datetime
            try:
                entry_date = timezone.make_aware(
                    timezone.datetime.combine(entry.date, timezone.datetime.min.time()),
                    timezone.utc,
                )
            except Exception:  # pylint: disable=broad-except
                entry_date = entry.date

            _e = self.model(
                character=owner,
                id=pk,
                date=entry_date,
                type_id=entry.type_id,
                system_id=entry.solar_system_id,
                quantity=entry.quantity,
            )
            if pk in existings_pks:
                old_events.append(_e)
            else:
                new_events.append(_e)

        if new_events:
            self.bulk_create(
                new_events, batch_size=LEDGER_BULK_BATCH_SIZE, ignore_conflicts=True
            )

        if old_events:
            self.bulk_update(
                old_events, fields=["quantity"], batch_size=LEDGER_BULK_BATCH_SIZE
            )

        self._update_mining_price(owner=owner)

    def _update_mining_price(self, owner: "CharacterOwner") -> None:
        """Update prices for mining ledger entries."""
        # Update EveMarketPrice on a Daily basis
        self.model.update_evemarket_price()

        mining_ledger = owner.mining_ledger.filter(price_per_unit__isnull=True)
        logger.debug(
            f"Checking {mining_ledger.count()} mining ledger entries for missing prices."
        )

        updated_entries = []
        for entry in mining_ledger:
            npc_price = entry.get_npc_price()
            if npc_price is not None:
                entry.price_per_unit = npc_price
                updated_entries.append(entry)

        if updated_entries:
            self.bulk_update(
                updated_entries,
                fields=["price_per_unit"],
                batch_size=LEDGER_BULK_BATCH_SIZE,
            )

        logger.debug(
            f"Updated prices for {len(updated_entries)}({owner.character_name}) mining ledger entries."
        )
