import os
import threading

from parallels.core.logging import get_logger
from parallels.core.registry import Registry
from parallels.core.runners.base import BaseRunner
from parallels.core.utils.common import cached
from parallels.core.utils.common.threading_utils import synchronized
from parallels.core.utils.entity import Entity
from parallels.core.utils.file_utils import read_file_contents
from parallels.plesk.utils.xml_rpc.plesk.operator.extension import ExtensionCallOperator
from parallels.plesk_multi_server import messages

logger = get_logger(__name__)


class ServiceNodeSSHKey(Entity):
    def __init__(self, private_key_filename, public_key):
        """Class constructor

        :type private_key_filename: str | unicode
        :type public_key: str | unicode
        """
        self._private_key_filename = private_key_filename
        self._public_key = public_key

    @property
    def private_key_filename(self):
        """Path to a file with private key

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

    @property
    def public_key(self):
        """Public key as string

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


class ServiceNodeSSHKeyPool(object):
    """
    :type _keys: dict[str | unicode, parallels.plesk_multi_server.connections.ssh_key_pool.ServiceNodeSSHKey]
    :type _lock: threading.Lock
    """
    def __init__(self):
        self._keys = {}
        self._lock = threading.Lock()

    @staticmethod
    @synchronized
    @cached
    def get_instance():
        return ServiceNodeSSHKeyPool()

    def get(self, pms_management_server, node_ip):
        """Configure SSH key authentication for specified service node, return private key

        :type pms_management_server: parallels.plesk.plesk_multi_server.connections.target_server.PleskMultiServerManagementServer
        :type node_ip: str | unicode
        :rtype: str | unicode
        """
        with self._lock:
            if node_ip not in self._keys:
                # Generate keys pair
                migrator_server = Registry.get_instance().get_context().migrator_server
                ssh_keys_dir = migrator_server.get_session_file_path('ssh-keys')
                with migrator_server.runner() as runner:
                    assert isinstance(runner, BaseRunner)
                    runner.mkdir(ssh_keys_dir)
                    private_key_filename = os.path.join(ssh_keys_dir, 'id_rsa.%s' % node_ip)
                    public_key_filename = '%s.pub' % private_key_filename
                    runner.execute_command(
                        'echo y | ssh-keygen -q -t rsa -P "" -f {private_key_filename}',
                        dict(
                            private_key_filename=private_key_filename
                        )
                    )
                    public_key = read_file_contents(public_key_filename).strip()
                    runner.remove_file(public_key_filename)

                # Put public key to service node
                pms_api = pms_management_server.plesk_multi_server_api()
                pms_api.send_multi_server(
                    ExtensionCallOperator.PleskMultiServer.PublicSSHKeyPutOperator(public_key=public_key),
                    node_ip=node_ip
                ).check()

                # Store in list of keys
                self._keys[node_ip] = ServiceNodeSSHKey(
                    public_key=public_key, private_key_filename=private_key_filename
                )

            return self._keys[node_ip].private_key_filename

    def remove_all(self, pms_management_server):
        """Remove all keys from service nodes and from this server. Use at clean up.

        :type pms_management_server: parallels.plesk.plesk_multi_server.connections.target_server.PleskMultiServerManagementServer
        :rtype: None
        """
        migrator_server = Registry.get_instance().get_context().migrator_server
        pms_api = pms_management_server.plesk_multi_server_api()

        with migrator_server.runner() as runner:
            for node_ip, key_info in self._keys.items():
                try:
                    runner.remove_file(key_info.private_key_filename)
                    pms_api.send_multi_server(
                        ExtensionCallOperator.PleskMultiServer.PublicSSHKeyDeleteOperator(
                            public_key=key_info.public_key
                        ),
                        node_ip=node_ip
                    ).check()
                except Exception as e:
                    logger.exception()
                    logger.fwarn(messages.FAILED_TO_REMOVE_SSH_KEY, node_ip=node_ip, reason=e)
