import re
import sys

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, \
    DatabaseAdjustQuery, 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.common.string_utils import safe_utf8_decode
from parallels.core.utils.line_processor import LineProcessor
from parallels.core.utils.message_utils import multi_line_message
from parallels.core.utils.migrator_utils import safe_idn_decode
from parallels.plesk.source.web import messages

logger = get_logger(__name__)


class PrestashopModel(ApplicationModel):
    @property
    def id(self):
        return 'prestashop'

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

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

        :type server: parallels.core.connections.server.Server
        :type base_path: str
        :rtype: parallels.core.application.prestashop.PrestashopInstance
        """
        return PrestashopInstance(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)
        # 'config/settings.inc.php' is deprecated in PS 1.7
        prestashop1_6_config = 'config/settings.inc.php'
        prestashop1_7_config = 'app/config/parameters.php'
        if path_endswith(binary_file_path, prestashop1_6_config):
            file_contents = self._safe_get_file_contents(server, binary_file_path)
            if file_contents is None:
                return None
            if '_PS_' not in file_contents:
                return None

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

        elif path_endswith(binary_file_path, prestashop1_7_config):
            file_contents = self._safe_get_file_contents(server, binary_file_path)
            if file_contents is None:
                return None
            if 'ps_' not in file_contents:
                return None

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

        return None

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


class PrestashopInstance(SingleConfigApplicationInstance):
    PRESTASHOP_V1_6 = '1.6'
    PRESTASHOP_V1_7 = '1.7'

    @property
    def description(self):
        return safe_format(messages.APPLICATION_DESCRIPTION_PRESTASHOP, base_path=self.base_path)

    @property
    @cached
    def _v1_6_config_file_path(self):
        return self.server.join_file_path(self.base_path, 'config', 'settings.inc.php')

    @property
    @cached
    def _v1_7_config_file_path(self):
        return self.server.join_file_path(self.base_path, 'app', 'config', 'parameters.php')

    @property
    @cached
    def config_file_path(self):
        if self.version == self.PRESTASHOP_V1_6:
            return self._v1_6_config_file_path
        if self.version == self.PRESTASHOP_V1_7:
            return self._v1_7_config_file_path
        return None

    @property
    @cached
    def version(self):
        with self.server.runner() as runner:
            if runner.file_exists(self._v1_6_config_file_path):
                file_contents = runner.get_file_contents(self._v1_6_config_file_path)
                if '_PS_' in file_contents:
                    return self.PRESTASHOP_V1_6
            if runner.file_exists(self._v1_7_config_file_path):
                file_contents = runner.get_file_contents(self._v1_7_config_file_path)
                if 'ps_' in file_contents:
                    return self.PRESTASHOP_V1_7
        return 'unknown'

    def get_databases(self):
        if self.version == self.PRESTASHOP_V1_6:
            php_code = multi_line_message("""
            <?php
            include '{config_file_path}';
            echo json_encode(array(
                'host' => _DB_SERVER_,
                'username' => _DB_USER_,
                'password' => _DB_PASSWD_,
                'name' => _DB_NAME_,
                'prefix' => _DB_PREFIX_,
            ));
            """).format(config_file_path=self.config_file_path)
            return [get_database(php_code, self)]
        elif self.version == self.PRESTASHOP_V1_7:
            php_code = multi_line_message("""
            <?php
            $conf = include '{config_file_path}';
            $dbconf = $conf['parameters'];
            echo json_encode(array(
                'host' => $dbconf['database_host'].':'.$dbconf['database_port'],
                'username' => $dbconf['database_user'],
                'password' => $dbconf['database_password'],
                'name' => $dbconf['database_name'],
                'prefix' => $dbconf['database_prefix'],
            ));
            """).encode('ascii').format(config_file_path=self.config_file_path)
            return [get_database(php_code, self)]

        return []

    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 [
            '/app/cache/',
            '/cache/smarty/compile/',
            '/cache/smarty/cache/'
        ]

    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]
        """
        domain = safe_utf8_decode(domain)

        issues = []

        if not domain_changed and not url_changed:
            return issues

        rel_path = url
        if sys.version_info.major >= 3:
            str_type = str
        else:
            str_type = unicode
        if isinstance(url, str_type):
            rel_path = url.encode('utf-8')
        if rel_path != '':
            rel_path = rel_path.rstrip('/') + '/'
        replace_map = {
            # Cosmetic replacement in comment
            re.compile('^#Domain: .*$'):
                '#Domain: {domain}'.format(domain=safe_idn_decode(domain).encode('utf-8')),
            # The most important replacement
            re.compile('^RewriteRule \. - \[E=REWRITEBASE:/.*\]$'):
                'RewriteRule . - [E=REWRITEBASE:/{relative_path}]'.format(relative_path=rel_path),
            # Replacement for correct displaying on 404 error page
            re.compile('^ErrorDocument 404 .*/index\.php\?controller=404$'):
                'ErrorDocument 404 /{relative_path}index.php?controller=404'.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 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]
        """
        issues = []

        if not domain_changed and not url_changed:
            return issues

        for query in self._get_base_url_adjust_queries(domain, url, database.prefix):
            try:
                # adjust
                db_connection_args = dict(
                    user=database.username,
                    database=database.name,
                    charset='utf8',
                    host=database.host,
                    port=database.port,
                    password=database.password,
                    autocommit=True
                )
                self._server.mysql_query(db_connection_args, query.query_str, query.query_args)
                # logging
                logger.fdebug(
                    messages.QUERY_FOR_BASE_URL_ADJUSTMENT_EXECUTED,
                    query=query.query_str % query.query_args
                )
                logger.finfo(query.message)
                issues.append(
                    Issue('adjust-base-url-in-database', Issue.SEVERITY_INFO, query.message)
                )
            except Exception as e:
                # failure logging
                logger.exception()
                logger.fwarn(
                    messages.FAILED_TO_EXECUTE_QUERY_FOR_BASE_URL_ADJUSTMENT,
                    query=query.query_str % query.query_args, error=e
                )
                issues.append(
                    Issue('adjust-base-url-in-database', Issue.SEVERITY_INFO, query.failure_message)
                )
        return issues

    @staticmethod
    def _get_base_url_adjust_queries(domain, relative_path, prefix=None):
        """Returns queries which can be executed to adjust base URL in application database

        :type domain: str | unicode
        :type relative_path: str | unicode
        :type prefix: str | unicode | None
        :rtype: list[parallels.core.application.utils.DatabaseAdjustQuery]
        """
        path = relative_path
        if not path.startswith('/'):
            path = '/' + path
        if not path.endswith('/'):
            path += '/'
        if prefix is None:
            prefix = ''

        domain_db = domain.lower()

        return [
            DatabaseAdjustQuery(
                query_str="""UPDATE {prefix}configuration
SET value = %(domain)s
WHERE name = 'PS_SHOP_DOMAIN'""".format(prefix=prefix),
                query_args=dict(
                    domain=domain_db
                ),
                message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_PS_SHOP_DOMAIN, prefix=prefix, domain=domain_db
                ),
                failure_message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_PS_SHOP_DOMAIN_FAILURE, prefix=prefix, domain=domain_db
                ),
            ),
            DatabaseAdjustQuery(
                query_str="""UPDATE {prefix}configuration
SET value = %(domain)s
WHERE name = 'PS_SHOP_DOMAIN_SSL'""".format(prefix=prefix),
                query_args=dict(
                    domain=domain_db
                ),
                message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_PS_SHOP_DOMAIN_SSL, prefix=prefix, domain=domain_db
                ),
                failure_message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_PS_SHOP_DOMAIN_SSL_FAILURE, prefix=prefix, domain=domain_db
                )
            ),
            DatabaseAdjustQuery(
                query_str="""UPDATE {prefix}shop_url
SET domain = %(domain)s 
WHERE id_shop_url = '1' 
AND id_shop = '1'""".format(prefix=prefix),
                query_args=dict(
                    domain=domain_db
                ),
                message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_DOMAIN, prefix=prefix, domain=domain_db
                ),
                failure_message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_DOMAIN_FAILURE, prefix=prefix, domain=domain_db
                )
            ),
            DatabaseAdjustQuery(
                query_str="""UPDATE {prefix}shop_url
SET domain_ssl = %(domain)s 
WHERE id_shop_url = '1' 
AND id_shop = '1'""".format(prefix=prefix),
                query_args=dict(
                    domain=domain_db
                ),
                message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_DOMAIN_SSL, prefix=prefix, domain=domain_db
                ),
                failure_message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_DOMAIN_SSL_FAILURE, prefix=prefix, domain=domain_db
                )
            ),
            DatabaseAdjustQuery(
                query_str="""UPDATE {prefix}shop_url
SET physical_uri = %(path)s
WHERE id_shop_url = '1' 
AND id_shop = '1'""".format(prefix=prefix),
                query_args=dict(
                    path=path
                ),
                message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_PHYSICAL_URI, prefix=prefix, path=path
                ),
                failure_message=safe_format(
                    messages.ADJUST_PRESTASHOP_DB_PHYSICAL_URI_FAILURE, prefix=prefix, path=path
                )
            ),
        ]

    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
        """
        if self.version == self.PRESTASHOP_V1_7:
            # Example:
            #     'database_host' => 'localhost',
            return replace_database_host_line(
                config_file_contents,
                'database_host', """['"]database_host['"]\s*=>\s*['"]""", """['"],""",
                database_hostname_mapping,
                regex=True
            )
        else:
            # default config has line like:
            # define('_DB_SERVER_', '192.168.1.2');
            return replace_database_host_line(
                config_file_contents,
                '_DB_SERVER_', """define\(['"]_DB_SERVER_['"]\s*,\s*['"]""", """(:|['"]\);)""",
                database_hostname_mapping,
                regex=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
        """
        if self.version == self.PRESTASHOP_V1_7:
            # Example:
            #     'database_name' = > 'prestashop_6',
            return replace_line_bounds(
                config_file_contents,
                ['database_name'], ["""['"]database_name['"]\s*=>\s*['"]"""], """['"],""",
                database_name_mapping, regex=True
            )
        else:
            # default config has line like:
            # define('_DB_NAME_', 'prestashop1');
            return replace_line_bounds(
                config_file_contents,
                ['_DB_NAME_'], ["""['"]_DB_NAME_['"]\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
        """
        if self.version == self.PRESTASHOP_V1_7:
            # Example:
            #     'database_user' => 'prestashop_f',
            return replace_line_bounds(
                config_file_contents,
                ['database_user'], ["""['"]database_user['"]\s*=>\s*['"]"""], """['"],""",
                database_user_name_mapping, regex=True
            )
        else:
            # default config has line like:
            # define('_DB_USER_', 'prestashop1');
            return replace_line_bounds(
                config_file_contents,
                ['_DB_USER_'], ["""['"]_DB_USER_['"]\s*,\s*['"]"""], """['"]""",
                database_user_name_mapping, regex=True
            )
