import re

from parallels.core import get_logger, safe_utf8_encode
from parallels.core.application.base import ApplicationModel, SingleConfigApplicationInstance
from parallels.core.application.utils import \
    replace_database_host_line, replace_line_bounds, path_endswith, get_database
from parallels.core.reports.model.issue import Issue
from parallels.core.utils.common import cached, safe_format, safe_string_repr
from parallels.core.utils.line_processor import LineProcessor, ReplaceResults
from parallels.core.utils.message_utils import multi_line_message
from parallels.plesk.source.web import messages

logger = get_logger(__name__)


class DrupalModel(ApplicationModel):
    @property
    def id(self):
        return 'drupal'

    @property
    def name(self):
        return 'Drupal'

    def get_instance(self, server, base_path):
        """Retrieve instance of Drupal

        :type server: parallels.core.connections.server.Server
        :type base_path: str
        :rtype: parallels.core.application.drupal.DrupalInstance
        """
        return DrupalInstance(server, safe_utf8_encode(base_path))

    def get_supported_database_types(self):
        return [self.DATABASE_TYPE_MYSQL]

    def _search_file(self, server, file_path, file_name):
        binary_file_path = safe_utf8_encode(file_path)
        drupal_config = 'sites/default/settings.php'
        if path_endswith(binary_file_path, drupal_config):
            file_contents = self._safe_get_file_contents(server, binary_file_path)
            if file_contents is None:
                return None
            if 'drupal' not in file_contents.lower():
                return None
        else:
            return None

        return self.get_instance(server, server.get_base_path(binary_file_path, depth=3))

    def _search_directory(self, server, directory_path, directory_name, directory_tree):
        return None


class DrupalInstance(SingleConfigApplicationInstance):
    @property
    def description(self):
        return safe_format(messages.APPLICATION_DESCRIPTION_DRUPAL, base_path=self.base_path)

    @property
    @cached
    def config_file_path(self):
        return self.server.join_file_path(self.base_path, 'sites', 'default', 'settings.php')

    def get_databases(self):
        php_code = multi_line_message("""
        <?php
        include '{config_file_path}';
        $conf = $databases['default']['default'];
        echo json_encode(array(
            'host' => $conf['host'].':'.$conf['port'],
            'username' => $conf['username'],
            'password' => $conf['password'],
            'name' => $conf['database'],
            'prefix' => $conf['prefix'],
        ));
        """).encode('ascii').format(config_file_path=self.config_file_path)
        return [get_database(php_code, self)]

    def fix_files(self, domain, url, domain_changed=False, url_changed=False):
        """Fix files, set proper domain and URL

        :type domain: str | unicode
        :type url: str | unicode
        :type domain_changed: bool
        :type url_changed: bool
        :rtype: list[parallels.core.reports.model.issue.Issue]
        """
        issues = []

        if not url_changed:
            return issues

        rel_path = url
        if isinstance(url, unicode):
            rel_path = url.encode('utf-8')
        if rel_path != '':
            rel_path = rel_path.rstrip('/') + '/'
        # htaccess replacements
        replace_map = {
            re.compile('^\s*RewriteBase /.*$'):
                '  RewriteBase /{relative_path}'.format(relative_path=rel_path.rstrip('/')),
            re.compile('^\s*ErrorDocument 404 .*/index\.php\s*$'):
                'ErrorDocument 404 /{relative_path}index.php'.format(relative_path=rel_path)
        }

        new_issues = []
        with self.server.runner() as runner:
            htaccess_path = self.server.join_file_path(self.base_path, '.htaccess')
            if not runner.file_exists(htaccess_path):
                return issues
            htaccess_file_contents = runner.get_file_contents(htaccess_path)
            processor = LineProcessor(htaccess_file_contents)
            for line in processor.iter_lines():
                for pattern, replacement in replace_map.items():
                    if pattern.match(line.contents) and replacement != line.contents:
                        new_issues.append(Issue(
                            'adjust-base-url-in-files',
                            Issue.SEVERITY_INFO,
                            safe_format(
                                messages.ADJUSTED_BASE_URL_IN_FILE,
                                filename=htaccess_path,
                                line_number=line.number,
                                before_fix=line.contents,
                                after_fix=safe_string_repr(replacement, omit_quotes=True, encoding='utf8')
                            )
                        ))
                        line.contents = replacement
            new_htaccess_file_contents = processor.serialize()
            if new_htaccess_file_contents != htaccess_file_contents:
                runner.upload_file_content(htaccess_path, new_htaccess_file_contents)
                issues.extend(new_issues)
        return issues

    def get_path_excludes(self, domain_changed=False, url_changed=False):
        """Returns paths which can be excluded from copying
        Some applications can be broken if base URL was changed and file cache contains old value
        As a simple solution copying of a cache can be skipped

        :type domain_changed: bool
        :type url_changed: bool
        :rtype: list[str | unicode]
        """
        return [
            '/sites/default/files/css/',
            '/sites/default/files/js/',
            '/sites/default/files/php/'
        ] if url_changed else []

    def fix_databases(self, database, domain, url, domain_changed=False, url_changed=False):
        """Adjust application databases, set proper application`s domain and URL

        :type database: parallels.core.application.utils.ApplicationInstanceDatabase
        :type domain: str | unicode
        :type url: str | unicode
        :type domain_changed: bool
        :type url_changed: bool
        :rtype: list[parallels.core.reports.model.issue.Issue]
        """
        if not url_changed:
            return []

        db_connection_args = dict(
            user=database.username,
            database=database.name,
            charset='utf8',
            host=database.host,
            port=database.port,
            password=database.password,
            autocommit=True
        )
        # Iter over all tables with cache for all versions of Drupal
        truncated_tables = []
        for table in self._get_cache_tables():
            table_name = '{prefix}{table}'.format(prefix=database.prefix, table=table)
            query = 'TRUNCATE {table_name}'.format(table_name=table_name)
            try:
                # Try to truncate table
                self._server.mysql_query(db_connection_args, query)
                logger.fdebug(messages.QUERY_FOR_BASE_URL_ADJUSTMENT_EXECUTED, query=query)
                truncated_tables.append(table_name)
            except Exception:
                # List of queries contains queries for different versions of Drupal
                # We do not detect version of Drupal for now and try to execute each query
                # Expected, that some queries can fail, just log exception in debug log and go ahead
                logger.exception()
        if len(truncated_tables) == 0:
            return []
        logger.finfo(
            messages.ADJUST_DRUPAL_DB_TRUNCATE_CACHE,
            truncated_tables=', '.join(truncated_tables)
        )
        return [Issue(
            'adjust-base-url-in-database',
            Issue.SEVERITY_INFO,
            safe_format(
                messages.ADJUST_DRUPAL_DB_TRUNCATE_CACHE,
                truncated_tables=', '.join(truncated_tables)
            )
        )]

    @staticmethod
    def _get_cache_tables():
        """Returns list of known tables with cache in all versions of Drupal

        :rtype: list[str | unicode]
        """
        return [
            # Drupal 8
            'cache_config',
            'cache_container',
            'cache_data',
            'cache_default',
            'cache_discovery',
            'cache_dynamic_page_cache',
            'cache_entity',
            'cache_menu',
            'cache_render',
            'cache_toolbar',
            # older versions of Drupal
            'cache',
            'cache_block',
            'cache_bootstrap',
            'cache_field',
            'cache_filter',
            'cache_form',
            'cache_image',
            'cache_menu',
            'cache_page',
            'cache_path',
            'cache_token',
            'cache_update'
        ]

    def _get_database_hostname_replace_result(self, config_file_contents, database_hostname_mapping):
        """Replace database hostnames in configuration file of application, provided by string

        :type config_file_contents: str | unicode
        :type database_hostname_mapping: dict[str | unicode, str | unicode]
        :rtype: parallels.core.utils.line_processor.ReplaceResults
        """
        # Default config has line like:
        # "'host' => '192.168.1.2'"
        return replace_database_host_line(
            config_file_contents,
            "host", """['"]host['"]\s*=>\s*['"]""", """['"],""",
            database_hostname_mapping,
            regex=True,
            # Avoid replacing all "localhost" entries (e.g. memcached server option, PHP comments, etc) if we
            # migrate from "localhost" to remote MySQL server; perform replacement only in exact place
            # with database hostname. If option is set to False, the function will replace "localhost" with
            # remote server IP in each line, where we have "host" string, which means that every "localhost"
            # entry will be replaced with remote server IP.
            boundaries_only=True
        )

    def _get_database_name_replace_result(self, config_file_contents, database_name_mapping):
        """Replace database names in configuration file of application, provided by string

        :type config_file_contents: str | unicode
        :type database_name_mapping: dict[str | unicode, str | unicode]
        :rtype: parallels.core.utils.line_processor.ReplaceResults
        """
        # Default config has line like:
        # "'database' => 'mydrupal1'"
        return replace_line_bounds(
            config_file_contents, ["'database'", '"database"'], ["""['"]database['"]\s*=>\s*['"]"""], """['"]""",
            database_name_mapping,
            regex=True
        )

    def _get_database_user_name_replace_result(self, config_file_contents, database_user_name_mapping):
        """Replace database user names in configuration file of application, provided by string

        :type config_file_contents: str | unicode
        :type database_user_name_mapping: dict[str | unicode, str | unicode]
        :rtype: parallels.core.utils.line_processor.ReplaceResults
        """
        # Default config has line like:
        # "'username' => 'mydrupal1'"
        initial_replace_results = replace_line_bounds(
            config_file_contents, ["'username'", '"username"'], ["""['"]username['"]\s*=>\s*['"]"""], """['"]""",
            database_user_name_mapping,
            regex=True
        )
        overrides_replace_results = replace_line_bounds(
            initial_replace_results.new_contents,
            ["'username'", '"username"'], ["""\[['"]username['"]\]\s*=\s*['"]"""], """['"]\s*;\s*""",
            database_user_name_mapping,
            regex=True
        )
        return ReplaceResults(
            overrides_replace_results.new_contents,
            initial_replace_results.changed_lines + overrides_replace_results.changed_lines
        )
