# Python 2/3 compatibility
try:
    from urlparse import urlunparse
except ImportError:
    from urllib.parse import urlunparse


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, path_endswith, replace_line_bounds, \
    ApplicationInstanceDatabase, split_by_start_end, split_db_host, DatabaseAdjustQuery, get_database
from parallels.core.reports.model.issue import Issue
from parallels.core.utils.common import cached, safe_format, is_empty
from parallels.core.utils.message_utils import multi_line_message
from parallels.plesk.source.web import messages

logger = get_logger(__name__)


class MagentoModel(ApplicationModel):
    @property
    def id(self):
        return 'magento'

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

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

        :type server: parallels.core.connections.server.Server
        :type base_path: str
        :rtype: parallels.core.application.magento.MagentoInstance
        """
        return MagentoInstance(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)
        magento1_config = 'app/etc/local.xml'
        magento2_config = 'app/etc/config.php'
        if path_endswith(binary_file_path, magento1_config):
            file_contents = self._safe_get_file_contents(server, binary_file_path)
            if file_contents is None:
                return None
            if 'magento' not in file_contents.lower():
                return None
        elif path_endswith(binary_file_path, magento2_config):
            file_contents = self._safe_get_file_contents(server, binary_file_path)
            if file_contents is None:
                return None
            if 'magento_' 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 MagentoInstance(SingleConfigApplicationInstance):
    MAGENTO_V1 = '1'
    MAGENTO_V2 = '2'

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

    @property
    @cached
    def _v1_config_file_path(self):
        return self.server.join_file_path(self.base_path, 'app', 'etc', 'local.xml')

    @property
    @cached
    def _v2_config_file_path(self):
        return self.server.join_file_path(self.base_path, 'app', 'etc', 'env.php')

    @property
    @cached
    def config_file_path(self):
        if self.version == self.MAGENTO_V1:
            return self._v1_config_file_path
        if self.version == self.MAGENTO_V2:
            return self._v2_config_file_path
        return None

    @property
    @cached
    def version(self):
        with self.server.runner() as runner:
            if runner.file_exists(self._v1_config_file_path):
                return self.MAGENTO_V1
            if runner.file_exists(self._v2_config_file_path):
                return self.MAGENTO_V2
        return 'unknown'

    def get_databases(self):
        if self.version == self.MAGENTO_V1:
            with self.server.runner() as runner:
                file_contents = runner.get_file_contents(self.config_file_path)
                if file_contents is not None:
                    username = self._get_v1_db_connection_config_param('username', file_contents)
                    password = self._get_v1_db_connection_config_param('password', file_contents)
                    db_name = self._get_v1_db_connection_config_param('dbname', file_contents)
                    host = self._get_v1_db_connection_config_param('host', file_contents)
                    host, port, socket = split_db_host(host)
                    prefix = self._get_v1_db_connection_config_param('table_prefix', file_contents)

                    if (
                        not is_empty(username) and
                        not is_empty(password) and
                        not is_empty(db_name) and
                        not is_empty(host)
                    ):
                        return [
                            ApplicationInstanceDatabase(
                                'mysql', host, port, socket, username, password, db_name, prefix
                            )
                        ]
        elif self.version == self.MAGENTO_V2:
            php_code = multi_line_message("""
            <?php
            $conf = include '{config_file_path}';
            $dbconf = $conf['db']['connection']['default'];
            echo json_encode(array(
                'host' => $dbconf['host'],
                'username' => $dbconf['username'],
                'password' => $dbconf['password'],
                'name' => $dbconf['dbname'],
                'prefix' => $conf['db']['table_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 [
            '/var/cache/',
            '/var/page_cache/'
        ]

    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_WARNING, 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.endswith('/'):
            path += '/'
        unsecure_base_url = urlunparse((
            'http',  # scheme
            domain,  # netloc
            path,  # url
            None, None, None  # params, query, fragment
        ))
        secure_base_url = urlunparse((
            'https',  # scheme
            domain,  # netloc
            path,  # url
            None, None, None  # params, query, fragment
        ))
        if prefix is None:
            prefix = ''
        return [
            DatabaseAdjustQuery(
                query_str="""UPDATE {prefix}core_config_data 
SET value = %(base_url)s 
WHERE path = 'web/unsecure/base_url'""".format(prefix=prefix),
                query_args=dict(
                    base_url=unsecure_base_url
                ),
                message=safe_format(
                    messages.ADJUST_MAGENTO_DB_WEB_UNSECURE_BASE_URL, prefix=prefix, base_url=unsecure_base_url
                ),
                failure_message=safe_format(
                    messages.ADJUST_MAGENTO_DB_WEB_UNSECURE_BASE_URL_FAILURE, prefix=prefix, base_url=unsecure_base_url
                )
            ),
            DatabaseAdjustQuery(
                query_str="""UPDATE {prefix}core_config_data 
SET value = %(base_url)s 
WHERE path = 'web/secure/base_url'""".format(prefix=prefix),
                query_args=dict(
                    base_url=secure_base_url
                ),
                message=safe_format(
                    messages.ADJUST_MAGENTO_DB_WEB_SECURE_BASE_URL, prefix=prefix, base_url=secure_base_url
                ),
                failure_message=safe_format(
                    messages.ADJUST_MAGENTO_DB_WEB_SECURE_BASE_URL_FAILURE, prefix=prefix, base_url=secure_base_url
                )
            )
        ]

    @staticmethod
    def _get_v1_db_connection_config_param(param, config_content):
        """
        Get database connection parameter from Magento 1.x config (local.xml file).
        String with parameter looks like:

            <host><![CDATA[localhost:3306]]></host>

        Pass to function 'host' as 'param' and local.xml file's content as 'config_content'
        to receive 'localhost:3306'.

        :type param: str | unicode
        :type config_content: str | unicode
        :rtype: str | unicode
        """
        items = split_by_start_end(
            '<{param}><![CDATA['.format(param=param),
            ']]></{param}>'.format(param=param),
            config_content
        )
        if items is None or len(items) != 3:
            return None

        _, value, _ = items

        return value

    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.MAGENTO_V2:
            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
            )
        else:
            # Default config has line like:
            # <host><![CDATA[10.52.143.116:3306]]></host>
            return replace_database_host_line(
                config_file_contents,
                'host', '<host><\!\[CDATA\[', '[:\]]',
                database_hostname_mapping,
                regex=True,
                # Avoid replacing all "localhost" entries (e.g. memcached server option, XML 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
        """
        if self.version == self.MAGENTO_V2:
            return replace_line_bounds(
                config_file_contents,
                ['dbname'], ["""['"]dbname['"]\s*=>\s*['"]"""],  """['"],""",
                database_name_mapping,
                regex=True
            )
        else:
            # Default config has line like:
            # <dbname><![CDATA[magento_8]]></dbname>
            return replace_line_bounds(
                config_file_contents, ['dbname'], ['<dbname><![CDATA['], ']', database_name_mapping
            )

    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.MAGENTO_V2:
            return replace_line_bounds(
                config_file_contents,
                ['username'], ["""['"]username['"]\s*=>\s*['"]"""],  """['"],""",
                database_user_name_mapping,
                regex=True
            )
        else:
            # Default config has line like:
            # <username><![CDATA[magento_8]]></username>
            return replace_line_bounds(
                config_file_contents, ['username'], ['<username><![CDATA['], ']', database_user_name_mapping
            )
