from contextlib import contextmanager

from parallels.core import messages
from parallels.core.connections.mssql_remote_server import MSSQLRemoteServer
from parallels.core.connections.server import Server
from parallels.core.registry import Registry
from parallels.core.utils.common import cached
from parallels.core.utils.common.ip import resolve
from parallels.core.utils.database_server_type import DatabaseServerType
from parallels.core.utils.database_utils import is_local_database_host, list_databases, check_connection, \
    get_server_version, split_mssql_host_parts_str, list_database_users


class DatabaseServer(Server):
    """Base interface for working with database servers in migrator"""

    def description(self):
        if self.host() == 'localhost':
            return messages.DATABASE_SERVER_DESCRIPTION % (self.type_description, self.ip(), self.port())
        else:
            return messages.DATABASE_SERVER_DESCRIPTION % (self.type_description, self.host(), self.port())

    @property
    def type_description(self):
        db_server_types = {
            DatabaseServerType.MYSQL: 'MySQL',
            DatabaseServerType.POSTGRESQL: 'PostgreSQL',
            DatabaseServerType.MSSQL: 'MSSQL'
        }
        return db_server_types.get(self.type().lower(), self.type())

    @cached
    def ip(self):
        if self.physical_server is not None:
            # if we have physical server - take IP from it
            return self.physical_server.ip()

        if is_local_database_host(self.host(), self.type(), []):
            # it means database server, local to panel server
            return self.panel_server.ip()

        return resolve(self.host())

    def type(self):
        raise NotImplementedError()

    def host(self):
        raise NotImplementedError()

    def port(self):
        raise NotImplementedError()

    def user(self):
        raise NotImplementedError()

    @cached
    def password(self):
        raise NotImplementedError()

    @contextmanager
    def runner(self):
        """Alias for utilities_server.runner function"""
        with self.utilities_server.runner() as runner:
            yield runner

    def get_session_file_path(self, filename):
        """Alias for utilities_server.get_session_file_path function

        :type filename: str | unicode
        :rtype: str | unicode
        """
        return self.utilities_server.get_session_file_path(filename)

    def get_session_dir_path(self):
        """Alias for utilities_server.session_dir function"""
        return self.utilities_server.get_session_dir_path()

    def is_windows(self):
        """Alias for utilities_server.is_windows function"""
        return self.utilities_server.is_windows()

    def is_local(self):
        """Check whether this database server is local to corresponding panel server

        :rtype: bool
        """
        return is_local_database_host(self.host(), self.type(), [self.panel_server.ip()])

    @property
    def panel_server(self):
        """Get panel server that controls this database server.

        For example, single panel Plesk server may control one MySQL server on the same physical server (on localhost)
        and an external MySQL server located on another physical server. For both MySQL database servers,
        this function will return single object that corresponds to Plesk server.
        """
        raise NotImplementedError()

    @property
    def physical_server(self):
        """Get physical server on which database server is located.

        By physical we mean the server, where database files, processes and services are running.
        If we don't have access to the physical server, this function must return None.

        :rtype: parallels.core.connections.physical_server.PhysicalServer | None
        """
        if self.is_local():
            # This database is located on local panel server
            return self.panel_server
        else:
            if self.type() == DatabaseServerType.MSSQL:
                # Special handling for MSSQL, for which physical access is strongly recommended
                no_access_servers = Registry.get_instance().get_context().mssql_servers_without_physical_access
                if no_access_servers.has_server(self.panel_server.ip(), self.host()):
                    # We checked and found that we don't actually have access to that server
                    return None
                else:
                    host, _, _ = split_mssql_host_parts_str(self.host())
                    return MSSQLRemoteServer(host, self.panel_server)
            else:
                # This database is located on external server
                # We don't have physical access to it, so return None
                return None

    @property
    def utilities_server(self):
        """Get server on which migrator should run database utilities to work with that database server

        The major aspect here is that the server must have access to database server by database protocols.
        Also it should have database utilities installed, but that is not guaranteed.
        """
        # TODO clarify return type
        if self.physical_server is not None:
            # If we have access to physical server, or it is the same as panel server - return it
            return self.physical_server
        else:
            # If we don't have access to physical server, try to execute database utilities on panel server.
            # Usually it provides utilities (mysqldump, pg_dump, mysql, psql, etc), but sometimes - not,
            # and there could be version mismatch
            return self.panel_server

    def list_databases(self):
        """List databases on that server.

        Returns list of databases names or None if that function is not supported for that database server type.

        :rtype: set[str | unicode] | None
        """
        return list_databases(self)

    def list_database_users(self):
        """List database users on that server.

        Returns list of database user names or None if that function is not supported for that database server type.

        :rtype: set[str | unicode] | None
        """
        return list_database_users(self)

    def check_connection(self):
        """Check connection to that database server.

        :raises parallels.core.utils.database_utils.DatabaseServerConnectionException:
        """
        return check_connection(self)

    @property
    @cached
    def server_version(self):
        """Get version of specified database server. Now works for MySQL only.

        Returns tuple of version parts, for example, ('5', '5', '41-MariaDB'). If not able to determine the version,
        returns None.

        :rtype: tuple[str | unicode] | None
        """
        return get_server_version(self)

    @property
    def mysql_bin(self):
        """Path to MySQL client binary, to be used in commands. By default 'mysql' is used as client binary.

        :rtype: str | unicode
        """
        return self.panel_server.get_path_to_mysql()

    def __hash__(self):
        return hash((self.type(), self.host(), self.port()))

    def __eq__(self, other):
        return (
            isinstance(self, DatabaseServer) and
            other.type() == self.type() and other.host() == self.host() and other.port() == self.port() and
            other.panel_server == self.panel_server
        )
