import copy
import json

from .experimental_models import (Query, QueryCondition, Scaffold)
from ..api.paths import (COMPOUNDS_PATH, LIVE_REPORTS_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 Live Report query based on the specified conditions and expression.
        Subsequently do an advanced search query on the Live Report.
        Note: The search result will exclude the entity IDs already present in the LR.

        :type live_report_id: :class:`str`
        :param live_report_id: Live Report 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 entity_alias_search(self, aliases):
        """
        Searches for availability of entity aliases

        :param aliases: entity aliases/IDs to search for
        :type aliases: :class:`list` of :class:`str`

        :return: Entity Alias search results
        :rtype: :class:`dict`
        """
        # TODO(badlato): Create classes to replace the output dict
        query = {'search_type': 'alias', 'entity_alias_query': {'aliases': aliases}}
        # TODO(badlato): Add path to api.paths.py
        return self.client.post(service_path='/entity_alias', path='/search', json_data=query)

    def import_generic_entity_async(self,
                                    filename,
                                    live_report_id,
                                    entities,
                                    import_type='single_entity',
                                    identifier=None):
        """
        Imports simple generic entity

        :param filename: path of file to import as the entity
        :type filename: :class:`str`

        :param live_report_id: live report ID to import into
        :type live_report_id: :class:`str`

        :param entities: entity ID spec with keys: index, id, update_if_empty, delete_model_results
        :type entities: :class:`list` of :class:`dict`

        :param import_type: single_entity, csv_row_per_entity, zip_file_per_entity, mae_record_per_entity
        :type import_type: :class:`str`

        :param identifier: name of column in CSV that holds IDs
        :type identifier: :class:`str`

        :return: Async task
        :rtype: :class:`dict`
        """
        # TODO(badlato): Create classes to replace the input & output dicts
        import_request = {
            "type": import_type,
            "live_report_id": live_report_id,
            "entities": entities,
            "identifier": identifier,
        }

        data = {'import_request': json.dumps(import_request)}
        files = {'import_data': (filename, open(filename, 'rb'), '')}
        # TODO(badlato): Add path to api.paths.py
        response = self.client.post(
            service_path='/generic_entity',
            path='/import/async_task',
            data=data,
            files=files,
            request_specific_headers={'Content-Type': 'multipart/form-data'})
        return response

    def import_generic_entity(self,
                              filename,
                              live_report_id,
                              entities,
                              import_type='single_entity',
                              identifier=None):
        """
        synchronous generic entity import

        :param identifier: name of column in CSV that holds IDs
        :type identifier: :class:`str`

        :param import_type: type of import. e.g. 'single_entity' or 'csv_row_per_entity'
        :type import_type: :class:`str`

        :param filename: path of file to import as the entity
        :type filename: :class:`str`

        :param live_report_id: live report ID to import into
        :type live_report_id: :class:`str`

        :param entities: entity ID spec with keys: index, id, update_if_empty, delete_model_results. ex: [{"index": 0, "id": "generic_from_ldclient", "update_if_empty": False, "delete_model_results": False }]
        :type entities: :class:`list` of :class:`dict`

        :return: import response
        :rtype: :class:`dict`
        """
        # TODO(badlato): Create classes to replace the input & output dicts
        response = self.import_generic_entity_async(filename,
                                                    live_report_id,
                                                    entities,
                                                    import_type=import_type,
                                                    identifier=identifier)
        print(response)
        result_url = self.wait_and_get_result_url(response['task_id'])
        print(result_url)
        results = self.get_task_result(result_url)

        return results

    def parse_csv_for_generic_entity_import(self, filename, identifier):
        """
        Parse out generic entity IDs to be imported

        :param filename: path of file to parse
        :type filename: :class:`str`

        :param identifier: name of column in CSV that holds IDs
        :type identifier: :class:`str`

        :return: parse response
        :rtype: :class:`dict`
        """
        import_request = {"type": "csv_row_per_entity", "identifier": identifier}

        data = {'import_request': json.dumps(import_request)}
        files = {'import_data': (filename, open(filename, 'rb'), '')}
        response = self.client.post(
            service_path='/generic_entity',
            path='/import/parse',
            data=data,
            files=files,
            request_specific_headers={'Content-Type': 'multipart/form-data'})
        return response

    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)

    def list_manageable_files(self, project_id):
        """
        List all manageable files in the specified project

        :param project_id: ID of the project to search
        :type project_id: :class:`str`

        :return: List of manageable files
        :rtype: :class:`list` of :class:`dict`
        """
        request = {"search_type": "project", "project_query": {"project_id": project_id}}

        response = self.client.post('/manageable_file', path='/search', json_data=request)
        return 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)
