"""
FTreesSearchTool.py - functions for searching chemical space using the Feature Trees (FTrees) tool.

Use of this code is subject to the terms of the StarDrop License Agreement and
use, copying or redistribution of this file for use by other persons or
organisations is prohibited

Copyright(C) Optibrium Ltd 2026
support@optibrium.com

The FTrees tool was created and is maintained BioSolveIT.
"""

import PythonWidgets
import FTreesWidgets
import socket
import os
import time
import logging
import subprocess
import webbrowser
import platform


import app
import FTreesCSVHelper
import FTreesConfig as config

# Set up logging module
log = logging.getLogger('JanssenModels')
log.setLevel(config.log_level)
log_path = config.log_path
handler = logging.FileHandler(log_path, mode='w')
formatter = logging.Formatter("%(asctime)s;%(levelname)s;%(message)s")
handler.setFormatter(formatter)
log.addHandler(handler)

socket.setdefaulttimeout(60)

FTREES_PATH_FILE = "path_to_FTrees_executable.txt"


def perform_ftrees_search(appui, space_config):
    """
    Perform a search of chemical space for compounds similar to the selected compound.

    One molecule is provided.
    This tool creates a data set of the similar molecules, gives their chemical space name and
    shows the reagents which can be used to create the compound.

    :param appui:    The StarDrop appui object
    :param space_config: A set of options to control the search and import
    :return: None
    """
    try:
        # get currently selected molecule
        smiles_input = appui.getCurrentMolecules()

        # if no molecule is selected, tell user to select one and re-run
        if not smiles_input:
            appui.show(
                "Please select a molecule before searching for similar compounds")
        elif len(smiles_input) > 1:
            appui.show(
                "Please select exactly one molecule")

        else:
            # get the compound ID for the query compound. If none, don't set an ID for the query molecule
            try:
                query_id = appui.getSelectedIDs()[0]
            except IndexError:
                query_id = ""

            smiles_input = smiles_input[0]
            log.info("Running {0} query for {1}".format(space_config.vendor, smiles_input))

            if os.path.isfile(space_config.space_file_lookup):
                with open(space_config.space_file_lookup, 'r') as file:
                    space_file = file.read()

            else:
                appui.show("Please browse to {0} space file".format(space_config.vendor))
                browser = PythonWidgets.PyFileDialog()
                browser.setWindowTitle('Find space file')
                path_to_space = browser.run()
                if path_to_space:
                    space_file = path_to_space[0]
                else:
                    return

            log.info("Path to space file: {0}".format(space_file))
            # Ask user for input parameters
            params = FTreesWidgets.FTreesWidget()
            params.setWindowTitle("Choose search parameters")

            if params.run():
                similarity = params.getSpinBoxValue()
                if similarity < 0.8:
                    min_sim_threshold = similarity

                else:
                    min_sim_threshold = 0.8
                similarity = round(similarity, 2)

                num_of_results = params.getMaxNumResults()
                num_of_results = int(num_of_results)

                # warn users if the results will take a very long time to return
                if num_of_results > 99999:
                    warning_dlg = PythonWidgets.Warning()
                    warning_dlg.setMessage("It may take a long time to return so many compounds, "
                                           "are you sure you would like to continue?")

                    if warning_dlg.run():
                        _run_command(appui,
                                     smiles_input,
                                     space_file,
                                     similarity,
                                     min_sim_threshold,
                                     num_of_results,
                                     query_id,
                                     space_config)

                else:
                    _run_command(appui,
                                 smiles_input,
                                 space_file,
                                 similarity,
                                 min_sim_threshold,
                                 num_of_results,
                                 query_id,
                                 space_config)

    except Exception as e:
        log.error(repr(e))


def _run_command(appui,
                 smiles_input,
                 space_file,
                 similarity,
                 min_sim_threshold,
                 num_of_results,
                 query_id,
                 space_config):
    """
    Perform a search of the chemical space for compounds similar to the selected compound.

    This tool runs the FTrees program via the command line, then sends the output CSV file to the
    CSV helper to package it into a StarDrop data set.

    :param appui:                The StarDrop appui object
    :param smiles_input:         The selected compound
    :param space_file:           The chemical space file
    :param similarity:           The target similarity to the selected compound
    :param min_sim_threshold:    The minimum similarity which will be returned. If the target similarity
                                is 0.8 or greater, it will be 0.8, else it will be the same as the
                                target similarity
    :param num_of_results:       The maximum number of results to be returned
    :param space_config:        A set of options to control the search and import
    :return: None
    """
    try:
        # show progress bar
        prog_dlg = PythonWidgets.SimpleProgress()
        prog_dlg.setWindowTitle('Finding {0} compounds'.format(space_config.vendor))
        prog_dlg.setMessage('Searching...')
        prog_dlg.show()
        prog_dlg.setProgress(8)

        # An SD file is created for the input molecule as this is the format required for the FTrees program
        smiles_file = open(space_config.ref_molecule_file, 'w')
        smiles_file.write(appui.getMolFileFromSmiles(smiles_input))
        smiles_file.close()

        directory = os.getcwd()
        path_to_ftrees = ""

        # get the path to the FTrees executable, this is stored in a file after a successful execution
        if os.path.isfile(FTREES_PATH_FILE):
            with open(FTREES_PATH_FILE, 'r') as file:
                path_to_ftrees = file.read()

        cancel = False
        no_lic_file = False
        message = ""

        if not path_to_ftrees:

            if platform.system() == 'Windows':
                appui.show('Please browse to FTrees application')
                browser = PythonWidgets.PyFileDialog()
                browser.setWindowTitle("Please browse to FTrees application")
                path_to_ftrees = browser.run()
                if path_to_ftrees:
                    path_to_ftrees = path_to_ftrees[0]
                else:
                    appui.show('No FTrees application was selected. Ending process.')
                    exit()
            elif platform.system() == 'Darwin':
                path_to_ftrees = '/Applications/FTrees.app/Contents/MacOS/Ftrees'

        log.info("Path to FTrees: {0}".format(path_to_ftrees))
        try:
            check_license = subprocess.Popen([path_to_ftrees, "--license-info"], stdout=subprocess.PIPE)

            # If license has expired or isn't in the FTrees directory, try to find another version
            # of FTrees which is licensed and use that for the search.
            for item in check_license.stdout:
                message += item.decode()
            # If it doesn't try to find the license file elsewhere but simply prints a message
            if "expired" in message:
                appui.show("License has expired" + "\n\n" + message)
            elif "Cannot find license file" in message:
                appui.show("No license file found" + "\n\n" + message)
            else:
                log.info("FTrees License OK")
                log.debug(message)

        except Exception as e:
            log.error("Failure when locating and checking FTrees")
            log.error(str(e))

        if not cancel and not no_lic_file:

            ref_mol_file = os.path.join(directory, space_config.ref_molecule_file)
            try:
                output_file = os.path.join(directory, space_config.results_file)
                # Output file is actually named as OUTPUT_1.csv - weird FTrees behaviour apparently
                results_file = "{0}_1{1}".format(*os.path.splitext(output_file))
            except Exception as e:
                print(e)

            progress = 10.0

            error_text = None

            try:

                num_of_results = str(num_of_results)
                similarity = str(similarity)
                min_sim_threshold = str(min_sim_threshold)
                command_params = [path_to_ftrees, "-i", ref_mol_file, "-s", space_file, "-o", output_file,
                                  "--maxNofResults", num_of_results, "--minSimilarityThreshold",
                                  min_sim_threshold, "--targetSimilarity", similarity]
                if platform.system() == "Windows":
                    info = subprocess.STARTUPINFO()
                    info.dwFlags = subprocess.STARTF_USESTDHANDLES | subprocess.STARTF_USESHOWWINDOW
                    info.wShowWindow = subprocess.SW_HIDE
                    check_success = subprocess.Popen(command_params,
                                                     startupinfo=info,
                                                     stdout=subprocess.PIPE,
                                                     stderr=subprocess.PIPE)
                else:
                    check_success = subprocess.Popen(command_params,
                                                     stdout=subprocess.PIPE,
                                                     stderr=subprocess.PIPE)
                check_count = 0
                message_index = 0
                check_interval = 10  # seconds
                prog_dlg.setMessage(space_config.progress_messages[0])
                while check_success.poll() is None and (check_count * check_interval)/60 < config.session_timeout:
                    if check_count % 3 == 0:
                        message_index = message_index + 1 if message_index < 2 else 0
                        prog_dlg.setMessage(space_config.progress_messages[message_index])
                    time.sleep(check_interval)
                    progress += 1.0
                    prog_dlg.setProgress(progress)
                    check_count += 1
                    output = check_success.communicate()
                    error_text = output[1].decode()
                if check_success.poll() is None:
                    prog_dlg.hide()
                    appui.show("Operation timed out ({0} minutes)".format(config.session_timeout))
                    return

            except Exception as e:
                log.error("Error running FTrees")
                log.error(str(e))
                prog_dlg.hide()
                if "The system cannot find the file specified" in str(e):
                    appui.show('Cannot locate FTrees application')
                else:
                    appui.show("Error encountered")
                return

            prog_dlg.setProgress(87)

            if not error_text:
                try:
                    dataset = FTreesCSVHelper.create_data_set(results_file, config,
                                                              space_config.id_column_name, space_config.base_url)
                    os.remove(results_file)
                except Exception as e:
                    log.error("Unable to create data set")
                    log.error(str(e))
                    dataset = None

                prog_dlg.setProgress(95)

                if 'https' in query_id:
                    query_id = query_id.split(">")[1]
                    query_id = query_id.split("<")[0]

                if dataset:
                    appui.exportDS("{0} {1} search".format(query_id, space_config.vendor), dataset.pack())
                else:
                    prog_dlg.hide()
                    appui.showError("Unable to package the results into a StarDrop data set")

                # If the run was successful, write the paths to the FTrees executable and chemical space file to
                # text files for future use.
                with open(FTREES_PATH_FILE, 'w') as FTrees_path:
                    FTrees_path.write(path_to_ftrees)

                with open(space_config.space_file_lookup, 'w') as file:
                    file.write(space_file)

            else:
                try:
                    formatted = error_text
                    if "\r\n" in formatted:
                        formatted = formatted.replace("\r\n", "")
                        if "ERROR:" in formatted:
                            formatted = formatted.replace(" ERROR: ", "", 1)

                    if "Space does not exist" in formatted:
                        appui.show("Space file was not found\n\nPlease browse to {0} file".format(space_config.vendor))
                        browser = PythonWidgets.PyFileDialog()
                        browser.setWindowTitle('Find space file')
                        path_to_space = browser.run()
                        space_file = path_to_space[0]
                        with open(space_config.sp, 'w+') as file:
                            file.write(space_file)
                        _run_command(appui,
                                     smiles_input,
                                     space_file,
                                     similarity,
                                     min_sim_threshold,
                                     num_of_results,
                                     query_id,
                                     space_config)
                    else:
                        appui.show("Error: " + formatted)
                except Exception as e:
                    print(e)

            prog_dlg.setProgress(99)
            prog_dlg.hide()

    except Exception as e:
        log.error("Run command failed")
        log.error(str(e))


def send_compound_request(appui, vendor_config):
    """
    This tool creates an email to send to a vendor requesting a quote for the listed compounds.
    Any compounds selected in the data set will be included in the email. The email is created
    in the default email client.

    :param appui:               The StarDrop appui object
    :param vendor_config: A set of options to control the request
    :return: None
    """
    try:
        cancelled = False
        # get currently selected molecule
        smiles_input = appui.getCurrentMolecules()
        if len(smiles_input) > 350:
            cancelled = True
            appui.show('Please select fewer compounds')

        # if no molecule is selected, tell user to select one and re-run
        if not smiles_input:
            cancelled = True
            appui.show(
                "Please select at least one molecule before creating the quote request")

        if not cancelled:
            # Import the current data set to get the structures
            dataset = appui.importDS("")
            ds = app.Dataset()
            ds.parse(dataset)

            # Find the structure column
            structure_column = -1
            result_name_col = -1
            for col_index in range(ds.columnSz()):
                col_type = ds.columnType(col_index)
                if ds.columnName(col_index) == vendor_config.id_column_name:
                    result_name_col = col_index
                if structure_column == -1 and col_type == app.mol:
                    structure_column = col_index

            if result_name_col == -1:
                appui.show("No result name to send to vendor, only SMILES string will be included")
                list_of_comps = ""
                for item in smiles_input:
                    for row_index in range(ds.rowSz()):
                        smiles = ds.stringval(structure_column, row_index)
                        if item == smiles:
                            list_of_comps += 'SMILES: ' + smiles + '%0D%0D'

            else:
                list_of_comps = ""
                for item in smiles_input:
                    for row_index in range(ds.rowSz()):
                        smiles = ds.stringval(structure_column, row_index)
                        if result_name_col == -1:
                            result_name = ""
                        else:
                            result_name = ds.stringval(result_name_col, row_index)
                        if item == smiles:
                            if "__" not in result_name:
                                result_name = result_name.split(">")[1]
                                result_name = result_name.split("<")[0]
                            list_of_comps += 'SMILES: ' + item + '%0DResult name: ' + result_name + '%0D%0D'

            # Create the email containing the list of compounds. If there is no default email client,
            # show an error
            mail_header = "mailto:{0}?subject={1} compound request for quote".format(vendor_config.email_address,
                                                                                     vendor_config.vendor)
            if len(smiles_input) == 1:
                try:
                    webbrowser.open("{0}&body=Hello,%0D%0DPlease provide me with a quote for "
                                    "<amount in mg or g> of the following compound:%0D%0D {1}"
                                    "%0DKind regards,%0D<your name>%0D%0D".format(mail_header, list_of_comps),
                                    new=1)
                except Exception as e:
                    appui.show(f'Could not find default email client ({repr(e)})')
            else:
                try:
                    webbrowser.open("{0}&body=Hello,%0D%0DPlease provide me with a quote for "
                                    "<amount in mg or g> of each of the following compounds:%0D%0D{1}"
                                    "%0DKind regards,%0D<your name>%0D%0D".format(mail_header, list_of_comps),
                                    new=1)
                except Exception as e:
                    appui.show(f'Could not find default email client ({repr(e)})')

    except Exception as e:
        log.error(f"Error sending e-mail request ({repr(e)})")
