# Standard Library
from datetime import datetime, timezone

"""Celery tasks for Incursions module."""

# Standard Library
from typing import TYPE_CHECKING

# Third Party
from celery import shared_task
from ninja import Schema

# Django
from django.db import IntegrityError, transaction
from django.db.models import Count
from django.utils.timezone import now

# Alliance Auth (Core)
from allianceauth.eveonline.models import EveCharacter, EveFactionInfo
from allianceauth.services.hooks import get_extension_logger
from allianceauth.services.tasks import QueueOnce
from esi.exceptions import HTTPNotModified

# Alliance Auth (External Libs)
from eve_sde.models import Constellation, ItemType, SolarSystem

from incursions.api.helpers.sse import SSEClient, SSEEvent
from incursions.api.waitlist import WaitlistUpdateSchema
from incursions.app_settings import INCURSIONS_MINIMUM_FLEET_SIZE, SSE_SECRET, SSE_SITE_URL
from incursions.helpers import (
    embed_boss_spawned, embed_ended, embed_established, embed_established_addendum, embed_mobilizing,
    embed_waitlist_state, embed_withdrawing,
)
from incursions.models.incursion import Incursion, IncursionInfluence, IncursionsConfig
from incursions.models.waitlist import ActiveFleet, Fleet, FleetActivity, Waitlist, WaitlistEntry, WaitlistEntryFit
from incursions.providers import get_incursions_incursions

if TYPE_CHECKING:
    # Third Party
    from celery.result import AsyncResult

    # https://github.com/sbdchd/celery-types
    classes = [
        AsyncResult,
    ]

    for cls in classes:
        setattr(cls, "__class_getitem__", classmethod(lambda cls, *args, **kwargs: cls))

LAST_MODIFIED_HEADER = "Last-Modified"
LAST_MODIFIED_FORMAT = "%a, %d %b %Y %H:%M:%S %Z"


def _parse_last_modified(value: str | None) -> datetime:
    """Parse HTTP Last-Modified header into aware UTC datetime.

    Falls back to timezone.now() on parse failure or missing header.
    """
    if not value:
        return datetime.now(timezone.utc)
    try:
        return datetime.strptime(str(value), LAST_MODIFIED_FORMAT).replace(tzinfo=timezone.utc)
    except Exception:
        logger.debug("Failed to parse Last-Modified header '%s'", value, exc_info=True)
        return datetime.now(timezone.utc)


logger = get_extension_logger(__name__)
sse_client = SSEClient(SSE_SITE_URL, SSE_SECRET)


class NewFleetCompSchema(Schema):
    fleet_id: int


@shared_task(base=QueueOnce)
def update_incursions() -> None:
    """Fetch current incursions and reconcile state transitions & influence logs."""
    try:
        incursions, response = get_incursions_incursions()
    except HTTPNotModified:
        return

    header_time = (
        _parse_last_modified(response.headers.get(LAST_MODIFIED_HEADER))  # type: ignore[arg-type]
        if response else datetime.now(timezone.utc)
    )
    active_constellations: list[int] = []
    influence_buffer: list[IncursionInfluence] = []

    for data in incursions:
        constellation_id = data.constellation_id
        if constellation_id is None:
            logger.warning("Incursion payload missing constellation_id: %s", data)
            continue
        active_constellations.append(constellation_id)
        state_raw = str(data.state).strip()  # handle trailing spaces
        has_boss_flag = str(data.has_boss).lower() == "true"
        try:
            constellation = Constellation.objects.get(constellation_id=constellation_id)
        except Constellation.DoesNotExist:
            logger.warning("Constellation %s not found in DB", constellation_id)
            continue

        try:
            inc = Incursion.objects.get(constellation=constellation, ended_timestamp__isnull=True)
            # State transition handling
            if state_raw == "mobilizing" and inc.state != Incursion.States.MOBILIZING:
                inc.mobilizing_timestamp = header_time
                inc.state = Incursion.States.MOBILIZING
                if has_boss_flag:
                    inc.has_boss = True
                inc.save(update_fields=["mobilizing_timestamp", "state", "has_boss"])
            elif state_raw == "withdrawing" and inc.state != Incursion.States.WITHDRAWING:
                inc.withdrawing_timestamp = header_time
                inc.state = Incursion.States.WITHDRAWING
                if has_boss_flag:
                    inc.has_boss = True
                inc.save(update_fields=["withdrawing_timestamp", "state", "has_boss"])
            elif state_raw == "established":
                # No change needed besides maybe boss flag
                if has_boss_flag and not inc.has_boss:
                    inc.has_boss = True
                    inc.save(update_fields=["has_boss"])
        except Incursion.DoesNotExist:
            # Create new incursion record
            try:
                inc = Incursion.objects.create(
                    constellation=constellation,
                    faction=EveFactionInfo.objects.get_or_create(faction_id=data.faction_id)[0],
                    has_boss=has_boss_flag,
                    staging_solar_system=SolarSystem.objects.get(system_id=data.staging_solar_system_id),
                    state=state_raw,
                    type=data.type,
                    established_timestamp=header_time,
                )
                if state_raw == "mobilizing":
                    inc.mobilizing_timestamp = header_time
                    inc.save(update_fields=["mobilizing_timestamp"])
                elif state_raw == "withdrawing":
                    inc.mobilizing_timestamp = header_time
                    inc.withdrawing_timestamp = header_time
                    inc.save(update_fields=["mobilizing_timestamp", "withdrawing_timestamp"])
            except Exception:
                logger.exception("Failed creating incursion for constellation %s", constellation_id)
                continue

        # Influence logging (buffered for bulk insert)
        try:
            influence_buffer.append(IncursionInfluence(
                incursion=inc,
                influence=data.influence,
                timestamp=header_time,
            ))
        except Exception:
            logger.debug("Unable to stage influence record for constellation %s", constellation_id, exc_info=True)

    # Bulk create influences ignoring duplicates
    if influence_buffer:
        try:
            IncursionInfluence.objects.bulk_create(influence_buffer, ignore_conflicts=True)
        except Exception:
            logger.debug("Bulk create influence failed, attempting individual inserts", exc_info=True)
            for obj in influence_buffer:
                try:
                    obj.save()
                except IntegrityError:
                    pass

    # Mark ended incursions
    for ended in (Incursion.objects.filter(ended_timestamp__isnull=True).exclude(constellation_id__in=active_constellations)):
        ended.ended_timestamp = header_time
        ended.state = Incursion.States.ENDED
        ended.save(update_fields=["ended_timestamp", "state"])


@shared_task()
def incursion_established(incursion_pk: int) -> None:
    incursion = Incursion.objects.get(pk=incursion_pk)
    for webhook in IncursionsConfig.get_solo().status_webhooks.all():
        if incursion.is_high_sec == webhook.security_high:
            webhook.send_embed(embed=embed_established(incursion))
            webhook.send_embed(embed=embed_established_addendum(incursion))
        elif incursion.is_low_sec == webhook.security_low:
            webhook.send_embed(embed=embed_established(incursion))
            webhook.send_embed(embed=embed_established_addendum(incursion))
        elif incursion.is_null_sec == webhook.security_null:
            webhook.send_embed(embed=embed_established(incursion))
            webhook.send_embed(embed=embed_established_addendum(incursion))


@shared_task()
def incursion_mobilizing(incursion_pk: int) -> None:
    incursion = Incursion.objects.get(pk=incursion_pk)
    for webhook in IncursionsConfig.get_solo().status_webhooks.all():
        if incursion.is_high_sec == webhook.security_high:
            webhook.send_embed(embed=embed_mobilizing(incursion))
        elif incursion.is_low_sec == webhook.security_low:
            webhook.send_embed(embed=embed_mobilizing(incursion))
        elif incursion.is_null_sec == webhook.security_null:
            webhook.send_embed(embed=embed_mobilizing(incursion))


@shared_task()
def incursion_withdrawing(incursion_pk: int) -> None:
    incursion = Incursion.objects.get(pk=incursion_pk)
    for webhook in IncursionsConfig.get_solo().status_webhooks.all():
        if incursion.is_high_sec == webhook.security_high:
            webhook.send_embed(embed=embed_withdrawing(incursion))
        elif incursion.is_low_sec == webhook.security_low:
            webhook.send_embed(embed=embed_withdrawing(incursion))
        elif incursion.is_null_sec == webhook.security_null:
            webhook.send_embed(embed=embed_withdrawing(incursion))


@shared_task()
def incursion_ended(incursion_pk: int) -> None:
    incursion = Incursion.objects.get(pk=incursion_pk)
    for webhook in IncursionsConfig.get_solo().status_webhooks.all():
        if incursion.is_high_sec == webhook.security_high:
            webhook.send_embed(embed=embed_ended(incursion))
        elif incursion.is_low_sec == webhook.security_low:
            webhook.send_embed(embed=embed_ended(incursion))
        elif incursion.is_null_sec == webhook.security_null:
            webhook.send_embed(embed=embed_ended(incursion))


@shared_task()
def incursion_boss_spawned(incursion_pk: int) -> None:
    incursion = Incursion.objects.get(pk=incursion_pk)
    for webhook in IncursionsConfig.get_solo().status_webhooks.all():
        if incursion.is_high_sec == webhook.security_high:
            webhook.send_embed(embed=embed_boss_spawned(incursion))
        elif incursion.is_low_sec == webhook.security_low:
            webhook.send_embed(embed=embed_boss_spawned(incursion))
        elif incursion.is_null_sec == webhook.security_null:
            webhook.send_embed(embed=embed_boss_spawned(incursion))


@shared_task()
def waitlist_state(waitlist_pk: int, is_open: bool) -> None:
    waitlist = Waitlist.get_solo()
    for webhook in IncursionsConfig.get_solo().waitlist_webhooks.all():
        webhook.send_embed(embed=embed_waitlist_state(waitlist=waitlist, is_open=is_open))


@shared_task(base=QueueOnce)
def update_all_fleets() -> None:
    active = ActiveFleet.get_solo()
    fleet = active.fleet
    if not fleet or not getattr(fleet, 'pk', None):
        logger.warning("ActiveFleet has no attached fleet to update")
        return
    update_fleet(fleet.pk)  # type: ignore[arg-type]


def update_fleet(fleet_id: int) -> None:

    fleet = Fleet.objects.select_related('boss').get(pk=fleet_id)

    if fleet.is_updating is False:
        return

    new_comp_detected = False
    changed_waitlists = False
    waitlist = Waitlist.get_solo()

    # TODO: expand nuanced error handling (throttle, retry, partial data)
    try:
        esi_fleet = fleet.get_fleet_members()
        fleet.last_updated = now()
        fleet.save(update_fields=["last_updated"])
    except Exception as e:
        logger.error(f"Fleet {fleet.pk} failed to get fleet esi: {e}")
        fleet.is_updating = False
        fleet.save(update_fields=["is_updating"])
        return

    fleet_members = {m.character_id: m for m in esi_fleet}
    fleet_member_ids = list(fleet_members.keys())

    # Snapshot current active members by EVE character ID (not FK PK) for accurate leave detection.
    old_fleet_members = list(
        FleetActivity.objects.filter(fleet=fleet, has_left=False).values_list(
            'character__character_id',
            flat=True,
        )
    )

    # Clean waitlist
    with transaction.atomic():
        count, _deleted = WaitlistEntryFit.objects.filter(character__character_id__in=fleet_member_ids).delete()
        WaitlistEntry.objects.annotate(fit_count=Count('waitlistentryfit')).filter(fit_count=0).delete()

    if count > 0:
        changed_waitlists = True

    # Update fleet composition
    with transaction.atomic():
        min_required = INCURSIONS_MINIMUM_FLEET_SIZE
        if len(fleet_members) < min_required:
            logger.debug(
                "Fleet %s below min_required (%s < %s) - skipping comp update",
                fleet.pk,
                len(fleet_members),
                min_required,
            )
            return

        for char_id, fleet_member in fleet_members.items():
            try:
                character = EveCharacter.objects.get(character_id=char_id)
            except EveCharacter.DoesNotExist:
                logger.debug("Character %s missing in DB; skipping FleetActivity create", char_id)
                continue
            fa, created = FleetActivity.objects.update_or_create(
                character=character,
                fleet=fleet,
                has_left=False,  # Make a NEW entry if rejoin, otherwise times get messed up
                defaults={
                    "ship": ItemType.objects.get(id=fleet_member.ship_type_id),
                    "last_seen": now(),
                    "is_boss": fleet.boss.character_id == char_id,
                    "has_left": False,
                })
            if created:
                new_comp_detected = True

        updated_count = (
            FleetActivity.objects.filter(
                character__character_id__in=old_fleet_members,
                fleet=fleet,
            )
            .exclude(character__character_id__in=fleet_member_ids)
            .update(has_left=True, last_seen=now())
        )
        if updated_count > 0:
            new_comp_detected = True

    # Notify frontend via SSE or signals

    if new_comp_detected:
        sse_payload = NewFleetCompSchema(fleet_id=waitlist.pk).model_dump()
        sse_client.submit([SSEEvent.new_json(topic="fleet_comp", event="comp_update", data=sse_payload)])
    if changed_waitlists:
        sse_payload = WaitlistUpdateSchema(waitlist_id=waitlist.pk).model_dump()
        sse_client.submit([SSEEvent.new_json(topic="waitlist", event="waitlist_update", data=sse_payload)])
