# -*- coding: utf-8 -*-
"""
@author: Noemi E. Cazzaniga - 2026
@email: noemi.cazzaniga@polimi.it
"""

import requests
import time
from pandas import DataFrame


__ra__ = {'timeout': 120.}
__h__ = {}
__BASE_URL__ = 'https://faostatservices.fao.org/api/v1/'



def set_requests_args(**kwargs):
    """
    Allows to set some arguments for "requests":
    - token : JWT token for Authorization Bearer header. (optional)
       Default: None.
    - username : username for the Faostat API. (optional)
       Used to get the token when token is not provided.
    - password : password for the Faostat API. (optional)
       Used to get the token when token is not provided.
    - lang : language of the results (en, fr, es, ar, zh, ru). (optional)
       Default: 'en'.
    - timeout: how long to wait for the server before raising an error. (optional)
        Default: 120 sec.
    - proxies : dict with protocol, user and password. (optional)
        For the Faostat API, only https proxy.
        Examples: {'https': 'http://myuser:mypass@123.45.67.89:1234'}
                  {'https': 'https://123.45.67.89:1234'}
        Default: None.
    - verify : for the server’s TLS certificate. (optional)
        Default: None.
    - cert : user-provided SSL certificate. (optional)
       Default: None.

    Returns
    -------
    None.

    """
    global __ra__, __h__, __BASE_URL__

    assert set(kwargs.keys()).issubset({"timeout", "proxies", "verify", "cert", "token", "lang", "username", "password"}), "Wrong arguments."
    assert (bool(kwargs.get('username', False)) == bool(kwargs.get('password', False))), "Incomplete access credentials."
    assert (kwargs.get('token', False) != bool(kwargs.get('username', False) and kwargs.get('password', False))), "Either token or username+password must be provided."
    
    lang = kwargs.pop('lang', 'en')
    __BASE_URL__ += lang + '/'
    
    token = kwargs.pop('token', None)
    if not token:
        __user__ = kwargs.pop('username')
        __pass__ = kwargs.pop('password')
    
    for k in kwargs:
        __ra__[k] = kwargs[k]

    if not token:
        # Programmatic retrieval of token via login
        data = {'username': __user__, 'password': __pass__}
        response = __postresp__('https://faostatservices.fao.org/api/v1/auth/login', data)
        token = format(response.json()['AuthenticationResult']['AccessToken'])
    __h__['Authorization'] = 'Bearer {0}'.format(token)


def get_requests_args():
    """
    Get the current set arguments for "requests".
    
    Returns
    -------
    a dict with arg names and their respective values.
    Username and password are not retrievable.
    """
    res = __ra__.copy()
    res['token'] = __h__.get('Authorization',' ').split(' ')[1]
    res['lang'] = __BASE_URL__.split('/')[-2]
    return res


def __getresp__(url, params=None):
    """
    Makes a request and returns the response.

    Parameters
    ----------
    url : str
        URL for the request.
    params : dict
        username and password:
            {key: value, ...}.

    Raises
    ------
    Exception
        if there is a connection problem,
        if there is a timeout problem,
        if the dataset was not found.

    Returns
    -------
    resp : response

    """
    with requests.get(url, params=params, headers=__h__, **__ra__) as resp:
        if resp.status_code == 500 and resp.text == 'Index: 0, Size: 0':
            print("{0} not found in the Faostat server".format(url.split('?')[0].split('/')[-1]))
            raise Exception()
        if resp.status_code == 524:
            raise TimeoutError()
        # print(resp.url)
        resp.raise_for_status()
        return resp


def __postresp__(url, data):
    """
    Makes a request and returns the response.

    Parameters
    ----------
    url : str
        URL for the request.
    data : dict, optional
        parameters to pass in URLs:
            {key: value, ...}.
        The default is None.

    Raises
    ------
    Exception
        if there is a connection problem,
        if there is a timeout problem.

    Returns
    -------
    resp : response

    """
    with requests.post(url, data=data, **__ra__) as resp:
        # print(resp.url)
        if resp.status_code == 524:
            raise TimeoutError()
        resp.raise_for_status()
        return resp


def list_datasets():
    """
    Returns a list of datasets available on the Faostat server.

    Returns
    -------
    l : list
        list of available datasets and some metadata.

    """
    l = [('code',
          'label',
          'date_update',
          'note_update',
          'release_current',
          'state_current',
          'year_current',
          'release_next',
          'state_next',
          'year_next'),
         ]
    response = __getresp__(__BASE_URL__ + 'groups')
    groups = [el['code'] for el in response.json()['data']]
    for group_code in groups:
        url = __BASE_URL__ + 'domains/' + group_code
        response = __getresp__(url)
        domains = response.json()['data']
        for d in domains:
            if d.get('date_update', None) is not None:
                l.append((d.get('code', None),
                          d.get('label', None),
                          d['date_update'],
                          d.get('note_update', None),
                          d.get('release_current', None),
                          d.get('state_current', None),
                          d.get('year_current', None),
                          d.get('release_next', None),
                          d.get('state_next', None),
                          d.get('year_next', None))
                          )
    return l


def list_datasets_df():
    """
    Returns a pandas dataframe listing the datasets available on the Faostat server.

    Returns
    -------
    l : dataframe
        list of available datasets and some metadata.

    """
    d = list_datasets()
    return DataFrame(d[1:], columns=d[0])


def list_pars(code):
    """
    Given the code of a Faostat dataset,
    returns the available parameters as list.

    Parameters
    ----------
    code : str
        code of the dataset.

    Returns
    -------
    d : list
        parameters for data filtering.

    """
    url = __BASE_URL__ + 'dimensions/' + code
    d = [('parameter code', 'coding_systems', 'subdimensions {code: meaning}')]
    response = __getresp__(url)
    data = response.json()['data']
    for par in data:
        code = par['id']
        subdimensions = dict([(sd['id'], sd['label']) for sd in par['subdimensions']])
        coding_systems = par['subdimensions'][0]['coding_systems']  ## all the subdimensions have same coding_systems
        d.append((code, coding_systems, subdimensions))
    return d


def list_pars_df(code):
    """
    Given the code of a Faostat dataset,
    returns the available parameters as list.

    Parameters
    ----------
    code : str
        code of the dataset.

    Returns
    -------
    d : dataframe
        parameters for data filtering.

    """
    d = list_pars(code)
    return DataFrame(d[1:], columns=d[0])


def get_par(code, par):
    """
    Given the code of a Faostat dataset,
    and the name of one of its parameters or subdimension,
    returns the available values as dict or dataframe.

    Parameters
    ----------
    code : str
        code of the dataset.
    par : str
        code of the parameter or the subdimension.

    Returns
    -------
    d : dict
        {label: code, ...}.

    """
    url = __BASE_URL__ + 'codes/' + par + '/' + code
    response = __getresp__(url)
    data = response.json()['data']
    d = dict()
    for el in data:
        d[el['label']] = el['code']
    return d


def get_par_df(code, par):
    """
    Given the code of a Faostat dataset,
    and the name of one of its parameters or subdimension,
    returns the available values as dict or dataframe.

    Parameters
    ----------
    code : str
        code of the dataset.
    par : str
        code of the parameter or the subdimension.

    Returns
    -------
    d : dataframe
         with label, code, aggregate type.

    """
    url = __BASE_URL__ + 'codes/' + par + '/' + code
    response = __getresp__(url)
    data = response.json()['data']
    d = []
    for el in data:
        d.append((el['label'], el['code'], el['aggregate_type']))
    d = DataFrame(d, columns=['label','code','aggregate_type'])
    return d


def get_data(code, **kwargs):
    """
    Given the code of a Faostat dataset,
    returns the data as a list of tuples.
    To download only a subset of the dataset, you need to pass pars={key: value, ...}:
    from the codes obtained with get_par_list and get_par.

    Parameters
    ----------
    code : str
        code of the dataset.
    pars : dict, optional
        parameters to retrieve a subset of the dataset:
            {key: value, ...}.
        The default is {}.
    coding : dict, optional
        coding_system of the parameters:
            {key: value, ...}.
        The default is {}.
    show_flags : bool, optional
        True to download also the data flags.
        The default is False.
    null_values : bool, optional
        True to download also the null values.
        The default is False.
    show_notes : bool, optional
        True to download also the notes.
        The default is False.
    strval : bool, optional
        True to keep the values as provided by Faostat (as str),
        False to try to convert them to numbers.
        The default is True.
    limit : int, optional
        limit of the results. If set to a positive value, pagination is used. 
        If -1, the entire dataset is requested at once.
        The default is -1.

    Returns
    -------
    l : list
        data with header.

    """
    assert set(kwargs.keys()).issubset({'pars', 'coding', 'show_flags', 'null_values', 
                 'show_notes', 'strval', 'limit'}), "Error: unexpected kwarg."

    pars = kwargs.get('pars', {})
    coding = kwargs.get('coding', {})
    show_flags = kwargs.get('show_flags', False)
    null_values = kwargs.get('null_values', False)
    show_notes = kwargs.get('show_notes', False)
    strval = kwargs.get('strval', True)
    limit = kwargs.get('limit', -1)

    url = __BASE_URL__ + 'data/' + code

    params = list(pars.items())
    # print('s',params)
    for codsys in coding:
        params += [(codsys + '_cs', coding[codsys]),]
    params += [('show_codes', True),
               ('show_unit', True),
               ('show_flags', show_flags),
               ('show_notes', show_notes),
               ('null_values', null_values),
               ('limit', limit),
               ('output_type', 'objects')
              ]
    # print(params)
    

    if limit == -1:
        response = __getresp__(url, params=params)
        try:
            dsd = response.json()['metadata']['dsd']
        except:
            print('Warning: seems like no data are available for your selection.')
            return []
        header = tuple([d['label'] for d in dsd])
        data = response.json()['data']
    else:
        data = []
        header = None
        offset = 0
        while True:
            pageparams = params + [('offset', offset),]
            response = __getresp__(url, params=pageparams)
            if not header:
                try:
                    dsd = response.json()['metadata']['dsd']
                except:
                    print('Warning: seems like no data are available for your selection.')
                    return []
                header = tuple([d['label'] for d in dsd])
            batch = response.json().get('data', [])
            data.extend(batch)
            
            if len(batch) < limit:
                break
            
            offset += limit
            time.sleep(1)

    l = [header,]
    if strval:
        for row in data:
            l += [tuple([row.get(h, None) for h in header]),]
    else:
        for row in data:
            r = []
            for h in header:
                if h != 'Value' and 'Code' not in h:
                    r.append(row.get(h, None))
                else:
                    try:
                        r.append(eval(row.get(h, None)))
                    except:
                        r.append(row.get(h, None))
            l.append(tuple(r))
    return l


def get_data_df(code, **kwargs):
    """
    Given the code of a Faostat dataset,
    returns the data as pandas dataframe.
    To download only a subset of the dataset, you need to pass pars={key: value, ...}:
    from the codes obtained with get_par_list and get_par.
     
    Parameters
    ----------
    code : str
        code of the dataset.
    pars : dict, optional
        parameters to retrieve a subset of the dataset:
            {key: value, ...}.
        The default is {}.
    coding : dict, optional
        coding_system of the parameters:
            {key: value, ...}.
        The default is {}.
    show_flags : bool, optional
        True to download also the data flags.
        The default is False.
    null_values : bool, optional
        True to download also the null values.
        The default is False.
    show_notes : bool, optional
        True to download also the notes.
        The default is False.
    strval : bool, optional
        True to keep the values as provided by Faostat (as str),
        False to try to convert them to numbers.
        The default is True.
    limit : int, optional
        limit of the results. If set to a positive value, pagination is used. 
        If -1, the entire dataset is requested at once.
        The default is -1.
     
    Returns
    -------
    l : dataframe
        data with header.
     
    """
    d = get_data(code, **kwargs)
    return DataFrame(d[1:], columns=d[0])
