import json
import requests
import math

from pkg_resources import get_distribution
from .exceptions import ApiError

from .decorators import pandasize

__all__ = ['OpenAQ']

__version__ = get_distribution('py-openaq').version

class API(object):
    """Generic API wrapper object.
    """
    def __init__(self, **kwargs):
        self._key       = kwargs.pop('key', '')
        self._pswd      = kwargs.pop('pswd', '')
        self._version   = kwargs.pop('version', None)
        self._baseurl   = kwargs.pop('baseurl', None)
        self._headers   = {'content-type': 'application/json'}

    def _make_url(self, endpoint, **kwargs):
        """Internal method to create a url from an endpoint.

        :param endpoint: Endpoint for an API call

        :type endpoint: string

        :returns: url
        """
        endpoint = "{}/{}/{}".format(self._baseurl, self._version, endpoint)

        extra = []
        for key, value in kwargs.items():
            if isinstance(value, list) or isinstance(value, tuple):
                #value = ','.join(value)
                for v in value:
                    extra.append("{}={}".format(key, v))
            else:
                extra.append("{}={}".format(key, value))

        if len(extra) > 0:
            endpoint = '?'.join([endpoint, '&'.join(extra)])

        return endpoint

    def _send(self, endpoint, method = 'GET', **kwargs):
        """Make an API call of any method

        :param endpoint: API endpoint
        :param method: API call type. Options are PUT, POST, GET, DELETE

        :type endpoint: string
        :type method: string

        :returns: (status_code, json_response)

        :raises ApiError: raises an exception
        """
        auth = (self._key, self._pswd)
        url  = self._make_url(endpoint, **kwargs)

        if method == 'GET':
            resp = requests.get(url, auth = auth, headers = self._headers)
        else:
            raise ApiError("Invalid Method")

        res = resp.json()

        # Add a 'pages' attribute to the meta data
        try:
            res['meta']['pages'] = math.ceil(res['meta']['found'] / res['meta']['limit'])
        except:
            pass

        return resp.status_code, res

    def _get(self, url, **kwargs):
        return self._send(url, 'GET', **kwargs)

class OpenAQ(API):
    """Create an instance of the OpenAQ API

    :param version: API version.
    :param kwargs: API options.

    :type version: string
    """
    def __init__(self, version = 'v1', **kwargs):
        self._baseurl = 'https://api.openaq.org'

        super(OpenAQ, self).__init__(version = version, baseurl = self._baseurl)

    @pandasize()
    def cities(self, **kwargs):
        """Returns a listing of cities within the platform.

        :param country: limit results by a certain country
        :param limit: limit results in the query. Default is 100. Max is 10000.
        :param page: paginate through the results. Default is 1.
        :param df: convert the output from json to a pandas DataFrame
        :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local

        :return: dictionary containing the *city*, *country*, *count*, and number of *locations*

        :type country: 2-digit ISO code
        :type limit: number
        :type page: number
        :type df: boolean
        :type index: string

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.cities()
        >>> resp['results']
        [
            {
                "city": "Amsterdam",
                "country": "NL",
                "count": 21301,
                "locations": 14
            },
            {
                "city": "Badhoevedorp",
                "country": "NL",
                "count": 2326,
                "locations": 1
            },
            ...
        ]
        """
        return self._get('cities', **kwargs)

    @pandasize()
    def countries(self, **kwargs):
        """Returns a listing of all countries within the platform

        :param limit: change the number of results returned. Max is 10000. Default is 100.
        :param page: paginate through results. Default is 1.
        :param df: return the results as a pandas DataFrame
        :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local

        :type limit: int
        :type page: int
        :type df: boolean
        :type index: string

        :return: dictionary containing the *code*, *name*, *count*, *cities*, and *locations*.

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.countries()
        >>> resp['results']
        [
            {
                "cities": 174,
                "code": "AT",
                "count": 121987,
                "locations": 174,
                "name": "Austria"
            },
            {
                "cities": 28,
                "code": "AU",
                "count": 1066179,
                "locations": 28,
                "name": "Australia",
            },
            ...
        ]
        """
        return self._get('countries', **kwargs)

    @pandasize()
    def latest(self, **kwargs):
        """Provides the latest value of each parameter for each location

        :param city: limit results by a certain city. Defaults to ``None``.
        :param country: limit results by a certain country. Should be a 2-digit
                        ISO country code. Defaults to ``None``.
        :param location: limit results by a city. Defaults to ``None``.
        :param parameter: limit results by a specific parameter. Options include [
                            pm25, pm10, so2, co, no2, o3, bc]
        :param has_geo: filter items that do or do not have geographic information.
        :param coordinates: center point (`lat`, `long`) used to get measurements within a
                                certain area. (Ex: coordinates=40.23,34.17)
        :param radius: radius (in meters) used to get measurements. Must be used with coordinates.
                        Default value is 2500.
        :param limit: change the number of results returned. Max is 1000. Default is 100.
        :param page: paginate through the results.
        :param df: return results as a pandas DataFrame
        :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local

        :type city: string
        :type country: string
        :type location: string
        :type parameter: string
        :type has_geo: boolean
        :type coordinates: string
        :type radius: int
        :type limit: int
        :type page: int
        :type df: boolean
        :type index: string

        :return: dictionary containing the *location*, *country*, *city*, and number of *measurements*

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.latest()
        >>> resp['results']
        [
            {
                "location": "Punjabi Bagh",
                "city": "Delhi",
                "country": "IN",
                "measurements": [
                    {
                        "parameter": "so2",
                        "value": 7.8,
                        "unit": "ug/m3",
                        "lastUpdated": "2015-07-24T11:30:00.000Z"
                    },
                    {
                        "parameter": "co",
                        "value": 1.3,
                        "unit": "mg/m3",
                        "lastUpdated": "2015-07-24T11:30:00.000Z"
                    },
                    ...
                ]
                ...
            }
        ]
        """
        return self._get('latest', **kwargs)

    @pandasize()
    def locations(self, **kwargs):
        """Provides metadata about distinct measurement locations

        :param city: Limit results by one or more cities. Defaults to ``None``. Can define as a single city
                        (ex. city = 'Delhi'), a list of cities (ex. city = ['Delhi', 'Mumbai']), or as a tuple
                        (ex. city = ('Delhi', 'Mumbai')).
        :param country: Limit results by one or more countries. Should be a 2-digit
                        ISO country code as a string, a list, or a tuple. See `city` for details.
        :param location: Limit results by one or more locations.
        :param parameter: Limit results by one or more parameters. Options include [
                            pm25, pm10, so2, co, no2, o3, bc]
        :param has_geo: Filter items that do or do not have geographic information.
        :param coordinates: center point (`lat`, `long`) used to get measurements within a
                                certain area. (Ex: coordinates=40.23,34.17)
        :param nearest: get the X nearest number of locations to `coordinates`. Must be used
                        with coordinates. Wins over `radius` if both are present. Will add the
                        `distance` property to locations.
        :param radius: radius (in meters) used to get measurements. Must be used with coordinates.
                        Default value is 2500.
        :param limit: change the number of results returned. Max is 1000. Default is 100.
        :param page: paginate through the results.
        :param df: return results as a pandas DataFrame
        :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local

        :type city: string, array, or tuple
        :type country: string, array, or tuple
        :type location: string, array, or tuple
        :type parameter: string, array, or tuple
        :type has_geo: boolean
        :type coordinates: string
        :type nearest: int
        :type radius: int
        :type limit: int
        :type page: int
        :type df: boolean
        :type index: string

        :return: a dictionary containing the *location*, *country*, *city*, *count*,
                    *sourceName*, *sourceNames*, *firstUpdated*, *lastUpdated*, *parameters*, and *coordinates*

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.locations()
        >>> resp['results']
        [
            {
                "count": 4242,
                "sourceName": "Australia - New South Wales",
                "firstUpdated": "2015-07-24T11:30:00.000Z",
                "lastUpdated": "2015-07-24T11:30:00.000Z",
                "parameters": [
                    "pm25",
                    "pm10",
                    "so2",
                    "co",
                    "no2",
                    "o3"
                ],
                "country": "AU",
                "city": "Central Coast",
                "location": "wyong"
            },
            ...
        ]
        """
        return self._get('locations', **kwargs)

    @pandasize()
    def measurements(self, **kwargs):
        """Provides data about individual measurements

        :param city: Limit results by a certain city. Defaults to ``None``.
        :param country: Limit results by a certain country. Should be a 2-digit
                        ISO country code. Defaults to ``None``.
        :param location: Limit results by a city. Defaults to ``None``.
        :param parameter: Limit results by one or more parameters. Options include [
                            pm25, pm10, so2, co, no2, o3, bc]
        :param has_geo: Filter items that do or do not have geographic information.
        :param coordinates: center point (`lat`, `long`) used to get measurements within a
                        certain area. (Ex: coordinates=40.23,34.17)
        :param radius: radius (in meters) used to get measurements. Must be used with `coordinates`.
                        Default value is 2500.
        :param value_from: Show results above a value threshold. Must be used with `parameter`.
        :param value_to: Show results below a value threshold. Must be used with `parameter`.
        :param date_from: Show results after a certain date. Format should be ``Y-M-D``.
        :param date_to: Show results before a certain date. Format should be ``Y-M-D``.
        :param sort: The sort order (``asc`` or ``desc``). Must be used with `order_by`.
        :param order_by: Field to sort by. Must be used with **sort**.
        :param include_fields: Include additional fields in the output. Allowed values are: *attribution*,
                            *averagingPeriod*, and *sourceName*.
        :param limit: Change the number of results returned.
        :param page: Paginate through the results
        :param df: return the results as a pandas DataFrame
        :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local

        :type city: string
        :type country: string
        :type location: string
        :type parameter: string, array, or tuple
        :type has_geo: boolean
        :type coordinates: string
        :type radius: int
        :type value_from: number
        :type value_to: number
        :type date_from: date
        :type date_to: date
        :type sort: string
        :type order_by: string
        :type include_fields: array
        :type limit: number
        :type page: number
        :type df: boolean
        :type index: string

        :return: a dictionary containing the *date*, *parameter*, *value*, *unit*,
            *location*, *country*, *city*, *coordinates*, and *sourceName*.

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.measurements(city = 'Delhi')
        >>> resp['results']
        {
            "parameter": "Ammonia",
            "date": {
                "utc": "2015-07-16T20:30:00.000Z",
                'local': "2015-07-16T18:30:00.000-02:00"
            },
            "value": "72.9",
            "unit": "ug/m3",
            "location": "Anand Vihar",
            "country": "IN",
            "city": "Delhi",
            "coordinates": {
                "latitude": 43.34,
                "longitude": 23.04
            },
            "attribution": {
                "name": "SINCA",
                "url": "http://sinca.mma.gob.cl/"
            },
            {
                "name": "Ministerio del Medio Ambiente"
            }
            ...
        }
        """
        return self._get('measurements', **kwargs)

    def fetches(self, **kwargs):
        """Provides data about individual fetch operations that are used to populate
        data in the platform.

        :param limit: change the number of results returned. Max is 10000. Default is 100.
        :param page: paginate through the results. Default is 1.

        :type limit: int
        :type page: int

        :return: dictionary containing the *timeStarted*, *timeEnded*, *count*, and *results*

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.fetches()
        >>> resp
        {
            "meta": {
                "name": "openaq-api",
                "license":
                "website":
                "page": 1,
                "limit": 100,
                "found": 3,
                "pages": 1
            },
            "results": [
                {
                    "count": 0,
                    "results": [
                        {
                            "message": "New measurements inserted for Mandir Marg: 1",
                            "failures": {},
                            "count": 0,
                            "duration": 0.153,
                            "sourceName": "Mandir Marg"
                        },
                        {
                            "message": "New measurements inserted for Sao Paulo: 1898",
                            "failures": {},
                            "count": 1898,
                            "duration": 16.918,
                            "sourceName": "Sao Paulo"
                        },
                        ...
                    ],
                    "timeStarted": "2016-02-07T15:25:04.603Z",
                    "timeEnded": "2016-02-07T15:25:04.793Z",
                }
            ]
        }
        """
        return self._get('fetches', **kwargs)

    def parameters(self):
        """
        Provides a simple listing of parameters within the platform.

        :return: a dictionary containing the *id*, *name*, *description*, and
            *preferredUnit*.

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.parameters()
        >>> resp['results']
        [
            {
                "id": "pm25",
               "name": "PM2.5",
               "description": "Particulate matter less than 2.5 micrometers in diameter",
               "preferredUnit": "ug/m3"
            }
            ...
        ]
        """
        return self._get('parameters')

    @pandasize()
    def sources(self, **kwargs):
        """
        Provides a list of data sources.

        :param limit: Change the number of results returned.
        :param page: Paginate through the results
        :param df: return the results as a pandas DataFrame
        :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local

        :type limit: number
        :type page: number
        :type df: boolean
        :type index: string

        :return: a dictionary containing the *url*, *adapter*, *name*, *city*,
            *country*, *description*, *resolution*, *sourceURL*, and *contacts*.

        :Example:

        >>> import openaq
        >>> api = openaq.OpenAQ()
        >>> status, resp = api.sources()
        >>> resp['results']
        [
            {
                "url": "http://airquality.environment.nsw.gov.au/aquisnetnswphp/getPage.php?reportid=2",
                "adapter": "nsw",
                "name": "Australia - New South Wales",
                "city": "",
                "country": "AU",
                "description": "Measurements from the Office of Environment & Heritage of the New South Wales government.",
                "resolution": "1 hr",
                "sourceURL": "http://www.environment.nsw.gov.au/AQMS/hourlydata.htm",
                "contacts": [
                    "olaf@developmentseed.org"
                ]
            }
            ...
        ]
        """

        return self._get('sources', **kwargs)

    def __repr__(self):
        return "OpenAQ API"
