import os
import hashlib
import sys
from collections import defaultdict

from parallels.core import get_logger
from parallels.core.registry import Registry
from parallels.core.utils.common import mkdir_p, default
from parallels.core.utils.entity import Entity
from parallels.core.utils.json_utils import write_json, read_json
from parallels.plesk.source.mail import messages

logger = get_logger(__name__)


class StatisticsTransferMail(Entity):
    """Statistics for session of 'transfer-mail' command

    It includes information about session timestamp, source and target mailboxes etc.
    By session here we mean single execution of 'transfer-mail' entry point (not migration session directory).
    """
    DUMP_FILE_PREFIX = 'mail-migration-raw'

    def __init__(
            self,
            start_time,
            extension_version,
            source_email,
            target_email,
            statistics_version=2,
            end_time=None,
            start_copy_time=None,
            end_copy_time=None,
            restored_overall_messages_num=None,
            restored_overall_messages_length=None,
            imap_vendor=None,
            status=None,
            imap_encryption=None,
            ignore_security_warnings=False
    ):
        self._start_time = start_time
        self._end_time = end_time
        self._start_copy_time = start_copy_time
        self._end_copy_time = end_copy_time
        self._extension_version = extension_version
        self._source_email = source_email
        self._target_email = target_email
        self._statistics_version = statistics_version
        self._restored_overall_messages_num = restored_overall_messages_num
        self._restored_overall_messages_length = restored_overall_messages_length
        self._imap_vendor = imap_vendor
        self._status = status
        self._imap_encryption = imap_encryption
        self._ignore_security_warnings = ignore_security_warnings

    @property
    def start_time(self):
        """Timestamp when 'transfer-mail' command was started.

        :rtype: int
        """
        return self._start_time

    @property
    def source_email(self):
        """Mailbox where we should copy mail FROM.

        :rtype: str | unicode
        """
        return self._source_email

    @property
    def target_email(self):
        """Mailbox where we should copy mail TO.

        :rtype: str | unicode
        """
        return self._target_email

    @property
    def end_time(self):
        """Timestamp when 'transfer-mail' command was finished.

        :rtype: int
        """
        return self._end_time

    @end_time.setter
    def end_time(self, end_time):
        self._end_time = end_time

    @property
    def extension_version(self):
        """Version of Plesk Migrator on which this session was started.

        :rtype: str | unicode
        """
        return self._extension_version

    @property
    def statistics_version(self):
        """Format version of that structure.

        :rtype: int
        """
        return self._statistics_version

    @property
    def start_copy_time(self):
        """Timestamp when 'copy-mail-messages' action was started.

        :rtype: int
        """
        return self._start_copy_time

    @start_copy_time.setter
    def start_copy_time(self, start_copy_time):
        self._start_copy_time = start_copy_time

    @property
    def end_copy_time(self):
        """Timestamp when 'copy-mail-messages' action was finished

        :rtype: int
        """
        return self._end_copy_time

    @end_copy_time.setter
    def end_copy_time(self, end_copy_time):
        self._end_copy_time = end_copy_time

    @property
    def restored_overall_messages_num(self):
        """Number of transferred messaged when 'copy-mail-messages' action was finished

        :rtype: int
        """
        return self._restored_overall_messages_num

    @restored_overall_messages_num.setter
    def restored_overall_messages_num(self, restored_overall_messages_num):
        self._restored_overall_messages_num = restored_overall_messages_num

    @property
    def restored_overall_messages_length(self):
        """Overall length of transferred messaged when 'copy-mail-messages' action was finished

        :rtype: int
        """
        return self._restored_overall_messages_length

    @restored_overall_messages_length.setter
    def restored_overall_messages_length(self, restored_overall_messages_length):
        self._restored_overall_messages_length = restored_overall_messages_length

    @property
    def imap_vendor(self):
        """
        Source imap server vendor
        :rtype: str | unicode
        """
        return self._imap_vendor

    @imap_vendor.setter
    def imap_vendor(self, imap_vendor):
        """

        :type imap_vendor: str | unicode
        :rtype: None
        """
        self._imap_vendor = imap_vendor

    @property
    def status(self):
        """
        Status of migration result
        :rtype: str | unicode
        """
        return self._status

    @status.setter
    def status(self, status):
        """

        :type status: str | unicode
        :rtype: None
        """
        self._status = status

    @property
    def imap_encryption(self):
        """
        parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair#imap_encryption
        :rtype: str
        """
        return self._imap_encryption

    @imap_encryption.setter
    def imap_encryption(self, imap_encryption):
        """
        parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair#imap_encryption
        :type imap_encryption: str
        :rtype: None
        """
        self._imap_encryption = imap_encryption

    @property
    def ignore_security_warnings(self):
        """
        parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair#ignore_security_warnings
        :rtype: bool
        """
        return self._ignore_security_warnings

    @ignore_security_warnings.setter
    def ignore_security_warnings(self, ignore_security_warnings):
        """
        parallels.plesk.source.mail.migrated_mail_address_pair.MigratedMailAddressPair#ignore_security_warnings
        :type ignore_security_warnings: bool
        :rtype: None
        """
        self._ignore_security_warnings = ignore_security_warnings

    def write(self):
        """Write data raw statistics file

        :rtype: None
        """
        statistics_dir = os.path.join(Registry.get_instance().get_var_dir(), 'stats')
        if not os.path.exists(statistics_dir):
            mkdir_p(statistics_dir)
        filename_part = list()
        filename_part.append(self.DUMP_FILE_PREFIX)
        filename_part.append(str(self.start_time))
        filename_part.append(
            hashlib.md5(
                self.source_email.encode('idna') +
                "-" +
                self.target_email.encode('idna')
            ).hexdigest()
        )
        filename = '-'.join(filename_part) + '.json'
        filepath = os.path.join(statistics_dir, filename)
        statistics = self.as_dictionary()
        write_json(filepath, statistics, pretty_print=True)


class MailStatisticsSummarizer(object):
    """Collect, write and summarize mail migration statistics data

    Here we have statistics:
    'transfer-mail' sessions,
    By session here we mean single execution of command 'transfer-mail'
    """

    @staticmethod
    def summarize():
        summary_data = []

        # read sessions
        stats_dir = os.path.join(Registry.get_instance().get_var_dir(), 'stats')
        if not os.path.isdir(stats_dir):
            return summary_data
        sess_transfer_mail = MailStatisticsSummarizer._get_sessions(stats_dir, StatisticsTransferMail.DUMP_FILE_PREFIX)

        # group sessions by unique (source, target) mail pairs
        unique_sessions = MailStatisticsSummarizer._group_sessions(sess_transfer_mail)

        # summarize statistics
        for sessions in unique_sessions.values():
            summary_session = MailStatisticsSummarizer._summarize_transfer_mail_sessions(sessions)
            if len(summary_session) > 0:
                summary_data.append(
                    summary_session
                )

        return summary_data

    @staticmethod
    def _get_sessions(path, prefix):
        """Read sessions from files with defined prefix in name

        :type path: str | unicode
        :type prefix: str | unicode
        :rtype: list[dict]
        """
        sessions = []
        for item in os.listdir(path):
            file_path = os.path.join(path, item)
            if not os.path.isfile(file_path):
                continue
            if not item.startswith(prefix):
                continue
            try:
                data = default(read_json(file_path), {})
            except Exception as e:
                logger.exception()
                logger.fdebug(messages.FAILED_READ_SESSION_FILE, file=file_path, error=e)
                continue
            if isinstance(data, dict):
                sessions.append(data)
            else:
                logger.fdebug(
                    messages.UNEXPECTED_SESSION_CONTENT, file=file_path,
                    expected_type=type(dict()), got_type=type(data)
                )
        return sessions

    @staticmethod
    def _group_sessions(sessions):
        """Group sessions sessions by unique (source, target) mail pairs
        Build the structure like the following:
        {
            (source, target): = [sessions_transfer_mail]
        }
        :type sessions: list
        :rtype: dict
        """
        unique_sessions = {}
        for session in sessions:
            source_email = session.get('source_email')
            target_email = session.get('target_email')
            if source_email is None or target_email is None:
                continue
            key = (source_email, target_email)
            if key not in unique_sessions:
                unique_sessions[key] = []
            unique_sessions[key].append(session)
        return unique_sessions

    @staticmethod
    def _summarize_transfer_mail_sessions(sessions):
        """Summarize by 'transfer-mail' sessions

        :type sessions: list
        :rtype: dict
        """
        summary_data = {}
        total_time = 0
        total_copy_time = 0
        restored_overall_messages_num = 0
        restored_overall_messages_length = 0
        statuses = defaultdict(int)

        for session in sessions:
            end_time = MailStatisticsSummarizer._get_int(session, 'end_time')
            if end_time == 0:
                continue
            start_time = MailStatisticsSummarizer._get_int(session, 'start_time')
            if start_time >= summary_data.get('last_start_time', 0):
                summary_data.update(MailStatisticsSummarizer._get_common_params(session))
            if 0 < start_time < end_time:
                total_time = total_time + (end_time - start_time)
            start_copy_time = MailStatisticsSummarizer._get_int(session, 'start_copy_time')
            end_copy_time = MailStatisticsSummarizer._get_int(session, 'end_copy_time')
            if 0 < start_copy_time < end_copy_time:
                total_copy_time = total_copy_time + (end_copy_time - start_copy_time)
            restored_overall_messages_length += MailStatisticsSummarizer._get_int(
                session, 'restored_overall_messages_length'
            )
            restored_overall_messages_num += MailStatisticsSummarizer._get_int(session, 'restored_overall_messages_num')
            statuses[MailStatisticsSummarizer._get_str(session, 'status', 'Unknown')] += 1

        if not (
                total_time == 0
                and total_copy_time == 0
                and restored_overall_messages_length == 0
                and restored_overall_messages_num == 0
                and len(statuses) == 0
                and len(summary_data) == 0
        ):
            summary_data.update(total_time=total_time,
                            total_copy_time=total_copy_time,
                            restored_overall_messages_length=restored_overall_messages_length,
                            restored_overall_messages_num=restored_overall_messages_num,
                            statuses=statuses
                            )
        return summary_data

    @staticmethod
    def _get_int(session, field, default_value=0):
        """

        :type session: dict
        :type field: str | unicode
        :type default_value: int
        :rtype:
        """
        myfield = session.get(field)
        if type(myfield) is int:
            return myfield
        else:
            return default_value

    @staticmethod
    def _get_str(session, field, default_value=None):
        """

        :type session: dict
        :type field: str | unicode
        :type default_value: str | None
        :rtype: str | unicode | None
        """
        if sys.version_info.major >= 3:
            bytes_type = bytes
        else:
            bytes_type = basestring
        myfield = session.get(field)
        if isinstance(myfield, bytes_type):
            return myfield
        else:
            return default_value

    @staticmethod
    def _get_bool(session, field, default_value=False):
        """

        :type session: dict
        :type field: str | unicode
        :type default_value: bool
        :rtype: bool
        """
        myfield = session.get(field)
        if isinstance(myfield, bool):
            return myfield
        else:
            return default_value

    @staticmethod
    def _get_common_params(session):
        """Get common parameters from session

        :type session: dict
        :rtype: dict
        """
        if sys.version_info.major >= 3:
            bytes_type = bytes
        else:
            bytes_type = basestring

        common_params = {
            'start_time': 'last_start_time',
            'extension_version': 'extension_version',
            'imap_vendor': 'imap_vendor',
            'statistics_version': 'statistics_version',
            'imap_encryption': 'imap_encryption',
            'ignore_security_warnings': 'ignore_security_warnings'
        }
        result = {}

        for raw_name, sum_name in common_params.items():
            value = session.get(raw_name)
            if value:
                if isinstance(value, bytes_type) and len(value) == 0:
                    continue
                result[sum_name] = value

        return result
