import json
import os
import re
from collections import defaultdict

from parallels.core import messages
from parallels.core.logging import get_logger
from parallels.core.registry import Registry
from parallels.core.utils.common import default, mkdir_p
from parallels.core.utils.entity import Entity
from parallels.core.utils.json_utils import read_json, write_json
from parallels.core.utils.migrator_utils import get_version
from parallels.plesk.source.web.statistics import StatisticsSummarizer
from parallels.plesk.source.mail.statistics import MailStatisticsSummarizer

logger = get_logger(__name__)


class SubscriptionStatus(object):
    SUCCESS = 'success'
    WARNING = 'warning'
    FAILED = 'failed'


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

    Here we have 2 kinds of statistics:
    1) Per-session statistics. By session here we mean single execution of 'transfer-accounts'
    entry point (not migration session directory).
    2) Per-subscription statistics. For each migrated subscription we update migration status once
    'transfer-accounts' was finished.
    """
    def __init__(self):
        self._statistics_dir = os.path.join(Registry.get_instance().get_var_dir(), 'stats')
        self._raw_statistics_file = os.path.join(self._statistics_dir, 'raw.json')
        self._summary_statistics_file = os.path.join(self._statistics_dir, 'summary.json')
        self._data = {}
        try:
            self._data = default(read_json(self._raw_statistics_file), {})
        except Exception:
            logger.exception()
            logger.warn(messages.FAILED_TO_READ_STATISTICS_FILE.format(filename=self._raw_statistics_file))

    def save_session_statistics(self, statistics):
        """Save session statistics: information about source and target servers, number of migrated subscriptions, etc

        By session here we mean single execution of 'transfer-accounts' entry point (not migration session directory).

        Start time is a session key - if it already exists in statistics file, it will be updated.

        :type statistics: parallels.core.statistics.SessionStatistics
        :rtype: None
        """
        if 'sessions' not in self._data:
            self._data['sessions'] = {}
        self._data['sessions'][statistics.start_time] = statistics.as_dictionary()
        self.write()

    def set_subscription_status(self, subscription_name, status):
        """Set status of migration for specified subscription in statistics raw file

        :type subscription_name: str | unicode
        :type status: str | unicode
        :rtype: None
        """
        if 'subscriptions' not in self._data:
            self._data['subscriptions'] = {}
        self._data['subscriptions'][subscription_name] = status

    def write(self):
        """Write data from this object to a raw statistics file

        :rtype: None
        """
        if not os.path.exists(self._statistics_dir):
            mkdir_p(self._statistics_dir)
        write_json(self._raw_statistics_file, self._data, pretty_print=True)

    def summarize(self):
        """Summarize raw statistics file, so it does not contain subscription names, but just their counters

        Prints summary to stdout, writes additionally writes to a file.

        :rtype: None
        """
        subscriptions_by_status = defaultdict(int)
        for subscription, status in self._data.get('subscriptions', {}).items():
            subscriptions_by_status[status] += 1

        summarized = {
            'sessions': self._data.get('sessions', {}),
            'subscriptions': dict(subscriptions_by_status),
            'migrator_version': get_version(),
            'site-migrations': StatisticsSummarizer.summarize(),
            'mail-migrations': MailStatisticsSummarizer.summarize()
        }

        summarized_sanitized = PleskStatisticsSanitizer().sanitize(summarized)

        if not os.path.exists(self._statistics_dir):
            mkdir_p(self._statistics_dir)
        write_json(
            self._summary_statistics_file, summarized_sanitized, pretty_print=True
        )
        print(json.dumps(summarized_sanitized, sort_keys=True, indent=4))

    def get_migrated_subscriptions_list(self):
        """Get list of subscriptions that were migrated in previous migration sessions

        :rtype: list[str | unicode]
        """
        return self._data.get('subscriptions', {}).keys()


class PleskStatisticsSanitizer(object):
    def sanitize(self, data):
        data = self._filter_none_values(data)
        data = self._fix_numeric_indexes(data)
        data = self._reindex_lists(data)
        return data

    def _filter_none_values(self, data):
        """Remove None (null) value: they could break statistics reporting to KA

        See bugs: PPPM-4535, PPP-35196.
        """
        if isinstance(data, dict):
            return {k: self._filter_none_values(v) for k, v in data.items() if v is not None}
        elif isinstance(data, list) or isinstance(data, tuple):
            return [self._filter_none_values(v) for v in data if v is not None]
        else:
            return data

    def _fix_numeric_indexes(self, data):
        """Append literal suffix to indexes which look like integers: they could break statistics reporting to KA

        See bug: PPPM-4535.
        """
        if isinstance(data, dict):
            return {self._fix_numeric_index(k): self._fix_numeric_indexes(v) for k, v in data.items()}
        elif isinstance(data, list) or isinstance(data, tuple):
            return [self._fix_numeric_indexes(v) for v in data]
        else:
            return data

    @staticmethod
    def _fix_numeric_index(index):
        if re.match(r'^\d+$', str(index)):
            return "s" + str(index)
        else:
            return index

    def _reindex_lists(self, data):
        """Convert lists to dictionaries with numeric keys starting at 1: lists could break statistics reporting to KA

        See bug: PPP-35474.
        """
        if isinstance(data, list) or isinstance(data, tuple):
            return {"TYPE": "array", "VALUE": [self._reindex_lists(v) for v in data]}
        elif isinstance(data, dict):
            return {k: self._reindex_lists(v) for k, v in data.items()}
        else:
            return data


class SessionStatistics(Entity):
    """Per-session migration statistics

    It includes information about source and target servers, number of migrated subscriptions, etc.
    By session here we mean single execution of 'transfer-accounts' entry point (not migration session directory).
    """
    def __init__(
        self, migrator_version,
        source_server_ip, target_server_ip, source_server_type, target_server_type,
        source_os_name, source_os_version, source_os_architecture,
        target_os_name, target_os_version, target_os_architecture,
        source_panel_version, target_panel_version,
        subscriptions_on_target, subscriptions_on_source,
        migrated_subscriptions, remigrated_subscriptions,
        start_time, clients_on_source, clients_on_target,
        end_time=None, migration_time=None, finished=False,
        warning_subscriptions=None, failed_subscriptions=None,
        warning_remigrated_subscriptions=None, failed_remigrated_subscriptions=None,
        session_statistics_version=2
    ):
        self._migrator_version = migrator_version
        self._source_server_ip = source_server_ip
        self._target_server_ip = target_server_ip
        self._source_server_type = source_server_type
        self._target_server_type = target_server_type
        self._source_os_name = source_os_name
        self._source_os_version = source_os_version
        self._source_os_architecture = source_os_architecture
        self._target_os_name = target_os_name
        self._target_os_version = target_os_version
        self._target_os_architecture = target_os_architecture
        self._source_panel_version = source_panel_version
        self._target_panel_version = target_panel_version
        self._subscriptions_on_target = subscriptions_on_target
        self._subscriptions_on_source = subscriptions_on_source
        self._migrated_subscriptions = migrated_subscriptions
        self._remigrated_subscriptions = remigrated_subscriptions
        self._clients_on_source = clients_on_source
        self._clients_on_target = clients_on_target
        self._start_time = start_time
        self._end_time = end_time
        self._migration_time = migration_time
        self._finished = finished
        self._warning_subscriptions = warning_subscriptions
        self._failed_subscriptions = failed_subscriptions
        self._warning_remigrated_subscriptions = warning_remigrated_subscriptions
        self._failed_remigrated_subscriptions = failed_remigrated_subscriptions
        self._session_statistics_version = session_statistics_version

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

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

    @property
    def source_server_ip(self):
        """IP address of the source server.

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

    @property
    def target_server_ip(self):
        """IP address of the target server.

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

    @property
    def source_server_type(self):
        """Panel type on the source server, for example "plesk", "confixx", "cpanel".

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

    @property
    def target_server_type(self):
        """Panel type on the target server. Always equal to "plesk" now.

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

    @property
    def source_os_name(self):
        """Name of operating system on the source server.

        Possible values: 'windows', 'centos', 'rhel', 'debian', 'ubuntu', 'suse', 'linux'
        (for any Linux server except for detected CentOS, RHEL, Debian, Ubuntu or SuSE).

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

    @property
    def source_os_version(self):
        """Version of operating system on the source server.

        Examples: "6" for CentOS 6.1, "2012 R2 Standard" for Microsoft Windows 2012 R2 Standard.

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

    @property
    def source_os_architecture(self):
        """Architecture of the source server: "x86" (32 bit) or "x86_64" (64 bit)

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

    @property
    def target_os_name(self):
        """Name of operating system on the target server.

        Possible values: 'windows', 'centos', 'rhel', 'debian', 'ubuntu', 'suse', 'linux'
        (for any Linux server except for detected CentOS, RHEL, Debian, Ubuntu or SuSE).

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

    @property
    def target_os_version(self):
        """Version of operating system on the target server.

        Examples: "6" for CentOS 6.1, "2012 R2 Standard" for Microsoft Windows 2012 R2 Standard.

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

    @property
    def target_os_architecture(self):
        """Architecture of the target server: "x86" (32 bit) or "x86_64" (64 bit)

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

    @property
    def source_panel_version(self):
        """Source panel version. Now reported for Plesk only, with 3 significant digits, for example [12, 5, 30].

        :rtype: list[int]
        """
        return self._source_panel_version

    @property
    def target_panel_version(self):
        """Target Plesk version, with 3 significant digits, for example [12, 5, 30].

        :rtype: list[int]
        """
        return self._target_panel_version

    @property
    def subscriptions_on_target(self):
        """Total number of subscriptions on the target server before migration was started.

        :rtype: int
        """
        return self._subscriptions_on_target

    @property
    def subscriptions_on_source(self):
        """Total number of subscriptions on the source server.

        :rtype: int
        """
        return self._subscriptions_on_source

    @property
    def migrated_subscriptions(self):
        """Number of subscriptions which customer selected to migrate.

        :rtype: int
        """
        return self._migrated_subscriptions

    @property
    def remigrated_subscriptions(self):
        """Number of subscriptions selected to migrate, which were already migrated some time ago in another session.

        Note: "already migrated" does not mean that subscription already exists on the target server.
        Customer could migrate subscription once, then remove it from target, and then migrate it again

        :rtype: int
        """
        return self._remigrated_subscriptions

    @property
    def clients_on_source(self):
        """Total number of clients on the source server.

        :rtype: int
        """
        return self._clients_on_source

    @property
    def clients_on_target(self):
        """Total number of clients on the target server before migration was started.

        :rtype: int
        """
        return self._clients_on_target

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

        :rtype: int
        """
        return self._start_time

    @property
    def end_time(self):
        """Timestamp when "transfer-accounts" 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-accounts" command.
        So, migration list generation, pre-migration checks, and so on, are not included in that time.

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

        :rtype: int
        """
        return self._migration_time

    @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 warning_subscriptions(self):
        """Number of subscriptions which have warnings.

        For such subscriptions, there is at least one "warning" in final migration report for that subscription,
        but there are no "errors".

        :rtype: int
        """
        return self._warning_subscriptions

    @warning_subscriptions.setter
    def warning_subscriptions(self, warning_subscriptions):
        self._warning_subscriptions = warning_subscriptions

    @property
    def warning_remigrated_subscriptions(self):
        """Number of subscriptions with warnings, which were already migrated some time ago in another session.

        By "with warnings" we mean there is at least one "warning" in final migration report for that
        subscription, but there are no "errors".

        :rtype: int
        """
        return self._warning_remigrated_subscriptions

    @warning_remigrated_subscriptions.setter
    def warning_remigrated_subscriptions(self, warning_remigrated_subscriptions):
        self._warning_remigrated_subscriptions = warning_remigrated_subscriptions

    @property
    def failed_subscriptions(self):
        """Number of subscriptions which failed to migrate.

        Failed to migrate means there is at least one "error" in final migration report for that subscription.

        :rtype: int
        """
        return self._failed_subscriptions

    @failed_subscriptions.setter
    def failed_subscriptions(self, failed_subscriptions):
        self._failed_subscriptions = failed_subscriptions

    @property
    def failed_remigrated_subscriptions(self):
        """Number of failed to migrate subscriptions, which were already migrated some time ago in another session.

        By "failed to migrate" we mean there is at least one "error" in final migration report for that subscription.

        :rtype: int
        """
        return self._failed_remigrated_subscriptions

    @failed_remigrated_subscriptions.setter
    def failed_remigrated_subscriptions(self, failed_remigrated_subscriptions):
        self._failed_remigrated_subscriptions = failed_remigrated_subscriptions

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

        :rtype: int
        """
        return self._session_statistics_version
