"""All functionality needed to setup a computer."""

import os
from copy import copy
from os import path
from subprocess import CalledProcessError, call, check_output

import deprecation
import ipywidgets as ipw
import pexpect
import shortuuid
from aiida.common import NotExistent
from aiida.orm import Computer, QueryBuilder, User
from aiida.transports.plugins.ssh import parse_sshconfig
from IPython.display import clear_output
from traitlets import (
    Bool,
    Dict,
    Float,
    Instance,
    Int,
    Unicode,
    Union,
    link,
    observe,
    validate,
)

STYLE = {"description_width": "200px"}


@deprecation.deprecated(
    deprecated_in="1.3",
    removed_in="2.0",
    details="Use the ComputationalResourcesWidget instead.",
)
class SshComputerSetup(ipw.VBox):
    """Setup password-free access to a computer."""

    setup_counter = Int(0)  # Traitlet to inform other widgets about changes
    hostname = Unicode()
    port = Union([Unicode(), Int()])
    use_proxy = Bool()
    username = Unicode()
    proxy_hostname = Unicode()
    proxy_username = Unicode()

    def __init__(self, **kwargs):
        computer_image = ipw.HTML(
            '<img width="200px" src="../miscellaneous/images/computer.png">'
        )

        # Username.
        inp_username = ipw.Text(
            description="SSH username:", layout=ipw.Layout(width="350px"), style=STYLE
        )
        link((inp_username, "value"), (self, "username"))

        # Port.
        inp_port = ipw.IntText(
            description="SSH port:",
            value=22,
            layout=ipw.Layout(width="350px"),
            style=STYLE,
        )
        link((inp_port, "value"), (self, "port"))

        # Hostname.
        inp_computer_hostname = ipw.Text(
            description="Computer hostname:",
            layout=ipw.Layout(width="350px"),
            style=STYLE,
        )
        link((inp_computer_hostname, "value"), (self, "hostname"))

        # Upload private key directly.
        self._inp_password = ipw.Password(
            description="SSH password:",
            layout=ipw.Layout(width="150px"),
            style=STYLE,
            disabled=False,
        )
        self._inp_private_key = ipw.FileUpload(
            accept="",
            layout=ipw.Layout(width="350px"),
            style=STYLE,
            description="Private key",
            multiple=False,
            disabled=True,
        )
        self._verification_mode = ipw.Dropdown(
            options=["password", "private_key"],
            layout=ipw.Layout(width="350px"),
            style=STYLE,
            value="password",
            description="verification mode:",
            disabled=False,
        )
        self._verification_mode.observe(
            self.on_use_verification_mode_change, names="value"
        )

        # Proxy ssh settings.
        inp_use_proxy = ipw.Checkbox(value=False, description="Use proxy")
        inp_use_proxy.observe(self.on_use_proxy_change, names="value")
        link((inp_use_proxy, "value"), (self, "use_proxy"))

        inp_proxy_hostname = ipw.Text(
            description="Proxy server address:",
            layout=ipw.Layout(width="350px"),
            style=STYLE,
        )
        link((inp_proxy_hostname, "value"), (self, "proxy_hostname"))

        self._use_diff_proxy_username = ipw.Checkbox(
            value=False,
            description="Use different username and password",
            layout={"width": "initial"},
        )
        self._use_diff_proxy_username.observe(
            self.on_use_diff_proxy_username_change, names="value"
        )
        inp_proxy_username = ipw.Text(
            value="",
            description="Proxy server username:",
            layout=ipw.Layout(width="350px"),
            style=STYLE,
        )
        link((inp_proxy_username, "value"), (self, "proxy_username"))

        self._inp_proxy_password = ipw.Password(
            value="",
            description="Proxy server password:",
            layout=ipw.Layout(width="138px"),
            style=STYLE,
        )

        # Setup ssh button and output.
        btn_setup_ssh = ipw.Button(description="Setup ssh")
        btn_setup_ssh.on_click(self.on_setup_ssh)
        self._setup_ssh_out = ipw.Output()

        # Defining widgets positions.
        computer_ssh_box = ipw.VBox(
            [
                inp_computer_hostname,
                inp_port,
                inp_username,
                self._verification_mode,
                self._inp_password,
                self._inp_private_key,
                inp_use_proxy,
            ],
            layout=ipw.Layout(width="400px"),
        )
        self._proxy_user_password_box = ipw.VBox(
            [inp_proxy_username, self._inp_proxy_password],
            layout={"visibility": "hidden"},
        )
        self._proxy_ssh_box = ipw.VBox(
            [
                inp_proxy_hostname,
                self._use_diff_proxy_username,
                self._proxy_user_password_box,
            ],
            layout={"visibility": "hidden", "width": "400px"},
        )

        children = [
            ipw.HBox([computer_image, computer_ssh_box, self._proxy_ssh_box]),
            btn_setup_ssh,
            self._setup_ssh_out,
        ]
        super().__init__(children, **kwargs)

    @staticmethod
    def _ssh_keygen():
        """Generate ssh key pair."""
        fname = path.expanduser("~/.ssh/id_rsa")
        if not path.exists(fname):
            print("Creating ssh key pair")
            # returns non-0 if the key pair already exists
            call(
                [
                    "ssh-keygen",
                    "-f",
                    fname,
                    "-t",
                    "rsa",
                    "-b",
                    "4096",
                    "-m",
                    "PEM",
                    "-N",
                    "",
                ]
            )

    def is_host_known(self, hostname=None):
        """Check if the host is known already."""
        if hostname is None:
            hostname = self.hostname
        fname = path.expanduser("~/.ssh/known_hosts")
        if not path.exists(fname):
            return False
        return call(["ssh-keygen", "-F", hostname]) == 0

    def _make_host_known(self, hostname, proxycmd=None):
        """Add host information into known_hosts file."""
        proxycmd = [] if proxycmd is None else proxycmd
        fname = path.expanduser("~/.ssh/known_hosts")
        print(f"Adding keys from {hostname} to {fname}")
        try:
            hashes = check_output(
                proxycmd + ["ssh-keyscan", "-p", str(self.port), "-H", hostname]
            )
        except CalledProcessError:
            print(f"Couldn't add keys from {hostname} to {fname}. Aborting.")
            return False
        with open(fname, "a") as fobj:
            fobj.write(hashes.decode("utf-8"))
        return True

    def can_login(self, silent=False):
        """Check if it is possible to login into the remote host."""
        if self.username is None:  # if I can't find the username - I must fail
            return False
        userhost = self.username + "@" + self.hostname
        if not silent:
            print(f"Trying ssh {userhost} -p {self.port}... ", end="")
        # With BatchMode on, no password prompt or other interaction is attempted,
        # so a connect that requires a password will fail.
        ret = call(
            [
                "ssh",
                userhost,
                "-p",
                str(self.port),
                "-o",
                "BatchMode=yes",
                "-o",
                "ConnectTimeout=5",
                "true",
            ]
        )
        if not silent:
            print("Ok" if ret == 0 else "Failed")
        return ret == 0

    def is_in_config(self):
        """Check if the config file contains host information."""
        fname = path.expanduser("~/.ssh/config")
        if not path.exists(fname):
            return False
        cfglines = open(fname).read().split("\n")
        return "Host " + self.hostname in cfglines

    def _write_ssh_config(self, proxycmd="", private_key_abs_fname=None):
        """Put host information into the config file."""
        fname = path.expanduser("~/.ssh/config")
        print(f"Adding section to {fname}")
        with open(fname, "a") as file:
            file.write(f"Host {self.hostname} \n")
            file.write(f"User {self.username} \n")
            file.write(f"Port {self.port} \n")
            if private_key_abs_fname:
                file.write(f"IdentityFile {private_key_abs_fname} \n")
            if proxycmd:
                file.write(f"ProxyCommand ssh -q -Y {proxycmd} netcat %h %p\n")
            file.write("ServerAliveInterval 5\n")

    @staticmethod
    def _add_private_key(private_key_fname, private_key_content):
        """
        param private_key_fname: string
        param private_key_content: bytes
        """
        fname = path.expanduser(f"~/.ssh/{private_key_fname}")
        if path.exists(fname):
            # if file already exist and have the same content
            with open(fname, "rb") as file:
                content = file.read()
                if content == private_key_content:
                    return path.basename(fname)

            fname = fname + "_" + shortuuid.uuid()
        with open(fname, "wb") as file:
            file.write(private_key_content)

        os.chmod(fname, 0o600)

        return fname

    @staticmethod
    def _send_pubkey(hostname, username, password, proxycmd=""):
        """Send a publick key to a remote host."""
        from pexpect import TIMEOUT

        timeout = 10
        print(f"Sending public key to {hostname}... ", end="")
        str_ssh = f"ssh-copy-id {username}@{hostname}"
        if proxycmd:
            str_ssh += ' -o "ProxyCommand ssh -q -Y ' + proxycmd + ' netcat %h %p\n"'
        child = pexpect.spawn(str_ssh)
        try:
            index = child.expect(
                [
                    "s password:",  # 0
                    "All keys were skipped because they already exist on the remote system",  # 1
                    "ERROR: No identities found",  # 2
                    "Could not resolve hostname",  # 3
                    pexpect.EOF,
                ],
                timeout=timeout,
            )  # final
        except TIMEOUT:
            print(f"Exceeded {timeout} s timeout")
            return False

        possible_output = {
            1: {
                "message": "Keys are already present on the remote machine",
                "status": True,
            },
            2: {
                "message": "Failed\nLooks like the key pair is not present in ~/.ssh folder",
                "status": False,
            },
            3: {"message": "Failed\nUnknown hostname", "status": False},
        }
        if index == 0:
            child.sendline(password)
            try:
                child.expect("Now try logging into", timeout=5)
            except TIMEOUT:
                print("Failed\nPlease check your username and/or password")
                return False
            print("Ok")
            child.close()
            return True

        if index in possible_output:
            print(possible_output[index]["message"])
            return possible_output[index]["status"]

        print("Failed\nUnknown problem")
        print(child.before, child.after)
        child.close()
        return False

    def _configure_proxy(self, password, proxy_password):
        """Configure proxy server."""

        # If proxy IS required.
        if self.use_proxy:
            # Again, some standard checks whether proxy server parameters are provided.
            if self.proxy_hostname is None:  # hostname
                print("Please specify the proxy server hostname")
                return False, ""

            # If proxy username and password should be different from the main computer - they must be provided.
            if self._use_diff_proxy_username.value:

                # Check username.
                if self.proxy_username is not None:
                    proxy_username = self.proxy_username
                else:
                    print("Please specify the proxy server username")
                    return False, ""

                # Check password.
                if not proxy_password.strip():
                    print("Please specify the proxy server password")
                    return False, ""

            # If username and password are the same as for the main computer.
            else:
                proxy_username = self.username
                proxy_password = password

            # Make proxy server known.
            if not self.is_host_known(self.proxy_hostname):
                success = self._make_host_known(self.proxy_hostname)
                if not success:
                    return False, ""

            # Finally trying to connect.
            if self._send_pubkey(self.proxy_hostname, proxy_username, proxy_password):
                return True, proxy_username + "@" + self.proxy_hostname

            print(
                f"Could not send public key to {self.proxy_hostname} (proxy server), sorry :-("
            )
            return False, ""

        # If proxy is NOT required.
        return True, ""

    def on_setup_ssh(self, change):
        """Setup ssh, password and private key are supported"""
        with self._setup_ssh_out:
            mode = self._verification_mode.value
            self._on_setup_ssh(mode, change)

    def _on_setup_ssh(
        self, mode, change
    ):  # pylint: disable=unused-argument,too-many-return-statements,too-many-branches
        """ATTENTION: modifying the order of operations in this function can lead to unexpected problems"""
        clear_output()

        # If hostname is not provided - do not do anything.
        if self.hostname is None:  # check hostname
            print("Please specify the computer hostname")
            return

        # Check if password-free access was enabled earlier.
        if self.can_login():
            print("Password-free access is already enabled")
            # It can still happen that password-free access is enabled
            # but host is not present in the config file - fixing this.
            if not self.is_in_config():
                self._write_ssh_config()  # We do not use proxy here, because if computer
                # can be accessed without any info in the config - proxy is not needed.
                self.setup_counter += (
                    1  # Only if config file has changed - increase setup_counter.
                )
            return

        # If couldn't login in the previous step, chek whether all required information is provided.
        if self.username is None:  # check username
            print("Please enter your ssh username")
            return

        if mode == "password":
            self._ssh_keygen()
            # Temporary passwords.
            password = self.__password
            proxy_password = self.__proxy_password
            private_key_abs_fname = None
            if not password.strip():  # check password
                print("Please enter your ssh password")
                return
        if mode == "private_key":
            # unwrap private key file and setting temporary private_key content
            private_key_fname, private_key_content = self.__private_key
            proxy_password = self.__proxy_password
            password = None
            if private_key_fname is None:  # check private key file
                print("Please upload your private key file")
                return

            # write private key in ~/.ssh/ and use the name of upload file,
            # if exist, generate random string and append to filename then override current name.
            private_key_abs_fname = self._add_private_key(
                private_key_fname, private_key_content
            )

        # get the right commands to access the proxy server (if provided)
        success, proxycmd = self._configure_proxy(password, proxy_password)
        if not success:
            return

        # make host known by ssh on the proxy server
        if not self.is_host_known():
            success = self._make_host_known(
                self.hostname, ["ssh"] + [proxycmd] if proxycmd else []
            )
            if not success:
                return

        if mode == "password":
            # sending public key to the main host
            if not self._send_pubkey(self.hostname, self.username, password, proxycmd):
                print("Could not send public key to {self.hostname}, sorry :-(")
                return

        # modify the ssh config file if necessary
        if not self.is_in_config():
            self._write_ssh_config(proxycmd, private_key_abs_fname)

        # FOR LATER: add a check if new config is different from the current one. If so
        # infrom the user about it.

        # final check
        if self.can_login():
            self.setup_counter += 1
            print("Automatic ssh setup successful :-)")
            return
        print("Automatic ssh setup failed, sorry :-(")
        return

    def on_use_verification_mode_change(
        self, change
    ):  # pylint: disable=unused-argument
        """which verification mode is chosen."""
        if self._verification_mode.value == "password":
            self._inp_password.disabled = False
            self._inp_private_key.disabled = True
        if self._verification_mode.value == "private_key":
            self._inp_password.disabled = True
            self._inp_private_key.disabled = False

    def on_use_proxy_change(self, change):
        """If proxy check-box is clicked."""
        if change["new"]:
            self._proxy_ssh_box.layout.visibility = "visible"
        else:
            self._proxy_ssh_box.layout.visibility = "hidden"
            self._use_diff_proxy_username.value = False

    def on_use_diff_proxy_username_change(
        self, change
    ):  # pylint: disable=unused-argument
        """If using different username for proxy check-box is clicked."""
        if change["new"]:
            self._proxy_user_password_box.layout.visibility = "visible"
        else:
            self._proxy_user_password_box.layout.visibility = "hidden"

    # Keep this function only because it might be used later.
    # What it does: looks inside .ssh/config file and loads computer setup from
    # there (if present)
    #     def _get_from_config(self, b):
    #         config = parse_sshconfig(self.hostname)
    #         if 'user' in config:
    #             self._inp_username.value = config['user']
    #         else:
    #             self._inp_username.value = ''
    #         if 'proxycommand' in config:
    #             self._use_proxy.value = True
    #             proxy = ''.join([ s for s in config['proxycommand'].split() if '@' in s])
    #             username, hostname = proxy.split('@')
    #             self._inp_proxy_address.value = hostname
    #             if username != self.username:
    #                 self._use_diff_proxy_username.value = True
    #                 self.proxy_username = username
    #         else:
    #             self._use_proxy.value = False

    @property
    def __password(self):
        """Returning the password and immediately destroying it"""
        passwd = copy(self._inp_password.value)
        self._inp_password.value = ""
        return passwd

    @property
    def __proxy_password(self):
        """Returning the password and immediately destroying it"""
        passwd = copy(self._inp_proxy_password.value)
        self._inp_proxy_password.value = ""
        return passwd

    @property
    def __private_key(self):
        """unwrap private key file and setting filename and file content"""
        if self._inp_private_key.value:
            (fname, _value), *_ = self._inp_private_key.value.items()
            content = copy(_value["content"])
            self._inp_private_key.value.clear()
            self._inp_private_key._counter = 0  # pylint: disable=protected-access
            return fname, content
        return None, None

    @observe("proxy_hostname")
    def _observe_proxy_hostname(self, _=None):
        """Enable 'use proxy' widget if proxy hostname is provided."""
        if self.proxy_hostname:
            self.use_proxy = True

    @observe("proxy_username")
    def _observe_proxy_username(self, _=None):
        """Enable 'use proxy' and 'use different proxy username' widgets if proxy username is provided."""
        if self.proxy_username:
            self.use_proxy = True
            self._use_diff_proxy_username.value = True

    @validate("port")
    def _validate_port(self, provided):  # pylint: disable=no-self-use
        return int(provided["value"])


@deprecation.deprecated(
    deprecated_in="1.3",
    removed_in="2.0",
    details="Use the ComputationalResourcesWidget instead.",
)
class AiidaComputerSetup(ipw.VBox):
    """Inform AiiDA about a computer."""

    label = Unicode()
    hostname = Unicode()
    description = Unicode()
    work_dir = Unicode()
    mpirun_command = Unicode()
    mpiprocs_per_machine = Union([Unicode(), Int()])
    prepend_text = Unicode()
    append_text = Unicode()
    transport = Unicode()
    scheduler = Unicode()
    safe_interval = Union([Unicode(), Float()])

    def __init__(self, **kwargs):
        from aiida.schedulers import Scheduler
        from aiida.transports import Transport

        # List of widgets to be displayed.
        inp_computer_name = ipw.Text(
            value="",
            placeholder="Will only be used within AiiDA",
            description="AiiDA computer name:",
            layout=ipw.Layout(width="500px"),
            style=STYLE,
        )
        link((inp_computer_name, "value"), (self, "label"))

        # Hostname.
        inp_computer_hostname = ipw.Text(
            description="Hostname:", layout=ipw.Layout(width="500px"), style=STYLE
        )
        link((inp_computer_hostname, "value"), (self, "hostname"))

        # Computer description.
        inp_computer_description = ipw.Text(
            value="",
            placeholder="No description (yet)",
            description="Computer description:",
            layout=ipw.Layout(width="500px"),
            style=STYLE,
        )
        link((inp_computer_description, "value"), (self, "description"))

        # Directory where to run the simulations.
        inp_computer_workdir = ipw.Text(
            value="/scratch/{username}/aiida_run",
            description="Workdir:",
            layout=ipw.Layout(width="500px"),
            style=STYLE,
        )
        link((inp_computer_workdir, "value"), (self, "work_dir"))

        # Mpirun command.
        inp_mpirun_cmd = ipw.Text(
            value="mpirun -n {tot_num_mpiprocs}",
            description="Mpirun command:",
            layout=ipw.Layout(width="500px"),
            style=STYLE,
        )
        link((inp_mpirun_cmd, "value"), (self, "mpirun_command"))

        # Number of CPUs per node.
        inp_computer_ncpus = ipw.IntText(
            value=12,
            step=1,
            description="Number of CPU(s) per node:",
            layout=ipw.Layout(width="270px"),
            style=STYLE,
        )
        link((inp_computer_ncpus, "value"), (self, "mpiprocs_per_machine"))

        # Transport type.
        inp_transport_type = ipw.Dropdown(
            value="ssh",
            options=Transport.get_valid_transports(),
            description="Transport type:",
            style=STYLE,
        )
        link((inp_transport_type, "value"), (self, "transport"))

        # Safe interval.
        inp_safe_interval = ipw.FloatText(
            value=30.0,
            description="Min. connection interval (sec):",
            layout=ipw.Layout(width="270px"),
            style=STYLE,
        )
        link((inp_safe_interval, "value"), (self, "safe_interval"))

        # Scheduler.
        inp_scheduler = ipw.Dropdown(
            value="slurm",
            options=Scheduler.get_valid_schedulers(),
            description="Scheduler:",
            style=STYLE,
        )
        link((inp_scheduler, "value"), (self, "scheduler"))

        # Use login shell.
        self._use_login_shell = ipw.Checkbox(value=True, description="Use login shell")

        # Prepend text.
        inp_prepend_text = ipw.Textarea(
            placeholder="Text to prepend to each command execution",
            description="Prepend text:",
            layout=ipw.Layout(width="400px"),
        )
        link((inp_prepend_text, "value"), (self, "prepend_text"))

        # Append text.
        inp_append_text = ipw.Textarea(
            placeholder="Text to append to each command execution",
            description="Append text:",
            layout=ipw.Layout(width="400px"),
        )
        link((inp_append_text, "value"), (self, "append_text"))

        # Buttons and outputs.
        btn_setup_comp = ipw.Button(description="Setup computer")
        btn_setup_comp.on_click(self._on_setup_computer)
        btn_test = ipw.Button(description="Test computer")
        btn_test.on_click(self.test)
        self._setup_comp_out = ipw.Output(layout=ipw.Layout(width="500px"))
        self._test_out = ipw.Output(layout=ipw.Layout(width="500px"))

        # Organize the widgets
        children = [
            ipw.HBox(
                [
                    ipw.VBox(
                        [
                            inp_computer_name,
                            inp_computer_hostname,
                            inp_computer_description,
                            inp_computer_workdir,
                            inp_mpirun_cmd,
                            inp_computer_ncpus,
                            inp_transport_type,
                            inp_safe_interval,
                            inp_scheduler,
                            self._use_login_shell,
                        ]
                    ),
                    ipw.VBox([inp_prepend_text, inp_append_text]),
                ]
            ),
            ipw.HBox([btn_setup_comp, btn_test]),
            ipw.HBox([self._setup_comp_out, self._test_out]),
        ]
        super().__init__(children, **kwargs)

    def _configure_computer(self):
        """Create AuthInfo."""
        print(f"Configuring '{self.label}'")
        sshcfg = parse_sshconfig(self.hostname)
        authparams = {
            "compress": True,
            "key_filename": os.path.expanduser(
                sshcfg.get("identityfile", ["~/.ssh/id_rsa"])[0]
            ),
            "gss_auth": False,
            "gss_deleg_creds": False,
            "gss_host": self.hostname,
            "gss_kex": False,
            "key_policy": "WarningPolicy",
            "load_system_host_keys": True,
            "port": sshcfg.get("port", 22),
            "timeout": 60,
            "use_login_shell": self._use_login_shell.value,
            "safe_interval": self.safe_interval,
        }
        if "user" in sshcfg:
            authparams["username"] = sshcfg["user"]
        else:
            print(
                f"SSH username is not provided, please run `verdi computer configure {self.label}` "
                "from the command line."
            )
            return
        if "proxycommand" in sshcfg:
            authparams["proxy_command"] = sshcfg["proxycommand"]
        aiidauser = User.objects.get_default()
        from aiida.orm import AuthInfo

        authinfo = AuthInfo(
            computer=Computer.objects.get(label=self.label), user=aiidauser
        )
        authinfo.set_auth_params(authparams)
        authinfo.store()
        print(check_output(["verdi", "computer", "show", self.label]).decode("utf-8"))

    def _on_setup_computer(self, _=None):
        """When setup computer button is pressed."""
        with self._setup_comp_out:
            clear_output()
            if self.label is None:  # check hostname
                print("Please specify the computer name (for AiiDA)")
                return
            try:
                computer = Computer.objects.get(label=self.label)
                print(f"A computer called {self.label} already exists.")
                return
            except NotExistent:
                pass

            print(f"Creating new computer with name '{self.label}'")
            computer = Computer(
                label=self.label, hostname=self.hostname, description=self.description
            )
            computer.set_transport_type(self.transport)
            computer.set_scheduler_type(self.scheduler)
            computer.set_workdir(self.work_dir)
            computer.set_mpirun_command(self.mpirun_command.split())
            computer.set_default_mpiprocs_per_machine(self.mpiprocs_per_machine)
            if self.prepend_text:
                computer.set_prepend_text(self.prepend_text)
            if self.append_text:
                computer.set_append_text(self.append_text)

            computer.store()
            self._configure_computer()

    def test(self, _=None):
        with self._test_out:
            clear_output()
            print(
                check_output(
                    ["verdi", "computer", "test", "--print-traceback", self.label]
                ).decode("utf-8")
            )

    @validate("mpiprocs_per_machine")
    def _validate_mpiprocs_per_machine(self, provided):  # pylint: disable=no-self-use
        return int(provided["value"])

    @validate("safe_interval")
    def _validate_safe_interval(self, provided):  # pylint: disable=no-self-use
        return float(provided["value"])


@deprecation.deprecated(
    deprecated_in="1.3",
    removed_in="2.0",
    details="Use the ComputerDropdownWidget instead.",
)
class ComputerDropdown(ipw.VBox):
    """Widget to select a configured computer.

    Attributes:
        selected_computer(Unicode or Computer): Trait that points to the selected Computer instance.
            It can be set either to an AiiDA Computer instance or to a computer label (will
            automatically be replaced by the corresponding Computer instance). It is linked to the
            'value' trait of `self._dropdown` widget.

        computers(Dict): Trait that contains a dictionary (label => Computer instance) for all
        computers found in the AiiDA database. It is linked to the 'options' trait of
        `self._dropdown` widget.

        allow_select_disabled(Bool):  Trait that defines whether to show disabled computers.
    """

    selected_computer = Union([Unicode(), Instance(Computer)], allow_none=True)
    computers = Dict(allow_none=True)
    allow_select_disabled = Bool(False)

    def __init__(self, description="Select computer:", path_to_root="../", **kwargs):
        """Dropdown for configured AiiDA Computers.

        description (str): Text to display before dropdown.

        path_to_root (str): Path to the app's root folder.
        """

        self.output = ipw.HTML()
        self._dropdown = ipw.Dropdown(
            options={},
            value=None,
            description=description,
            style={"description_width": "initial"},
            disabled=True,
        )
        link((self, "computers"), (self._dropdown, "options"))
        link((self._dropdown, "value"), (self, "selected_computer"))

        btn_refresh = ipw.Button(description="Refresh", layout=ipw.Layout(width="70px"))
        btn_refresh.on_click(self.refresh)

        self.observe(self.refresh, names="allow_select_disabled")

        self._setup_another = ipw.HTML(
            value=f"""<a href={path_to_root}aiidalab-widgets-base/notebooks/setup_computer.ipynb target="_blank">
            Setup new computer</a>"""
        )

        children = [
            ipw.HBox([self._dropdown, btn_refresh, self._setup_another]),
            self.output,
        ]
        self.refresh()
        super().__init__(children=children, **kwargs)

    def _get_computers(self):
        """Get the list of available computers."""

        # Getting the current user.
        user = User.objects.get_default()

        return {
            c[0].label: c[0]
            for c in QueryBuilder().append(Computer).all()
            if c[0].is_user_configured(user)
            and (self.allow_select_disabled or c[0].is_user_enabled(user))
        }

    def refresh(self, _=None):
        """Refresh the list of configured computers."""
        self.output.value = ""
        with self.hold_trait_notifications():  # pylint: disable=not-context-manager
            self._dropdown.options = self._get_computers()
        if not self.computers:
            self.output.value = "No computers found."
            self._dropdown.disabled = True
        else:
            self._dropdown.disabled = False

        self._dropdown.value = None

    @validate("selected_computer")
    def _validate_selected_computer(self, change):
        """Select computer either by label or by class instance."""
        computer = change["value"]
        self.output.value = ""
        if not computer:
            return None
        if isinstance(computer, str):
            if computer in self.computers:
                return self.computers[computer]
            self.output.value = f"""Computer instance '<span style="color:red">{computer}</span>'
            is not configured in your AiiDA profile."""
            return None

        if isinstance(computer, Computer):
            if computer.label in self.computers:
                return computer
            self.output.value = f"""Computer instance '<span style="color:red">{computer.label}</span>'
            is not configured in your AiiDA profile."""
        return None
