import requests
import time


def get_dovetail_request_headers(api_key: str)->str:
    """Constructs a request header for dovetail.

    :param api_key: str: A dovetail api key.

    :returns: dovetail_request_headers: str: The dovetail request header.
    """
    dovetail_request_headers = {
        "accept": "application/json",
        "authorization": f"Bearer {api_key}"
        }
    return dovetail_request_headers

def parse_dovetail_response(dovetail_response):
    response_data = dovetail_response.json()["data"]
    response_object_ids = []
    response_object_details = {} 
    for object_data in response_data:
        object_id = object_data["id"]
        response_object_ids.append(object_id)
        response_object_details[object_id] = {key: value for key, value in object_data.items() if key != 'id'}
    next_page_cursor = dovetail_response.json()["page"]["next_cursor"]
    return response_object_ids, response_object_details, next_page_cursor

API_ENDPOINTS = {
    "projects": "https://dovetail.com/api/v1/projects",
    "notes": "https://dovetail.com/api/v1/notes",
    "highlights": "https://dovetail.com/api/v1/highlights",
    "insights": "https://dovetail.com/api/v1/insights",
    "tags": "https://dovetail.com/api/v1/tags"
}

def get_dovetail_note_metadata(api_key, dovetail_note_id):
    print(f"Retrieving the note '{dovetail_note_id}' metadatas ...")
    request_headers = get_dovetail_request_headers(api_key)
    get_note_metadata_query = f"{API_ENDPOINTS['notes']}/{dovetail_note_id}"
    dovetail_response = requests.get(get_note_metadata_query, headers=request_headers)
    dovetail_note_metadata = dovetail_response.json()["data"]
    dovetail_note_metadata = {key: value for key, value in dovetail_note_metadata.items() if key != 'id'}
    return dovetail_note_metadata


def export_dovetail_note_content(api_key, dovetail_note_id):
    print(f"Exporting the note '{dovetail_note_id}' content ...")
    NOTE_EXPORT_FORMAT = "markdown" # Notes can as well be exported with the 'html' format
    request_headers = get_dovetail_request_headers(api_key)
    get_note_query = f"{API_ENDPOINTS['notes']}/{dovetail_note_id}/export/{NOTE_EXPORT_FORMAT}"
    dovetail_response = requests.get(get_note_query, headers=request_headers)
    dovetail_note_content = dovetail_response.json()["data"]
    return dovetail_note_content


class DovetailProjectsCrawler:
    API_QUERIES_PER_MINUTE_LIMIT = 200 # See also: https://developers.dovetail.com/docs/rate-limits

    def __init__(self, api_key):
        self.api_key = api_key
        self.request_headers = get_dovetail_request_headers(api_key)

        self.project_ids = set()
        self.project_details = {}

        self.note_ids = set()
        self.note_details = {}

        self.highlight_ids = set()
        self.highlight_details = {}

        self.insight_ids = set()
        self.insight_details = {}

        self.tag_ids = set()
        self.tag_details = {}

    def collect_data_from_endpoint(self, endpoint_type: str, endpoint_filters: str="", parent_project_id: str="", list_all:bool=True):
        """        
        """
        ALLOWED_ENDPOINT_TYPES = ["projects", "notes", "highlights", "insights", "tags"]
        endpoint_object_ids = set()
        endpoint_object_details = {}
        if endpoint_type not in ALLOWED_ENDPOINT_TYPES:
            log_message = f"You set a bad value to 'endpoint_type': the current value is '{endpoint_type}' while allowed values are '{ALLOWED_ENDPOINT_TYPES}'."
            raise ValueError(log_message)
        
        log_message = f"Listing{' all ' if list_all else ' '}objects in the endpoint '{endpoint_type}'"
        if parent_project_id:
            log_message_enrichment  = f" (Related to the project '{parent_project_id}')."
        else:
            log_message_enrichment = "."
        log_message += log_message_enrichment
        print(log_message)
        api_query = API_ENDPOINTS[endpoint_type]

        if endpoint_filters or parent_project_id:
            api_query = f"{api_query}?"
            if parent_project_id:
                parent_project_filter = f"filter[project_id]={parent_project_id}"
                if endpoint_filters:
                    parent_project_filter = f"&{parent_project_filter}"
            api_query = f"{api_query}{endpoint_filters if endpoint_filters else ''}{parent_project_filter if parent_project_filter else ''}"

        dovetail_response = requests.get(api_query, headers=self.request_headers)
        response_object_ids, response_object_details, next_page_cursor = parse_dovetail_response(dovetail_response)
        endpoint_object_ids.update(response_object_ids)
        endpoint_object_details.update(response_object_details)
        
        if list_all and next_page_cursor:
            self.wait()
        elif not next_page_cursor:
            self.wait()
        if list_all:
            continue_collection = True
            if not next_page_cursor:
                continue_collection = False
            while continue_collection:
                api_query = f"{API_ENDPOINTS['projects']}?page[start_cursor]={next_page_cursor}"
                if endpoint_filters:
                    api_query = f"{api_query}&{endpoint_filters}"

                dovetail_response = requests.get(api_query, headers=self.request_headers)
                response_object_ids, response_object_details, next_page_cursor = parse_dovetail_response(dovetail_response)
                endpoint_object_ids.update(response_object_ids)
                endpoint_object_details.update(response_object_details)

                if not next_page_cursor:
                    continue_collection = False
                self.wait()

        self.update_collected_data(endpoint_type, endpoint_object_ids, endpoint_object_details, parent_project_id)
        print(f"The data from '{len(endpoint_object_ids)}' objects has been successfully collected from the endpoint '{endpoint_type}'!")
        
    
    def update_collected_data(self, endpoint_type, object_ids: set, object_details:dict, parent_project_id: str=""):
        """
        """
        if endpoint_type == "projects":
            self.project_ids.update(object_ids)
            self.project_details.update(object_details)

        elif endpoint_type == "notes":
            self.note_ids.update(object_ids)
            self.note_details.update(object_details)
             
        elif endpoint_type == "highlights":
            self.highlight_ids.update(object_ids)
            self.highlight_details.update(object_details)
        
        elif endpoint_type == "insights":
            self.insight_ids.update(object_ids)
            self.insight_details.update(object_details)
        
        elif endpoint_type == "tags":
            self.tag_ids.update(object_ids)
            self.tag_details.update(object_details)

        if parent_project_id:
            self.project_details[parent_project_id][endpoint_type] = object_details
    
    def extract_all_project_notes_data(self, project_id):
        project_details = self.project_details.get(project_id)
        if project_details:
            project_notes = project_details.get('notes')
            if project_notes:
                n_notes = len(project_notes)
                note_ids = list(project_notes)
                for note_index, note_id, in enumerate(note_ids):
                    print(f"Handling the note '{note_id}' from the project '{project_id}' n° {note_index+1}/{n_notes}")
                    note_data = project_notes[note_id]
                    note_is_deleted = note_data.get("deleted")
                    if note_is_deleted is False:
                        dovetail_note_metadata = get_dovetail_note_metadata(self.api_key, note_id)
                        self.wait()
                        dovetail_note_content = export_dovetail_note_content(self.api_key, note_id)
                        self.wait()
                        self.project_details[project_id]["notes"][note_id].update(dovetail_note_metadata)
                        self.project_details[project_id]["notes"][note_id]["content"] = dovetail_note_content
                    else:
                        self.project_details[project_id]["notes"][note_id]["content"] = ""
            else:
                raise ValueError(f"There is no 'notes' data associated with the project '{project_id}' in the object 'project_details': "\
                                 f"You can try using the function 'collect_data_from_endpoint' on the 'notes' enpoint first, setting the parameter 'parent_project_id' to '{project_id}'...")

        else:
            raise ValueError(f"There is no data associated with the project '{project_id}' in the object 'project_details'!")
        
    def wait(self,):
        strict_query_per_limit = self.API_QUERIES_PER_MINUTE_LIMIT - 0.1 * self.API_QUERIES_PER_MINUTE_LIMIT
        waiting_seconds_between_queries = 60.0 / strict_query_per_limit
        print(f"Waiting {waiting_seconds_between_queries: .2f} seconds ...")
        time.sleep(waiting_seconds_between_queries)