import copy
import json

from .experimental_models import (Query, QueryCondition, Scaffold, Plot)
from ..api.paths import (ASYNC_PATH, COMPOUNDS_PATH, IMPORT_PATH,
                         LIVE_REPORTS_PATH, PLOT_PATH, TOOLS_PATH,
                         SCAFFOLDS_PATH)
from ..client import LDClient
from ..decorators import _enforce_string_ids, deprecated


class ExperimentalLDClient(LDClient):
    """
    NOTE: This is an internal component of ldclient and interfacing with it directly is NOT fully supported

    Functions in the package are
     1. NEW
     2. NOT well tested,
     3. and, MAY break

    Also, these functions may break on upgrades.
    """

    @deprecated(version="8.11", replacement="update_advanced_search_query")
    def update_lr_query_and_search_compounds(self, live_report_id, project_id,
                                             conditions, expression):
        """
        Update the LiveReport query based on the specified conditions and expression.
        Subsequently do an advanced search query on the LiveReport.
        Note: The search result will exclude the entity IDs already present in the LR.

        :type live_report_id: :class:`str`
        :param live_report_id: LiveReport ID

        :type project_id: :class:`int`
        :param project_id: Project ID

        :type conditions: :class:`list` of :class:`experimental_models.QueryCondition`
        :param conditions: List of query conditions for search

        :type expression: :class:`str`
        :param expression: Advanced query expression

        :rtype: :class:`list` of :class:`str`
        :return: List of compound ids that match the search
        """

        new_conditions = QueryCondition.as_list(conditions)
        query_conditions = []
        for condition in new_conditions:
            query_condition = copy.deepcopy(condition)
            query_condition["inverted"] = False
            if (condition.get("type") == "observation"):
                query_condition["create_assay_limited_addable_column"] = False
                query_condition["delimiter"] = "__DEBUG__"
                query_condition["observation_date_inverted"] = False
                query_condition["operator"] = "="
                query_condition["related_column_conditions"] = []
            query_conditions.append(query_condition)
        current_query = self.client.get(
            service_path=LIVE_REPORTS_PATH,
            path='/{0}/query'.format(live_report_id))
        current_query["conditions"] = query_conditions
        current_query["expression"] = expression
        self.client.put(service_path=LIVE_REPORTS_PATH,
                        path='/{0}/query'.format(live_report_id),
                        data=json.dumps(current_query))

        search_request = {
            "compound_search_type": "ADVANCED_QUERY_SEARCH",
            "project_id": project_id,
            "live_report_id": live_report_id,
        }
        response = self.client.post(service_path=COMPOUNDS_PATH,
                                    path='/search',
                                    data=json.dumps(search_request))
        return [compound["id"] for compound in response["results"]]

    def restandardize(self, entity_ids):
        """
        Restandardize just the selected list of entities. The endpoint only attempts to handle the easy
        cases, and reject harder cases (like virtual collapsing onto another virtual).

        :param entity_ids - entityIds to be restandardized
        :type entity_ids - list of str
        """
        return self.client.post(service_path=TOOLS_PATH,
                                path="/restandardize",
                                json_data=entity_ids)

    def move_data(self, data_type, from_entity_id, to_entity_id):
        """
        Move data from one entity to another

        :param data_type: what data to be moved. Can be
            IMPORTED_DATA - non-DI'd assay data (3D data excluded)
            FREEFORM_DATA - FFC data, skips columns that would require overwriting a pre-existing value
            FREEFORM_DATA_FORCE_OVERWRITE - FFC data, overwrites pre-existing values
            MAESTRO_3D - assay 3D data, mostly from maestro imports
            COMMENTS
            RATIONALES
        :type data_type: str
        :type from_entity_id: str
        :type to_entity_id: str
        :return:
        """
        request = {
            "from_entity_id": from_entity_id,
            "to_entity_id": to_entity_id,
            "data_type": data_type,
        }
        return self.client.post(service_path=TOOLS_PATH,
                                path='/move_data',
                                json_data=request)

    @_enforce_string_ids("live_report_id")
    def get_advanced_search_query(self, live_report_id):
        """
        Get the Advanced Search Query of a LiveReport.

        :type live_report_id: :class:`str`
        :param live_report_id: The ID of a LiveReport.

        :return: The Advanced Search Query of the LiveReport specified.
        :rtype: :class:`~experimental_models.Query`
        """
        response = self.client.get(service_path=LIVE_REPORTS_PATH,
                                   path='/{0}/query'.format(live_report_id))
        return Query.from_dict(response)

    @_enforce_string_ids("live_report_id")
    def update_advanced_search_query(self, live_report_id, query):
        """
        Update the Advanced Search Query of a LiveReport.

        :type live_report_id: :class:`str`
        :param live_report_id: ID of the LiveReport to be updated.

        :type query: :class:`~experimental_models.Query`
        :param query: The updated Advanced Search Query object.

        :return: The updated Advanced Search Query of the LiveReport specified.
        :rtype: :class:`~experimental_models.Query`
        """
        response = self.client.put(service_path=LIVE_REPORTS_PATH,
                                   path='/{0}/query'.format(live_report_id),
                                   json_data=query.as_dict())
        return Query.from_dict(response)

    @_enforce_string_ids("live_report_id")
    def execute_advanced_search_query(self, live_report_id):
        """
        Execute an Advanced Search Query and return ids of entities found.
        Note that this does NOT modify the LiveReport specified.

        :type live_report_id: :class:`str`
        :param live_report_id: ID of the LiveReport to be updated.

        :type project_id: :class:`str`
        :param project_id: ID of the Project that the LiveReport belongs to.

        :return: The ids of entities found by the Advanced Search Query.
        :rtype: :class:`list` of :class:`str`
        """
        project_id = self.live_report(live_report_id).project_id
        data = {
            "compound_search_type": "ADVANCED_QUERY_SEARCH",
            "live_report_id": live_report_id,
            "project_id": project_id
        }
        response = self.client.post(service_path=COMPOUNDS_PATH,
                                    path='/search',
                                    json_data=data)
        return self._get_compound_ids_from_response(response)

    def create_scaffold(self, scaffold):
        """
        Create a new scaffold.

        :type scaffold: :class:`~models.Scaffold`
        :param scaffold: The Scaffold object to create.

        :rtype: :class:`~models.Scaffold`
        :return: The new Scaffold.
        """
        data = scaffold.as_dict()
        response = self.client.post(SCAFFOLDS_PATH, '', json_data=data)
        return Scaffold.from_dict(response)

    def get_scaffolds(self):
        """
        Get all scaffolds in LiveDesign.

        :rtype: :class:`list` of :class:`~models.Scaffold`
        :return: The list of Scaffolds.
        """
        response = self.client.get(SCAFFOLDS_PATH, '')
        return Scaffold.from_list(response)

    @_enforce_string_ids('live_report_id')
    def get_scaffolds_by_live_report_id(self, live_report_id):
        """
        Get all scaffolds in a LiveReport.

        :type live_report_id: :class:`str`
        :param live_report_id: The ID of the LiveReport to search for scaffolds

        :rtype: :class:`list` of :class:`~models.Scaffold`
        :return: The list of Scaffolds.
        """
        data = {'live_report_id': live_report_id}
        response = self.client.post(SCAFFOLDS_PATH, '/search', json_data=data)
        return Scaffold.from_list(response)

    def create_plot(self, plot):
        """
        Creates a new plot

        :type plot: :class:`~models.Plot`
        :param plot: The plot object to create

        :rtype :class:`~models.Plot`
        :return: The new Plot
        """
        payload = plot.as_dict()
        response = self.client.post(service_path=PLOT_PATH,
                                    path='',
                                    json_data=payload)
        return Plot.from_dict(response)

    def update_plot(self, plot):
        """
        Updates a plot

        :type plot: :class:`~models.Plot`
        :param plot: Updated plot object

        :rtype :class:`~models.Plot`
        :return: The updated Plot 
        """
        payload = plot.as_dict()
        response = self.client.put(service_path=PLOT_PATH,
                                   path='/{}'.format(plot.id),
                                   json_data=payload)
        return Plot.from_dict(response)

    def async_tasks(self, task_ids, status_types=None):
        """
        Searches for async tasks

        :type task_ids: :class:`list` of :class:`int`
        :param task_ids: The task_ids to search for

        :type status_types: :class:`list` of :class:`str`
        :param status_types: The status type for the tasks to search for

        :rtype: :class:`list` of :class:`dict`
        :return: List of Async tasks
        """
        all_status_types = [
            "finished", "running_stage2", "failed", "new", "submitted",
            "running", "importing", "imported", "callback_finished",
            "malformed_output", "killed"
        ]

        if status_types is None:
            status_types = all_status_types

        payload = {
            "search_type": "id_status_type",
            "id_and_status_search_query": {
                "async_task_ids": [str(task_id) for task_id in task_ids],
                "status_types": status_types
            }
        }
        response = self.client.post(service_path=ASYNC_PATH,
                                    path='/search',
                                    json_data=payload)
        return response["results"]

    @_enforce_string_ids('project_id')
    def create_compound(self, structure, project_id, scientist):
        """
        Creates a new compound

        :type structure: :class:`str`
        :param structure: The structure for the compound

        :type project_id: :class:`str`
        :param project_id: The project id where the compound is to be added

        :type scientist: :class:`str`
        :param scientist: Name of the scientist

        :rtype: :class:`str`
        :return: ID of the created compound
        """
        payload = {
            "structure": structure,
            "project_id": project_id,
            "lots": [{
                "number": 1,
                "scientist": scientist
            }]
        }
        response = self.client.post(service_path=COMPOUNDS_PATH,
                                    path='',
                                    json_data=payload)
        return response[0]["id"]

    @_enforce_string_ids('live_report_id')
    def live_report_results(self,
                            live_report_id,
                            view_details,
                            report_level="PARENT"):
        """
        Get live report results

        :type live_report_id: :class:`str`
        :param live_report_id: The LiveReport ID to get the results from

        :type view_details: :class:`list` of :class:`dict`
        :param view_details: The view details for the results

        :type report_level: :class:`str`
        :param report_level: The report level
        """
        payload = {
            "live_report_id": live_report_id,
            "report_level": report_level,
            "view_details": view_details
        }
        response = self.client.post(service_path=LIVE_REPORTS_PATH,
                                    path='/results',
                                    json_data=payload)
        return response

    @_enforce_string_ids('live_report_id')
    def load_csv_async(self, live_report_id, filename, file_content=None):
        """
        Load compounds into Live Design via CSV asynchronously

        :type live_report_id: :class:`str`
        :param live_report_id: The Live Report to add the compounds too

        :type filename: :class:`str`
        :param filename: Name of the CSV file to load

        :type file_content: :class:`bytes`
        :param file_content: the content of the CSV to load. If none is specified, attempt to get the file content
            using filename and the filesystem

        :rtype: :class:`str`
        :return: The task_id of the import upload task
        """
        import_settings = {
            "identifier": "Compound Structure",
            "type": "CSV_ROW_PER_COMPOUND",
            "project_name": "Global",
            "live_report_name": None,
            "source_live_report_id": str(live_report_id),
            "published": False,
            "new_live_report": False,
            "compounds_only": True,
            "columns_only": False,
            "add_data_to_live_report": True
        }

        data = {'import_settings': json.dumps(import_settings)}
        if file_content is None:
            with open(filename, 'rb') as input_file:
                file_content = input_file.read()

        files = {'import_data': file_content}
        response = self.client.post(
            service_path=IMPORT_PATH,
            path=ASYNC_PATH,
            data=data,
            files=files,
            request_specific_headers={'Content-Type': 'multipart/form-data'})

        return response['task_id']

    def compound_search_by_id_async(self, query, database_names, project_id,
                                    live_report_id):
        """
        Searches and adds the compounds that match the search query

        :type query: :class:`str`
        :param query: Corporate ID query string, as would be entered through the GUI search by ID function
            e.g. "V*" to search for compounds with IDs that start with "V"

        :type database_names: :class:`list` of :class:`str`
        :param database_names: List of database names

        :type project_id: :class:`str`
        :param project_id: Project in which to perform search

        :type live_report_id: :class:`str`
        :param live_report_id: LiveReport in which to perform search 

        :rtype: :class:`str`
        :return: The task_id of the compound search task
        """
        payload = {
            "compound_search_type": "CORPORATE_ID_SEARCH",
            "project_id": project_id,
            "live_report_id": live_report_id,
            "corporate_id_search_query": {
                "query": query
            },
            "database_search_query": {
                "databases": database_names
            },
        }
        response = self.client.post(service_path='/compounds',
                                    path='/search/async',
                                    json_data=payload)

        return response["task_id"]

    # NOTE(prakash) : This method is going to be re-exposed in ldclient once SS-37980 has been fixed
    def get_manageable_file(self, manageable_file_id):
        """
        Download a manageable file by ID (used for generic entities)

        :param manageable_file_id: the ID of the file to download
        :type manageable_file_id: :class:`str`

        :return: The file contents
        :rtype: :class:`bytes`
        """
        return self.client.get(service_path='/manageable_file',
                               path='/{}'.format(manageable_file_id),
                               json_response=False)
