import copy
import hashlib
import json
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta

import pytz

from core.call_lookup_helper import get_call_info
from core.sig_lookup_helper import populate_missing_sig_ref_info
from core.utils import get_flag_for_dxcc


@dataclass
class Alert:
    """Data class that defines an alert."""

    # Unique identifier for the alert
    id: str | None = None
    # Callsigns of the operators that has been alerted
    dx_calls: list | None = None
    # Names of the operators that has been alerted
    dx_names: list | None = None
    # Country of the DX operator
    dx_country: str | None = None
    # Country flag of the DX operator
    dx_flag: str | None = None
    # Continent of the DX operator
    dx_continent: str | None = None
    # DXCC ID of the DX operator
    dx_dxcc_id: int | None = None
    # CQ zone of the DX operator
    dx_cq_zone: int | None = None
    # ITU zone of the DX operator
    dx_itu_zone: int | None = None
    # Intended frequencies & modes of operation. Essentially just a different kind of comment field.
    freqs_modes: str | None = None
    # Start time of the activation, UTC seconds since UNIX epoch
    start_time: float | None = None
    # Start time of the activation of the alert, ISO 8601
    start_time_iso: str | None = None
    # End time of the activation, UTC seconds since UNIX epoch. Optional
    end_time: float | None = None
    # End time of the activation of the alert, ISO 8601
    end_time_iso: str | None = None
    # Time that this software received the alert, UTC seconds since UNIX epoch. This is used with the "since_received"
    # call to our API to receive all data that is new to us, even if by a quirk of the API it might be older than the
    # list time the client polled the API.
    received_time: float | None = None
    # Time that this software received the alert, ISO 8601
    received_time_iso: str | None = None
    # Comment made by the alerter, if any
    comment: str | None = None
    # Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
    sig: str | None = None
    # SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
    sig_refs: list | None = None
    # Whether this alert is for a DXpedition, as opposed to e.g. an xOTA programme.
    is_dxpedition: bool = False
    # Where we got the alert from, e.g. "POTA", "SOTA"...
    source: str | None = None
    # The ID the source gave it, if any.
    source_id: str | None = None

    def infer_missing(self, credentials=None):
        """Infer missing parameters where possible"""

        try:
            # If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but
            # clients can still reliably parse it as a number.
            if not self.start_time:
                self.start_time = 0

            # If we don't have a received time, this has just been received so set that to "now"
            if not self.received_time:
                self.received_time = datetime.now(pytz.UTC).timestamp()

            # Fill in ISO versions of times, in case the client prefers that
            if self.start_time and not self.start_time_iso:
                self.start_time_iso = datetime.fromtimestamp(self.start_time, pytz.UTC).isoformat()
            if self.end_time and not self.end_time_iso:
                self.end_time_iso = datetime.fromtimestamp(self.end_time, pytz.UTC).isoformat()
            if self.received_time and not self.received_time_iso:
                self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()

            # DX country, continent, zones etc. from callsign. CQ/ITU zone are better looked up with a location but we don't
            # have a real location for alerts.
            if self.dx_calls and self.dx_calls[0]:
                call_info = get_call_info(self.dx_calls[0], credentials)
                if self.dx_calls and self.dx_calls[0] and not self.dx_country:
                    self.dx_country = call_info.country
                if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
                    self.dx_continent = call_info.continent
                if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone:
                    self.dx_cq_zone = call_info.cq_zone
                if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone:
                    self.dx_itu_zone = call_info.itu_zone
                if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
                    self.dx_dxcc_id = call_info.dxcc_id
                if self.dx_dxcc_id and not self.dx_flag:
                    self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)

            # Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
            # in its initial call, we use this code to populate the rest of the data. This includes working out grid refs
            # from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
            if self.sig_refs and len(self.sig_refs) > 0:
                for sig_ref in self.sig_refs:
                    populate_missing_sig_ref_info(sig_ref)

            # If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
            # and apply it to the whole spot.
            if self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0] and not self.sig:
                self.sig = self.sig_refs[0].sig

            # Create an ID based on the source and source ID if possible, as these guaranee uniqueness. If there is no
            # source ID, use a combination of callsign and start time. Excluding things like the comment here allows for
            # user updates of their alert comments without duplicating in the system.
            if not self.id:
                if self.source and self.source_id:
                    self.id = hashlib.sha256(str({"s": self.source, "sid": self.source_id}).encode("utf-8")).hexdigest()
                else:
                    self.id = hashlib.sha256(
                        str({"s": self.source, "c": self.dx_calls, "t": self.start_time}).encode("utf-8")).hexdigest()

            # DX operator name lookup, using QRZ.com/HamQTH.
            if self.dx_calls and not self.dx_names:
                self.dx_names = list(
                    map(lambda c: get_call_info(c, credentials).name, self.dx_calls))

        except Exception as e:
            logging.error("Exception while inferring missing data from spot", e, exc_info=True)

    def to_json(self):
        """JSON serialise"""

        return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)

    def expired(self):
        """Decide if this alert has expired (in which case it should not be added to the system in the first place, and not
        returned by the web server if later requested, and removed by the cleanup functions). "Expired" is defined as
        either having an end_time in the past, or if it only has a start_time, then that start time was more than 3 hours
        ago. If it somehow doesn't have a start_time either, it is considered to be expired."""

        return not self.start_time or (self.end_time and self.end_time < datetime.now(pytz.UTC).timestamp()) or (
                not self.end_time and self.start_time < (datetime.now(pytz.UTC) - timedelta(hours=3)).timestamp())
