"""IMAP Query builder"""
import datetime
import itertools
import functools
import collections

from .utils import cleaned_uid_set, short_month_names, quote


class LogicOperator(collections.UserString):
    def __init__(self, *converted_strings, **unconverted_dicts):
        self.converted_strings = converted_strings
        for val in converted_strings:
            if not any(isinstance(val, t) for t in (str, collections.UserString)):
                raise ValueError('Unexpected type "{}" for converted part, str like obj expected'.format(type(val)))
        self.converted_params = ParamConverter(unconverted_dicts).convert()
        if not any((self.converted_strings, self.converted_params)):
            raise ValueError('{} expects params'.format(self.__class__.__name__))
        super().__init__(self.combine_params())

    def combine_params(self) -> str:
        """combine self.converted_strings and self.converted_params to IMAP search criteria format"""
        raise NotImplementedError

    @staticmethod
    def prefix_join(operator: str, params: iter) -> str:
        """Join params by prefix notation rules, enclose in parenthesis"""
        return '({})'.format(functools.reduce(lambda a, b: '{}{} {}'.format(operator, a, b), params))


class AND(LogicOperator):
    """When multiple keys are specified, the result is the intersection of all the messages that match those keys."""

    def combine_params(self) -> str:
        return self.prefix_join('', itertools.chain(self.converted_strings, self.converted_params))


class OR(LogicOperator):
    """OR <search-key1> <search-key2> Messages that match either search key."""

    def combine_params(self) -> str:
        return self.prefix_join('OR ', itertools.chain(self.converted_strings, self.converted_params))


class NOT(LogicOperator):
    """NOT <search-key> Messages that do not match the specified search key."""

    def combine_params(self) -> str:
        return 'NOT {}'.format(self.prefix_join('', itertools.chain(self.converted_strings, self.converted_params)))


# Short alias set:
A = AND
O = OR  # noqa
N = NOT


class Q(AND):
    def __init__(self, *args, **kwargs):
        import warnings
        warnings.warn('alias Q are deprecated and will be removed soon, use A instead')
        super().__init__(*args, **kwargs)


class Header:
    __slots__ = ('name', 'value')

    def __init__(self, name: str, value: str):
        if not isinstance(name, str):
            raise ValueError('Header-name expected str value, "{}" received'.format(type(name)))
        self.name = quote(name)
        if not isinstance(value, str):
            raise ValueError('Header-value expected str value, "{}" received'.format(type(value)))
        self.value = quote(value)

    def __str__(self):
        return '{0.name}: {0.value}'.format(self)


H = Header  # Short alias


class ParamConverter:
    """Convert search params to IMAP format"""

    multi_key_allowed = (
        'keyword', 'no_keyword', 'from_', 'to', 'subject', 'body', 'text', 'bcc', 'cc',
        'date', 'date_gte', 'date_lt', 'sent_date', 'sent_date_gte', 'sent_date_lt',
        'header', 'gmail_label',
    )

    def __init__(self, params: dict):
        self.params = params

    def _gen_values(self, key, value) -> iter:
        """Values generator"""
        # single value
        if key not in self.multi_key_allowed or isinstance(value, str):
            yield value
        else:
            try:
                # multiple values
                for i in iter(value):
                    yield i
            except TypeError:
                # single value
                yield value

    def convert(self) -> [str]:
        """
        :return: params in IMAP format
        """
        converted = []
        for key, raw_val in self.params.items():
            for val in self._gen_values(key, raw_val):
                convert_func = getattr(self, 'convert_{}'.format(key), None)
                if not convert_func:
                    raise KeyError('"{}" is an invalid parameter.'.format(key))
                converted.append(convert_func(key, val))
        return converted

    @classmethod
    def format_date(cls, value: datetime.date) -> str:
        """To avoid locale affects"""
        return '{}-{}-{}'.format(value.day, short_month_names[value.month - 1], value.year)

    @staticmethod
    def cleaned_str(key, value) -> str:
        if type(value) is not str:
            raise ValueError('"{}" expected str value, "{}" received'.format(key, type(value)))
        return str(value)

    @staticmethod
    def cleaned_date(key, value) -> datetime.date:
        if type(value) is not datetime.date:
            raise ValueError('"{}" expected datetime.date value, "{}" received'.format(key, type(value)))
        return value

    @staticmethod
    def cleaned_bool(key, value) -> bool:
        if type(value) is not bool:
            raise ValueError('"{}" expected bool value, "{}" received'.format(key, type(value)))
        return bool(value)

    @staticmethod
    def cleaned_true(key, value) -> True:
        if value is not True:
            raise ValueError('"{}" expected "True", "{}" received'.format(key, type(value)))
        return True

    @staticmethod
    def cleaned_uint(key, value) -> int:
        if type(value) is not int or int(value) < 0:
            raise ValueError('"{}" expected int value >= 0, "{}" received'.format(key, type(value)))
        return int(value)

    @staticmethod
    def cleaned_uid(key, value) -> str:
        try:
            uid_set = cleaned_uid_set(value)
        except ValueError as e:
            raise ValueError('{} parse error: {}'.format(key, str(e)))
        return uid_set

    @staticmethod
    def cleaned_header(key, value) -> H:
        if not isinstance(value, H):
            raise ValueError('"{}" expected Header (H) value, "{}" received'.format(key, type(value)))
        return value

    def convert_answered(self, key, value):
        """Messages [with/without] the Answered flag set. (ANSWERED, UNANSWERED)"""
        return 'ANSWERED' if self.cleaned_bool(key, value) else 'UNANSWERED'

    def convert_seen(self, key, value):
        """Messages that [have/do not have] the Seen flag set. (SEEN, UNSEEN)"""
        return 'SEEN' if self.cleaned_bool(key, value) else 'UNSEEN'

    def convert_flagged(self, key, value):
        """Messages [with/without] the Flagged flag set. (FLAGGED, UNFLAGGED)"""
        return 'FLAGGED' if self.cleaned_bool(key, value) else 'UNFLAGGED'

    def convert_draft(self, key, value):
        """Messages that [have/do not have] the Draft flag set. (DRAFT, UNDRAFT)"""
        return 'DRAFT' if self.cleaned_bool(key, value) else 'UNDRAFT'

    def convert_deleted(self, key, value):
        """Messages that [have/do not have] the Deleted flag set. (DELETED, UNDELETED)"""
        return 'DELETED' if self.cleaned_bool(key, value) else 'UNDELETED'

    def convert_keyword(self, key, value):
        """Messages with the specified keyword flag set. (KEYWORD)"""
        return 'KEYWORD {}'.format(self.cleaned_str(key, value))

    def convert_no_keyword(self, key, value):
        """Messages that do not have the specified keyword flag set. (UNKEYWORD)"""
        return 'UNKEYWORD {}'.format(self.cleaned_str(key, value))

    def convert_from_(self, key, value):
        """Messages that contain the specified string in the envelope structure's FROM field."""
        return 'FROM {}'.format(quote(self.cleaned_str(key, value)))

    def convert_to(self, key, value):
        """Messages that contain the specified string in the envelope structure's TO field."""
        return 'TO {}'.format(quote(self.cleaned_str(key, value)))

    def convert_subject(self, key, value):
        """Messages that contain the specified string in the envelope structure's SUBJECT field."""
        return 'SUBJECT {}'.format(quote(self.cleaned_str(key, value)))

    def convert_body(self, key, value):
        """Messages that contain the specified string in the body of the message."""
        return 'BODY {}'.format(quote(self.cleaned_str(key, value)))

    def convert_text(self, key, value):
        """Messages that contain the specified string in the header or body of the message."""
        return 'TEXT {}'.format(quote(self.cleaned_str(key, value)))

    def convert_bcc(self, key, value):
        """Messages that contain the specified string in the envelope structure's BCC field."""
        return 'BCC {}'.format(quote(self.cleaned_str(key, value)))

    def convert_cc(self, key, value):
        """Messages that contain the specified string in the envelope structure's CC field."""
        return 'CC {}'.format(quote(self.cleaned_str(key, value)))

    def convert_date(self, key, value):
        """
        Messages whose internal date (disregarding time and timezone)
        is within the specified date. (ON)
        """
        return 'ON {}'.format(self.format_date(self.cleaned_date(key, value)))

    def convert_date_gte(self, key, value):
        """
        Messages whose internal date (disregarding time and timezone)
        is within or later than the specified date. (SINCE)
        """
        return 'SINCE {}'.format(self.format_date(self.cleaned_date(key, value)))

    def convert_date_lt(self, key, value):
        """
        Messages whose internal date (disregarding time and timezone)
        is earlier than the specified date. (BEFORE)
        """
        return 'BEFORE {}'.format(self.format_date(self.cleaned_date(key, value)))

    def convert_sent_date(self, key, value):
        """
        Messages whose [RFC-2822] Date: header (disregarding time and timezone)
        is within the specified date. (SENTON)
        """
        return 'SENTON {}'.format(self.format_date(self.cleaned_date(key, value)))

    def convert_sent_date_gte(self, key, value):
        """
        Messages whose [RFC-2822] Date: header (disregarding time and timezone)
        is within or later than the specified date. (SENTSINCE)
        """
        return 'SENTSINCE {}'.format(self.format_date(self.cleaned_date(key, value)))

    def convert_sent_date_lt(self, key, value):
        """
        Messages whose [RFC-2822] Date: header (disregarding time and timezone)
        is earlier than the specified date. (SENTBEFORE)
        """
        return 'SENTBEFORE {}'.format(self.format_date(self.cleaned_date(key, value)))

    def convert_size_gt(self, key, value):
        """Messages with an [RFC-2822] size larger than the specified number of octets. (LARGER)"""
        return 'LARGER {}'.format(self.cleaned_uint(key, value))

    def convert_size_lt(self, key, value):
        """Messages with an [RFC-2822] size smaller than the specified number of octets. (SMALLER)"""
        return 'SMALLER {}'.format(self.cleaned_uint(key, value))

    def convert_new(self, key, value):
        """
        Messages that have the Recent flag set but not the Seen flag.
        This is functionally equivalent to "(RECENT UNSEEN)".
        """
        self.cleaned_true(key, value)
        return 'NEW'

    def convert_old(self, key, value):
        """
        Messages that do not have the Recent flag set.
        This is functionally equivalent to "NOT RECENT" (as opposed to "NOT NEW").
        """
        self.cleaned_true(key, value)
        return 'OLD'

    def convert_recent(self, key, value):
        """Messages that have the Recent flag set."""
        self.cleaned_true(key, value)
        return 'RECENT'

    def convert_all(self, key, value):
        """All messages in the mailbox; the default initial key for ANDing."""
        self.cleaned_true(key, value)
        return 'ALL'

    def convert_header(self, key, value):
        """
        Messages that have a header with the specified field-name (as defined in [RFC-2822])
        and that contains the specified string in the text of the header (what comes after the colon).
        If the string to search is zero-length, this matches all messages that have a header line
        with the specified field-name regardless of the contents.
        """
        return 'HEADER {0.name} {0.value}'.format(self.cleaned_header(key, value))

    def convert_uid(self, key, value):
        """Messages with unique identifiers corresponding to the specified unique identifier set."""
        return 'UID {}'.format(self.cleaned_uid(key, value))

    def convert_gmail_label(self, key, value):
        return 'X-GM-LABELS {}'.format(quote(self.cleaned_str(key, value)))