# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
External Kernel connection widget
"""

# Standard library imports
import json
import os.path as osp

# Third party imports
from jupyter_client.connect import find_connection_file
from jupyter_core.paths import jupyter_runtime_dir
from jsonschema import ValidationError, validate as json_validate
from qtpy.compat import getopenfilename
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (QCheckBox, QDialog, QDialogButtonBox, QGridLayout,
                            QGroupBox, QHBoxLayout, QLabel, QLineEdit,
                            QMessageBox, QPushButton, QRadioButton,
                            QSpacerItem, QVBoxLayout)

# Local imports
from spyder.api.config.mixins import SpyderConfigurationAccessor
from spyder.api.translations import _
from spyder.api.widgets.dialogs import SpyderDialogButtonBox
from spyder.config.base import get_home_dir


KERNEL_CONNECTION_SCHEMA = {
    "type": "object",
    "properties": {
        "shell_port": {"type": "integer"},
        "iopub_port": {"type": "integer"},
        "stdin_port": {"type": "integer"},
        "control_port": {"type": "integer"},
        "hb_port": {"type": "integer"},
        "ip": {"type": "string", "format": "ipv4"},
        "key": {"type": "string", "minLength": 10},
        "transport": {"type": "string", "enum": ["tcp", "ipc"]},
        "signature_scheme": {"type": "string", "pattern": r"^hmac-.+$"},
        "kernel_name": {"type": "string"},
        "jupyter_session": {"type": "string"},
    },
    "required": [
        "shell_port",
        "iopub_port",
        "stdin_port",
        "control_port",
        "hb_port",
        "ip",
        "key",
        "transport",
        "signature_scheme",
        "kernel_name",
    ],
    "additionalProperties": False,
}


class KernelConnectionDialog(QDialog, SpyderConfigurationAccessor):
    """Dialog to connect to existing kernels (either local or remote)."""

    CONF_SECTION = 'existing-kernel'

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle(_('Connect to existing kernel'))

        main_label = QLabel(_(
            "<p>Please select the JSON connection file (<i>e.g.</i> "
            "<tt>kernel-1234.json</tt>) of the existing kernel, and enter "
            "the SSH information if connecting to a remote machine. "
            "To learn more about starting external kernels and connecting "
            "to them, see <a href=\"https://docs.spyder-ide.org/current/panes/"
            "ipythonconsole.html#using-external-kernels\">"
            "our documentation</a>.</p>"))
        main_label.setWordWrap(True)
        main_label.setAlignment(Qt.AlignJustify)
        main_label.setOpenExternalLinks(True)

        # Connection file
        cf_label = QLabel(_('Connection file:'))
        self.cf = QLineEdit()
        self.cf.setPlaceholderText(_('Kernel connection file path'))
        self.cf.setMinimumWidth(350)
        cf_open_btn = QPushButton(_('Browse'))
        cf_open_btn.clicked.connect(self.select_connection_file)

        # Signals
        self.cf.textChanged.connect(self.activate_button)

        cf_layout = QHBoxLayout()
        cf_layout.addWidget(cf_label)
        cf_layout.addWidget(self.cf)
        cf_layout.addWidget(cf_open_btn)

        # Remote kernel groupbox
        self.rm_group = QGroupBox(_("This is a remote kernel (via SSH)"))

        # SSH connection
        hn_label = QLabel(_('Hostname:'))
        self.hn = QLineEdit()
        pn_label = QLabel(_('Port:'))
        self.pn = QLineEdit()
        self.pn.setMaximumWidth(75)

        un_label = QLabel(_('Username:'))
        self.un = QLineEdit()

        # SSH authentication
        auth_group = QGroupBox(_("Authentication method:"))
        self.pw_radio = QRadioButton()
        pw_label = QLabel(_('Password:'))
        self.kf_radio = QRadioButton()
        kf_label = QLabel(_('SSH keyfile:'))

        self.pw = QLineEdit()
        self.pw.setEchoMode(QLineEdit.Password)
        self.pw_radio.toggled.connect(self.pw.setEnabled)
        self.kf_radio.toggled.connect(self.pw.setDisabled)

        self.kf = QLineEdit()
        kf_open_btn = QPushButton(_('Browse'))
        kf_open_btn.clicked.connect(self.select_ssh_key)
        kf_layout = QHBoxLayout()
        kf_layout.addWidget(self.kf)
        kf_layout.addWidget(kf_open_btn)

        kfp_label = QLabel(_('Passphase:'))
        self.kfp = QLineEdit()
        self.kfp.setPlaceholderText(_('Optional'))
        self.kfp.setEchoMode(QLineEdit.Password)

        self.kf_radio.toggled.connect(self.kf.setEnabled)
        self.kf_radio.toggled.connect(self.kfp.setEnabled)
        self.kf_radio.toggled.connect(kf_open_btn.setEnabled)
        self.kf_radio.toggled.connect(kfp_label.setEnabled)
        self.pw_radio.toggled.connect(self.kf.setDisabled)
        self.pw_radio.toggled.connect(self.kfp.setDisabled)
        self.pw_radio.toggled.connect(kf_open_btn.setDisabled)
        self.pw_radio.toggled.connect(kfp_label.setDisabled)

        # SSH layout
        ssh_layout = QGridLayout()
        ssh_layout.addWidget(hn_label, 0, 0, 1, 2)
        ssh_layout.addWidget(self.hn, 0, 2)
        ssh_layout.addWidget(pn_label, 0, 3)
        ssh_layout.addWidget(self.pn, 0, 4)
        ssh_layout.addWidget(un_label, 1, 0, 1, 2)
        ssh_layout.addWidget(self.un, 1, 2, 1, 3)

        # SSH authentication layout
        auth_layout = QGridLayout()
        auth_layout.addWidget(self.pw_radio, 1, 0)
        auth_layout.addWidget(pw_label, 1, 1)
        auth_layout.addWidget(self.pw, 1, 2)
        auth_layout.addWidget(self.kf_radio, 2, 0)
        auth_layout.addWidget(kf_label, 2, 1)
        auth_layout.addLayout(kf_layout, 2, 2)
        auth_layout.addWidget(kfp_label, 3, 1)
        auth_layout.addWidget(self.kfp, 3, 2)
        auth_group.setLayout(auth_layout)

        # Remote kernel layout
        rm_layout = QVBoxLayout()
        rm_layout.addLayout(ssh_layout)
        rm_layout.addSpacerItem(QSpacerItem(0, 8))
        rm_layout.addWidget(auth_group)
        self.rm_group.setLayout(rm_layout)
        self.rm_group.setCheckable(True)
        self.rm_group.toggled.connect(self.pw_radio.setChecked)

        # Ok and Cancel buttons
        self.accept_btns = SpyderDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            Qt.Horizontal,
            self,
        )
        self.button_ok = self.accept_btns.button(QDialogButtonBox.Ok)
        self.button_ok.setEnabled(False)

        self.accept_btns.accepted.connect(self.save_connection_settings)
        self.accept_btns.accepted.connect(self._validate_connection_file)
        self.accept_btns.rejected.connect(self.reject)

        # Save connection settings checkbox
        self.save_layout = QCheckBox(self)
        self.save_layout.setText(_("Save connection settings"))

        btns_layout = QHBoxLayout()
        btns_layout.addWidget(self.save_layout)
        btns_layout.addWidget(self.accept_btns)

        # Dialog layout
        layout = QVBoxLayout(self)
        layout.addWidget(main_label)
        layout.addSpacerItem(QSpacerItem(0, 8))
        layout.addLayout(cf_layout)
        layout.addSpacerItem(QSpacerItem(0, 12))
        layout.addWidget(self.rm_group)
        layout.addLayout(btns_layout)

        self.cf.setFocus()
        self.load_connection_settings()

    def activate_button(self):
        if self.cf.text() == "":
            self.button_ok.setEnabled(False)
        else:
            self.button_ok.setEnabled(True)

    def load_connection_settings(self):
        """Load the user's previously-saved kernel connection settings."""
        existing_kernel = self.get_conf("settings", {})

        connection_file_path = existing_kernel.get("json_file_path", "")
        is_remote = existing_kernel.get("is_remote", False)
        username = existing_kernel.get("username", "")
        hostname = existing_kernel.get("hostname", "")
        port = str(existing_kernel.get("port", 22))
        is_ssh_kf = existing_kernel.get("is_ssh_keyfile", False)
        ssh_kf = existing_kernel.get("ssh_key_file_path", "")

        if connection_file_path != "":
            self.cf.setText(connection_file_path)
        if username != "":
            self.un.setText(username)
        if hostname != "":
            self.hn.setText(hostname)
        if ssh_kf != "":
            self.kf.setText(ssh_kf)
        self.rm_group.setChecked(is_remote)
        self.pn.setText(port)
        self.kf_radio.setChecked(is_ssh_kf)
        self.pw_radio.setChecked(not is_ssh_kf)

        try:
            import keyring
            ssh_passphrase = keyring.get_password("spyder_remote_kernel",
                                                  "ssh_key_passphrase")
            ssh_password = keyring.get_password("spyder_remote_kernel",
                                                "ssh_password")
            if ssh_passphrase:
                self.kfp.setText(ssh_passphrase)
            if ssh_password:
                self.pw.setText(ssh_password)
        except Exception:
            pass

    def _validate_connection_file(self):
        cf_path = osp.dirname(self.cf.text())
        cf_filename = osp.basename(self.cf.text())

        try:
            # We do this so that users can paste only the kernel id
            if not cf_path:
                if not cf_filename.startswith("kernel-"):
                    cf_filename = "kernel-" + cf_filename
                if not cf_filename.endswith(".json"):
                    cf_filename += ".json"

            connection_file = find_connection_file(
                filename=cf_filename, path=cf_path if cf_path else None
            )
        except OSError:
            connection_file = None

        if connection_file is None:
            QMessageBox.critical(
                self,
                _('Error'),
                _(
                    "The connection file you passed doesn't exist or can't be "
                    "found by Spyder."
                ),
                QMessageBox.Ok
            )
        else:
            try:
                with open(connection_file, 'r') as f:
                    snippets = json.load(f)
                json_validate(
                    instance=snippets, schema=KERNEL_CONNECTION_SCHEMA
                )
                self.accept()
            except ValidationError as e:
                QMessageBox.critical(
                    self,
                    _('Error'),
                    _(
                        "The connection file you passed is not valid.<br><br>"
                        "The following issue was found: {}"
                    ).format(e.message),
                    QMessageBox.Ok
                )

    def save_connection_settings(self):
        """Save user's kernel connection settings."""

        if not self.save_layout.isChecked():
            return

        is_ssh_key = bool(self.kf_radio.isChecked())
        connection_settings = {
            "json_file_path": self.cf.text(),
            "is_remote": self.rm_group.isChecked(),
            "username": self.un.text(),
            "hostname": self.hn.text(),
            "port": self.pn.text(),
            "is_ssh_keyfile": is_ssh_key,
            "ssh_key_file_path": self.kf.text()
        }
        self.set_conf("settings", connection_settings)

        try:
            import keyring
            if is_ssh_key:
                keyring.set_password("spyder_remote_kernel",
                                     "ssh_key_passphrase",
                                     self.kfp.text())
            else:
                keyring.set_password("spyder_remote_kernel",
                                     "ssh_password",
                                     self.pw.text())
        except Exception:
            pass

    def select_connection_file(self):
        cf = getopenfilename(self, _('Select kernel connection file'),
                             jupyter_runtime_dir(), '*.json;;*.*')[0]
        self.cf.setText(cf)

    def select_ssh_key(self):
        kf = getopenfilename(self, _('Select SSH keyfile'),
                             get_home_dir(), '*.pem;;*')[0]
        self.kf.setText(kf)

    @staticmethod
    def get_connection_parameters(parent=None, dialog=None):
        if not dialog:
            dialog = KernelConnectionDialog(parent)
        result = dialog.exec_()
        is_remote = bool(dialog.rm_group.isChecked())
        accepted = result == QDialog.Accepted

        if is_remote:
            def falsy_to_none(arg):
                return arg if arg else None
            if dialog.hn.text() and dialog.un.text():
                port = dialog.pn.text() if dialog.pn.text() else '22'
                hostname = "{0}@{1}:{2}".format(dialog.un.text(),
                                                dialog.hn.text(),
                                                port)
            else:
                hostname = None
            if dialog.pw_radio.isChecked():
                password = falsy_to_none(dialog.pw.text())
                keyfile = None
            elif dialog.kf_radio.isChecked():
                keyfile = falsy_to_none(dialog.kf.text())
                password = falsy_to_none(dialog.kfp.text())
            else:  # imposible?
                keyfile = None
                password = None
            return (dialog.cf.text(), hostname, keyfile, password, accepted)
        else:
            path = dialog.cf.text()
            _dir, filename = osp.dirname(path), osp.basename(path)
            if _dir == '' and not filename.endswith('.json'):
                path = osp.join(jupyter_runtime_dir(), 'kernel-'+path+'.json')
            return (path, None, None, None, accepted)
