import argparse
import logging
import os.path
from datetime import date, datetime
from sys import maxsize, stdout
from typing import Any, Hashable, Iterable, Mapping, Self, Type, TypeVar

from frozendict import frozendict
from scooze.catalogs import ExtendedEnum, Format

DEFAULT_BULK_FILE_DIR = "./data/bulk"
DEFAULT_DECKS_DIR = "./data/decks"

## Generic Types
T = TypeVar("T")  # generic type
V = TypeVar("V")  # generic value type
E = TypeVar("E", bound=ExtendedEnum)  # generic Enum type
N = TypeVar("N", bound=ExtendedEnum)  # generic Enum (for mapping values) type
FloatableT = TypeVar("FloatableT", float, int, str)  # type that can normalize to float


def get_logger(
    filename: str,
    logger_name: str,
    file_logging_level: int = logging.DEBUG,
    console_logging_level: int = logging.WARNING,
    formatter: logging.Formatter = logging.Formatter("%(asctime)s - %(name)s:%(levelname)s - %(message)s"),
) -> logging.Logger:
    """
    Helper function to get a new logger.

    Args:
        filename: Filename of the log file.
        logger_name: The logger's name.
        file_logging_level: Logging level for the log file.
        console_logging_level: Logging level for stdout.
        formatter: The Formatter to be used in messages generated by this
        logger.

    Returns:
        A new logger.
    """

    # Create directory if not exists
    filepath = os.path.join("logs", filename)
    os.makedirs(os.path.dirname(filepath), exist_ok=True)

    # Handlers
    fh = logging.FileHandler(filepath, mode="a", encoding="UTF-8", delay=False)
    fh.setLevel(file_logging_level)
    ch = logging.StreamHandler(stdout)
    ch.setLevel(console_logging_level)

    # Formatting
    fh.setFormatter(formatter)
    ch.setFormatter(formatter)

    # Create the logger
    logger = logging.getLogger(logger_name)
    logger.setLevel(logging.DEBUG)
    logger.addHandler(fh)
    logger.addHandler(ch)

    return logger


class SmartFormatter(argparse.RawDescriptionHelpFormatter, argparse.HelpFormatter):
    def _split_lines(self, text, width):
        if text.startswith("R|"):
            return text[2:].splitlines()
        # this is the RawTextHelpFormatter._split_lines
        return argparse.HelpFormatter._split_lines(self, text, width)


# region Deck Format Helpers


def max_relentless_quantity(name: str) -> int:
    """
    Given a card name, what is the maximum quantity of a card in a deck?
    """

    match name:
        case "Seven Dwarves":
            return 7
        case "Nazgûl" | "Nazgul":
            return 9
        case (
            "Plains"
            | "Island"
            | "Swamp"
            | "Mountain"
            | "Forest"
            | "Wastes"
            | "Snow-Covered Plains"
            | "Snow-Covered Island"
            | "Snow-Covered Swamp"
            | "Snow-Covered Mountain"
            | "Snow-Covered Forest"
            | "Dragon's Approach"
            | "Persistent Petitioners"
            | "Rat Colony"
            | "Relentless Rats"
            | "Shadowborn Apostle"
        ):
            return maxsize
        case _:
            return 0  # helps identify new relentless cards


def max_card_quantity(fmt: Format) -> int:
    """
    Given a Format, what is the maximum quantity of a card in a deck?
    """

    match fmt.value:
        case Format.LIMITED:
            return maxsize

        case (
            Format.BRAWL
            | Format.COMMANDER
            | Format.DUEL
            | Format.GLADIATOR
            | Format.HISTORICBRAWL
            | Format.OATHBREAKER
            | Format.PAUPERCOMMANDER
            | Format.PREDH
        ):
            return 1

        case (
            Format.ALCHEMY
            | Format.EXPLORER
            | Format.FUTURE
            | Format.HISTORIC
            | Format.LEGACY
            | Format.MODERN
            | Format.OLDSCHOOL
            | Format.PAUPER
            | Format.PENNY
            | Format.PIONEER
            | Format.PREMODERN
            | Format.STANDARD
            | Format.VINTAGE
        ):
            return 4

        case Format.NONE | _:
            return maxsize


def main_size(fmt: Format) -> tuple[int, int]:
    """
    Given a Format, what are the required min and max size for a main deck?
    """

    match fmt.value:
        case Format.LIMITED:
            return 40, maxsize

        case Format.OATHBREAKER:
            return 58, 58

        case (
            Format.ALCHEMY
            | Format.EXPLORER
            | Format.FUTURE
            | Format.HISTORIC
            | Format.LEGACY
            | Format.MODERN
            | Format.OLDSCHOOL
            | Format.PAUPER
            | Format.PENNY
            | Format.PIONEER
            | Format.PREMODERN
            | Format.STANDARD
            | Format.VINTAGE
        ):
            return 60, maxsize

        case (Format.BRAWL | Format.HISTORICBRAWL | Format.PAUPERCOMMANDER | Format.PREDH):
            return 99, 99

        case Format.COMMANDER | Format.DUEL:
            return 98, 99  # Accounting for Partner

        case Format.GLADIATOR:
            return 100, 100

        case Format.NONE | _:
            return 0, maxsize


def side_size(fmt: Format) -> tuple[int, int]:
    """
    Given a Format, what are the min and max size for a sideboard?
    """

    match fmt.value:
        case Format.LIMITED:
            return 0, maxsize

        case (
            Format.ALCHEMY
            | Format.EXPLORER
            | Format.FUTURE
            | Format.HISTORIC
            | Format.LEGACY
            | Format.MODERN
            | Format.OLDSCHOOL
            | Format.PAUPER
            | Format.PENNY
            | Format.PIONEER
            | Format.PREMODERN
            | Format.STANDARD
            | Format.VINTAGE
        ):
            return 0, 15

        case (
            Format.BRAWL
            | Format.COMMANDER
            | Format.DUEL
            | Format.GLADIATOR
            | Format.HISTORICBRAWL
            | Format.OATHBREAKER
            | Format.PAUPERCOMMANDER
            | Format.PREDH
        ):
            return 0, 0

        case Format.NONE | _:
            return 0, maxsize


def cmdr_size(fmt: Format) -> tuple[int, int]:
    """
    Given a Format, what are the min and max size for a command zone?
    """

    match fmt.value:
        case (
            Format.ALCHEMY
            | Format.EXPLORER
            | Format.FUTURE
            | Format.GLADIATOR
            | Format.HISTORIC
            | Format.LEGACY
            | Format.LIMITED
            | Format.MODERN
            | Format.OLDSCHOOL
            | Format.PAUPER
            | Format.PENNY
            | Format.PIONEER
            | Format.PREMODERN
            | Format.STANDARD
            | Format.VINTAGE
        ):
            return 0, 0

        case (Format.BRAWL | Format.HISTORICBRAWL | Format.PAUPERCOMMANDER | Format.PREDH):
            return 1, 1

        case Format.COMMANDER | Format.DUEL:
            return 1, 2  # Accounting for Partner

        case Format.OATHBREAKER:
            return 2, 2

        case Format.NONE | _:
            return 0, maxsize


# endregion


# region Helper Classes

# region Base Classes


class ComparableObject:
    """
    A simple base class to support comparable objects.
    """

    @property
    def __key__(self) -> tuple[Any, ...]:
        return tuple(getattr(self, k) for k in self.__dict__.keys())

    def __eq__(self, other: Self):
        return self.__key__ == other.__key__

    def __ne__(self, other: Self):
        return not (self == other)


class HashableObject(ComparableObject, Hashable):
    """
    A simple base class to support hashable objects.
    """

    def __hash__(self):
        return hash(self.__key__)


# endregion

# region JSON Utils


class JsonNormalizer:
    """
    A simple class to be used when normalizing non-serializable data from JSON.
    """

    @classmethod
    def to_date(cls, d: date | str | None) -> date:
        """
        Normalize a date.

        Args:
            d: A date to normalize.

        Returns:
            A date.
        """

        if d is None or isinstance(d, date):
            return d

        return datetime.strptime(d, "%Y-%m-%d").date()  # NOTE: maybe store date format

    @classmethod
    def to_enum(cls, e: Type[E], v) -> E | None:
        """
        Normalize an Enum.

        Args:
            e: A type of Enum to normalize to.
            v: A value to normalize.

        Returns:
            An instance of the given type of Enum.
        """

        if v is None:
            return v
        elif v in e.list():
            return e(v)

        return e[v]

    @classmethod
    def to_float(cls, f: FloatableT | None) -> float | None:
        """
        Normalize a float.

        Args:
            f: A float to normalize.

        Returns:
            A float.
        """

        if f is None or isinstance(f, float):
            return f

        return float(f)

    @classmethod
    def to_frozendict(
        cls, d: Mapping[T, V] | None, convert_key_to_enum: type[E] = None, convert_value_to_enum: type[N] = None
    ) -> frozendict[T | E, V | N] | None:
        """
        Normalize a frozendict.

        Args:
            d: A frozendict to normalize.
            convert_key_to_enum: A type of Enum to normalize keys to.
            convert_value_to_enum: A type of Enum to normalize values to.

        Returns:
            A frozendict.
        """

        if d is None:
            return d

        return frozendict(
            {
                JsonNormalizer.to_enum(e=convert_key_to_enum, v=k)
                if convert_key_to_enum
                else k: JsonNormalizer.to_enum(e=convert_value_to_enum, v=v)
                if convert_value_to_enum
                else v
                for k, v in d.items()
            }
        )

    @classmethod
    def to_frozenset(cls, s: Iterable[T] | None, convert_to_enum: type[E] = None) -> frozenset[T | E] | None:
        """
        Normalize a frozenset.

        Args:
            s: A frozenset to normalize.
            convert_to_enum: A type of Enum to normalize values to.

        Returns:
            A frozenset.
        """

        if s is None:
            return s

        return frozenset({JsonNormalizer.to_enum(e=convert_to_enum, v=v) if convert_to_enum else v for v in s})

    @classmethod
    def to_tuple(cls, t: Iterable[T] | None, convert_to_enum: type[E] = None) -> tuple[T | E] | None:
        """
        Normalize a tuple.

        Args:
            t: A tuple to normalize.
            convert_to_enum: A type of Enum to normalize values to.

        Returns:
            A tuple.
        """

        if t is None:
            return t

        return tuple([JsonNormalizer.to_enum(e=convert_to_enum, v=v) if convert_to_enum else v for v in t])


# endregion

# region Dict Diff


class DictDiff(ComparableObject):
    """
    Represents a diff between two dicts.

    Attributes:
        contents (dict[Any, tuple[int, int]]): The contents of this diff.
    """

    def __init__(self, contents: dict[T, tuple[int, int]]):
        self.contents = contents

    def __len__(self):
        return len(self.contents)

    def __str__(self):
        return "\n".join([f"{key}: {counts}" for key, counts in self.contents.items()]) + "\n"

    # Source:  https://code.activestate.com/recipes/576644-diff-two-dictionaries/#c9
    @classmethod
    def get_diff(cls, d1: dict, d2: dict, NO_KEY: Any = 0) -> "DictDiff":
        """
        Generate a diff between two dicts.

        Args:
            d1: The first dict.
            d2: The second dict.
            NO_KEY: Default value to use when a key is in one dict, but not the
              other.

        Returns:
            A dict with all keys from both dicts. The values are tuple(v, v)
            for the values in each dict.
        """

        both = d1.keys() & d2.keys()
        diff = {k: (d1[k], d2[k]) for k in both if d1[k] != d2[k]}
        diff.update({k: (d1[k], NO_KEY) for k in d1.keys() - both})
        diff.update({k: (NO_KEY, d2[k]) for k in d2.keys() - both})
        return DictDiff(diff)

    # endregion


# endregion
