from parallels.core.logging import get_logger
from parallels.core.runners.base import BaseRunner
from parallels.core.runners.entities import ExecutionOptions
from parallels.core.utils.common import safe_string_repr
from parallels.hosting_check.utils.text_utils import format_list
from parallels.hosting_check import DomainIssue
from parallels.hosting_check import Severity
from parallels.hosting_check import PostgreSQLDatabaseIssueType
from parallels.hosting_check import NonZeroExitCodeException
from parallels.hosting_check.messages import MSG

logger = get_logger(__name__)


class PostgreSQLDatabaseChecker(object):
    def check(self, domains_to_check):
        """
        :type domains_to_check: list[parallels.hosting_check.DomainPostgreSQLDatabases]
        """
        issues = []
        for domain_to_check in domains_to_check:
            issues += self._check_single_domain(domain_to_check)
        return issues

    def _check_single_domain(self, domain_to_check):
        """
        :type domain_to_check: parallels.hosting_check.DomainPostgreSQLDatabases
        """
        issues = []

        for database in domain_to_check.databases:
            try:
                self._check_single_database(domain_to_check, database, issues)
            except KeyboardInterrupt:
                # for compatibility with python 2.4
                raise
            except Exception as e:
                logger.exception()
                issues.append(
                    DomainIssue(
                        domain_name=domain_to_check.domain_name,
                        severity=Severity.WARNING, 
                        category=PostgreSQLDatabaseIssueType.INTERNAL_ERROR,
                        problem=MSG(
                            PostgreSQLDatabaseIssueType.INTERNAL_ERROR,
                            database=database.target_name,
                            reason=safe_string_repr(e)
                        )
                    )
                )

        return issues

    def _check_single_database(self, domain_to_check, database, issues):
        """
        :type domain_to_check: parallels.hosting_check.DomainPostgreSQLDatabases
        :type database: parallels.hosting_check.PostgreSQLDatabase
        :type issues: list
        :rtype: None
        """
        logger.debug("Check PostgreSQL database '%s'", database.target_name)
        if not self._database_exists_panel(
            domain_to_check, database, issues
        ):
            # skip all the other checks - no sense to check 
            # if it even was not created in the panel
            return

        if not self._check_login_to_target_db(
            domain_to_check, database, issues
        ):
            # skip all the other checks - no sense to check 
            # if we got error when trying to login into target database
            return

        self._check_database_tables(domain_to_check, database, issues)
        self._check_database_users(domain_to_check, database, issues)

    @staticmethod
    def _check_database_tables(domain_to_check, database, issues):
        """
        :type domain_to_check: parallels.hosting_check.DomainPostgreSQLDatabases
        :type database: parallels.hosting_check.PostgreSQLDatabase
        :type issues: list
        :rtype: None
        """
        source_tables = None
        try:
            source_tables = _list_pgsql_tables(
                database.source_name, database.source_access
            )
        except KeyboardInterrupt:
            # for compatibility with python 2.4
            raise
        except Exception as e:
            issues.append(
                DomainIssue(
                    domain_name=domain_to_check.domain_name,
                    severity=Severity.ERROR, 
                    category=PostgreSQLDatabaseIssueType.FAILED_TO_FETCH_TABLES_FROM_SOURCE,
                    problem=MSG(
                        PostgreSQLDatabaseIssueType.FAILED_TO_FETCH_TABLES_FROM_SOURCE,
                        server=database.source_access.host,
                        database=database.source_name,
                        reason=safe_string_repr(e),
                    ) 
                )
            )

        target_tables = None
        if source_tables is not None:
            try:
                target_tables = _list_pgsql_tables(
                    database.target_name, database.target_access
                )
            except KeyboardInterrupt:
                # for compatibility with python 2.4
                raise
            except Exception as e:
                issues.append(
                    DomainIssue(
                        domain_name=domain_to_check.domain_name,
                        severity=Severity.ERROR, 
                        category=PostgreSQLDatabaseIssueType.FAILED_TO_FETCH_TABLES_FROM_TARGET,
                        problem=MSG(
                            PostgreSQLDatabaseIssueType.FAILED_TO_FETCH_TABLES_FROM_TARGET,
                            server=database.source_access.host,
                            database=database.target_name,
                            reason=safe_string_repr(e),
                        ) 
                    )
                )

        if source_tables is not None and target_tables is not None:
            if not (target_tables >= source_tables):
                difference = source_tables - target_tables
                issues.append(
                    DomainIssue(
                        domain_name=domain_to_check.domain_name,
                        severity=Severity.ERROR,
                        category=PostgreSQLDatabaseIssueType.DIFFERENT_TABLES_SET,
                        problem=MSG(
                            PostgreSQLDatabaseIssueType.DIFFERENT_TABLES_SET,
                            database=database.target_name,
                            tables_list=format_list(difference)
                        )
                    )
                )

    def _check_database_users(self, domain_to_check, database, issues):
        """
        :type domain_to_check: parallels.hosting_check.DomainPostgreSQLDatabases
        :type database: parallels.hosting_check.PostgreSQLDatabase
        :type issues: list
        :rtype: None
        """
        for user in database.users:
            if not self._database_user_exists_panel(
                domain_to_check, database, user, issues
            ):
                # proceed to the next checks only if user exist on the
                # target panel
                return

            if not _check_db_access(
                database.target_name, database.target_access, user
            ):
                issues.append(
                    DomainIssue(
                        domain_name=domain_to_check.domain_name,
                        severity=Severity.ERROR, 
                        category=PostgreSQLDatabaseIssueType.FAILED_TO_EXECUTE_SIMPLE_QUERY_AS_USER,
                        problem=MSG(
                            PostgreSQLDatabaseIssueType.FAILED_TO_EXECUTE_SIMPLE_QUERY_AS_USER,
                            user=user.login,
                            database=database.target_name
                        )
                    )
                )

    @staticmethod
    def _check_login_to_target_db(domain_to_check, database, issues):
        """
        :type domain_to_check: parallels.hosting_check.DomainPostgreSQLDatabases
        :type database: parallels.hosting_check.PostgreSQLDatabase
        :type issues: list
        :rtype: None
        """
        try:
            _execute_pgsql_query(
                database.target_name, database.target_access,
                database.target_access.admin_user,
                query='select 1'
            )
        except NonZeroExitCodeException as e:
            if 'could not connect to server' in e.stderr:
                issues.append(
                    DomainIssue(
                        domain_name=domain_to_check.domain_name,
                        severity=Severity.ERROR, 
                        category=PostgreSQLDatabaseIssueType.CONNECTION_ERROR,
                        problem=MSG(
                            PostgreSQLDatabaseIssueType.CONNECTION_ERROR,
                            database=database.target_name,
                            server=database.target_access.host,
                        )
                    )
                )
            elif ('database "%s" does not exist' % database.target_name) in e.stderr:
                issues.append(
                    DomainIssue(
                        domain_name=domain_to_check.domain_name,
                        severity=Severity.ERROR, 
                        category=PostgreSQLDatabaseIssueType.DATABASE_DOES_NOT_EXIST,
                        problem=MSG(
                            PostgreSQLDatabaseIssueType.DATABASE_DOES_NOT_EXIST,
                            database=database.target_name,
                            server=database.target_access.host,
                        )
                    )
                )
            else:
                issues.append(
                    DomainIssue(
                        domain_name=domain_to_check.domain_name,
                        severity=Severity.ERROR, 
                        category=PostgreSQLDatabaseIssueType.PSQL_CLIENT_GENERIC_ERROR,
                        problem=MSG(
                            PostgreSQLDatabaseIssueType.PSQL_CLIENT_GENERIC_ERROR,
                            database=database.target_name,
                            server=database.target_access.host,
                            stdout=e.stdout,
                            stderr=e.stderr
                        )
                    )
                )

            # in any case that's an error - return False
            return False

        return True

    @staticmethod
    def _database_exists_panel(domain_to_check, database, issues):
        """
        :type domain_to_check: parallels.hosting_check.DomainPostgreSQLDatabases
        :type database: parallels.hosting_check.PostgreSQLDatabase
        :type issues: list
        :rtype: None
        """
        domain_target_panel_databases = domain_to_check.target_panel_databases
        if domain_target_panel_databases is None:
            return True

        target_panel_databases = set([])
        for db in domain_target_panel_databases:
            target_panel_databases.add(db.name)

        if (
            target_panel_databases is not None and
            database.target_name not in target_panel_databases
        ):
            issues.append(
                DomainIssue(
                    domain_name=domain_to_check.domain_name,
                    severity=Severity.ERROR, 
                    category=PostgreSQLDatabaseIssueType.DATABASE_DOES_NOT_EXIST_IN_PANEL,
                    problem=MSG(
                        PostgreSQLDatabaseIssueType.DATABASE_DOES_NOT_EXIST_IN_PANEL,
                        database=database.target_name,
                    )
                )
            )
            return False
        else:
            return True

    @staticmethod
    def _database_user_exists_panel(domain_to_check, database, user, issues):
        """
        :type domain_to_check: parallels.hosting_check.DomainPostgreSQLDatabases
        :type database: parallels.hosting_check.PostgreSQLDatabase
        :type user: parallels.hosting_check.User
        :type issues: list
        :rtype: None
        """
        domain_target_panel_databases = domain_to_check.target_panel_databases
        if domain_target_panel_databases is not None:
            target_panel_databases = dict()
            for db in domain_target_panel_databases:
                target_panel_databases[db.name] = db.users
            target_panel_users = target_panel_databases[database.target_name]
        else:
            target_panel_users = None

        if (
            target_panel_users is not None and
            user.login not in target_panel_users
        ):
            issues.append(
                DomainIssue(
                    domain_name=domain_to_check.domain_name,
                    severity=Severity.ERROR, 
                    category=PostgreSQLDatabaseIssueType.DATABASE_USER_DOES_NOT_EXIST_IN_PANEL,
                    problem=MSG(
                        PostgreSQLDatabaseIssueType.DATABASE_USER_DOES_NOT_EXIST_IN_PANEL,
                        user=user.login, database=database.target_name
                    )
                )
            )
            return False
        else:
            return True


def _list_pgsql_tables(database_name, database_access):
    query = (
        u"SELECT tablename FROM pg_catalog.pg_tables "
        u"WHERE schemaname NOT IN ('pg_catalog', 'information_schema')"
    )
    output = _execute_pgsql_query(
        database_name, database_access, database_access.admin_user, query
    )
    return set(output.strip().split('\n'))


def _check_db_access(database_name, database_access, user):
    try:
        stdout = _execute_pgsql_query(
            database_name, database_access, user,
            'select 1'
        )
        return stdout.strip() == '1'
    except NonZeroExitCodeException:
        # simply return false if psql client binary returned 
        # non-zero exit code
        return False


def _execute_pgsql_query(database_name, database_access, user, query):
    """
    :type database_name: str | unicode 
    :type database_access: parallels.hosting_check.PostgreSQLDatabaseAccess 
    :type user: parallels.hosting_check.User 
    :type query: str | unicode 
    :rtype: str 
    """
    env = {
        'PGUSER': user.login,
        'PGPASSWORD': user.password,
        'PGDATABASE': database_name
    }
    if database_access.host != 'localhost':
        env['PGHOST'] = database_access.host
        env['PGPORT'] = database_access.port

    command = 'psql -A -t -q -c {query}'
    args = dict(query=query)

    with database_access.server.runner() as runner:
        assert isinstance(runner, BaseRunner)
        return runner.execute_command(command, args, ExecutionOptions(environment_variables=env)).stdout
