import requests
import xml.etree.ElementTree as ET
from datetime import datetime
from .errors import DimensionsOrKwargsError, NotAListError, TooManyDimensionsError, TooManyDimensionsError2, DifferentDimensionValueError, KwargsError, OtherResponseCodeError, WrongFormatError, MappingsError, OldVersionError
from .rate_limiter import rate_limiter
from .search import write_to_csv
from ._logging import logger


def get_data(dataflow_id, timeout=60, dimensions=[], force_url=False, start_period="", end_period="", updated_after="", returned="list", csv_name="", select_last_edition=True, debug_url=False, test_rows = 0, **kwargs):
    """  

    Parameters
    ----------
    dataflow_id : String, 
        the dataflow id of the dataset.
    timeout : Int, 
        the maximum time before the request is aborted.
    dimensions : List, 
        an ordered list of strings of the dimensions. Make sure to leave it null if you use kwargs. The default is [].
    force_url : Bool, 
        used to force the URL request even if the they were not checked against the allowed dimensions. The default is False.
     debug_url : Bool, 
         used to obtain the generated URL for manual debugging. The default is False.
    start_period : Int, 
        used to filter for start period. The default is "".
    end_period : Int, 
        used to filter for end period. The default is "".
    updated_after : Int, 
        used to filter for update period. The default is "".
    returned : String, 
        "" or "csv", the format to be returned. The default is "" to get a list of dictionaries.
    csv_name : String,
        string with the path of the csv; if left null, it will auto-generate a name and place it in the directory of the script.
    select_last_edition : Bool,
        default True, set on False if you want to manually select the edition.
    debug_url : Bool,
        default False, set on True to print the URL on screen.
    test_rows = Int,
        default 0, if it is a number higher than 0 the request will deliver the top n rows, if lower it will deliver the last n rows.
    
    **kwargs : Key=value, 
        each kwarg will be used in place of the keys of the URL. Can't be used together with the dimensions list. Usage: freq="Q", correz="W"...


    Returns
    -------
    list of dictionaries : Returns a list of dictionaries with all the dataflows.
    csv file : Creates a csv file in the path of your code if you choose the csv.

    """
    if returned == "dataframe":
        raise OldVersionError()
    # We should fetch_dimensions only if they are needed. If the already have the dimensions_list, we save one request to the Istat server.
    def check_dimensions(timeout):
        nonlocal dimensions_list # Nonlocal call is fundamental to avoid multiple dimensions_lists calls.
        if dimensions_list == []:
            dimensions_list = get_dimensions(dataflow_id, timeout)
        return dimensions_list
    
    
    ### Wrapped request function.##############
    @rate_limiter
    def fetch_data(api_url, timeout=timeout):
        response = requests.get(api_url, timeout=timeout)
        return response
    ###########################################  
    
    
    dimensions = [string.upper() for string in dimensions]
    dimensions_list = [] # Initialize to avoid using complex syntax to check if this exists
    if returned != "list" and returned != "csv":
        raise WrongFormatError()
    if dimensions and kwargs:
        raise DimensionsOrKwargsError
    elif not dimensions and not kwargs:
        dimensions = ["all"]
    elif not isinstance(dimensions, list):
        raise NotAListError
        return None
    elif not force_url and dimensions:
        # Sometimes url checker can bug out for undiscovered reasons, in this case you are free to force the program to request data
        dimensions_list = check_dimensions(timeout)
        if len(dimensions) != max(item["order"] for item in dimensions_list):
            raise TooManyDimensionsError(dimensions, max(item["order"] for item in dimensions_list))
        
        
        counter = 1
        for _, user_dim in enumerate(dimensions):
            user_dim = user_dim.upper()
            if user_dim != "":
                counter_dim_list = [dim for dim in dimensions_list if dim["order"] == counter]
                dimension_values = [dim["dimension_value"] for dim in counter_dim_list]
                if user_dim in dimension_values:
                    counter += 1
                else:
                    dimension_ids = list({dim["dimension_id"] for dim in counter_dim_list})
                    dimension_values_list = dimension_values
                    raise DifferentDimensionValueError(user_dim, dimension_ids, dimension_values_list)
            else:
                counter += 1 
                
                
    elif kwargs: # The check must be done even if force_url==True as the program needs to fetch the positioning for dimension values.
        dimensions_list = check_dimensions(timeout)
        # Check how many dimensions there are
        for _ in range(max(item["order"] for item in dimensions_list)):
            dimensions.append("")
        for key, value in kwargs.items():
            check = False
            while not check:
                for row in dimensions_list:
                    if key.casefold() == row["dimension_id"].casefold():
                        if value.casefold() == row["dimension_value"].casefold():
                            dimensions[row["order"]-1] = value # Order-1 is needed as order row starts from 1, order for lists starts from 0
                            check = True
                if check:
                    break
                raise KwargsError(key, value)
                
    
    # Important part: this feature allows users to always select the latest edition ISTAT has for the dataflow.
    # It uses the function find_last_edition to iter and find the last edition and automatically fills it for the user.
    # It won't work if an edition is manually added via kwargs or dimensions. It also checks whether the correct number of dimensions was added in order to gracefully fail.
    # There are multiple checks: 
    # Check if dimensions_list is already fetched from the controlo before to avoid another call;
    # Check if kwargs are used and t_bis is not defined;
    # Check if the dimensions are less than the order position for the dimensions.
    if select_last_edition==True:
        dimensions_list = check_dimensions(timeout)
        last_edition = find_last_edition(dimensions_list)
        if last_edition is not None:
            filtered_list = [dim for dim in dimensions_list if dim["dimension_id"] == "T_BIS"]
            order_values = [dim["order"] for dim in filtered_list]
            order = int(order_values[0])-1
            if kwargs and "t_bis" in kwargs:
                logger.warning("You passed an edition value 't_bis=x' with select_last_edition set on True. Remove the t_bis variable if you want the module to automatically fetch the last edition.")
                pass
            if order >= len(dimensions):
                raise TooManyDimensionsError2(dimensions, max(item["order"] for item in dimensions_list))
            if dimensions[order] != "":
                logger.warning("You passed an edition value 't_bis=x' with select_last_edition set on True. Remove the t_bis variable if you want the module to automatically fetch the last edition.")
                pass
            else:
                try:
                    dimensions[order] = last_edition
                except TooManyDimensionsError:
                    raise TooManyDimensionsError
        else:
            logger.info("Edition dimension not found for %s. Skipping auto-fetching.", dataflow_id)
            
            
    # Checking if time periods are formatted right and building the strings
    dim_string = '.'.join(dimensions)
    if start_period=="" and end_period=="":
        period_string = "all?"
    elif end_period=="":
        period_string = f"all?startPeriod={start_period}"
    elif start_period=="":
        period_string = f"all?endPeriod={end_period}"
    elif start_period=="" and end_period=="" and isinstance(start_period, int):
        period_string = f"all?startPeriod={start_period}&endPeriod={end_period}"
        period_string=""
    if updated_after != "":
        if period_string:
            period_string.append(f"&updatedAfter={updated_after}")
        else:
            period_string = f"all=updatedAfter={updated_after}"
    elif updated_after == "":
        easter_egg = "You've been blessed! Hello, pleased to meet you!"
        
        
    # Checking if it is a test call and how to build the string. Negative number in test_rows will prompt a last n observations instaed of a top n observations.
    test_string = ""
    if test_rows < 0:
        test_string = f"lastNObservations={test_rows*-1}"
    elif test_rows > 0:
        test_string = f"firstNObservations={test_rows}"
    # We must check and see if period_string is a value; if it is an empty string, then we must skip the &.
    if test_string != "" and period_string == "all?":
        test_string = f"{test_string}"
    elif test_string != "" and period_string != "all?":
        test_string = f"&{test_string}"
        
    
    # Build the string and make the request: if the response is 200, then keep going.
    api_url = rf"https://esploradati.istat.it/SDMXWS/rest/data/{dataflow_id}/{dim_string}/{period_string}{test_string}"
    if debug_url==True:
        print(api_url)
    response = fetch_data(api_url, timeout=timeout)
    response_code = response.status_code
    response_text = response.text
    if response_code == 500:
        raise MappingsError(dataflow_id, response_code, response_text)
    elif response_code != 200:
        raise OtherResponseCodeError(response_code, response_text)
    elif response.status_code == 200:
        response = response.content.decode('utf-8-sig')
        tree = ET.ElementTree(ET.fromstring(response)) 
        namespaces = {
            'message': 'http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message',
            'generic': 'http://www.sdmx.org/resources/sdmxml/schemas/v2_1/data/generic',
            'common': 'http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common'
        }
        
        data = []
        for series in tree.findall('.//generic:Series', namespaces):
            series_key = {}
            series_key_element = series.find('generic:SeriesKey', namespaces)
            if series_key_element is not None:
                for value in series_key_element.findall('generic:Value', namespaces):
                    key_id = value.get('id')
                    value_text = value.get('value')
                    series_key[key_id] = value_text

            for obs in series.findall('generic:Obs', namespaces):
                obs_data = series_key.copy()
                obs_dimension = obs.find('generic:ObsDimension', namespaces)
                if obs_dimension is not None:
                    obs_data['TIME_PERIOD'] = obs_dimension.get('value')
                obs_value = obs.find('generic:ObsValue', namespaces)
                if obs_value is not None:
                    obs_data['OBS_VALUE'] = obs_value.get('value')
                data.append(obs_data)
                
        if not data:
            print("No data retrieved. Open a request on GitHub, please.")
            return None        
        else:
            if returned == "list":
                return data
            elif returned == "csv":
                if csv_name == "":
                    csv_name = f"{dataflow_id}_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
                write_to_csv(data, csv_name)


def find_last_edition(data): # Convoluted function that gets the last_edition based on the dimensions_list.
    try:
        edition_list = [item for item in data if item["dimension_id"] == "T_BIS"]
    except:
        return None

    edition_values = [item["dimension_value"] for item in edition_list]
    date_info = []

    for edition in edition_values:
        year = int(edition[0:4])
        month_start = edition.find("M") + 1
        month_end = month_start + 2 if edition[month_start:month_start + 2].isdigit() else month_start + 1
        month = int(edition[month_start:month_end])

        if "G" in edition:
            day_start = edition.find("G") + 1
            day_end = day_start + 2 if edition[day_start:day_start + 2].isdigit() else day_start + 1
            day = int(edition[day_start:day_end])
            date_part = f"{year}{str(month).zfill(2)}{str(day).zfill(2)}"
        else:
            day = ""
            date_part = f"{year}{str(month).zfill(2)}"

        suffix = ""
        if "_" in edition:
            suffix_pos = edition.find("_")
            suffix = edition[suffix_pos:]

        date_info.append((int(date_part), suffix))

    if not date_info:
        return None

    date_info.sort(reverse=True, key=lambda x: x[0])
    last_date, last_suffix = date_info[0]

    last_date_str = str(last_date)
    year = last_date_str[0:4]
    yearless = last_date_str[4:]

    if "G" in edition_list[0]:
        month = yearless[0:2] if yearless[0] != "0" else yearless[1:2]
        day = yearless[-2:] if yearless[-2] != "0" else yearless[-1]
        chosen_edition = f"{year}M{month}G{day}{last_suffix}"
    else:
        month = yearless[0:2] if yearless[0] != "0" else yearless[1:2]
        chosen_edition = f"{year}M{month}{last_suffix}"

    return chosen_edition

    
def get_dimensions(dataflow_id, timeout=60, lang="en", returned="list", debug_url=False, csv_name=""):
    """
    

    Parameters
    ----------
    dataflow_id : String, 
        the dataflow id of the dataset.
    timeout : Int, 
        the maximum time before the request is aborted. Normally passed by get_data in normal get_data queries. Default is 60.
    lang : String, 
        "en" or "it", the language the search will be performed in. The default is "en".
    get : Bool, 
        used only when called by the function get_dataframe() with force_url=False. The default is False.
    returned : String, 
        "" or "csv", the format to be returned. The default is "".
    debug_url: Bool, 
        set to True if you want the URL the request is being sent to printed to the console.
    csv_name : String,
        string with the path of the csv; if left null, it will auto-generate a name and place it in the directory of the script.

    Returns
    -------
    list of dictionaries : Returns a list of dicts with all the dataflows if you choose the dataframe.
    csv file: Creates a csv file in the path of your code if you choose the csv.

    """
    if returned == "dataframe":
        raise OldVersionError()
        
    ### Wrapped request function.############## 
    @rate_limiter
    def fetch_dimensions(api_url, timeout=timeout):
        response = requests.get(api_url, timeout=timeout)
        return response
    ###########################################
    
    if "," in dataflow_id:        
        parts = dataflow_id.split(",")
        dataflow_id = parts[1]
        
    if returned != "list" and returned != "csv":
        raise WrongFormatError()
    namespaces = {
        'message': 'http://www.sdmx.org/resources/sdmxml/schemas/v2_1/message',
        'structure': 'http://www.sdmx.org/resources/sdmxml/schemas/v2_1/structure',
        'common': 'http://www.sdmx.org/resources/sdmxml/schemas/v2_1/common',
        'xml': 'http://www.w3.org/XML/1998/namespace'
    }
    data_url = f"https://esploradati.istat.it/SDMXWS/rest/availableconstraint/{dataflow_id}/?references=all&detail=full"
    if debug_url == True:
        print(data_url)

    response = fetch_dimensions(data_url, timeout=timeout)
    codelist_list = []
    response_code = response.status_code
    response_text = response.text
    if response_code == 200:
        response = response.content.decode("utf-8-sig")
    elif response_code == 500:
        raise MappingsError(dataflow_id, response_code, response_text)
    else:
        raise OtherResponseCodeError(response_code)           
    tree = ET.ElementTree(ET.fromstring(response))
    cube_region = tree.find('.//structure:CubeRegion', namespaces)
    key_values = cube_region.findall('.//common:KeyValue', namespaces)

    codelist_list = []

    for codelist in tree.findall(".//structure:Codelist", namespaces):
        codelist_id = codelist.get('id')[3:]  # The prefix "CL_" must be removed as it is internal
        codelist_name = codelist.find(f'.//common:Name[@xml:lang="{lang}"]', namespaces).text

        for code in codelist.findall('.//structure:Code', namespaces):
            code_id = code.get('id')
            code_name = code.find(f'.//common:Name[@xml:lang="{lang}"]', namespaces).text

            for idx, key_value in enumerate(key_values):
                for value in key_value.findall('common:Value', namespaces):
                    if value.text == code_id:
                        codelist_list.append({
                            'dimension_id': codelist_id,
                            'dimension_name': codelist_name,
                            'dimension_value': code_id,
                            'value_explanation': code_name,
                            'order': idx + 1
                        })
                        break
        
    codelist_list = sorted(codelist_list, key=lambda x: x["order"])
    if returned == "list":
        return codelist_list
    elif returned == "csv":
        if csv_name == "":
            csv_name = f"{dataflow_id}_dimensions.csv"
        write_to_csv(codelist_list, csv_name)

        