import os
from contextlib import contextmanager

from parallels.core.reports.model.issue import Issue
from parallels.core.utils.json_utils import write_json, read_json
from parallels.core.utils.locks.file_lock import FileLock


class StatusStorage(object):
    STATUS_QUEUED = 'queued'
    STATUS_CANCELLED = 'cancelled'
    STATUS_IN_PROGRESS = 'in-progress'
    STATUS_SUCCESS = 'success'
    STATUS_FAILURE = 'failure'
    STATUS_WARNING = 'warning'

    STEP_PREPARE_TARGET = 'prepare-target'
    STEP_CREATE_TARGET_MAILBOX = 'create-target-mailbox'
    STEP_PREPARE_CONNECT_SOURCE = 'prepare-connect-source'
    STEP_DETECT_SOURCE_IMAP_HOST = 'detect-source-imap-host'
    STEP_LOGIN_SOURCE = 'login-source'
    STEP_DETECT_FOLDER_SEPARATOR_SOURCE = 'detect-folder-separator-source'
    STEP_CONNECT_SOURCE = 'connect-source'
    STEP_TRANSFER_MESSAGES = 'transfer-messages'
    STEP_FINALIZE = 'finalize'

    REPORTING_MODE_SYNC = 'sync'
    REPORTING_MODE_ASYNC = 'async'
    # for use in parallels.plesk.source.mail.actions.statistics.finalize_transfer_mail.FinalizeTransferMailStatistics
    ERROR_CONNECT = 'connect'
    ERROR_SECURITY = 'security'
    ERROR_SECURITY_INVALID_CERTIFICATE = 'security-invalid-certificate'
    ERROR_AUTHENTICATION = 'authentication'
    ERROR_IMAP_HOST_AUTODETECT = 'imap-host-autodetect'

    def __init__(self, global_session_file, global_status_lock_file, task_session_file=None):
        """
        :type global_session_file: str | unicode
        :type global_status_lock_file: str | unicode
        :type task_session_file: str | unicode | None
        """
        self._global_session_file = global_session_file
        self._global_status_lock = FileLock(
            os.path.dirname(global_status_lock_file), os.path.basename(global_status_lock_file)
        )
        self._task_session_file = task_session_file
        self._task_data = {}
        self._reporting_mode = self.REPORTING_MODE_SYNC

        self._has_errors = False
        self._has_warnings = False

    def has_errors(self):
        return self._has_errors

    def has_warnings(self):
        return self._has_warnings

    def set_reporting_mode(self, reporting_mode):
        """
        :type reporting_mode: str | unicode
        :rtype: None
        """
        self._reporting_mode = reporting_mode

    def get_reporting_mode(self):
        """
        :rtype: str
        """
        return self._reporting_mode

    def set_task_status(self, status):
        """
        :type status: str | unicode
        :rtype: None
        """
        self._task_data['status'] = status
        self._save_task_status()

    def set_task_step(self, step):
        """
        :type step: str | unicode | None
        :rtype: None
        """
        self._task_data['step'] = step
        self._save_task_status()

    def set_sync_task_step(self, step):
        """Set currently executed step, only when we report progress in sync mode

        :type step: str | unicode | None
        :rtype: None
        """
        if self.get_reporting_mode() == StatusStorage.REPORTING_MODE_SYNC:
            self.set_task_step(step)

    def get_task_step(self):
        """
        :rtype step: str | unicode | None
        """
        return self._task_data.get('step')

    def set_task_error(self, error_id, error_message, error_details=None):
        """
        :type error_id: str | unicode | None
        :type error_message: str | unicode | None
        :type error_details: str | unicode | None
        :rtype: None
        """
        self._task_data['error_id'] = error_id
        self._task_data['error_message'] = error_message
        self._task_data['error_details'] = error_details
        self._save_task_status()

    def set_global_status(self, source_email, target_mailname, status):
        with self._acquire_global_status_lock():
            data = self._load_global_status()
            mail_address_pair = self._init_mail_pair(data, source_email, target_mailname)
            mail_address_pair['status'] = status
            self._save_global_status(data)

    def set_global_restored_message_num(self, source_email, target_mailname, restored_message_num):
        """
        :type source_email: str | unicode
        :type target_mailname: str | unicode
        :type restored_message_num: int | None
        :rtype: None
        """
        data = self._load_global_status()
        mail_address_pair = self._init_mail_pair(data, source_email, target_mailname)
        mail_address_pair['restored_message_num'] = restored_message_num
        self._save_global_status(data)

    def add_global_issue(self, source_email, target_mailname, issue):
        """
        :type source_email: str | unicode
        :type target_mailname: str | unicode
        :type issue: parallels.core.reports.model.issue.Issue
        :rtype: None
        """
        if issue.severity == Issue.SEVERITY_ERROR:
            self._has_errors = True
        elif issue.severity == Issue.SEVERITY_WARNING:
            self._has_warnings = True

        with self._acquire_global_status_lock():
            data = self._load_global_status()
            mail_address_pair = self._init_mail_pair(data, source_email, target_mailname)
            if mail_address_pair.get('issues') is None:
                mail_address_pair['issues'] = []
            mail_address_pair['issues'].append(self._issue_as_dict(issue))
            self._save_global_status(data)

    def add_issue(self, source_email, target_mailname, issue):
        """
        :type source_email: str | unicode
        :type target_mailname: str | unicode
        :type issue: parallels.core.reports.model.issue.Issue
        :rtype: None
        """
        if self._reporting_mode == self.REPORTING_MODE_ASYNC:
            with self._acquire_global_status_lock():
                data = self._load_global_status()
                mail_address_pair = self._init_mail_pair(data, source_email, target_mailname)
                if mail_address_pair.get('issues') is None:
                    mail_address_pair['issues'] = []
                mail_address_pair['issues'].append(self._issue_as_dict(issue))
                self._save_global_status(data)

    @staticmethod
    def _init_mail_pair(data, source_email, target_mailname):
        if 'mail_address_pairs' not in data:
            data['mail_address_pairs'] = []

        current_mail_address_pair = None

        for mail_address_pair in data['mail_address_pairs']:
            if (
                mail_address_pair['source_email'] == source_email and
                mail_address_pair['target_mailname'] == target_mailname
            ):
                current_mail_address_pair = mail_address_pair
                break

        if current_mail_address_pair is None:
            current_mail_address_pair = dict(
                source_email=source_email,
                target_mailname=target_mailname
            )
            data['mail_address_pairs'].append(current_mail_address_pair)

        return current_mail_address_pair

    @staticmethod
    def _issue_as_dict(issue):
        """Convert issue object to dictionary, saving limited list of fields - which are actually needed for GUI

        :type issue: parallels.core.reports.model.issue.Issue
        :rtype: dict
        """
        return dict(
            execution_date=issue.execution_date,
            severity=issue.severity,
            problem_text=issue.problem_text,
            details_text=issue.details_text
        )

    def _save_global_status(self, data):
        write_json(self._global_session_file, data, pretty_print=True)

    def _save_task_status(self):
        if self._task_session_file is not None:
            write_json(self._task_session_file, self._task_data, pretty_print=True)

    def _load_global_status(self):
        data = read_json(self._global_session_file)
        if data is not None:
            return data
        else:
            return {}

    @contextmanager
    def _acquire_global_status_lock(self):
        try:
            self._global_status_lock.acquire_block()
            yield
        finally:
            self._global_status_lock.release()
