package be.iminds.ilabt.jfed.ui.javafx.scanner;

import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetailsFactory;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Testbed;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.TestbedBuilder;
import be.iminds.ilabt.jfed.highlevel.history.ApiCallHistory;
import be.iminds.ilabt.jfed.lowlevel.ApiInfo;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.scanner.AuthorityScannerInput;
import be.iminds.ilabt.jfed.scanner.AuthorityType;
import be.iminds.ilabt.jfed.scanner.ScanFeedback;
import be.iminds.ilabt.jfed.scanner.ServerScanner;
import be.iminds.ilabt.jfed.ui.javafx.log_gui.LogHistoryPanel;
import be.iminds.ilabt.jfed.util.IOUtils;
import be.iminds.ilabt.jfed.util.JavaFXLogger;
import be.iminds.ilabt.jfed.util.KeyUtil;
import be.iminds.ilabt.util.jsonld.JsonLdObjectsMetaData;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Pair;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.*;

/**
 * FXML Controller class
 *
 * @author wim
 */
public class ScanAuthorityDialogController {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ScanAuthorityDialogController.class);

    private final JavaFXLogger logger;
    private final GeniUserProvider geniUserProvider;
    private final JFedPreferences jFedPreferences;
    private final JFedConnectionProvider jFedConnectionProvider;
    private final SerializableApiCallDetailsFactory serializableApiCallDetailsFactory;

    @FXML
    private BorderPane pane;

    @FXML
    private LogHistoryPanel logHistoryPanel;

    @FXML
    private RadioButton basicRadioButton;

    @FXML
    private Label overviewLabelCon;
    @FXML
    private Label overviewLabelGV;
    @FXML
    private Label overviewLabelUrn;
    @FXML
    private Label overviewLabelType;

    @FXML
    private Pane inputPane;
    @FXML
    private TabPane tabs;

    @FXML
    private Tab securityTab;
    @FXML
    private Tab outputTab;
    @FXML
    private Tab overviewTab;
    @FXML
    private Tab debugTab;

    @FXML
    private TextField amUrlField;
    @FXML
    private TextField urnField;
    @FXML
    private ComboBox<AuthorityType> typeComboBox;

    @FXML
    private CheckBox proxyCheckBox;

    @FXML
    private HBox postScanButtons;
    @FXML
    private Button postScan1ShowButton;
    @FXML
    private HBox scanLogBox;
    @FXML
    private TextArea certTextArea;
    @FXML
    private TextArea scanLogTextArea;
    @FXML
    private ProgressIndicator progress1;
    @FXML
    private ProgressIndicator progress2;
    @FXML
    private Button scanButton;
    @FXML
    private Button cancelButton;

    @FXML
    private CheckBox trustCertsCheckbox;

    @FXML
    private Pane scannedFacts;

//    private StringProperty logtext = new SimpleStringProperty();

    private Stage stage;



    private ServerScanner scanner;

    private Map<ServerScanner.ScannedSingleFact, ScanFactPane> scanFactPaneByType;
    private Map<ServerScanner.ScannedSingleFact, ScanFactPane> scanFactPaneByHrn;
    private Map<ServerScanner.ScannedSingleFact, ScanFactPane> scanFactPaneByUrn;
    private Map<Pair<ApiInfo.Api, ServerScanner.ScannedSingleFact>, ScanFactPane> scanFactPaneByServertypeAndUrl;


    Thread scanThread = null;

    Testbed createTestbed = null;
    boolean cancelled = false;

    @Inject
    public ScanAuthorityDialogController(
            JavaFXLogger logger, GeniUserProvider geniUserProvider,
            JFedPreferences jFedPreferences, JFedConnectionProvider jFedConnectionProvider,
            SerializableApiCallDetailsFactory serializableApiCallDetailsFactory) {
        this.logger = logger;
        this.geniUserProvider = geniUserProvider;
        this.jFedPreferences = jFedPreferences;
        this.jFedConnectionProvider = jFedConnectionProvider;
        this.serializableApiCallDetailsFactory = serializableApiCallDetailsFactory;
    }

    /**
     * Initializes the controller class.
     */
    @FXML
    private void initialize() {
        ApiCallHistory apiCallHistory = new ApiCallHistory(logger, serializableApiCallDetailsFactory);
        logHistoryPanel.setApiCallHistory(apiCallHistory);

        inputPane.managedProperty().bind(inputPane.visibleProperty());
        tabs.managedProperty().bind(tabs.visibleProperty());
        postScanButtons.managedProperty().bind(postScanButtons.visibleProperty());
        tabs.visibleProperty().setValue(false);

        String validatorCss = getClass().getResource("validation.css").toExternalForm();
        String scannerCss = getClass().getResource("scanner.css").toExternalForm();
        pane.getStylesheets().add(validatorCss);
        pane.getStylesheets().add(scannerCss);
        trustCertsCheckbox.getStylesheets().add(validatorCss);

        amUrlField.textProperty().addListener((observableValue, s, s1) -> validate());

        ObservableList<AuthorityType> typeItems = FXCollections.observableArrayList();
        typeItems.addAll(AuthorityType.values());
        typeComboBox.setItems(typeItems);
        trustCertsCheckbox.setSelected(false);

//        scanLogTextArea.textProperty().bind(logtext);

//        String proxySetting = jFedPreferences.getString(GuiPreferenceKey.PREF_SSHPROXY_USE_FOR_JFED);
//        proxyCheckBox.setSelected(proxySetting.equals("ALWAYS"));
    }

    private void setScanDebugLogText(String text) {
//        logtext.setValue(text);
        scanLogTextArea.setText(text);
        scanLogTextArea.setScrollTop(Double.MAX_VALUE);
    }
    private void addScanDebugLogText(String text) {
//        logtext.setValue(logtext.getValue() + text);
        scanLogTextArea.appendText(text);
        scanLogTextArea.setScrollTop(Double.MAX_VALUE);
    }

    public void setStage(Stage stage) {
        this.stage = stage;
    }

    public void cancel() {
        typeComboBox.getSelectionModel().clearSelection();
        amUrlField.setText("");
        urnField.setText("");

        cancelled = true;
        if (stage != null)
            stage.close();
    }

    public void restart() {
        cancelled = false;
        if (stage != null)
            stage.close();
    }


    public void scan() {
        setScanDebugLogText("");
        trustCertsCheckbox.setSelected(false);
        trustCertsCheckbox.getStyleClass().removeAll("validation-error", "validation-warning");

        if (scanThread != null) {
            return;
        }

        inputPane.visibleProperty().setValue(false);
        tabs.visibleProperty().setValue(true);
        tabs.getSelectionModel().select(debugTab);
        pane.getScene().getWindow().sizeToScene();

        amUrlField.setDisable(true);
        proxyCheckBox.setDisable(true);
        urnField.setDisable(true);
        typeComboBox.setDisable(true);
        scanButton.setDisable(true);
        cancelButton.setDisable(true);
        scanLogBox.setVisible(true);
        progress1.setVisible(true);
        progress2.setVisible(true);
        postScanButtons.setVisible(false);

        Runnable scanner = () -> {
            try {
                performScan();
            } catch (final Throwable e) {

                Platform.runLater(() -> {
                    addScanDebugLogText("\nScanning encountered an error: " + e.getMessage() + "\n" + IOUtils.exceptionToStacktraceString(e) + "\n");
                    amUrlField.setDisable(false);
                    proxyCheckBox.setDisable(false);
                    urnField.setDisable(false);
                    typeComboBox.setDisable(false);
                    scanButton.setDisable(false);
                    cancelButton.setDisable(false);
                    scanLogBox.setVisible(true);
                    progress1.setVisible(false);
                    progress2.setVisible(false);

                    postScanButtons.setVisible(false);
                });
            }
        };
        Thread scanThread = new Thread(scanner);
        scanThread.start();
    }

    /* Should be called in seperate Thread */
    private void performScan() {
//        String proxySetting = jFedPreferences.getString(GuiPreferenceKey.PREF_SSHPROXY_USE_FOR_JFED);
//        if (!proxySetting.equals("ALWAYS") && proxyCheckBox.isSelected()) {
//            jFedPreferences.setString(GuiPreferenceKey.PREF_SSHPROXY_USE_FOR_JFED, "ALWAYS");
//        }
//        if (!proxySetting.equals("NEVER") && !proxyCheckBox.isSelected()) {
//            jFedPreferences.setString(GuiPreferenceKey.PREF_SSHPROXY_USE_FOR_JFED, "NEVER");
//        }
        boolean useProxy = proxyCheckBox.isSelected();


        scanner = null;
        List<String> inputUrns = new ArrayList<>();
        List<String> inputUrls = new ArrayList<>();
        if (!amUrlField.getText().trim().isEmpty())
            inputUrls.add(amUrlField.getText());
        if (!urnField.getText().trim().isEmpty())
            inputUrns.add(urnField.getText());
        AuthorityType authorityType = null;
        if (!typeComboBox.getSelectionModel().isEmpty())
            authorityType = typeComboBox.getSelectionModel().getSelectedItem();
        List<X509Certificate> trustedCerts = new ArrayList<>();

        List<X509Certificate> userCerts = new ArrayList<>();
        PrivateKey userPrivateKey = null;
        if (geniUserProvider.isUserLoggedIn()) {
            userCerts.addAll(geniUserProvider.getLoggedInGeniUser().getClientCertificateChain());
            userPrivateKey = geniUserProvider.getLoggedInGeniUser().getPrivateKey();
            Platform.runLater(() -> addScanDebugLogText(
                    "User " + geniUserProvider.getLoggedInGeniUser().getUserUrnString() + " is logged in: will use SSL with user authentication.\n"));
        } else
            Platform.runLater(() -> addScanDebugLogText("No user logged in: will use SSL without user authentication.\n"));

        AuthorityScannerInput input = new AuthorityScannerInput(
                inputUrns, inputUrls, trustedCerts, userCerts, userPrivateKey,
                authorityType, false/*userIsOfScannedAuthority*/);
        ScanFeedback feedback = new ScanFeedback() {
            @Override
            public boolean trustNonSelfSignedCert(List<X509Certificate> certificateList) {
                //TODO
                return true;
            }

            @Override
            public boolean trustSelfSignedCert(List<X509Certificate> certificateList) {
                //TODO
                return true;
            }

            @Override
            public boolean trustAlias(String urlHost, String certSubject) {
                //TODO
                return true;
            }
        };
        scanner = new ServerScanner(logger, input, feedback, jFedPreferences, useProxy, geniUserProvider, jFedConnectionProvider);
        scanner.addResultListener(line -> Platform.runLater(() -> addScanDebugLogText(line + "\n")));

        scanner.setMode(basicRadioButton.isSelected() ? ServerScanner.Mode.BASIC : ServerScanner.Mode.EXPERIMENTAL);

        scanner.scan();

        final StringBuilder certs = new StringBuilder();
        for (X509Certificate cert : scanner.getTrustedAuthCerts()) {
            certs.append(KeyUtil.x509certificateToPem(cert)).append('\n');
        }

        boolean foundUrn = scanner.getUrns().isEmpty() ? false :
                scanner.getUrns().getMostTrustedScannedSingleFact().getCertainty().isMoreCertainThan(ServerScanner.ScanCertainty.USER_INPUT);
        boolean foundType = scanner.getTypes().isEmpty() ? false :
                scanner.getTypes().getMostTrustedScannedSingleFact().getCertainty().isMoreCertainThan(ServerScanner.ScanCertainty.USER_INPUT);

        Platform.runLater(() -> {
            certTextArea.setText(certs.toString());
            scannedFacts.getChildren().clear();

            overviewLabelCon.setText(scanner.isConnectionSuccess() ? "OK" : "FAIL");
            overviewLabelGV.setText(scanner.isGetVersionSuccess() ? "OK" : "FAIL");
            overviewLabelUrn.setText(foundUrn ? "OK" : "FAIL");
            overviewLabelType.setText(foundType ? "OK" : "FAIL");

            for (Label l : Arrays.asList(overviewLabelCon, overviewLabelGV, overviewLabelUrn, overviewLabelType)) {
                if (l.getText().startsWith("OK"))
                    l.getStyleClass().add("overview-ok");
                else
                    l.getStyleClass().add("overview-fail");
            }
        });

        scanFactPaneByType = new HashMap<>();
        scanFactPaneByUrn = new HashMap<>();
        scanFactPaneByHrn = new HashMap<>();
        scanFactPaneByServertypeAndUrl = new HashMap<>();

        final List<ScanFactPane> panes = new ArrayList<>();
        for (ServerScanner.ScannedSingleFact fact : scanner.getTypes().getFacts()) {
            ScanFactPane pane = new ScanFactPane("type", fact);
            if (fact == scanner.getTypes().getMostTrustedScannedSingleFact())
                pane.setSelected(true);
            else
                pane.setSelected(false);
            panes.add(pane);
            scanFactPaneByType.put(fact, pane);
        }
        for (ServerScanner.ScannedSingleFact fact : scanner.getUrns().getFacts()) {
            ScanFactPane pane = new ScanFactPane("urn", fact);
            if (fact == scanner.getUrns().getMostTrustedScannedSingleFact())
                pane.setSelected(true);
            else
                pane.setSelected(false);
            panes.add(pane);
            scanFactPaneByUrn.put(fact, pane);
        }
        for (ServerScanner.ScannedSingleFact fact : scanner.getHrns().getFacts()) {
            ScanFactPane pane = new ScanFactPane("hrn", fact);
            if (fact == scanner.getHrns().getMostTrustedScannedSingleFact())
                pane.setSelected(true);
            else
                pane.setSelected(false);
            panes.add(pane);
            scanFactPaneByHrn.put(fact, pane);
        }
        for (Map.Entry<ApiInfo.Api, ServerScanner.ScannedInformation<String>> e : scanner.getUrlsByApi().entrySet()) {
            ApiInfo.Api st = e.getKey();
            ServerScanner.ScannedInformation<String> urls = e.getValue();
            for (ServerScanner.ScannedSingleFact fact : urls.getFacts()) {
                ScanFactPane pane = new ScanFactPane(st.getName() + " v" + st.getVersion() + " URL", fact);

                //only select URLs if they are NOT a guess
                //also exclude the user given URLs (because the user did NOT specify a version, but we did assume one)
                if (fact == urls.getMostTrustedScannedSingleFact()&& fact != null &&
                        fact.getCertainty().value > ServerScanner.ScanCertainty.GOOD_GUESS.value &&
                        fact.getCertainty() != ServerScanner.ScanCertainty.USER_INPUT)
                    pane.setSelected(true);
                else
                    pane.setSelected(false);
                panes.add(pane);
                scanFactPaneByServertypeAndUrl.put(new Pair<>(st, fact), pane);
            }
        }
        Platform.runLater(() -> scannedFacts.getChildren().setAll(panes));

        boolean atLeastUserProvidedUrn = scanner.getUrns().isEmpty() ? false :
                scanner.getUrns().getMostTrustedScannedSingleFact().getCertainty().isMoreOrEqualCertainThan(ServerScanner.ScanCertainty.USER_INPUT);

        //success, let user press OK
        Platform.runLater(() -> {
            addScanDebugLogText("\nScanning finished successfully. You can now review the scan results.\n");
            amUrlField.setDisable(false);
            urnField.setDisable(false);
            typeComboBox.setDisable(false);
            scanButton.setDisable(false);
            cancelButton.setDisable(false);
            scanLogBox.setVisible(true);
            progress1.setVisible(false);
            progress2.setVisible(false);


            postScanButtons.setVisible(true);
            postScan1ShowButton.setDisable(false);
            if (!foundUrn && !atLeastUserProvidedUrn)
                postScan1ShowButton.setText("Show (Incomplete) Results");
            else
                postScan1ShowButton.setText("Show Results");


            if (basicRadioButton.isSelected())
                tabs.getSelectionModel().select(overviewTab);
            else
                tabs.getSelectionModel().select(outputTab);
        });

        scanThread = null;
    }

    public void showScanResults() {
        if (scanner == null)
            return;

        if (scanner != null && !scanner.getTrustedAuthCerts().isEmpty() && !trustCertsCheckbox.isSelected()) {
            trustCertsCheckbox.getStyleClass().add("validation-error");
            tabs.getSelectionModel().select(securityTab);
            trustCertsCheckbox.requestFocus();
            return;
        }
        trustCertsCheckbox.getStyleClass().removeAll("validation-warning", "validation-error");


        ServerScanner.ScannedInformation<AuthorityType> types = new ServerScanner.ScannedInformation<>();
        Map<ApiInfo.Api, ServerScanner.ScannedInformation<String>> urlsByServerType = new HashMap<>();
        ServerScanner.ScannedInformation<String> hrns = new ServerScanner.ScannedInformation<>();
        ServerScanner.ScannedInformation<String> urns = new ServerScanner.ScannedInformation<>();
        Set<X509Certificate> trustedAuthCerts = scanner.getTrustedAuthCerts();
        String trustedAlias = scanner.getTrustedAlias();

        for (ServerScanner.ScannedSingleFact fact : scanner.getTypes().getFacts()) {
            ScanFactPane pane = scanFactPaneByType.get(fact);
            if (pane.isSelected()) types.add(fact);
        }
        for (ServerScanner.ScannedSingleFact fact : scanner.getUrns().getFacts()) {
            ScanFactPane pane = scanFactPaneByUrn.get(fact);
            if (pane.isSelected()) urns.add(fact);
        }
        for (ServerScanner.ScannedSingleFact fact : scanner.getHrns().getFacts()) {
            ScanFactPane pane = scanFactPaneByHrn.get(fact);
            if (pane.isSelected()) hrns.add(fact);
        }
        for (Map.Entry<ApiInfo.Api, ServerScanner.ScannedInformation<String>> e : scanner.getUrlsByApi().entrySet()) {
            ApiInfo.Api st = e.getKey();
            ServerScanner.ScannedInformation<String> urls = e.getValue();
            for (ServerScanner.ScannedSingleFact fact : urls.getFacts()) {
                ScanFactPane pane = scanFactPaneByServertypeAndUrl.get(new Pair<>(st, fact));
                if (pane.isSelected()) {
                    if (!urlsByServerType.containsKey(st))
                        urlsByServerType.put(st, new ServerScanner.ScannedInformation<>());
                    urlsByServerType.get(st).add(fact);
                }
            }
        }

        createTestbed =
                ServerScanner.createResult(types, urlsByServerType, hrns, urns, trustedAuthCerts, trustedAlias);
        createTestbed = new TestbedBuilder(createTestbed).createMinimized(JsonLdObjectsMetaData.Minimization.FULL_EMBED_ONLY_CHILDREN);

        if (stage != null)
            stage.close();
    }

    /**
     * Check if valid URL
     */
    public void validate() {
        amUrlField.getStyleClass().removeAll("validation-error", "validation-warning");
        try {
            URL test = new URL(amUrlField.getText());
        } catch (MalformedURLException e) {
            amUrlField.getStyleClass().add("validation-error");
        }
    }

    public String getAMUrl() {
        if (Objects.equals(amUrlField.getText(), "")) return null;
        return amUrlField.getText();
    }

    public Testbed getCreatedTestbed() {
        return createTestbed;
    }

    public boolean isCancelled() {
        return cancelled;
    }
}
