import imaplib
import smtplib
import poplib
import socket

from parallels.core.logging import get_logger
from parallels.core.utils.common import safe_string_repr
from parallels.hosting_check import DomainIssue
from parallels.hosting_check import Severity, MailAuthIssueType
from parallels.hosting_check.messages import MSG

logger = get_logger(__name__)


class MailAuthChecker(object):
    def __init__(self, messages_delta_limit=5):
        """
        :type messages_delta_limit: int
        """
        self.messages_delta_limit = messages_delta_limit

    def check(self, domains_to_check):
        """
        :type domains_to_check: list[parallels.hosting_check.DomainMailService]
        :rtype: list[parallels.hosting_check.DomainIssue]
        """
        issues = []

        for domain_to_check in domains_to_check:
            issues += self._check_single_domain(domain_to_check)
        return issues

    def _check_single_domain(self, domain_to_check):
        """
        :type domain_to_check: parallels.hosting_check.DomainMailService
        :rtype: None
        """
        issues = []

        def add_issue(severity, category, problem):
            issues.append(
                DomainIssue(
                    domain_name=domain_to_check.domain_name,
                    severity=severity, category=category, problem=problem
                )
            )

        if domain_to_check.mail_server_ip is None:
            add_issue(
                severity=Severity.WARNING, 
                category=MailAuthIssueType.MAIL_IP_NOT_DEFINED,
                problem=MSG(MailAuthIssueType.MAIL_IP_NOT_DEFINED)
            )
            return issues 

        exist_errors = []
        imap_errors = []
        imap_internal_errors = []
        smtp_errors = []
        smtp_internal_errors = []
        pop3_errors = []
        pop3_internal_errors = []
        messages_count_pop3_errors = []
        messages_count_internal_errors = []
        messages_count_significant_difference = []
        messages_count_unsignificant_difference = []

        for user in domain_to_check.users:
            base_login = user.login.split("@")[0]
            if (
                domain_to_check.target_panel_mail_users is not None and
                base_login not in domain_to_check.target_panel_mail_users
            ):
                exist_errors.append(user.login)
                continue

            try:
                self._check_imap_login(
                    domain_to_check.mail_server_ip, user.login, user.password
                )
            except MailServerConnectionError as e:
                logger.exception()
                imap_errors.append((user.login, safe_string_repr(e),))
            except KeyboardInterrupt:
                # for compatibility with python 2.4
                raise
            except Exception as e:
                logger.exception()
                imap_internal_errors.append((user.login, safe_string_repr(e),))

            try:
                self._check_smtp_login(
                    domain_to_check.mail_server_ip, user.login, user.password
                )
            except MailServerConnectionError as e:
                logger.exception()
                smtp_errors.append((user.login, safe_string_repr(e),))
            except KeyboardInterrupt:
                # for compatibility with python 2.4
                raise
            except Exception as e:
                logger.exception()
                smtp_internal_errors.append((user.login, safe_string_repr(e),))

            try:
                self._check_pop3_login(
                    domain_to_check.mail_server_ip, user.login, user.password
                )
                if (
                    domain_to_check.source_users is not None and
                    domain_to_check.source_mail_server_ip is not None
                ):
                    self._check_count_mail_messages(
                        domain_to_check, user, 
                        messages_count_significant_difference,
                        messages_count_unsignificant_difference,
                        messages_count_pop3_errors,
                        messages_count_internal_errors,
                    )
            except MailServerConnectionError as e:
                logger.exception()
                pop3_errors.append((user.login, safe_string_repr(e),))
            except KeyboardInterrupt:
                # for compatibility with python 2.4
                raise
            except Exception as e:
                logger.exception()
                pop3_internal_errors.append((user.login, safe_string_repr(e),))

        if len(exist_errors) > 0:
            add_issue(
                severity=Severity.ERROR,
                category=MailAuthIssueType.MAILUSER_NOT_EXIST,
                problem=MSG(
                    MailAuthIssueType.MAILUSER_NOT_EXIST,
                    mailusers_list=u", ".join(
                        '%s' % mailbox_fullname 
                        for mailbox_fullname in exist_errors
                    )
                )
            )

        if len(imap_errors) > 0:
            add_issue(
                severity=Severity.ERROR,
                category=MailAuthIssueType.CHECK_FAILED_IMAP,
                problem=self._format_error_list(
                    MailAuthIssueType.CHECK_FAILED_IMAP, 
                    imap_errors
                )
            )

        if len(imap_internal_errors) > 0:
            add_issue(
                severity=Severity.WARNING,
                category=MailAuthIssueType.INTERNAL_ERROR_IMAP,
                problem=self._format_error_list(
                    MailAuthIssueType.INTERNAL_ERROR_IMAP,
                    imap_internal_errors
                )
            )

        if len(smtp_errors) > 0:
            add_issue(
                severity=Severity.ERROR,
                category=MailAuthIssueType.CHECK_FAILED_SMTP,
                problem=self._format_error_list(
                    MailAuthIssueType.CHECK_FAILED_SMTP, 
                    smtp_errors
                )
            )

        if len(smtp_internal_errors) > 0:
            add_issue(
                severity=Severity.WARNING,
                category=MailAuthIssueType.INTERNAL_ERROR_SMTP,
                problem=self._format_error_list(
                    MailAuthIssueType.INTERNAL_ERROR_SMTP,
                    smtp_internal_errors
                )
            )

        if len(pop3_errors) > 0:
            add_issue(
                severity=Severity.ERROR, 
                category=MailAuthIssueType.CHECK_FAILED_POP3,
                problem=self._format_error_list(
                    MailAuthIssueType.CHECK_FAILED_POP3,
                    pop3_errors
                )
            )

        if len(pop3_internal_errors) > 0:
            add_issue(
                severity=Severity.WARNING, 
                category=MailAuthIssueType.INTERNAL_ERROR_POP3,
                problem=self._format_error_list(
                    MailAuthIssueType.INTERNAL_ERROR_POP3,
                    pop3_internal_errors
                )
            )

        if len(messages_count_pop3_errors) > 0:
            add_issue(
                severity=Severity.ERROR,
                category=MailAuthIssueType.MESSAGES_COUNT_POP3_ERROR,
                problem=self._format_error_list(
                    MailAuthIssueType.MESSAGES_COUNT_POP3_ERROR,
                    messages_count_pop3_errors
                )
            )

        if len(messages_count_internal_errors) > 0:
            add_issue(
                severity=Severity.WARNING,
                category=MailAuthIssueType.MESSAGES_COUNT_INTERNAL_ERROR,
                problem=self._format_error_list(
                    MailAuthIssueType.MESSAGES_COUNT_INTERNAL_ERROR,
                    messages_count_internal_errors
                )
            )

        if len(messages_count_significant_difference) > 0:
            add_issue(
                severity=Severity.ERROR,
                category=MailAuthIssueType.MESSAGES_COUNT_SIGNIFICANT_DIFFERENCE,
                problem=self._format_error_list(
                    MailAuthIssueType.MESSAGES_COUNT_SIGNIFICANT_DIFFERENCE,
                    messages_count_significant_difference
                )
            )

        if len(messages_count_unsignificant_difference) > 0:
            add_issue(
                severity=Severity.WARNING,
                category=MailAuthIssueType.MESSAGES_COUNT_INSIGNIFICANT_DIFFERENCE,
                problem=self._format_error_list(
                    MailAuthIssueType.MESSAGES_COUNT_INSIGNIFICANT_DIFFERENCE,
                    messages_count_unsignificant_difference
                )
            )

        return issues

    @staticmethod
    def _format_error_list(error_message, error_list):
        return MSG(
            error_message,
            mailusers_list=u"\n".join([
                "- %s: %s" % (mailbox_fullname, error_msg)
                for mailbox_fullname, error_msg in error_list
            ])
        )

    def _check_imap_login(self, host, login, password):
        imap = imaplib.IMAP4(host)
        try:
            imap.login(self._encode_idn_mailname(login), password.encode('utf-8'))
            # execute some command after login
            imap.list()
        except (imaplib.IMAP4.error, socket.error) as e:
            logger.exception()
            raise MailServerConnectionError(
                MSG('failed_to_login_imap', host=host, login=login, reason=safe_string_repr(e))
            )
        finally:
            imap.logout()

    def _check_smtp_login(self, host, login, password):
        smtp = smtplib.SMTP(host)
        try:
            # just login and quit - that's enough
            smtp.login(self._encode_idn_mailname(login), password.encode('utf-8'))
        except (smtplib.SMTPException, socket.error) as e:
            logger.exception()
            raise MailServerConnectionError(
                MSG('failed_to_login_smtp', host=host, login=login, reason=safe_string_repr(e))
            )
        finally:
            smtp.quit()

    def _check_pop3_login(self, host, login, password):
        pop3 = poplib.POP3(host)
        try:
            # just login and quit - that's enough
            pop3.user(self._encode_idn_mailname(login))
            pop3.pass_(password.encode('utf-8'))
        except (poplib.error_proto, socket.error) as e:
            logger.exception()
            raise MailServerConnectionError(
                MSG('failed_to_login_pop3', host=host, login=login, reason=safe_string_repr(e))
            )
        finally:        
            pop3.quit()

    def _get_count_mail_messages(self, host, login, password):
        # POP3 is always enabled, IMAP isn't enabled always.
        # For example, it disabled for PfW 8.x
        pop3 = poplib.POP3(host)
        try:
            pop3.user(self._encode_idn_mailname(login))
            pop3.pass_(password)
            count = pop3.stat()[0]
            return int(count)
        except (poplib.error_proto, socket.error) as e:
            logger.debug("Exception: ", exc_info=True)
            raise MailServerConnectionError("POP3 connection to '%s' as '%s' failed: %s" % (host, login, e))
        finally:        
            pop3.quit()

    def _check_count_mail_messages(
        self, domain_to_check, user,
        messages_count_significant_difference, 
        messages_count_unsignificant_difference,
        messages_count_errors, messages_count_internal_errors
    ):
        try:
            source_count = self._get_count_mail_messages(
                domain_to_check.source_mail_server_ip,
                user.login,
                self._get_password_by_login(
                    domain_to_check.source_users, user.login
                )
            )
            target_count = self._get_count_mail_messages(
                domain_to_check.mail_server_ip,
                user.login,
                user.password
            )
            if source_count > target_count + self.messages_delta_limit:
                e = MSG(
                    'mail_auth_different_messages_count',
                    source_count=source_count, target_count=target_count
                )
                messages_count_significant_difference.append((user.login, e,))
            elif source_count > target_count:
                e = MSG(
                    'mail_auth_different_messages_count',
                    source_count=source_count, target_count=target_count
                )
                messages_count_unsignificant_difference.append((user.login, e,))
        except MailServerConnectionError as e:
            logger.exception()
            messages_count_errors.append((user.login, safe_string_repr(e),))
        except KeyboardInterrupt:
            # for compatibility with python 2.4
            raise
        except Exception as e:
            logger.exception()
            messages_count_internal_errors.append((user.login, safe_string_repr(e),))

    @staticmethod
    def _get_password_by_login(source_users, login):
        for source_user in source_users:
            if source_user.login == login:
                return source_user.password
        raise Exception(MSG(
            'mail_auth_cant_find_mailuser_on_source', 
            login=login
        ))

    @staticmethod
    def _encode_idn_mailname(mailname):
        parts = mailname.split('@', 1)
        if len(parts) == 1:
            return parts
        else:
            return u'%s@%s' % (parts[0], parts[1].encode('idna'))


class MailServerConnectionError(Exception):
    pass
