import re
import sys

from parallels.core import get_logger, messages
from parallels.core.utils.common import is_empty
from parallels.core.utils.common.ip import resolve_all_safe
from parallels.core.utils.common.string_utils import safe_utf8_encode
from parallels.core.utils.entity import Entity
from parallels.core.utils.json_utils import decode_json
from parallels.core.utils.line_processor import LineProcessor, ChangedLineInfo, ReplaceResults


logger = get_logger(__name__)


def path_endswith(path, suffix):
    """Check whether the last part of the path matches specified suffix

    Works both for Windows and for Linux. It is considered that suffix contains entire item
    (filename or directory) - for example:
    path_endswith('/var/www/vhosts/mysite.tld/wordpress/wp-config.php', 'wordpress/wp-config.php') will match
    path_endswith('/var/www/vhosts/mysite.tld/mywordpress/wp-config.php', 'wordpress/wp-config.php') won't match

    :type path: str | unicode
    :type suffix: str | unicode
    :rtype: boolean
    """
    return path.replace('\\', '/').lower().endswith('/%s' % suffix.lower())


def replace_line_bounds(contents, condition_strs, start_strs, end_str, replace_mapping, regex=False):
    """Replace string in lines with provided conditions

    :type contents: str | unicode
    :type condition_strs: list[str]
    :type start_strs: list[str]
    :type end_str: str
    :type replace_mapping: dict[str | unicode, str | unicode]
    :type regex: bool
    :rtype: parallels.core.utils.line_processor.ReplaceResults
    """
    changed_lines = []
    processor = LineProcessor(contents)

    if isinstance(contents, str):
        condition_strs = [safe_utf8_encode(s) for s in condition_strs]
        start_strs = [safe_utf8_encode(s) for s in start_strs]
        end_str = safe_utf8_encode(end_str)
        replace_mapping = {safe_utf8_encode(k): safe_utf8_encode(v) for k, v in replace_mapping.items()}

    for line in processor.iter_lines():
        if any(c.lower() in line.contents.lower() for c in condition_strs):
            old_line_contents = line.contents

            split_result = None

            for start_str in start_strs:
                if regex:
                    split_result = _split_by_start_end_regex(
                        start_str, end_str, line.contents, case_insensitive=True
                    )
                else:
                    split_result = split_by_start_end(
                        start_str, end_str, line.contents, case_insensitive=True
                    )
                if split_result is not None:
                    break

            if split_result is None:
                continue

            before, data, after = split_result

            for replace_from, replace_to in replace_mapping.items():
                if data.lower() == replace_from.lower():
                    line.contents = before + replace_to + after
                    break

            if line.contents != old_line_contents:
                changed_lines.append(ChangedLineInfo(old_line_contents, line.contents, line.number))

    return ReplaceResults(processor.serialize(), changed_lines)


def replace_database_host_line(
    contents, condition_str, start_str, end_str, db_host_mapping, regex=False, boundaries_only=False
):
    """Replace database host in a line

    Algorithm is the following:
    for each line in contents, if line contains condition_str:
    1) if some source database host from database host mapping is in line - perform simple replace
    2) otherwise, consider everything between start_str and end_str as hostname, try to resolve it
    and then replace (if possible) according to database host mapping
    
    If boundaries_only variable is set to True, don't perform step #1, and replace only text within specified
    boundaries.

    :type contents: str | unicode
    :type condition_str: str
    :type start_str: str | unicode
    :type end_str: str | unicode
    :type db_host_mapping: dict[str | unicode, str | unicode]
    :type boundaries_only: bool
    :rtype: parallels.core.utils.line_processor.ReplaceResults
    """
    changed_lines = []
    processor = LineProcessor(contents)

    if isinstance(contents, str):
        condition_str = safe_utf8_encode(condition_str)
        start_str = safe_utf8_encode(start_str)
        end_str = safe_utf8_encode(end_str)
        db_host_mapping = {safe_utf8_encode(k): safe_utf8_encode(v) for k, v in db_host_mapping.items()}

    # filter out invalid empty values for source/target database hosts
    db_host_mapping = {k: v for k, v in db_host_mapping.items() if not is_empty(k) and not is_empty(v)}

    for line in processor.iter_lines():
        if condition_str in line.contents:
            old_line_contents = line.contents
            replaced = False
            if not boundaries_only:
                for source_db, target_db in db_host_mapping.items():
                    if source_db in line.contents:
                        # exact match for source database host
                        line.contents = line.contents.replace(source_db, target_db)
                        replaced = True

            if not replaced:
                # no exact match, try to find hostname in a line, resolve it and check if it is
                # in database host mapping
                if regex:
                    split_result = _split_by_start_end_regex(start_str, end_str, line.contents)
                else:
                    split_result = split_by_start_end(start_str, end_str, line.contents)
                if split_result is None:
                    continue

                before, hostname, after = split_result
                if hostname != 'localhost':
                    ips = resolve_all_safe(hostname)
                else:
                    ips = []

                for hostname_or_ip in ips + [hostname]:
                    if hostname_or_ip in db_host_mapping and db_host_mapping[hostname_or_ip] != hostname:
                        line.contents = before + db_host_mapping[hostname_or_ip] + after

            if line.contents != old_line_contents:
                changed_lines.append(ChangedLineInfo(old_line_contents, line.contents, line.number))

    return ReplaceResults(processor.serialize(), changed_lines)


def split_by_start_end(start_str, end_str, data_str, case_insensitive=False):
    """Split line into 3 parts: before start string, between start and end, after end string

    For example, if string is "aaaBEGINbbbENDccc", this function will return
    ('aaaBEGIN', 'bbb', 'ENDccc')
    If start string or end strings are not found, None is returned.

    :type start_str: str | unicode
    :type end_str: str | unicode
    :type data_str: str | unicode
    :type case_insensitive: bool
    :rtype: tuple[str | unicode] | None
    """
    if isinstance(data_str, str):
        start_str = safe_utf8_encode(start_str)
        end_str = safe_utf8_encode(end_str)

    search_data_str = data_str
    if case_insensitive:
        search_data_str = data_str.lower()

    search_start_str = start_str
    if case_insensitive:
        search_start_str = search_start_str.lower()

    search_end_str = end_str
    if case_insensitive:
        search_end_str = search_end_str.lower()

    if search_start_str in search_data_str:
        start_pos = search_data_str.find(search_start_str) + len(search_start_str)
        if search_end_str in search_data_str[start_pos + 1:]:
            end_pos = search_data_str[start_pos + 1:].find(search_end_str) + start_pos + 1
            return data_str[:start_pos], data_str[start_pos:end_pos], data_str[end_pos:]

    return None


def _split_by_start_end_regex(start_str, end_str, data_str, case_insensitive=False):
    """Split line into 3 parts: before start string, between start and end, after end string. Start and end are regex.

    For example, if string is "aaabbbccc", start is "a+" and end is "c+" this function will return
    ('aaa', 'bbb', 'ccc')
    If start string or end strings are not found, None is returned.

    :type start_str: str | unicode
    :type end_str: str | unicode
    :type data_str: str | unicode
    :type case_insensitive: bool
    :rtype: tuple[str | unicode] | None
    """
    regex_flags = re.IGNORECASE if case_insensitive else 0

    match_start = re.search(start_str, data_str, regex_flags)

    if match_start is None:
        return None

    start_str_end_pos = match_start.end()

    match_end = re.search(end_str, data_str[start_str_end_pos + 1:], regex_flags)

    if match_end is None:
        return None

    end_str_start_pos = match_end.start() + start_str_end_pos + 1

    return data_str[:start_str_end_pos], data_str[start_str_end_pos:end_str_start_pos], data_str[end_str_start_pos:]


class DatabaseAdjustQuery(object):
    def __init__(self, query_str, query_args, message, failure_message):
        """
        :type query_str: str | unicode
        :type query_args: tuple | list | dict
        :type message: str | unicode
        :type failure_message: str | unicode
        """
        self._query_str = query_str
        self._query_args = query_args
        self._message = message
        self._failure_message = failure_message

    @property
    def query_str(self):
        return self._query_str

    @property
    def query_args(self):
        return self._query_args

    @property
    def message(self):
        return self._message

    @property
    def failure_message(self):
        return self._failure_message


class ApplicationInstanceDatabase(Entity):
    def __init__(self, type, host, port, socket, username, password, name, prefix=None):
        self._type = type
        self._host = host
        self._port = port
        self._socket = socket
        self._username = username
        self._password = password
        self._name = name
        self._prefix = prefix

    @property
    def type(self):
        return self._type

    @property
    def host(self):
        return self._host

    @property
    def port(self):
        return self._port

    @property
    def socket(self):
        return self._socket

    @property
    def username(self):
        return self._username

    @property
    def password(self):
        return self._password

    @property
    def name(self):
        return self._name

    @property
    def prefix(self):
        return self._prefix


def iter_dir_tree_full_file_paths(server, tree, root_path):
    """
    :type server: parallels.core.connections.server.Server
    :type tree: list
    :type root_path: str | unicode
    :rtype: list[]
    """
    if sys.version_info.major >= 3:
        bytes_type = bytes
    else:
        bytes_type = basestring
    for item in tree:
        if isinstance(item, bytes_type):
            yield server.join_file_path(root_path, item)
        elif isinstance(item, dict):
            for subdir, subitems in item.items():
                for child_full_path in iter_dir_tree_full_file_paths(
                    server, subitems, server.join_file_path(root_path, subdir)
                ):
                    yield child_full_path


def get_database(php_code, application_instance):
    """Get database settings of application using passed PHP code.
    PHP code should return array with 'host', 'username', 'password', 'name' in json format.
    PHP code example for Wordpress:

        <?php
        include 'wp-config.php';
        echo json_encode(array(
            'host' => DB_HOST,
            'username' => DB_USER,
            'password' => DB_PASSWORD,
            'name' => DB_NAME
        ));

    Only MySQL is supported for now

    :type php_code: str | unicode
    :type application_instance: parallels.core.application.base.SingleConfigApplicationInstance
    :rtype: parallels.core.application.utils.ApplicationInstanceDatabase | None
    """
    try:
        with application_instance.server.runner() as runner:
            data = decode_json(runner.run_php(php_code))
            assert isinstance(data, dict)
    except Exception as e:
        logger.exception()
        logger.fwarn(
            messages.APPLICATION_GET_DATABASES_ERROR,
            application_instance_description=application_instance.description, reason=e
        )
        return ApplicationInstanceDatabase(
            'mysql', None, None, None, None, None, None, None
        )
    host, port, socket = split_db_host(data['host'])

    application_database = ApplicationInstanceDatabase(
        'mysql', host, port, socket, data['username'], data['password'], data['name'], data.get('prefix')
    )
    logger.fdebug(
        messages.APPLICATION_DETECTED_DATABASE,
        host=application_database.host, port=application_database.port,
        username=application_database.username, name=application_database.name,
        prefix=application_database.prefix
    )

    return application_database


def split_db_host(host_str):
    """Split string with database description to host, port, socket
    For example for string 'localhost:3306' will be returned: 'localhost', '3306', None

    :type host_str: str | unicode
    :rtype: tuple
    """
    host_parts = host_str.split(':')
    host = host_parts[0]
    port = host_parts[1] if len(host_parts) > 1 and host_parts[1].isdigit() else None
    socket = host_parts[1] if len(host_parts) > 1 and not host_parts[1].isdigit() else None
    return host, port, socket
