# Standard Library
import logging
from datetime import timedelta

# Third Party
from discord import SyncWebhook
from routing.routing import route_path_nodes
from solo.models import SingletonModel

# Django
from django.db import models
from django.utils.translation import gettext_lazy as _

# Alliance Auth (Core)
from allianceauth.eveonline.models import EveFactionInfo

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

logger = logging.getLogger(__name__)


class Incursion(models.Model):

    class States(models.TextChoices):
        """"
        Incursion Phases
        """
        ESTABLISHED = 'established', _("Incursion established")  # Up to 5 Days
        MOBILIZING = 'mobilizing', _("Incursion mobilizing")  # 48 Hours
        WITHDRAWING = 'withdrawing', _("Incursion withdrawing")  # 24 Hours
        # Our custom phase to archive finished incursions
        ENDED = 'ended', _("Ended")

    class Types(models.TextChoices):
        """
        I Think this used to be used for Trig Incursions etc, leaving the option in place to expand it later
        """
        INCURSION = 'Incursion', _("Sansha Incursion")

    constellation = models.ForeignKey(
        Constellation,
        blank=True,
        null=True,
        on_delete=models.CASCADE,
        related_name="+",
    )
    faction = models.ForeignKey(
        EveFactionInfo,
        verbose_name=_("The attacking Faction"),
        on_delete=models.CASCADE,
    )
    has_boss = models.BooleanField(
        _("Whether the final encounter has boss or not"), default=False
    )
    infested_solar_systems = models.ManyToManyField(
        SolarSystem, blank=True, related_name="+"
    )
    staging_solar_system = models.ForeignKey(
        SolarSystem, blank=True, null=True, on_delete=models.CASCADE, related_name="+"
    )

    state = models.CharField(
        _("The state of this incursion"),
        max_length=50, choices=States.choices, default=States.ESTABLISHED)
    type = models.CharField(
        _("The type of this incursion"), max_length=50,
        choices=Types.choices, default=Types.INCURSION)

    established_timestamp = models.DateTimeField(
        _("Established"), auto_now=False, auto_now_add=False, blank=True, null=True)
    mobilizing_timestamp = models.DateTimeField(
        _("Mobilizing"), auto_now=False, auto_now_add=False, blank=True, null=True)
    withdrawing_timestamp = models.DateTimeField(
        _("Withdrawing"), auto_now=False, auto_now_add=False, blank=True, null=True)
    ended_timestamp = models.DateTimeField(
        _("Ended"), auto_now=False, auto_now_add=False, blank=True, null=True)

    class Meta:
        default_permissions = ()
        verbose_name = _("Incursion")
        verbose_name_plural = _("Incursions")

    def __str__(self) -> str:
        if self.constellation and self.constellation.name:
            return str(self.constellation.name)
        return "Incursion (Unknown)"

    @property
    def established_utilization(self) -> float | None:
        if self.established_timestamp and self.mobilizing_timestamp:
            total = timedelta(days=5)
            delta = self.mobilizing_timestamp - self.established_timestamp
            # Clamp between 0 and 1 (unexpected ordering or overshoot safety)
            return max(0.0, min(1.0, delta / total))
        return None

    @property
    def mobilizing_utilization(self) -> float | None:

        if self.mobilizing_timestamp and self.withdrawing_timestamp:
            total = timedelta(days=2)
            delta = self.withdrawing_timestamp - self.mobilizing_timestamp
            return max(0.0, min(1.0, delta / total))
        return None

    @property
    def security_string(self) -> str:
        """Return localized human string for security band."""
        if self.is_high_sec:
            return _("High-Sec")
        if self.is_low_sec:
            return _("Low-Sec")
        if self.is_null_sec:
            return _("Null-Sec")
        return _("Unknown")

    @property
    def is_island(self) -> bool:
        """Determine if staging system is an "island" (needs low/null-sec transit).

        Uses a fixed hub system id (Jita: 30000142). Any route node with
        security < 0.5 implies a low/null sec hop, thus an island.
        Conservative fallback of True if data is unavailable.
        """
        HUB_SYSTEM_ID = 30000142
        if not self.staging_solar_system:
            return True
        try:
            for system in route_path_nodes(self.staging_solar_system.system_id, HUB_SYSTEM_ID):
                sec = getattr(system, "security_status", None)
                if sec is None:
                    continue
                if sec < 0.50:
                    return True
            return False
        except Exception as ex:
            logger.debug("Failed to compute island status for %s: %s", self.pk, ex)
            return True

    @property
    def influence(self) -> float:
        """Return latest influence value (0.0 if none available).

        Previously this property forgot to return the queryset result and always
        yielded None. Fixed + error handling.
        """
        try:
            return (
                IncursionInfluence.objects.filter(incursion=self)
                .latest("timestamp")
                .influence
            )
        except IncursionInfluence.DoesNotExist:  # type: ignore[attr-defined]
            return 0.0
        except Exception as ex:
            logger.debug("Influence lookup failed for %s: %s", self.pk, ex)
            return 0.0

    @property
    def is_high_sec(self) -> bool:
        sec = getattr(self.staging_solar_system, "security_status", None)
        return bool(sec is not None and sec >= 0.45)

    @property
    def is_low_sec(self) -> bool:
        sec = getattr(self.staging_solar_system, "security_status", None)
        return bool(sec is not None and 0.0 < sec < 0.45)

    @property
    def is_null_sec(self) -> bool:
        sec = getattr(self.staging_solar_system, "security_status", None)
        return bool(sec is not None and sec <= 0.0)


class IncursionInfluence(models.Model):
    incursion = models.ForeignKey(
        Incursion, verbose_name=_("Incursion"), on_delete=models.CASCADE
    )
    timestamp = models.DateTimeField(
        _("Timestamp"), auto_now=False, auto_now_add=False
    )
    influence = models.FloatField(
        _("Influence of this incursion as a float from 0 to 1"), default=0.0
    )

    class Meta:
        default_permissions = ()
        verbose_name = _("IncursionInfluence")
        verbose_name_plural = _("IncursionInfluences")
        get_latest_by = "timestamp"
        ordering = ["-timestamp"]
        constraints = [
            models.UniqueConstraint(
                fields=["incursion", "timestamp"],
                name="UniqueIncursionInfluenceLogTimestamp",
            ),
        ]
        indexes = [
            models.Index(fields=["incursion", "timestamp"], name="incursion_ts_idx")
        ]

    def __str__(self) -> str:
        return f"{self.incursion} @ {self.timestamp}"


class Webhook(models.Model):
    """Destinations for Relays.

    NOTE: URL kept as CharField to avoid a disruptive migration; consider
    changing to URLField in a future major update.
    """

    url = models.CharField(max_length=200)
    name = models.CharField(max_length=50)

    security_high = models.BooleanField(
        _("Notify on High Security Incursions")
    )
    security_low = models.BooleanField(
        _("Notify on Low Security Incursions")
    )
    security_null = models.BooleanField(
        _("Notify on Null Security Incursions")
    )

    def __str__(self) -> str:
        return f'"{self.name}"'

    class Meta:
        default_permissions = ()
        verbose_name = _("Destination Webhook")
        verbose_name_plural = _("Destination Webhooks")

    def send_embed(self, embed) -> None:
        """Send a Discord embed via stored webhook.

        Network / API failures are swallowed and logged so they don't block
        calling code (e.g. save signals).
        """
        try:  # pragma: no cover - external call
            webhook = SyncWebhook.from_url(self.url)
            webhook.send(embed=embed, username="AA Incursions")
        except Exception as ex:  # broad: library raises various
            logger.warning("Webhook send failed (%s): %s", self.pk, ex)


class IncursionsConfig(SingletonModel):
    status_webhooks = models.ManyToManyField(
        Webhook, verbose_name=_("Destination Webhook for Incursion Updates"), related_name="+", blank=True)
    waitlist_webhooks = models.ManyToManyField(
        Webhook, verbose_name=_("Destination Webhook for Waitlist Updates"), related_name="+", blank=True)

    def __str__(self) -> str:
        return "AA Incursions Settings"

    class Meta:
        default_permissions = ()
        verbose_name = _("AA Incursions Settings")
        verbose_name_plural = _("AA Incursions Settings")


class Focus(SingletonModel):
    incursion = models.ForeignKey(
        Incursion,
        verbose_name=_("Current Focus"),
        on_delete=models.CASCADE, blank=True, null=True)

    def __str__(self) -> str:
        try:
            return f"Current Focus: {self.incursion}"
        except Exception:
            return "No Focus Set"

    class Meta:  # pyright: ignore[reportIncompatibleVariableOverride]
        default_permissions = ()
        verbose_name = _("Current Focus")
        verbose_name_plural = _("Current Focus")
