"""
enamine_web_services.py - library for accessing Enamine Web Services

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
"""
import json
import uuid
from time import sleep

import requests
import base64

import PythonWidgets
from requests import RequestException

from enamine_exceptions import AuthenticationException, ConnectionException, DelayedResultNotFound, EnamineException

BASE_URL = "https://www.enaminestore.com/api/"
RETRY_COUNT = 60
NO_AUTHENTICATION_CODE = 'no authentication code'
DELAYED_RESPONSE_WAIT = 0.8


class EnamineWebServices:
    def __init__(self, username, authentication_code):
        """
        Create a new instance of Enamine WebServices with the provided
        credentials

        :param username:            The username to use while accessing the API
        :param authentication_code: The password to use while accessing the API
        """
        self.username = username
        self.authentication_code = authentication_code
        self.session = requests.Session()
        self.client_application_uuid = str(uuid.uuid4())
        self.auth_token = None
        self.refresh_token = None

    def _get_auth_token(self, auth_response: requests.Response):
        if 'refreshToken' in auth_response.json():
            self.refresh_token = auth_response.json()['refreshToken']

    def login(self):
        """
        Login or refresh login

        :raises AuthenticationException: If authentication fails
        """

        auth_url = f"{BASE_URL}v1/auth/as-customer/sign-in"
        refresh_url = f"{BASE_URL}v1/auth/refresh"

        try:
            if self.refresh_token:
                response = self.session.post(refresh_url, json={"refreshToken": self.refresh_token})
                if response.ok:
                    self._get_auth_token(response)
                    return

            request_body = {
                "email": base64.b64encode(self.username.encode('utf-8')).decode('utf-8'),
                "password": base64.b64encode(self.authentication_code.encode('utf-8')).decode('utf-8'),
                "remember": True
            }

            response = self.session.post(
                auth_url,
                json=request_body
            )
        except requests.exceptions.RequestException as e:
            raise ConnectionException from e

        if response.ok:
            self._get_auth_token(response)
            return

        raise AuthenticationException

    def load_molecules(self, molecule_ids: list[str], currency: str) -> dict:
        """
        Load a molecule with the provided Enamine molecule ID

        :param molecule_ids: Enamine molecule ID e.g. EN300-190278
        :param currency:    The currency selected(either USD or EUR)
        :return: Detailed information about the specified molecule
        """
        if not self.authentication_code:
            print(NO_AUTHENTICATION_CODE)
            return {}

        headers = {'Authorization': "login=" + str(self.username) + ", pass=" + str(self.authentication_code)}

        request_url = f"{BASE_URL}v2/catalog/search/by/codes"

        request_body = {"compounds": molecule_ids, "currency": currency}

        try:
            response = self.session.post(request_url, json=request_body, headers=headers)
            return response.json()
        except Exception as ex:
            raise ConnectionException from ex

    def chemical_search(self,
                        smiles: str,
                        search_type: str,
                        catalog: str,
                        similarity_index: float = 0.8,
                        currency: str = "USD") -> str:
        """
        Perform a chemical search based on the provided input SMILES using the
        specified search strategy. The input may be a full structure or a
        fragment based on the particular search type specified.

        :param smiles:              The structure/fragment to search for
        :param search_type:         The chemical search type to use. Substructure
                                    is "IN", Similarity is "LIKE" and Exact Match
                                    is "EQ".
        :param catalog:             The catalog to search (Enamine web services allows
                                    you to search: building-blocks, screening-compounds,
                                    made-building-blocks, real, bioactive-compounds)
        :param similarity_index:    If using a similarity search then this is
                                    used to specify similarity in the range 0-1
        :param currency:    The currency selected(either USD or EUR)
        :raises ConnectionException: If connection to enamine web services fails
        :return: A delayed result uuid
        """

        if not self.authentication_code:
            print(NO_AUTHENTICATION_CODE)
            return ""

        _search_mode = "screening-compounds"
        if catalog == "Screening compounds":
            _search_mode = "screening-compounds"
        elif catalog == "Building blocks":
            _search_mode = "building-blocks"
        elif catalog == "REAL database":
            _search_mode = "real"
        elif catalog == "Made Building blocks":
            _search_mode = "made-building-blocks"
        elif catalog == "Bioactive compounds":
            _search_mode = "bioactive-compounds"

        if search_type == "IN":
            url = f"{BASE_URL}v2/catalog/search/in/{_search_mode}/by/SMILEs/SUBSTRUCTURE"
        elif search_type == "LIKE":
            url = f"{BASE_URL}v2/catalog/search/in/{_search_mode}/by/SMILEs/SIMILARITY"
            if similarity_index:
                url = f"{url}/{similarity_index}"
        elif search_type == "EQ":
            url = f"{BASE_URL}v2/catalog/search/in/{_search_mode}/by/SMILEs/EXACT"
        else:
            raise EnamineException(f"Unknown search type: {search_type}")

        request_data = {
            "compounds": smiles,
            "currency": currency
        }

        headers = {
            "client-application-uuid": self.client_application_uuid
        }

        try:
            print(f"Sending client app id: {self.client_application_uuid}")
            response = self.session.post(url, json=request_data, headers=headers)
            return response.json()['delayedResultUuid']
        except Exception as e:
            raise ConnectionException from e

    def fetch_delayed_result(self,
                             delayed_result_uuid: str,
                             prog_dlg: PythonWidgets.SimpleProgress | None = None) -> dict:
        """
        Fetch a delayed result from the enamine web services

        :param delayed_result_uuid: the uuid of the delayed result to fetch
        :param prog_dlg: An optional progress dialog to update while fetching the result
        :return: A delayed result to fetch if available
        """

        if not self.authentication_code:
            print(NO_AUTHENTICATION_CODE)
            return {}

        url = f"{BASE_URL}v2/delayed-results/by/uuid/{delayed_result_uuid}"

        headers = {
            "client-application-uuid": self.client_application_uuid
        }

        start_progress = 15
        end_progress = 55
        for i in range(RETRY_COUNT):
            if prog_dlg is not None:
                progress = start_progress + ((end_progress - start_progress) * ((i + 1) / RETRY_COUNT))
                prog_dlg.setProgress(int(progress))

            try:
                print(f"Fetching client app id {self.client_application_uuid}")
                response = self.session.get(url, headers=headers)
                result_json = response.json()
                if response.ok:
                    if 'success' in result_json and 'delayedResult' in result_json and result_json['success'] is True:
                        return json.loads(result_json['delayedResult'])
            except RequestException as e:
                raise ConnectionException from e
            sleep(DELAYED_RESPONSE_WAIT)

        raise DelayedResultNotFound
