from collections import defaultdict

from parallels.core import messages
from parallels.core.logging import get_logger
from parallels.core.utils.entity import Entity
from parallels.core.utils.migrator_utils import normalize_domain_name
from parallels.core.utils.yaml_utils import write_yaml, read_yaml


logger = get_logger(__name__)


class NameMapping(object):
    def __init__(self):
        self._data = {}

    def get_new_name(self, type_id, hosting_object):
        """Get name for specified hosting object on target server.

        If name is not changed - it is returned as is.

        :type type_id: str | unicode
        :type hosting_object: parallels.core.utils.name_conflicts.ConflictingObject
        :rtype: str | unicode
        """
        mapping_key = NameMappingKey(
            type_id, hosting_object.get_subscription_name(), hosting_object.get_name(), hosting_object.get_key()
        )
        if mapping_key in self._data:
            return self._data[mapping_key]
        else:
            return hosting_object.get_name()

    def set_new_name(self, type_id, hosting_object, new_name):
        """Set name for specified hosting object on target server.

        :type type_id: str | unicode
        :type hosting_object: parallels.core.utils.name_conflicts.ConflictingObject
        :type new_name: str | unicode
        :rtype: None
        """
        mapping_key = NameMappingKey(
            type_id, hosting_object.get_subscription_name(), hosting_object.get_name(), hosting_object.get_key()
        )
        self._data[mapping_key] = new_name

    def reset_new_name(self, type_id, hosting_object):
        """Reset name for specified hosting object on target server - it will be the same as on source server

        :type type_id: str | unicode
        :type hosting_object: parallels.core.utils.name_conflicts.ConflictingObject
        :rtype: None
        """
        mapping_key = NameMappingKey(
            type_id, hosting_object.get_subscription_name(), hosting_object.get_name(), hosting_object.get_key()
        )
        if mapping_key in self._data:
            del self._data[mapping_key]

    def get_data(self):
        """Get name mapping data dictionary

        :rtype: dict[parallels.core.utils.name_conflicts.NameMappingKey, str | unicode]
        """
        return self._data


class PersistentNameMapping(NameMapping):
    """Name mapping which is saved to file, loaded from file"""

    def __init__(self, filename):
        super(PersistentNameMapping, self).__init__()
        self._filename = filename
        try:
            self._data = read_yaml(self._filename, file_may_be_absent=True, default={})
        except Exception as e:
            logger.exception()
            logger.fwarn(messages.FAILED_TO_READ_NAME_CONFLICTS_MAPPING, filename=self._filename, reason=e)
            self._data = {}

    def set_new_name(self, type_id, hosting_object, new_name):
        """Set name for specified hosting object on target server.

        :type type_id: str | unicode
        :type hosting_object: parallels.core.utils.name_conflicts.ConflictingObject
        :type new_name: str | unicode
        :rtype: None
        """
        super(PersistentNameMapping, self).set_new_name(type_id, hosting_object, new_name)
        self._write()

    def reset_new_name(self, type_id, hosting_object):
        """Reset name for specified hosting object on target server - it will be the same as on source server

        :type type_id: str | unicode
        :type hosting_object: parallels.core.utils.name_conflicts.ConflictingObject
        :rtype: None
        """
        super(PersistentNameMapping, self).reset_new_name(type_id, hosting_object)
        self._write()

    def _write(self):
        """Write name mapping data to a file

        :rtype: None
        """
        write_yaml(self._filename, self._data)


class ConflictingHostingObjectDescription(object):
    """Abstract class which describes how to resolve name conflicts for specific type of migrated object

    Examples of types of migrated objects: reseller, database, database user, system user.
    """

    def get_type_id(self):
        """Get string identifier of object type, used not to mix renames in persistent storage.

        For example, for system user it could return 'sysuser', for customer it could return 'customer',
        for reseller it could return 'reseller' and so on.

        :rtype: str | unicode
        """
        raise NotImplementedError()

    def list_source_objects(self):
        """Get list of objects in source panels, for which we should resolve name conflicts

        :rtype: list[parallels.core.utils.name_conflicts.SourceConflictingObject]
        """
        pass

    def list_target_objects(self):
        """Get list of objects on target panel, which may conflict with migrated objects from source panels

        :rtype: list[parallels.core.utils.name_conflicts.TargetConflictingObject]
        """
        pass

    # noinspection PyMethodMayBeStatic
    def get_rename_iterator(self, name):
        """Get way to generate new names - iterator that will return new names for the specified name.

        By default, SimpleRenameIterator with InfiniteDigitSuffixes suffixes is used. For example if
        name is "test", it will generate the following sequence (infinite): "test2", "test3", "test4", ...

        :type name: str | unicode
        :rtype: Iterable[str | unicode]
        """
        return SimpleRenameIterator(name, suffixes=InfiniteDigitSuffixes())

    # noinspection PyMethodMayBeStatic
    def is_name_case_insensitive(self):
        """Whether object names case sensitive or not

        :rtype: bool
        """
        return True


class ConflictingObject(object):
    """Abstract class to describe single conflicting object - e.g. one specific database"""

    def get_name(self):
        """Get name of the object - that one that should be changed in case of conflicts.

        :rtype: str | unicode
        """
        raise NotImplementedError()

    def get_key(self):
        """Get key of the object - what differs 2 objects with the same name.

        Objects with the same key and the same name conflict with each other.
        Objects with different keys never conflict.

        Example of key for database is the target database server (type - MySQL, MSSQL or PostgreSQL, host and port).
        Pay attention that conflicts happen on the target server, target database server should be used as a key,
        not source database server.

        :rtype: tuple | None
        """
        raise NotImplementedError()

    def get_subscription_name(self):
        """Get name of subscription which owns the object.

        That is necessary to allow 2 modes of conflict resolution:
        1. When conflicting objects are within the same subscription - considered the same, and should not be
        renamed. For example, if subscription 'example.com' has database 'testdb' both on a source and on a target,
        then during migration the database won't be renamed, database from the source server will overwrite database
        from the target server.
        2. When conflicting objects are always renamed.

        If None is returned then object is not related to any subscription.
        For example, for system user it could be a user which does not exists in control panel,
        but actually exists on system.

        :rtype: str | unicode | None
        """
        raise NotImplementedError()


# noinspection PyAbstractClass
class SourceConflictingObject(ConflictingObject):
    """Single conflicting object on the source server - e.g. one specific database on the source server"""

    def set_name(self, new_name):
        """Rename object in case of conflicts

        :type new_name: str | unicode
        """
        raise NotImplementedError()


# noinspection PyAbstractClass
class TargetConflictingObject(ConflictingObject):
    """Single conflicting object on the target server - e.g. one specific database on the target server"""
    pass


class InfiniteDigitSuffixes(object):
    """Generate digital suffixes for rename iterators"""

    def __init__(self, start=2):
        """
        :type start: int
        """
        self._i = start

    def next(self):
        """
        :rtype: str | unicode
        """
        value = self._i
        self._i += 1
        return str(value)

    def __iter__(self):
        return self


class SimpleRenameIterator(object):
    """Generate new names for objects by appending specified suffixes"""

    def __init__(self, initial_name, suffixes):
        """
        :type initial_name: str | unicode
        :type suffixes: Iterable[str | unicode]
        """
        self._suffixes = suffixes
        self._initial_name = initial_name

    def next(self):
        """
        :rtype: str | unicode
        """
        suffix = str(self._suffixes.next())
        return "%s%s" % (self._initial_name, suffix)

    def __iter__(self):
        """
        :rtype: Iterator[str | unicode]
        """
        return self


class TruncatingRenameIterator(object):
    """Generate new names for objects by appending specified suffixes and truncating name to specified length"""

    def __init__(self, initial_name, max_length, suffixes):
        """
        :type initial_name: str | unicode
        :type max_length: int
        :type suffixes: Iterable[str | unicode]
        """
        self._initial_name = initial_name
        self._max_length = max_length
        self._suffixes = suffixes

    def next(self):
        """
        :rtype: str | unicode
        """
        suffix = str(self._suffixes.next())
        if len(suffix) > self._max_length:
            raise StopIteration()
        return "%s%s" % (self._initial_name[:self._max_length - len(suffix)], suffix)

    def __iter__(self):
        """
        :rtype: Iterator[str | unicode]
        """
        return self


def resolve_conflicts(name_mapping, object_description, merge_subscriptions=False):
    """Run conflict resolution for objects of some type.

    Type of objects is described by object description, which could list source and target objects of that type.

    Results are saved into name mapping object, previous name mapping is taken from that object.

    Also a flag could be specified for "Merge subscription objects" mode:
    - When it is set to True, 2 objects with the same name and key within the same subscription are not considered
    conflicting and are not renamed. That is for usual migration.
    - When it is set to False, 2 objects with the same name and key are always considered conflicting, even
    when they are within the same subscription. That is for site migration, when for example you could migrate
    2 sites to the same subscription from different servers which have 2 different databases with the same name;
    one of the databases will be renamed in such case.

    :type name_mapping: parallels.core.utils.name_conflicts.NameMapping
    :type object_description: parallels.core.utils.name_conflicts.ConflictingHostingObjectDescription
    :type merge_subscriptions: bool
    """
    def normalize_name(n):
        """Normalize object name to be used in dictionary keys, comparison, etc - to consider case insensitive names

        :type n: str | unicode
        :rtype: str | unicode
        """
        if object_description.is_name_case_insensitive():
            return n.lower()
        else:
            return n

    # Step #1: compose lists of source and target hosting objects, grouped by object name and object key
    # Object name is a string, which we could change to avoid conflicts, for example database name.
    # Object key is an additional data (usually Python tuple):
    # - when 2 objects have the same name and the same key, they could possibly conflict
    # - when 2 objects have the same name, but different key, they do not conflict
    # Example of object key for database object is database server (host, port, type)
    hosting_objects = defaultdict(list)
    target_objects = defaultdict(list)
    """:type: dict[tuple, list[parallels.core.utils.name_conflicts.ConflictingObject]]"""

    for target_object in object_description.list_target_objects():
        group_key = (normalize_name(target_object.get_name()), target_object.get_key())
        hosting_objects[group_key].append(target_object)
        target_objects[group_key].append(target_object)

    for source_object in object_description.list_source_objects():
        group_key = (normalize_name(source_object.get_name()), source_object.get_key())
        hosting_objects[group_key].append(source_object)

    # Step #2: compose set of used renames.
    # During previous migrations and pre-migration checks, we selected new
    # names for conflicting objects. Here we compose list of them, to archive 2 goals:
    # - reuse previous re-name if possible (for example customer executed pre-checks and got a warning,
    # that object will be renamed from 'test' to 'test5'; during subsequent migrations, object should be renamed
    # to 'test5', but not 'test2' or 'test6', if that is still possible)
    # - avoid 2 different objects to be renamed to the same name.
    used_renames = set()

    for source_object in object_description.list_source_objects():
        new_name = name_mapping.get_new_name(object_description.get_type_id(), source_object)
        if new_name != source_object.get_name():
            used_renames.add((normalize_name(new_name), source_object.get_key()))

    # Step #3: run the conflict resolution algorithm - iterate over groups of objects and assign new names to the
    # conflicting ones.
    for group_objects in hosting_objects.values():
        # Situation #1: there are multiple objects with such name which could conflict
        if len(group_objects) > 1:
            first_hosting_object = group_objects[0]

            for hosting_object in group_objects[1:]:
                # Situation #1.1:
                # object does not actually conflict, but rather should be merged,
                # for example when subscription exists both on source and on target and has database with the
                # same name both on source and on target.
                if (
                    # "Merge subscription objects" mode is enabled
                    merge_subscriptions and
                    # Object belongs to the same subscription
                    (
                        normalize_domain_name(hosting_object.get_subscription_name()) ==
                        normalize_domain_name(first_hosting_object.get_subscription_name())
                    )
                ):
                    # Reset previously created mappings for that object, if any. For example, this could happen
                    # when customer executed pre-migration checks, got issue with 2 conflicting databases,
                    # and then renamed the database on the target server, so there is no conflict anymore.
                    name_mapping.reset_new_name(object_description.get_type_id(), hosting_object)
                    continue

                # Situation #1.2:
                # previously renamed object, when
                # 1. Object conflicts with some other object.
                # 2. Object was renamed during previous migrations or pre-checks.
                old_new_name = name_mapping.get_new_name(object_description.get_type_id(), hosting_object)
                if old_new_name != hosting_object.get_name():
                    old_mapping_group_by = (
                        normalize_name(old_new_name), hosting_object.get_key()
                    )
                    # If previous renaming does not conflict with other objects.
                    if (
                        # There are no objects with such name
                        old_mapping_group_by not in hosting_objects or
                        # There are objects with such name, but they should be merged
                        (
                            # "Merge subscription objects" mode is enabled
                            merge_subscriptions and
                            # All conflicting objects belong to the same subscription
                            (
                                all(
                                    (
                                        normalize_domain_name(h.get_subscription_name()) ==
                                        normalize_domain_name(hosting_object.get_subscription_name())
                                    )
                                    for h in hosting_objects[old_mapping_group_by]
                                )
                            )
                        )
                    ):
                        continue

                # Situation #1.3:
                # regular conflicting situation.
                # Try names according to rename iterator, which generates sequence of possible new names.
                for new_name in object_description.get_rename_iterator(hosting_object.get_name()):
                    # If new name does not conflict with any other names - use that name
                    if (
                        # New name does not conflict with names of objects on source and target servers
                        (normalize_name(new_name), hosting_object.get_key()) not in hosting_objects and
                        # New name does not conflict with any already used renames
                        (normalize_name(new_name), hosting_object.get_key()) not in used_renames
                    ):
                        name_mapping.set_new_name(object_description.get_type_id(), hosting_object, new_name)
                        used_renames.add((normalize_name(new_name), hosting_object.get_key()))
                        break
        # Situation #2: there is just one object with such name, so there is no conflict
        elif len(group_objects) == 1:
            first_hosting_object = group_objects[0]

            old_new_name = name_mapping.get_new_name(object_description.get_type_id(), first_hosting_object)
            old_mapping_group_by = (
                normalize_name(old_new_name), first_hosting_object.get_key()
            )
            if (
                # name was not changed by previous migrations or pre-checks
                old_new_name == first_hosting_object.get_name() or
                # name was changed by previous migrations or pre-checks, and
                # object with old renamed name does not exist in the current subscription
                (
                    old_mapping_group_by not in target_objects or (
                        len(target_objects[old_mapping_group_by]) > 0 and
                        (
                            normalize_domain_name(target_objects[old_mapping_group_by][0].get_subscription_name()) !=
                            normalize_domain_name(first_hosting_object.get_subscription_name())
                        )
                    )
                )
            ):
                # Reset all mappings created by previous migrations or pre-migration checks
                name_mapping.reset_new_name(object_description.get_type_id(), first_hosting_object)


class NameMappingKey(Entity):
    """Key to group hosting objects for name conflict resolution"""

    def __init__(self, type_id, subscription_name, object_name, object_key):
        """
        :type type_id: str | unicode
        :type subscription_name: str | unicode | None
        :type object_name: str | unicode
        :type object_key: tuple[str | unicode] | str | unicode | None
        """
        self._type_id = type_id
        self._subscription_name = normalize_domain_name(subscription_name)
        self._object_name = object_name
        self._object_key = object_key

    @property
    def type_id(self):
        """
        :rtype: str | unicode
        """
        return self._type_id

    @property
    def subscription_name(self):
        """
        :rtype: str | unicode | None
        """
        return self._subscription_name

    @property
    def object_name(self):
        """
        :rtype: str | unicode
        """
        return self._object_name

    @property
    def object_key(self):
        """
        :rtype: tuple[str | unicode] | str | unicode | None
        """
        return self._object_key

    def __hash__(self):
        return hash(self.type_id) ^ hash(self.subscription_name) ^ hash(self.object_name) ^ hash(self.object_key)
