import os
import sys

from collections import defaultdict

from parallels.core import get_logger
from parallels.core.application.drupal import DrupalModel
from parallels.core.application.joomla import JoomlaModel
from parallels.core.application.magento import MagentoModel
from parallels.core.application.prestashop import PrestashopModel
from parallels.core.application.wordpress import WordpressModel
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.core.utils.migrator_utils import safe_idn_decode
from parallels.plesk.source.web import messages

logger = get_logger(__name__)


class StatisticsGetSiteInfo(Entity):
    """Statistics for session of 'get-site-info' command

    It includes information about source FTP server, PHP version, detected apps, detection time etc.
    By session here we mean single execution of 'get-site-info' entry point (not migration session directory).
    """
    DUMP_FILE_PREFIX = 'site-migration-raw-get-site-info'

    def __init__(
            self, start_time, domain_id, migrator_version,
            statistics_version=1,
            end_time=None, detection_time=None, finished=False,
            ftp_version=None, php_version=None,
            apps=None,
            target_domain_name=None, target_domain_type=None, source_domain_name=None,
            advanced_screen_base_url=None, advanced_screen_document_root=None,
            source_is_windows=None
    ):
        self._start_time = start_time
        self._domain_id = domain_id
        self._end_time = end_time
        self._detection_time = detection_time
        self._migrator_version = migrator_version
        self._statistics_version = statistics_version
        self._finished = finished
        self._ftp_version = ftp_version
        self._php_version = php_version
        self._apps = apps
        self._target_domain_name = target_domain_name
        self._target_domain_type = target_domain_type
        self._source_domain_name = source_domain_name
        self._advanced_screen_base_url = advanced_screen_base_url
        self._advanced_screen_document_root = advanced_screen_document_root
        self._source_is_windows = source_is_windows

    @property
    def start_time(self):
        """Timestamp when 'get-site-info' command was started.

        :rtype: int
        """
        return self._start_time

    @property
    def end_time(self):
        """Timestamp when 'get-site-info' command was finished.

        :rtype: int
        """
        return self._end_time

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

    @property
    def detection_time(self):
        """Migration time in seconds.

        By migration time we mean plain execution time of 'get-site-info' command.

        The following is always true: detection_time = end_time - start_time.

        :rtype: int
        """
        return self._detection_time

    @property
    def domain_id(self):
        """Target domain id

        :rtype: int
        """
        return self._domain_id

    @property
    def finished(self):
        """Whether migration was finished or not.

        :rtype: bool
        """
        return self._finished

    @finished.setter
    def finished(self, finished):
        self._finished = finished

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

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

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

        :rtype: int
        """
        return self._statistics_version

    @property
    def ftp_version(self):
        """FTP server version of source website

        :rtype: str | unicode | None
        """
        return self._ftp_version

    @ftp_version.setter
    def ftp_version(self, ftp_version):
        self._ftp_version = ftp_version

    @property
    def php_version(self):
        """PHP version of source website

        :rtype: str | unicode | None
        """
        return self._php_version

    @php_version.setter
    def php_version(self, php_version):
        self._php_version = php_version

    @property
    def apps(self):
        """'applications' from site_info.json file

        :rtype: dict[str, list] | None
        """
        return self._apps

    @apps.setter
    def apps(self, apps):
        self._apps = apps

    @property
    def target_domain_name(self):
        """Target domain name

        :rtype: str | unicode | None
        """
        return self._target_domain_name

    @target_domain_name.setter
    def target_domain_name(self, target_domain_name):
        self._target_domain_name = target_domain_name

    @property
    def target_domain_type(self):
        """Target domain type: subscription, addon domain, subdomain

        :rtype: str | unicode | None
        """
        return self._target_domain_type

    @target_domain_type.setter
    def target_domain_type(self, target_domain_type):
        self._target_domain_type = target_domain_type

    @property
    def source_domain_name(self):
        """Source domain name

        :rtype: str | unicode | None
        """
        return self._source_domain_name

    @source_domain_name.setter
    def source_domain_name(self, source_domain_name):
        self._source_domain_name = source_domain_name

    @property
    def advanced_screen_base_url(self):
        """
        "Source website URL" can be defined on advanced "Import site" screen

        :rtype: str | unicode | None
        """
        return self._advanced_screen_base_url

    @advanced_screen_base_url.setter
    def advanced_screen_base_url(self, advanced_screen_base_url):
        self._advanced_screen_base_url = advanced_screen_base_url

    @property
    def advanced_screen_document_root(self):
        """
        "Import site" can be defined on advanced "Import site" screen

        :rtype: str | unicode | None
        """
        return self._advanced_screen_document_root

    @advanced_screen_document_root.setter
    def advanced_screen_document_root(self, advanced_screen_document_root):
        self._advanced_screen_document_root = advanced_screen_document_root

    @property
    def source_is_windows(self):
        """Is OS type is Windows on source web site

        :rtype: bool | None
        """
        return self._source_is_windows

    @source_is_windows.setter
    def source_is_windows(self, source_is_windows):
        self._source_is_windows = source_is_windows

    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 = '-'.join([self.DUMP_FILE_PREFIX, str(self.domain_id), str(self.start_time)]) + '.json'
        filepath = os.path.join(statistics_dir, filename)
        statistics = self.as_dictionary()
        write_json(filepath, statistics, pretty_print=True)


class StatisticsTransferSite(Entity):
    """Statistics for session of 'transfer-site' command

    It includes information about source application, migration time etc.
    By session here we mean single execution of 'transfer-site' entry point (not migration session directory).
    """
    DUMP_FILE_PREFIX = 'site-migration-raw-transfer-site'

    def __init__(
            self, start_time, domain_id, migrator_version,
            statistics_version=1,
            end_time=None, migration_time=None, finished=False,
            ftp_version=None, php_version=None,
            target_domain_name=None, target_domain_type=None, source_domain_name=None,
            migration_list=None,
            source_is_windows=None,
            folder_size=None, db_size=None
    ):
        self._start_time = start_time
        self._domain_id = domain_id
        self._end_time = end_time
        self._migration_time = migration_time
        self._migrator_version = migrator_version
        self._statistics_version = statistics_version
        self._finished = finished
        self._ftp_version = ftp_version
        self._php_version = php_version
        self._target_domain_name = target_domain_name
        self._target_domain_type = target_domain_type
        self._source_domain_name = source_domain_name
        self._migration_list = migration_list
        self._source_is_windows = source_is_windows
        self._folder_size = folder_size if folder_size is not None else 0
        self._db_size = db_size if db_size is not None else 0

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

        :rtype: int
        """
        return self._start_time

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

        :rtype: int
        """
        return self._end_time

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

    @property
    def migration_time(self):
        """Migration time in seconds.

        By migration time we mean plain execution time of 'transfer-site' command.

        The following is always true: migration_time = end_time - start_time.

        :rtype: int
        """
        return self._migration_time

    @property
    def domain_id(self):
        """Target domain id

        :rtype: int
        """
        return self._domain_id

    @property
    def finished(self):
        """Whether migration was finished or not.

        :rtype: bool
        """
        return self._finished

    @finished.setter
    def finished(self, finished):
        self._finished = finished

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

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

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

        :rtype: int
        """
        return self._statistics_version

    @property
    def ftp_version(self):
        """FTP server version of source website

        :rtype: str | unicode | None
        """
        return self._ftp_version

    @ftp_version.setter
    def ftp_version(self, ftp_version):
        self._ftp_version = ftp_version

    @property
    def php_version(self):
        """PHP version of source website

        :rtype: str | unicode | None
        """
        return self._php_version

    @php_version.setter
    def php_version(self, php_version):
        self._php_version = php_version

    @property
    def target_domain_name(self):
        """Target domain name

        :rtype: str | unicode | None
        """
        return self._target_domain_name

    @target_domain_name.setter
    def target_domain_name(self, target_domain_name):
        self._target_domain_name = target_domain_name

    @property
    def target_domain_type(self):
        """Target domain type: subscription, addon domain, subdomain

        :rtype: str | unicode | None
        """
        return self._target_domain_type

    @target_domain_type.setter
    def target_domain_type(self, target_domain_type):
        self._target_domain_type = target_domain_type

    @property
    def source_domain_name(self):
        """Source domain name

        :rtype: str | unicode | None
        """
        return self._source_domain_name

    @source_domain_name.setter
    def source_domain_name(self, source_domain_name):
        self._source_domain_name = source_domain_name

    @property
    def migration_list(self):
        """Migration list which contains apps, folders and databases

        :rtype: str | unicode | None
        """
        return self._migration_list

    @migration_list.setter
    def migration_list(self, migration_list):
        self._migration_list = migration_list

    @property
    def source_is_windows(self):
        """Is OS type is Windows on source web site

        :rtype: bool | None
        """
        return self._source_is_windows

    @source_is_windows.setter
    def source_is_windows(self, source_is_windows):
        self._source_is_windows = source_is_windows

    @property
    def folder_size(self):
        """Size of migrated folders in bytes

        :rtype: int
        """

        return self._folder_size

    @folder_size.setter
    def folder_size(self, folder_size):
        self._folder_size = folder_size

    @property
    def db_size(self):
        """Size of migrated databases in bytes

        :rtype: int
        """

        return self._db_size

    @db_size.setter
    def db_size(self, db_size):
        self._db_size = db_size

    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 = '-'.join([self.DUMP_FILE_PREFIX, str(self.domain_id), str(self.start_time)]) + '.json'
        filepath = os.path.join(statistics_dir, filename)
        statistics = self.as_dictionary()
        write_json(filepath, statistics, pretty_print=True)


class StatisticsSummarizer(object):
    """Collect, write and summarize site migration statistics data

    Here we have 2 kinds of statistics:
    1) 'get-site-info' sessions,
    2) 'transfer-site' sessions,
    By session here we mean single execution of command 'get-site-info' or 'transfer-site'
    """

    @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_get_site_info = StatisticsSummarizer.get_sessions(stats_dir, StatisticsGetSiteInfo.DUMP_FILE_PREFIX)
        sess_transfer_site = StatisticsSummarizer.get_sessions(stats_dir, StatisticsTransferSite.DUMP_FILE_PREFIX)

        # group sessions by unique (source, target) domain pairs
        uniq_sessions = {}
        StatisticsSummarizer._group_sessions(uniq_sessions, sess_get_site_info, 'sessions_get_site_info')
        StatisticsSummarizer._group_sessions(uniq_sessions, sess_transfer_site, 'sessions_transfer_site')

        # summarize statistics
        for i, sessions in enumerate(uniq_sessions.values()):
            summary_data.append(
                StatisticsSummarizer._summarize_by_sessions(
                    sessions.get('sessions_get_site_info', []),
                    sessions.get('sessions_transfer_site', [])
                )
            )

        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
        """
        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(unique_sessions, sessions, sessions_type):
        """Group sessions sessions by unique (source, target) domain pairs
        Build the structure like the following:
        {
            (source, target): {
                'sessions_get_site_info'=[sessions_get_site_info],
                'sessions_transfer_site'=[sessions_transfer_site]
            }
        }
        :type unique_sessions: dict
        :type sessions: list
        :type sessions_type: str | unicode
        :rtype: dict
        """
        for session in sessions:
            source_domain_name = session.get('source_domain_name')
            target_domain_name = session.get('target_domain_name')
            if source_domain_name is None or target_domain_name is None:
                continue
            key = (source_domain_name, target_domain_name)
            if key not in unique_sessions:
                unique_sessions[key] = {}
            if sessions_type not in unique_sessions[key]:
                unique_sessions[key][sessions_type] = []
            unique_sessions[key][sessions_type].append(session)

    @staticmethod
    def _summarize_by_sessions(sessions_get_site_info, sessions_transfer_site):
        apps = [
            WordpressModel(), JoomlaModel(), DrupalModel(), MagentoModel(), PrestashopModel()
        ]

        summary_data = {}
        summary_data.update(StatisticsSummarizer._summarize_get_site_info_sessions(sessions_get_site_info, apps))
        summary_data.update(StatisticsSummarizer._summarize_transfer_site_sessions(sessions_transfer_site, apps))
        summary_data['detect_attempts'] = len(sessions_get_site_info)

        return summary_data

    @staticmethod
    def _summarize_get_site_info_sessions(sessions, applications):
        """Summarize by 'get-site-info' sessions

        :type sessions: list
        :type applications: list[parallels.core.application.base.ApplicationModel]
        :rtype: dict
        """
        summary_data = {}

        for session in sessions:
            start_time = StatisticsSummarizer._get_int(session, 'start_time')
            if start_time >= summary_data.get('last_start_time', 0):
                summary_data.update(StatisticsSummarizer._get_common_params(session))

        for application in applications:
            unique_paths = set()
            for session in sessions:
                session_apps = default(session.get('apps'), {})
                unique_paths |= {path for path in session_apps.get(application.name, [])}
            if len(unique_paths) > 0:
                summary_data[application.id + '_found'] = len(unique_paths)

        detection_times = []
        for session in sessions:
            if not session.get('finished'):
                continue
            detection_times.append(StatisticsSummarizer._get_int(session, 'detection_time'))
        if len(detection_times) > 0:
            summary_data['average_detect_time'] = avg_int(detection_times)
            summary_data['max_detect_time'] = max(detection_times)

        return summary_data

    @staticmethod
    def _summarize_transfer_site_sessions(sessions, applications):
        """Summarize by 'transfer-site' sessions

        :type sessions: list
        :type applications: list[parallels.core.application.base.ApplicationModel]
        :rtype: dict
        """
        summary_data = {}
        counter_db_sizes = []
        counter_db_import_times = []
        counter_folder_sizes = []
        counter_folder_import_times = []
        coutner_app_import_times = defaultdict(list)
        coutner_app_folder_sizes = defaultdict(list)
        coutner_app_db_sizes = defaultdict(list)
        unique_databases = set()
        unique_folders = set()
        unique_apps = defaultdict(set)

        for session in sessions:
            # Common
            start_time = StatisticsSummarizer._get_int(session, 'start_time')
            if start_time >= summary_data.get('last_start_time', 0):
                summary_data.update(StatisticsSummarizer._get_common_params(session))
            # Skip unfinished sessions
            if not session.get('finished'):
                continue
            migration_list = session.get('migration_list')
            if migration_list is None:
                continue
            ml_apps = default(migration_list.get('filter_applications'), [])
            ml_databases = default(migration_list.get('include_databases'), [])
            ml_folders = default(migration_list.get('include_files'), [])
            # There will be only one not None element in migration list when migrating from GUI.
            # Also, the length of the element will be equal to 1.
            # Assume any other cases as custom migration and skip statistics for it.
            # Applications
            if len(ml_apps) == 1 and len(ml_databases) == 0 and len(ml_folders) == 0:
                migration_list_application = ml_apps[0]
                for app in applications:
                    if migration_list_application.get('application_type') != app.name:
                        continue
                    app_path = migration_list_application.get('source_path')
                    if app_path in unique_apps[app.name]:
                        continue
                    unique_apps[app.name].add(app_path)
                    coutner_app_import_times[app.id].append(StatisticsSummarizer._get_int(session, 'migration_time'))
                    coutner_app_folder_sizes[app.id].append(StatisticsSummarizer._get_int(session, 'folder_size'))
                    coutner_app_db_sizes[app.id].append(StatisticsSummarizer._get_int(session, 'db_size'))
            # Databases
            if len(ml_apps) == 0 and len(ml_databases) == 1 and len(ml_folders) == 0:
                migration_list_database = ml_databases[0]
                db_host = migration_list_database.get('host')
                db_port = migration_list_database.get('port')
                db_name = migration_list_database.get('name')
                if (db_host, db_port, db_name) not in unique_databases:
                    unique_databases.add((db_host, db_port, db_name))
                    counter_db_sizes.append(StatisticsSummarizer._get_int(session, 'db_size'))
                    counter_db_import_times.append(StatisticsSummarizer._get_int(session, 'migration_time'))
            # Folders
            if len(ml_apps) == 0 and len(ml_databases) == 0 and len(ml_folders) == 1:
                migration_list_folder = ml_folders[0]
                source_path = migration_list_folder.get('source_path')
                if source_path not in unique_folders:
                    unique_folders.add(source_path)
                    counter_folder_sizes.append(StatisticsSummarizer._get_int(session, 'folder_size'))
                    counter_folder_import_times.append(StatisticsSummarizer._get_int(session, 'migration_time'))
        # Calc sum, max and average values
        if len(unique_databases) > 0:
            summary_data['db_imported'] = len(unique_databases)
        if len(unique_folders) > 0:
            summary_data['folders_imported'] = len(unique_folders)
        for app in applications:
            if app.name in unique_apps:
                summary_data[app.id + '_imported'] = len(unique_apps[app.name])
            if len(default(coutner_app_import_times.get(app.id), [])) > 0:
                summary_data[app.id + '_max_import_time'] = max(coutner_app_import_times[app.id])
                summary_data[app.id + '_average_import_time'] = avg_int(coutner_app_import_times[app.id])
            if len(default(coutner_app_folder_sizes.get(app.id), [])) > 0:
                summary_data[app.id + '_max_folder_size'] = max(coutner_app_folder_sizes[app.id])
                summary_data[app.id + '_average_folder_size'] = avg_int(coutner_app_folder_sizes[app.id])
            if len(default(coutner_app_db_sizes.get(app.id), [])) > 0:
                summary_data[app.id + '_max_db_size'] = max(coutner_app_db_sizes[app.id])
                summary_data[app.id + '_average_db_size'] = avg_int(coutner_app_db_sizes[app.id])
        if len(counter_db_sizes) > 0:
            summary_data['max_db_size'] = max(counter_db_sizes)
            summary_data['average_db_size'] = avg_int(counter_db_sizes)
        if len(counter_folder_sizes) > 0:
            summary_data['max_folder_size'] = max(counter_folder_sizes)
            summary_data['average_folder_size'] = avg_int(counter_folder_sizes)
        if len(counter_db_import_times) > 0:
            summary_data['max_db_import_time'] = max(counter_db_import_times)
            summary_data['average_db_import_time'] = avg_int(counter_db_import_times)
        if len(counter_folder_import_times) > 0:
            summary_data['max_folder_import_time'] = max(counter_folder_import_times)
            summary_data['average_folder_import_time'] = avg_int(counter_folder_import_times)

        return summary_data

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

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

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

        :type session: dict
        :rtype: dict
        """

        common_params = {
            'start_time': 'last_start_time',
            'php_version': 'php_version',
            'source_is_windows': 'source_is_windows',
            'migrator_version': 'migrator_version',
            'statistics_version': 'statistics_version'
        }
        ftp_servers = {
            'ProFTPD': 'ProFTPD',
            'Microsoft FTP': 'Microsoft FTP',
            'Pure-FTPd': 'Pure-FTPd'
        }

        result = {}

        for raw_name, sum_name in common_params.items():
            result[sum_name] = session.get(raw_name)

        source_domain = session.get('source_domain_name')
        target_domain = session.get('target_domain_name')
        if source_domain is not None:
            try:
                result.update(idn_src=(
                    source_domain != source_domain.encode('idna') or source_domain != safe_idn_decode(source_domain)
                ))
            except UnicodeError as e:
                logger.fdebug(messages.FAILED_ENCODE_DOMAIN_IDNA, domain=source_domain, error=e)
        if target_domain is not None:
            try:
                result.update(idn_trg=(
                    target_domain != target_domain.encode('idna')) or target_domain != safe_idn_decode(target_domain)
                )
            except UnicodeError as e:
                logger.fdebug(messages.FAILED_ENCODE_DOMAIN_IDNA, domain=target_domain, error=e)
        if source_domain is not None and target_domain is not None:
            result.update(domain_changed=source_domain != target_domain)

        if sys.version_info.major >= 3:
            bytes_type = bytes
        else:
            bytes_type = basestring

        ftp_server_string = session.get('ftp_version')
        if ftp_server_string is None:
            result['ftp_version'] = ftp_server_string
        elif isinstance(ftp_server_string, bytes_type) and len(ftp_server_string) > 0:
            result['ftp_version'] = 'other'
            for search_str, report_str in ftp_servers.items():
                if search_str.lower() in ftp_server_string.lower():
                    result['ftp_version'] = report_str
        return result


def avg_int(integers):
    """Returns integer rounded average value from list

    :type integers: list[int]
    :rtype: int
    """
    return int(round(float(sum(integers)) / max(len(integers), 1)))
