import os
from contextlib import contextmanager

from parallels.core.utils.common import find_first
from parallels.core.utils.entity import Entity
from parallels.core.utils.json_utils import write_json, read_json
from parallels.core.utils.locks.file_lock import FileLock
from parallels.plesk.source.mail.utils import load_mail_migration_json_unencrypted_from_file


class SettingsStorage(object):
    """Permanent storage of transfer settings - e.g. source mailbox password, IMAP port, etc.

    Persistent settings are stored on filesystem and include various parameters, like IMAP port, IMAP host,
    IMAP directory separator, and so on. They are required to avoid asking customer for
    the settings each time, e.g. when he/she wants to re-sync mailbox. When customer starts initial import,
    he/she asked to provide the settings, and then the settings are saved to a file. When customer clicks
    on "re-sync" button, migrator reads the settings from a file.
    """

    def __init__(self, session_file, lock_file):
        """
        :type session_file: parallels.core.session_files.SessionFile
        :type lock_file: parallels.core.session_files.SessionFile
        """
        self._session_file = session_file
        self._lock = FileLock(os.path.dirname(lock_file.path), os.path.basename(lock_file.path))

    def set_settings(
        self,
        source_email, target_mailname,
        source_password, target_password, imap_host, imap_port, ignore_security_warnings,
        imap_encryption, imap_timeout, imap_folder_separator
    ):
        """Store persistent settings for specified mail address pair

        :type source_email: str | unicode
        :type target_mailname: str | unicode
        :type source_password: str | unicode
        :type target_password: str | unicode | None
        :type imap_port: int | None
        :type imap_host: str | unicode
        :type ignore_security_warnings: bool
        :type imap_encryption: str | unicode | None
        :type imap_timeout: int | None
        :type imap_folder_separator: str | unicode | None
        :rtype: None
        """
        with self._acquire_lock():
            data = self._load_encrypted()

            current_item = self._get_or_create_data_item(data, source_email, target_mailname)
            current_item['source_password'] = source_password
            current_item['target_password'] = target_password
            current_item['imap_host'] = imap_host
            current_item['imap_port'] = imap_port
            current_item['ignore_security_warnings'] = ignore_security_warnings
            current_item['imap_encryption'] = imap_encryption
            current_item['imap_timeout'] = imap_timeout
            current_item['imap_folder_separator'] = imap_folder_separator

            self._save(data)

    def set_imap_host(self, source_email, target_mailname, imap_host):
        """Store IMAP host in persistent storage for specified mail address pair

        :type source_email: str | unicode
        :type target_mailname: str | unicode
        :type imap_host: str | unicode
        """
        with self._acquire_lock():
            data = self._load_encrypted()

            current_item = self._get_or_create_data_item(data, source_email, target_mailname)
            current_item['imap_host'] = imap_host

            self._save(data)

    def get_settings(self, source_email, target_mailname):
        """Load persistent settings for specified mail address pair

        :type source_email: str | unicode
        :type target_mailname: str | unicode
        :rtype: parallels.plesk.source.mail.settings_storage.MailAddressPairSettings | None
        """
        with self._acquire_lock():
            settings_data = self._load_unencrypted()

        settings_item = find_first(
            settings_data,
            match_func=lambda item: (
                item.get('source_email') == source_email and
                item.get('target_mailname') == target_mailname
            ),
            default_value=None
        )

        if settings_item is not None:
            return MailAddressPairSettings(
                settings_item.get('source_password'),
                settings_item.get('target_password'),
                settings_item.get('imap_host'),
                settings_item.get('imap_port'),
                settings_item.get('ignore_security_warnings'),
                settings_item.get('imap_encryption'),
                settings_item.get('imap_timeout'),
                settings_item.get('imap_folder_separator'),
            )
        else:
            # When requested mail address pair does not exist in the storage, return object where each setting
            # is set to None
            return MailAddressPairSettings()

    @staticmethod
    def _get_or_create_data_item(data, source_email, target_mailname):
        """Get or create data item for specified mail address pair.

        Attention: this method modifies "data" variable - the method adds an item to the variable if
        item for corresponding mail address pair does not exist.

        :type data: list
        :type source_email: str | unicode
        :type target_mailname: str | unicode
        :return:
        """

        current_item = None
        for item in data:
            if item.get('source_email') == source_email and item.get('target_mailname') == target_mailname:
                current_item = item
                break
        if current_item is None:
            current_item = dict(
                source_email=source_email,
                target_mailname=target_mailname
            )
            data.append(current_item)
        return current_item

    def _save(self, data):
        """Save data to file

        :type data: list[dict]
        :rtype: None
        """
        write_json(self._session_file.path, data, pretty_print=True)

    def _load_encrypted(self):
        """Load data from file, do not perform decryption of passwords

        :rtype: list[dict]
        """
        data = read_json(self._session_file.path)
        if data is not None:
            return data
        else:
            return []

    def _load_unencrypted(self):
        """Load data from file, decrypt password to have them in plain text

        :rtype: list[dict]
        """
        return load_mail_migration_json_unencrypted_from_file(self._session_file.path)

    @contextmanager
    def _acquire_lock(self):
        try:
            self._lock.acquire_block()
            yield
        finally:
            self._lock.release()


class MailAddressPairSettings(Entity):
    """Permanently stored transfer settings for a source-target mail address pair"""

    def __init__(
        self, source_password=None, target_password=None, imap_host=None, imap_port=None,
        ignore_security_warnings=None, imap_encryption=None, imap_timeout=None, imap_folder_separator=None
    ):
        self._source_password = source_password
        self._target_password = target_password
        self._imap_host = imap_host
        self._imap_port = imap_port
        self._ignore_security_warnings = ignore_security_warnings
        self._imap_encryption = imap_encryption
        self._imap_timeout = imap_timeout
        self._imap_folder_separator = imap_folder_separator

    @property
    def source_password(self):
        """
        :rtype: str | unicode | None
        """
        return self._source_password

    @property
    def target_password(self):
        """
        :rtype: str | unicode | None
        """
        return self._target_password

    @property
    def imap_host(self):
        """
        :rtype: str | unicode | None
        """
        return self._imap_host

    @property
    def imap_port(self):
        """
        :rtype: int | None
        """
        return self._imap_port

    @property
    def ignore_security_warnings(self):
        """
        :rtype: bool | None
        """
        return self._ignore_security_warnings

    @property
    def imap_encryption(self):
        """
        :rtype: str | None
        """
        return self._imap_encryption

    @property
    def imap_timeout(self):
        """
        :rtype: int | None
        """
        return self._imap_timeout

    @property
    def imap_folder_separator(self):
        """
        :rtype: str | None
        """
        return self._imap_folder_separator
