"""
Chat list parser
"""

# Standard Library
from collections import defaultdict

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

# Alliance Auth
from allianceauth.eveonline.evelinks import dotlan, eveimageserver, evewho, zkillboard
from allianceauth.eveonline.models import EveCharacter
from allianceauth.services.hooks import get_extension_logger

# AA Intel Tool
from aa_intel_tool import __title__
from aa_intel_tool.app_settings import AppSettings
from aa_intel_tool.exceptions import ParserError
from aa_intel_tool.helper.data_structure import dict_to_list
from aa_intel_tool.helper.eve_character import (
    create_characters,
    fetch_character_ids_from_esi,
)
from aa_intel_tool.models import Scan, ScanData
from aa_intel_tool.parser.helper.db import safe_scan_to_db
from aa_intel_tool.providers import AppLogger

logger = AppLogger(my_logger=get_extension_logger(name=__name__), prefix=__title__)


def _get_character_info(scan_data: list) -> QuerySet[EveCharacter]:
    """
    Get Eve character information and affiliation from a list of character names

    :param scan_data: List of character names to get information for
    :type scan_data: list
    :return: QuerySet of EveCharacter objects matching the character names in the scan data
    :rtype: QuerySet[EveCharacter]
    """

    # Excluding corporation_id=1000001 (Doomheim) to potentially force an update here …
    eve_characters = EveCharacter.objects.filter(character_name__in=scan_data).exclude(
        corporation_id=1000001
    )

    # Check if we have to bother ESI or if we have all characters already
    if len(scan_data) != eve_characters.count():
        existing_character_names = set(
            eve_characters.values_list("character_name", flat=True)
        )

        characters_to_fetch = set(scan_data) - existing_character_names
        logger.debug(
            f"{len(characters_to_fetch)} character(s) need to be fetched from ESI: {characters_to_fetch}"
        )

        fetched_characters = fetch_character_ids_from_esi(
            characters_to_fetch=characters_to_fetch
        )

        logger.debug(f"Fetched character data from ESI: {fetched_characters}")

        if len(fetched_characters) > 0:
            new_eve_characters = create_characters(
                character_data_from_esi=fetched_characters
            )

            logger.debug(
                f"Created {len(new_eve_characters)} new EveCharacter object(s) from ESI data."
            )

            # Combine existing and new characters
            eve_characters = eve_characters | new_eve_characters

    return eve_characters


def _get_unaffiliated_alliance_info() -> dict:
    """
    Get the alliance_info dict for characters that are in no alliance

    :return: A dict with alliance information for unaffiliated characters
    :rtype: dict
    """

    return {
        "id": 1,
        "name": "",
        "ticker": "",
        "logo": eveimageserver.alliance_logo_url(alliance_id=1, size=32),
    }


def _parse_alliance_info(
    eve_character: EveCharacter, with_evelinks: bool = True
) -> dict:
    """
    Parse the alliance information from an EveCharacter

    :param eve_character: The EveCharacter object to parse the alliance information from
    :type eve_character: EveCharacter
    :param with_evelinks: Whether to include links to Eve-related websites in the alliance information
    :type with_evelinks: bool
    :return: A dict with alliance information for the given EveCharacter
    :rtype: dict
    """

    if eve_character.alliance_id is None:
        return _get_unaffiliated_alliance_info()

    alliance_info = {
        "id": eve_character.alliance_id,
        "name": eve_character.alliance_name,
        "ticker": eve_character.alliance_ticker,
        "logo": eve_character.alliance_logo_url_32,
    }

    # Add eve links if requested
    if with_evelinks:
        alliance_info.update(
            {
                "dotlan": dotlan.alliance_url(eve_character.alliance_name),
                "zkillboard": zkillboard.alliance_url(eve_character.alliance_id),
            }
        )

    return alliance_info


def _parse_corporation_info(
    eve_character: EveCharacter,
    with_alliance_info: bool = True,
    with_evelinks: bool = True,
) -> dict:
    """
    Parse the corporation information from an EveCharacter

    :param eve_character: The EveCharacter object to parse the corporation information from
    :type eve_character: EveCharacter
    :param with_alliance_info: Whether to include alliance information in the corporation information
    :type with_alliance_info: bool
    :param with_evelinks: Whether to include links to Eve-related websites in the corporation information
    :type with_evelinks: bool
    :return: A dict with corporation information for the given EveCharacter
    :rtype: dict
    """

    corporation_info = {
        "id": eve_character.corporation_id,
        "name": eve_character.corporation_name,
        "ticker": eve_character.corporation_ticker,
        "logo": eve_character.corporation_logo_url_32,
    }

    # Add eve links if requested
    if with_evelinks:
        corporation_info.update(
            {
                "dotlan": dotlan.corporation_url(name=eve_character.corporation_name),
                "zkillboard": zkillboard.corporation_url(
                    eve_id=eve_character.corporation_id
                ),
            }
        )

    # Add alliance info if requested
    if with_alliance_info:
        corporation_info["alliance"] = _parse_alliance_info(
            eve_character=eve_character, with_evelinks=with_evelinks
        )

    return corporation_info


def _parse_character_info(eve_character: EveCharacter) -> dict:
    """
    Parse the character information from an EveCharacter

    :param eve_character: The EveCharacter object to parse the character information from
    :type eve_character: EveCharacter
    :return: A dict with character information for the given EveCharacter
    :rtype: dict
    """

    return {
        "id": eve_character.character_id,
        "name": eve_character.character_name,
        "portrait": eve_character.portrait_url_32,
        "evewho": evewho.character_url(eve_character.character_id),
        "zkillboard": zkillboard.character_url(eve_character.character_id),
        "corporation": _parse_corporation_info(
            eve_character=eve_character, with_alliance_info=False, with_evelinks=False
        ),
        "alliance": _parse_alliance_info(
            eve_character=eve_character, with_evelinks=False
        ),
    }


def _parse_chatscan_data(eve_characters: QuerySet[EveCharacter]) -> dict:
    """
    Parse the chat scan data and return character information,
    corporation information and alliance information for each character

    :param eve_characters: A QuerySet of EveCharacter objects to parse the information from
    :type eve_characters: QuerySet[EveCharacter]
    :return: A dict with three keys: 'pilots', 'corporations' and 'alliances'. Each key contains a list of dicts with the respective information for each character, corporation and alliance in the chat scan.
    :rtype: dict
    """

    counter = defaultdict(int)
    alliance_info = {}
    corporation_info = {}
    character_info = {}

    # Loop through the characters
    for eve_character in list(eve_characters):
        alliance_name = eve_character.alliance_name or "Unaffiliated"
        corporation_name = eve_character.corporation_name

        counter[alliance_name] += 1
        counter[corporation_name] += 1

        # Alliance Info
        if alliance_name not in alliance_info:
            alliance_info[alliance_name] = _parse_alliance_info(eve_character)

        # Corporation Info
        if corporation_name not in corporation_info:
            corporation_info[corporation_name] = _parse_corporation_info(eve_character)

        # Character Info
        character_info[eve_character.character_name] = _parse_character_info(
            eve_character
        )

        # Update the counter
        alliance_info[alliance_name]["count"] = counter[alliance_name]
        corporation_info[corporation_name]["count"] = counter[corporation_name]

    return {
        "pilots": dict_to_list(input_dict=character_info),
        "corporations": dict_to_list(input_dict=corporation_info),
        "alliances": dict_to_list(input_dict=alliance_info),
    }


def parse(
    scan_data: list, safe_to_db: bool = True, ignore_limit: bool = False
) -> Scan | dict:
    """
    Parse chat list

    :param scan_data: List of character names to parse the chat list for
    :type scan_data: list
    :param safe_to_db: Whether the parsed data is safe to be saved to the database (i.e. whether it has been validated and cleaned). If True, the parsed data will be saved to the database and a Scan object will be returned. If False, the parsed data will not be saved to the database and a dict with the parsed data will be returned instead.
    :type safe_to_db: bool
    :param ignore_limit: Whether to ignore the maximum allowed number of pilots in a chat scan. If True, the parser will not check if the number of pilots in the scan exceeds the maximum allowed number and will parse the data regardless. If False, the parser will check if the number of pilots in the scan exceeds the maximum allowed number and will throw a ParserError if it does.
    :type ignore_limit: bool
    :return: A Scan object containing the parsed chat list data if safe_to_db is True, or a dict with the parsed chat list data if safe_to_db is False
    :rtype: Scan | dict
    """

    # Only parse the chat scan if the module is enabled
    if not AppSettings.INTELTOOL_ENABLE_MODULE_CHATSCAN:
        raise ParserError(message=str(_("The chat list module is currently disabled.")))

    logger.debug(msg=f"{len(scan_data)} name(s) to work through …")

    pilots_in_scan = len(scan_data)
    max_allowed_pilots = AppSettings.INTELTOOL_CHATSCAN_MAX_PILOTS

    # Check if the number of pilots in the scan exceeds the maximum allowed number
    if 0 < max_allowed_pilots < pilots_in_scan and not ignore_limit:
        logger.debug(
            msg=(
                f"Number of pilots in scan ({pilots_in_scan}) exceeds the maximum "
                f"allowed number ({max_allowed_pilots}). Throwing a tantrum …"
            )
        )

        # Throw a tantrum
        raise ParserError(
            message=ngettext(
                singular="Chat scans are currently limited to a maximum of {max_allowed_pilots} pilot per scan. Your list of pilots exceeds this limit.",
                plural="Chat scans are currently limited to a maximum of {max_allowed_pilots} pilots per scan. Your list of pilots exceeds this limit.",
                number=max_allowed_pilots,
            ).format(max_allowed_pilots=max_allowed_pilots)
        )

    eve_characters = _get_character_info(scan_data=scan_data)
    logger.debug(msg=f"Got {len(eve_characters)} EveCharacter object(s) back from AA …")

    # Parse the data
    parsed_chatscan = _parse_chatscan_data(eve_characters=eve_characters)
    parsed_data = {
        "pilots": {
            "section": ScanData.Section.PILOTLIST,
            "data": parsed_chatscan["pilots"],
        },
        "corporations": {
            "section": ScanData.Section.CORPORATIONLIST,
            "data": parsed_chatscan["corporations"],
        },
        "alliances": {
            "section": ScanData.Section.ALLIANCELIST,
            "data": parsed_chatscan["alliances"],
        },
    }

    return (
        parsed_data
        if not safe_to_db
        else safe_scan_to_db(scan_type=Scan.Type.CHATLIST, parsed_data=parsed_data)
    )
