import copy
import ntpath
import os
import pipes
import posixpath
import random
import string
import sys

from parallels.core.runners.unix.ssh import SSHRunner

# Python 2/3 compatibility
try:
    from urlparse import urljoin
except ImportError:
    from urllib.parse import urljoin, quote_plus
from contextlib import contextmanager, closing

import parallels.core
from parallels.core import messages
from parallels.core.logging import get_logger
from parallels.core.registry import Registry
from parallels.core.runners.web.exceptions import WebRpcAgentException
from parallels.core.runners.web.utils.stream_downloader.packet.error import PacketError
from parallels.core.runners.web.utils.stream_downloader.packet.proc_end import PacketProcEnd
from parallels.core.runners.web.utils.stream_downloader.packet.stderr_chunk import PacketStderrChunk
from parallels.core.runners.web.utils.stream_downloader.packet.stdout_chunk import PacketStdoutChunk
from parallels.core.runners.web.utils.stream_downloader.stream_packet_readers.plain_stream_packet_reader import \
    PlainStreamPacketReader
from parallels.core.utils import windows_utils, unix_utils
from parallels.core.utils.base64_utils import base64_decode
from parallels.core.utils.clean_logging import clean_dict_args_for_log
from parallels.core.utils.common import cached, default, safe_format, open_no_inherit
from parallels.core.utils.file_utils import get_base_path, get_relative_path
from parallels.core.utils.http_client import perform_http_request_response_text, HttpClientError, \
    perform_http_request_response_raw
from parallels.core.utils.json_utils import encode_json, decode_json
from parallels.core.utils.migrator_utils import get_package_extras_file_path
from parallels.core.utils.ssl_utils import is_ssl_validation_error

logger = get_logger(__name__)


class WebRPCAgent(object):
    # send data in raw format, encoded into JSON
    METHOD_RAW_DATA = 'raw-data'
    # send data in format so it is available in $_POST variable at the agent
    METHOD_POST_DATA = 'post-data'

    # request agent to provide result in JSON format
    RESULT_ENCODING_JSON_PLAIN = 'json_plain'
    # request agent to provide result in JSON format, where each string value is encoded with base64
    RESULT_ENCODING_JSON_BASE64 = 'json_base64'

    # placeholder in agent`s code for password constant
    PASSWORD_PLACEHOLDER = 'WEB_RPC_AGENT_PASSWORD_PLACEHOLDER'

    # If agent deploy mode is a single file mode, all files will be combined in a single file one by one.
    # Since any classes must be defined before the first usage in PHP, the file order matters.
    # Files from the following list will be used first in order defined in the list for building the single file.
    # Other files from agent`s directory will be added at the end of the single file in random order.
    FILES_ORDER = [
        'lib/result_encoder_json_plain.phpe',
        'lib/result_encoder_json_base64.phpe',
        'lib/result_encoder.phpe',
        'lib/task.phpe',
        'lib/stream_packet_writer.phpe',
        'lib/stream_result_task.phpe',
        'lib/agent.phpe',
        'lib/common_constants.phpe',
        'lib/directory_contents_writer.phpe',
        'lib/directory_stream_packet_writer.phpe',
        'lib/dispatcher.phpe',
        'lib/file_objects_writer.phpe',
        'lib/json_result_task.phpe',
        'lib/logger.phpe',
        'lib/php_functions.phpe',
        'lib/plain_stream_packet_writer.phpe',
        'lib/process_stream_packet_writer.phpe',
        'lib/registry.phpe',
        'lib/task_exception.phpe',
        'lib/utils/common.phpe',
        'lib/utils/file.phpe',
        'lib/utils/os.phpe',
        'task/download_dir.phpe',
        'task/echo.phpe',
        'task/get_base_path.phpe',
        'task/get_dir_tree.phpe',
        'task/get_file_contents.phpe',
        'task/get_file_names.phpe',
        'task/get_file_paths.phpe',
        'task/get_php_version.phpe',
        'task/is_file_exists.phpe',
        'task/is_windows.phpe',
        'task/mkdir.phpe',
        'task/proc_open.phpe',
        'task/proc_open_pipe.phpe',
        'task/remove_file.phpe',
        'task/upload_file_contents.phpe',
    ]

    def __init__(
        self, filesystem_runner,
        document_root_filesystem_runner_path, base_url, plesk_agent_id, verify_certificates=True,
        single_file_agent=False
    ):
        """
        :type filesystem_runner: parallels.core.runners.base.BaseRunner
        :type document_root_filesystem_runner_path: str | unicode
        :type base_url: str | unicode
        :type plesk_agent_id: str | unicode
        :type verify_certificates: bool
        :type single_file_agent: bool
        """
        self._filesystem_runner = filesystem_runner
        self._document_root_filesystem_runner_path = document_root_filesystem_runner_path
        self._base_url = base_url
        self._plesk_agent_id = plesk_agent_id
        self._method = self.METHOD_RAW_DATA
        self._verify_certificates = verify_certificates
        self._password = None
        self._single_file_agent = single_file_agent

    @property
    def url(self):
        return urljoin(self._base_url, '%s/index.php' % self.agent_dir_name)

    @property
    def document_root_filesystem_path(self):
        """Path to document root of a site to which Web RPC agent is installed.

        The path is acceptable by filesystem runner.

        :rtype: str | unicode
        """
        return self._document_root_filesystem_runner_path

    @property
    def web_rpc_agent_filesystem_path(self):
        """Path to a directory with Web RPC agent.

        The path is acceptable by filesystem runner.

        :rtype: str | unicode
        """
        return posixpath.join(self.document_root_filesystem_path, self.agent_dir_name)

    @property
    def agent_dir_name(self):
        """Name of a directory to which Web RPC agent is installed.

        :rtype: str | unicode
        """
        return 'plesk-migrator-agent-%s' % self._plesk_agent_id

    @property
    @cached
    def scripts_absolute_path(self):
        return self._process_request('get_base_path')

    @property
    def verify_certificates(self):
        """Is SSL certificates validation enabled

        :rtype: bool
        """
        return self._verify_certificates

    @property
    def password(self):
        """Authentication password

        :rtype: str | unicode | None
        """
        return self._password

    def deploy(self):
        # remove previously deployed agent scripts
        document_root_directories = self._filesystem_runner.get_directories_list(self.document_root_filesystem_path)
        if self.agent_dir_name in document_root_directories:
            self._filesystem_runner.remove_directory(self.web_rpc_agent_filesystem_path)

        # generate password
        size = 16
        chars = string.ascii_letters + string.digits
        self._password = ''.join(random.choice(chars) for _ in range(size))

        # create directory on source there web RPC agent scripts will be placed
        self._filesystem_runner.mkdir(self.web_rpc_agent_filesystem_path)
        if isinstance(self._filesystem_runner, SSHRunner):
            self._filesystem_runner.chmod(self.web_rpc_agent_filesystem_path, 0o755)
        # upload web RPC agent on remote server via FTP or SSH
        if self._single_file_agent:
            self._upload_agent_as_single_file()
        else:
            self._upload_agent()

    def connect(self):
        # detect method to send data and test the agent
        try:
            send_string = 'hello'
            response = self._process_request('echo', {'message': send_string})
            if response.get('message') != 'hello':
                self._method = self.METHOD_POST_DATA
                response = self._process_request('echo', {'message': send_string})
            if response.get('message') != 'hello':
                raise WebRpcAgentException(safe_format(
                    messages.WEB_RPC_AGENT_ECHO_COMMAND,
                    send=send_string, received=response.get('message')
                ))
        except Exception as e:
            logger.exception()
            if is_ssl_validation_error(e):
                raise WebRpcAgentException(messages.WEB_RPC_AGENT_SSL_CERTIFICATE_VALIDATION_FAILURE)
            raise WebRpcAgentException(messages.WEB_RPC_AGENT_FAILURE)

    def shutdown(self):
        self._filesystem_runner.remove_directory(self.web_rpc_agent_filesystem_path)

    @cached
    def is_windows(self):
        return self._process_request('is_windows')

    @cached
    def get_php_version(self):
        return self._process_request('get_php_version')

    @cached
    def get_absolute_filesystem_root_path(self):
        """Retrieve absolute path on source web server which point to the same directory as filesystem root

        :rtype: str
        """
        # e.g. /var/www/vhosts/example.com
        return get_base_path(
            # e.g. /var/www/vhosts/example.com/httpdocs/web-rpc-agent-12345
            self.scripts_absolute_path,
            get_relative_path(
                # e.g. /httpdocs/web-rpc-agent-12345
                self.web_rpc_agent_filesystem_path,
                '/', False
            ),
            self.is_windows(),
            relative_path_is_windows=False
        )

    def get_filesystem_path(self, path):
        is_absolute = ntpath.isabs(path) if self.is_windows() else posixpath.isabs(path)
        if is_absolute:
            relative_path = get_relative_path(
                path, self.get_absolute_filesystem_root_path(), False, self.is_windows(), self.is_windows()
            )
            return posixpath.join('/', relative_path)
        else:
            # assume that specified path related to the base agent directory
            return posixpath.join(self.web_rpc_agent_filesystem_path, path)

    def join_file_path(self, path, *paths):
        if self.is_windows():
            return ntpath.join(path, *paths)
        else:
            return posixpath.join(path, *paths)

    def is_file_exists(self, file_path):
        try:
            filesystem_file_path = self.get_filesystem_path(file_path)
            return self._filesystem_runner.file_exists(filesystem_file_path)
        except NotImplementedError:
            return self._process_request('is_file_exists', {'file_path': file_path})

    def get_file_names(self, path):
        return self._process_request('get_file_names', {'path': path})

    def get_file_paths(self, base_path, is_recursive):
        return self._process_request('get_file_paths', {'base_path': base_path, 'is_recursive': is_recursive})

    def get_dir_tree(self, base_path):
        return self._process_request(
            'get_dir_tree', {'base_path': base_path},
            result_encoding=self.RESULT_ENCODING_JSON_BASE64
        )

    def mkdir(self, dir_path):
        filesystem_dir_path = self.get_filesystem_path(dir_path)
        self._filesystem_runner.mkdir(filesystem_dir_path)

    def download_file(self, remote_file_path, local_file_path):
        filesytem_remote_file_path = self.get_filesystem_path(remote_file_path)
        self._filesystem_runner.get_file(filesytem_remote_file_path, local_file_path)

    def upload_file(self, local_file_path, remote_file_path):
        filesystem_remote_file_path = self.get_filesystem_path(remote_file_path)
        self._filesystem_runner.upload_file(local_file_path, filesystem_remote_file_path)

    def upload_file_contents(self, remote_file_path, content):
        filesystem_remote_file_path = self.get_filesystem_path(remote_file_path)
        self._filesystem_runner.upload_file_content(filesystem_remote_file_path, content)

    def remove_file(self, file_path):
        filesystem_file_path = self.get_filesystem_path(file_path)
        self._filesystem_runner.remove_file(filesystem_file_path)

    def get_file_contents(self, remote_file_path):
        """
        :type remote_file_path: str | unicode
        :rtype: str | unicode
        """
        return self._filesystem_runner.get_file_contents(
            self.get_filesystem_path(remote_file_path)
        )

    @contextmanager
    def deploy_probe(self, contents):
        probe_file_name = 'probe.php'
        probe_file_path = self.join_file_path(self.scripts_absolute_path, probe_file_name)
        self.upload_file_contents(probe_file_path, contents)
        yield urljoin(self._base_url, '%s/%s' % (self.agent_dir_name, probe_file_name))
        self.remove_file(probe_file_path)

    def sh_unchecked(self, command, arguments, execution_options):
        data, log_data = self._format_data_for_sh_unchecked(command, arguments, execution_options)

        result = self._process_request('proc_open', data, log_data=log_data)

        if not isinstance(result, dict) or {'exit_code', 'stdout', 'stderr'} != set(result):
            raise WebRpcAgentException(messages.WEB_RPC_AGENT_EXCEPTION_TASK_SH_UNCHECKED_WRONG_DATA)

        return result.get('exit_code'), result.get('stdout'), result.get('stderr')

    def piped_sh_unchecked(
        self,  command, arguments, execution_options,
        consume_stdout_function, consume_stderr_function, finalize_function
    ):
        """Execute command, and read stdout/stderr dynamically, as it goes.

        Stderr will be dropped on WebRpcAgent's side if it runs on Windows. This behaviour is caused by PHP
        limitations on Windows. PHP function stream_select does not work correct on Windows. There are no proper
        way available to handle both stdout and stderr pipes in Windows, avoid possible lock and keep compatibility
        with different PHP versions and environment.

        Consume stdout/stderr function is called once we get the next portion of data from command's stdout/stderr.
        The only argument it accepts is that data (as byte string).

        Once the command is finished, finalize function is called. The only argument is exit code of the command.

        :type command: str | unicode
        :type arguments: dict | None
        :type execution_options: parallels.core.runners.entities.ExecutionOptions
        :type consume_stdout_function: (str) -> None
        :type consume_stderr_function: (str) -> None
        :type finalize_function: (int) -> None
        """
        data, log_data = self._format_data_for_sh_unchecked(command, arguments, execution_options)

        self._piped_process_request(
            'proc_open_pipe', data, consume_stdout_function, consume_stderr_function, finalize_function,
            log_data=log_data
        )

    def format_command(self, cmd, args=None):
        """Format command arguments passed for 'sh_unchecked' methods into command line string

        This command line will be added to log and error messages, so it is expected that you could
        simply copy-paste it from logs or migrator output, and execute.

        :type cmd: str | unicode
        :type args: dict[str | unicode, str | unicode] | None
        :rtype: str | unicode
        """
        if args is not None and len(args) > 0:
            if self.is_windows():
                return windows_utils.format_command(cmd, **args)
            else:
                return unix_utils.format_command(cmd, **args)
        else:
            return cmd

    def _encode_data(self, data, log_data):
        """Encode request data and data prepared for log according to request method

        :type data: dict | None
        :type log_data: dict | None
        :rtype: tuple
        """
        if self._method == self.METHOD_RAW_DATA:
            request_data = encode_json(data)
            log_data = encode_json(default(log_data, data))
        else:
            request_data = urlencode_recursive(data)
            log_data = urlencode_recursive(default(log_data, data))

        return request_data, log_data

    def _format_request_url(self, task):
        """Build request

        :type task: str | unicode
        :rtype: str | unicode
        """
        return '{url}?task={task}'.format(url=self.url, task=task)

    def _add_password(self, data, log_data):
        """Add authentication password to data and log_data

        :type data: dict | None
        :type log_data: dict | None
        :rtype: tuple
        """
        if data is None:
            data = {}
        data.update(
            password=self._password
        )

        if log_data is None:
            log_data = {}
        log_data.update(
            clean_dict_args_for_log(
                dict(
                    password=self._password
                )
            )
        )

        return data, log_data

    def _format_data_for_sh_unchecked(self, command, arguments, execution_options):
        """Format data for request and logging for use with sh_unchecked or piped_sh_unchecked

        :type command: str | unicode
        :type arguments: dict[str | unicode, str | unicode] | None
        :type execution_options: parallels.core.runners.entities.ExecutionOptions
        :rtype: tuple[dict, dict]
        """
        return (
            {
                'cmd': self.format_command(command, arguments),
                'stdin_content': execution_options.stdin_content,
                'env': execution_options.environment_variables,
                'log_output': execution_options.log_output,
                'working_dir': execution_options.working_dir,
                'redirect_output_file': execution_options.redirect_output_file
            },
            {
                'cmd': self.format_command(command, clean_dict_args_for_log(arguments)),
                'stdin_content': execution_options.stdin_content,
                'env': clean_dict_args_for_log(execution_options.environment_variables),
                'log_output': execution_options.log_output,
                'working_dir': execution_options.working_dir,
                'redirect_output_file': execution_options.redirect_output_file
            }
        )

    def _process_request(
        self, task, data=None, log_response=True, log_data=None,
        result_encoding=RESULT_ENCODING_JSON_PLAIN, verify_certificates=None
    ):
        """Send request to web RPC agent to process given task with given data

        :type task: str  | unicode
        :type data: dict | None
        :type log_data: dict | None
        :type result_encoding: str | unicode
        :type verify_certificates: bool | None
        :rtype: int, dict
        """
        verify_certificates = default(verify_certificates, self._verify_certificates)

        request_url = self._format_request_url(task)
        if result_encoding != self.RESULT_ENCODING_JSON_PLAIN:
            request_url += '&result_encoding={result_encoding}'.format(result_encoding=result_encoding)

        if log_data is None and data is not None:
            log_data = copy.deepcopy(data)
        data, log_data = self._add_password(data, log_data)
        data_str, log_data_str = self._encode_data(data, log_data)

        context_message = messages.WEB_RPC_AGENT_REQUEST_WITH_DATA
        context_args = dict(task=task, data=log_data_str)
        with logger.debug_level_context(context_message, **context_args):
            logger.fdebug(
                messages.WEB_RPC_AGENT_REQUEST_DETAILS_WITH_DATA, url=request_url, data=log_data_str,
                command=safe_format(
                    'curl -d {data} {url}',
                    data=pipes.quote(log_data_str),
                    url=pipes.quote(request_url)
                )
            )

            try:
                response_data = perform_http_request_response_text(
                    request_url, data_str, verify_certificates=verify_certificates
                )
            except HttpClientError as e:
                try:
                    decoded_response_data = decode_json(e.content)
                    if not isinstance(decoded_response_data, dict):
                        error_message = None
                    else:
                        error_message = decoded_response_data.get('error')
                except Exception:
                    logger.exception()
                    error_message = None
                if error_message:
                    raise WebRpcAgentException(messages.WEB_RPC_AGENT_EXCEPTION_INTERNAL_ERROR.format(
                        task=task, error=error_message
                    ))
                else:
                    raise WebRpcAgentException(messages.WEB_RPC_AGENT_EXCEPTION_INTERNAL_ERROR_WO_MESSAGE.format(
                        task=task
                    ))
            try:
                decoded_response_data = decode_json(response_data)
                if result_encoding == self.RESULT_ENCODING_JSON_BASE64:
                    decoded_response_data = self._decode_base64_json(decoded_response_data)

            except Exception as e:
                logger.exception()
                raise WebRpcAgentException(messages.WEB_RPC_AGENT_EXCEPTION_RESPONSE_DECODE.format(
                    task=task, error=e
                ))
            if not isinstance(decoded_response_data, dict):
                raise WebRpcAgentException(messages.WEB_RPC_AGENT_EXCEPTION_RESPONSE_FORMAT.format(
                    task=task, actual_type=type(decoded_response_data), expected_type=type(dict)
                ))
            result = decoded_response_data.get('result')

            if log_response:
                logger.fdebug(messages.WEB_RPC_AGENT_RESPONSE_DATA, data=repr(result))

            return result

    def _piped_process_request(
        self, task, data, consume_stdout_function, consume_stderr_function, finalize_function,
        log_data=None, verify_certificates=None
    ):
        """Send request to web RPC agent to process given task with given data
        and read stdout/stderr dynamically, as it goes

        Consume stdout/stderr function is called once we get the next portion of data from command's stdout/stderr.
        The only argument it accepts is that data (as byte string).

        Once the command is finished, finalize function is called. The only argument is exit code of the command.

        :type task: str | unicode
        :type data: dict | None
        :type consume_stdout_function: (str) -> None
        :type consume_stderr_function: (str) -> None
        :type finalize_function: (int) -> None
        :type log_data: dict | None
        :type verify_certificates: bool | None
        """
        verify_certificates = default(verify_certificates, self._verify_certificates)
        request_url = self._format_request_url(task)

        if log_data is None and data is not None:
            log_data = copy.deepcopy(data)
        data, log_data = self._add_password(data, log_data)
        data_str, log_data_str = self._encode_data(data, log_data)

        context_message = messages.WEB_RPC_AGENT_REQUEST_WITH_DATA
        context_args = dict(task=task, data=log_data_str)
        with logger.debug_level_context(context_message, **context_args):
            logger.fdebug(
                messages.WEB_RPC_AGENT_REQUEST_DETAILS_WITH_DATA, url=request_url, data=log_data_str,
                command=safe_format(
                    'curl -d {data} {url}',
                    data=pipes.quote(log_data_str),
                    url=pipes.quote(request_url)
                )
            )

            try:
                with self._open_stream_packet_reader(
                        request_url, data_str, verify_certificates=verify_certificates
                ) as packet_reader:
                    for packet in packet_reader.iter_packets():
                        if isinstance(packet, PacketStdoutChunk):
                            consume_stdout_function(packet.data)
                        elif isinstance(packet, PacketStderrChunk):
                            consume_stderr_function(packet.data)
                        elif isinstance(packet, PacketProcEnd):
                            finalize_function(packet.exit_code)
                        elif isinstance(packet, PacketError):
                            raise WebRpcAgentException(messages.WEB_RPC_AGENT_ERROR_PACKET_PIPED.format(
                                error=packet.message
                            ))
            except WebRpcAgentException:
                raise
            except Exception as e:
                logger.exception()
                raise WebRpcAgentException(messages.WEB_RPC_AGENT_EXCEPTION_INTERNAL_ERROR.format(
                    task=task, error=e
                ))

    @contextmanager
    def _open_stream_packet_reader(
            self, url, data, headers=None, hostname=None, ip_address=None, verify_certificates=True
    ):
        """
        :type url: str | unicode
        :type data: str | unicode | None
        :type headers: dict | None
        :type hostname: str | unicode | None
        :type ip_address: str | unicode | None
        :type verify_certificates: bool
        """
        verify_certificates = default(verify_certificates, self._verify_certificates)
        response = perform_http_request_response_raw(
            url, data=data, headers=headers, hostname=hostname, ip_address=ip_address,
            verify_certificates=verify_certificates
        )
        with closing(response) as fp:
            yield PlainStreamPacketReader(fp)

    def _decode_base64_json(self, data):
        if sys.version_info.major >= 3:
            str_type = str
        else:
            str_type = unicode
        if isinstance(data, str) or isinstance(data, str_type):
            return base64_decode(data)
        elif isinstance(data, list) or isinstance(data, tuple):
            return [self._decode_base64_json(item) for item in data]
        elif isinstance(data, dict):
            return {self._decode_base64_json(key): self._decode_base64_json(value) for key, value in data.items()}

    def _upload_agent(self):
        logger.fdebug(messages.DEPLOY_WEB_RPC_AGENT_BY_COPY)

        migrator_server = Registry.get_instance().get_context().migrator_server
        web_rpc_agent_dir = get_package_extras_file_path(parallels.core, "web-rpc-agent")

        with migrator_server.runner() as local_runner:
            # iterate over directories and create it first
            for source_dir_path in local_runner.iter_dirs_list_nested(web_rpc_agent_dir):
                # Pass 'False' as the third argument to get_relative_path
                # because FTP/SSH always use '/' separator
                relative_path = get_relative_path(
                    source_dir_path, web_rpc_agent_dir,
                    False, migrator_server.is_windows(), migrator_server.is_windows()
                )

                # For development only. Skip copying of IDE files.
                if relative_path.startswith('.idea'):
                    continue

                self._filesystem_runner.mkdir(posixpath.join(self.web_rpc_agent_filesystem_path, relative_path))

            # upload files
            for source_file_path in local_runner.iter_files_list_nested(web_rpc_agent_dir):
                # Pass 'False' as the third argument to get_relative_path because FTP is always using '/' separator
                relative_path = get_relative_path(
                    source_file_path, web_rpc_agent_dir,
                    False, migrator_server.is_windows(), migrator_server.is_windows()
                )

                # For development only. Skip copying of IDE files.
                if relative_path.startswith('.idea'):
                    continue

                # PHP files have ".phpe" extension to avoid encrypting them by extension build scripts;
                # rename back to ".php" here so PHP files could work on web server
                if relative_path.endswith('.phpe'):
                    relative_path = relative_path[:-1]

                target_file_path = posixpath.join(self.web_rpc_agent_filesystem_path, relative_path)
                # flash password
                if relative_path == 'index.php':
                    with open_no_inherit(source_file_path, 'r') as f:
                        file_data = f.read()
                    file_data = file_data.replace(self.PASSWORD_PLACEHOLDER, self._password)
                    self._filesystem_runner.upload_file_content(target_file_path, file_data)
                else:
                    self._filesystem_runner.upload_file(source_file_path, target_file_path)

    def _upload_agent_as_single_file(self):
        logger.fdebug(messages.DEPLOY_WEB_RPC_AGENT_SINGLE_FILE)

        migrator_server = Registry.get_instance().get_context().migrator_server
        web_rpc_agent_dir = get_package_extras_file_path(parallels.core, "web-rpc-agent")

        # build agent files list
        agent_files = {}
        with migrator_server.runner() as local_runner:
            for source_file_path in local_runner.iter_files_list_nested(web_rpc_agent_dir):
                # Pass 'False' as the third argument to get_relative_path because FTP is always using '/' separator
                relative_path = get_relative_path(
                    source_file_path, web_rpc_agent_dir,
                    False, migrator_server.is_windows(), migrator_server.is_windows()
                )

                # For development only. Skip copying of IDE files.
                if relative_path.startswith('.idea'):
                    continue

                with open_no_inherit(source_file_path, 'r') as f:
                    file_data = f.read()
                agent_files.update({relative_path: file_data})

        # prepare single file content
        single_file_data = '<?php\n// Copyright 1999-2017. Plesk International GmbH. All rights reserved.\n'

        for priority_file in self.FILES_ORDER:
            single_file_data += self._prepare_agent_file_content(agent_files[priority_file], priority_file)

        for agent_file in sorted(agent_files):
            if agent_file not in self.FILES_ORDER:
                single_file_data += self._prepare_agent_file_content(agent_files[agent_file], agent_file)

        # save single file agent copy in session dir for debug purposes
        agent_dir = os.path.join(migrator_server.get_session_dir_path(), 'web-rpc-agent')
        if not os.path.exists(agent_dir):
            os.mkdir(agent_dir)
        single_file_path = os.path.join(agent_dir, 'index.php')
        with open_no_inherit(single_file_path, 'w') as f:
            f.write(single_file_data)

        # flash password
        single_file_data = single_file_data.replace(self.PASSWORD_PLACEHOLDER, self._password)

        # upload agent to the source server
        single_file_path = posixpath.join(self.web_rpc_agent_filesystem_path, 'index.php')
        self._filesystem_runner.upload_file_content(single_file_path, single_file_data)

    @staticmethod
    def _prepare_agent_file_content(content, filename):
        content = safe_format('\n\n\n// ***** {filename} *****\n', filename=filename).encode('utf8') + content
        content = content.replace(b'<?php', b'//<?php')
        content = content.replace(b'require_once', b'//require_once')
        return content


def urlencode_recursive(data):
    def flat_dict(dictionary, wrap_key=False):
        if sys.version_info.major >= 3:
            bytes_type = bytes
        else:
            bytes_type = str
        new_dict = {}
        for key, value in dictionary.items():
            key = quote_plus(bytes_type(key))
            if wrap_key:
                key = '[' + key + ']'
            if isinstance(value, dict):
                for new_key, new_value in flat_dict(value, True).items():
                    new_dict.update({key + new_key: new_value})
            else:
                value = quote_plus(bytes_type(value))
                new_dict.update({key: value})
        return new_dict
    return '&'.join([k + '=' + v for k, v in flat_dict(data).items()])
