from datetime import datetime, timezone

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

from celery import shared_task
from eve_sde.models import Constellation, ItemType, SolarSystem
from ninja import Schema

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

from allianceauth.eveonline.models import EveCharacter, EveFactionInfo
from allianceauth.services.hooks import get_extension_logger
from allianceauth.services.tasks import QueueOnce

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

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."""
    incursions, response = get_incursions_incursions()
    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.get('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.get('state', '')).strip()  # handle trailing spaces
        has_boss_flag = str(data.get('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.get('faction_id'))[0],
                    has_boss=has_boss_flag,
                    staging_solar_system=SolarSystem.objects.get(system_id=data.get('staging_solar_system_id')),
                    state=state_raw,
                    type=data.get('type', 'Incursion'),
                    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.get('influence', 0.0),
                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)])
