import numpy as np
import openrouteservice as ors
import requests
from requests.packages.urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
import logging
logging.basicConfig(level=logging.DEBUG)
import flexpolyline


from .utils import from_minutes_to_seconds
from .config import AVAILABLE_ISOCHRONES_APIS, ISOCHRONES_APIS_WITH_PYTHON_CLIENT, GEOJSON_POLYGON_STARTER
from .geographic_handling import (from_polygon_coordinates_to_geojson,
                                  reverse_polygon_coordinates,
                                  from_geojson_to_polygon_coordinates,
                                  extract_polygon_envelope_metadata,
                                  compute_geodesic_distance
                                 )

class IsochronesManager:
    OPEN_ROUTE_SERVICE_QUERY_TIMEOUT = 60
    OPEN_ROUTE_SERVICE_QUERY_RETRY_TIMEOUT = 60
    OPEN_ROUTE_SERVICE_N_COORDINATES_COUPLES = 2 # Intitially 5
    
    def __init__(self, isochrones_api_to_use, api_key, transportation_mode, isochrone_amplitudes_minutes,
                 isochrones_api_parameters):

        if not isochrones_api_to_use in AVAILABLE_ISOCHRONES_APIS:
            message = f"Isochrone api '{isochrones_api_to_use}' use is not implemented !"
            raise Exception(message)
        else:
            self.isochrones_api_to_use = isochrones_api_to_use
        self.api_key = api_key
        self.transportation_mode = transportation_mode
        self.isochrone_amplitudes_minutes = isochrone_amplitudes_minutes
        self.api_range = self.load_isochrones_range()
        self.range_type = self.load_isoline_type()
        self.n_isochrones = len(isochrone_amplitudes_minutes)
        self.isochrones_attributes = isochrones_api_parameters.get("isochrones_attributes")

        if self.isochrones_api_to_use in ISOCHRONES_APIS_WITH_PYTHON_CLIENT:
            self.client = self.generate_isochrone_api_client()
        else:
            self.request_session = requests.Session()
            # Adding a retry mechanism to the request session for code 429 (ie 'Too Many Requests')
            retries = Retry(total=10,
                            backoff_factor=1,
                            status_forcelist=[429],
                            method_whitelist=frozenset(['GET', 'POST']))
            self.request_session.mount('https://', HTTPAdapter(max_retries=retries))

    def load_isochrones_range(self):
        iscochrone_amplitudes_seconds = [from_minutes_to_seconds(minute_val) \
                                         for minute_val in self.isochrone_amplitudes_minutes]

        if self.isochrones_api_to_use in ["open_route_service"]:
            api_range = [second_val for second_val in iscochrone_amplitudes_seconds]

        elif self.isochrones_api_to_use in ["here"]:
            api_range = ",".join(str(second_val) for second_val in iscochrone_amplitudes_seconds)

        return api_range

    def load_isoline_type(self):
        #TODO :
        if self.isochrones_api_to_use == 'open_route_service':
            range_type = "time" # Isodistances --> range_type = "distance"
        elif self.isochrones_api_to_use == 'here':
            range_type = "time" # Isodistances --> range_type = "distance"
        return range_type

    def generate_isochrone_api_client(self):
        api_key = self.api_key
        if self.isochrones_api_to_use == 'open_route_service':
            client = ors.Client(key=api_key,
                                base_url="https://api.openrouteservice.org",
                                timeout=self.OPEN_ROUTE_SERVICE_QUERY_TIMEOUT,
                                retry_timeout=self.OPEN_ROUTE_SERVICE_QUERY_RETRY_TIMEOUT)
        return client

    def prepare_coordinates_for_query(self, coordinates):
        final_coordinates = []
        n_coordinates = len(coordinates)
        if self.isochrones_api_to_use == 'open_route_service':
            # OpenRouteService allows to query 'self.OPEN_ROUTE_SERVICE_N_COORDINATES_COUPLES' couple of coordinates per query : hence we split the original list
            # in a list of lists, each containing 'self.OPEN_ROUTE_SERVICE_N_COORDINATES_COUPLES' coordinates couples
            tmp_coordinates = []

            for index, couple in enumerate(coordinates):
                last_index = (index == n_coordinates - 1)
                if index != 0:
                    if (index) % self.OPEN_ROUTE_SERVICE_N_COORDINATES_COUPLES == 0:
                        final_coordinates.append(tmp_coordinates)
                        tmp_coordinates = []
                tmp_coordinates.append(couple)
                if last_index:
                    final_coordinates.append(tmp_coordinates)
                    tmp_coordinates = []

        return final_coordinates

    def build_request_query(self, latitude, longitude):
        if self.isochrones_api_to_use == 'here':
            query_starter = "https://isoline.router.hereapi.com/v8/isolines?"
            query_parameters = (
                f"apiKey={self.api_key}",
                f"transportMode={self.transportation_mode}",
                f"range[type]={self.range_type}",
                f"range[values]={self.api_range}",
                f"origin={latitude},{longitude}"
            )
            parameters_combination = "&".join(parameter for parameter in query_parameters)
            query = query_starter + parameters_combination
            return query
        pass

    def compute_isochrones_geojsons(self, latitudes, longitudes):
        isochrones_geojsons = {isochrone_amplitude: [] for isochrone_amplitude in self.isochrone_amplitudes_minutes}
        isochrones_lengths = {isochrone_amplitude: [] for isochrone_amplitude in self.isochrone_amplitudes_minutes}
        isochrones_widths = {isochrone_amplitude: [] for isochrone_amplitude in self.isochrone_amplitudes_minutes}

        if self.isochrones_api_to_use == 'open_route_service':
            coordinates = [[longitude, latitude] for latitude, longitude in zip(latitudes, longitudes)]
            final_coordinates = self.prepare_coordinates_for_query(coordinates)
            n_ensemble_of_coordinates = len(final_coordinates)

            for index, coordinates_ensemble in enumerate(final_coordinates):
                
                try:
                    print("querying ensemble of coordinates {} / {} | transportation_mode : {} " \
                          .format(index + 1, n_ensemble_of_coordinates, self.transportation_mode))
                    isochrones = self.client.isochrones(locations=coordinates_ensemble,
                                                        range_type=self.range_type,
                                                        units='km',
                                                        profile=self.transportation_mode,
                                                        range=self.api_range,
                                                        attributes=self.isochrones_attributes)
                    size_coordinates_ensemble = len(coordinates_ensemble)
                    isochrone_features = np.array(isochrones["features"]).reshape(size_coordinates_ensemble, self.n_isochrones)

                    for isochrone_index, isochrone_amplitude in enumerate(self.isochrone_amplitudes_minutes):
                        focus_isochrones_features = isochrone_features[:, isochrone_index] # 'focus_isochrones' is a numpy array
                        isochrones_geojsons[isochrone_amplitude] += list(focus_isochrones_features)
                
                except Exception as e:
                    print("query failed for this coordinates ensemble | Isochrones geojsons will be empty.")
                    print(f"The python exception is '{str(e)}'")
                    n_coordinates_in_ensemble = len(coordinates_ensemble)
                    focus_isochrones = [GEOJSON_POLYGON_STARTER for __ in range(n_coordinates_in_ensemble)]
                    
                    for isochrone_index, isochrone_amplitude in enumerate(self.isochrone_amplitudes_minutes):
                        isochrones_geojsons[isochrone_amplitude] += focus_isochrones

        elif self.isochrones_api_to_use == 'here':
            n_coordinates = len(latitudes)
            
            for coordinates_index, coordinates_data in enumerate(zip(latitudes, longitudes)):
                latitude = coordinates_data[0]
                longitude = coordinates_data[1]
                print("querying coordinates {} / {} | transportation_mode : {} "\
                      .format(coordinates_index + 1, n_coordinates, self.transportation_mode))
                isochrones_query = self.build_request_query(latitude, longitude)
                query_response = self.request_session.get(isochrones_query)
                response_status_code = query_response.status_code
                
                if response_status_code == 401:
                    message = f"Here server response is '{response_status_code}' (ie 'Unauthorized') :"\
                    "please check your API KEY state in your here developer console."
                    raise Exception(message)
                    
                if response_status_code == 200:
                    print(f"query response status : {response_status_code} (OK)")
                    isochrones_polygons = query_response.json()["isolines"]
                    
                    for isochrone_index, isochrone_amplitude in enumerate(self.isochrone_amplitudes_minutes):
                        encoded_isochrone_polygons = isochrones_polygons[isochrone_index] 
                        # Response can potentially contain multiple polygons 
                        # (See https://developer.here.com/documentation/isoline-routing-api/8.4.0/dev_guide/topics/multi-component-isoline.html) :
                        focus_encoded_isochrone_polygon = encoded_isochrone_polygons["polygons"][0]["outer"]
                        # Here isolines need to be decoded (See https://github.com/heremaps/flexible-polyline/tree/master/python) :
                        focus_isochrone_polygon = flexpolyline.decode(focus_encoded_isochrone_polygon)
                        focus_isochrone_polygon = reverse_polygon_coordinates(focus_isochrone_polygon)
                        focus_isochrone_geojson = from_polygon_coordinates_to_geojson(focus_isochrone_polygon)
                        isochrones_geojsons[isochrone_amplitude].append(focus_isochrone_geojson)
                
                else:
                    print(f"query response status : {response_status_code} | Isochrones geojsons will be empty.")
                    
                    for isochrone_index, isochrone_amplitude in enumerate(self.isochrone_amplitudes_minutes):
                        isochrones_geojsons[isochrone_amplitude].append(GEOJSON_POLYGON_STARTER)

        return isochrones_geojsons
    
    def compute_isochrone_radius(self, isochrone_geojson, isochrone_center_longitude, isochrone_center_latitude):
        isochrone_polygon = isochrone_geojson["geometry"]["coordinates"]
        isochrone_has_no_geojson = (isochrone_polygon == [[]])
        if isochrone_has_no_geojson:
            isochrone_radius = 0
        else:
            isochrone_coordinates = from_geojson_to_polygon_coordinates(isochrone_geojson)
            enveloppe_metadata = extract_polygon_envelope_metadata(isochrone_coordinates)
            envelope_data = enveloppe_metadata["envelope_data"]

            envelope_min_lat = envelope_data["min_lat"]
            envelope_min_lon = envelope_data["min_lon"]
            envelope_max_lat = envelope_data["max_lat"]
            envelope_max_lon = envelope_data["max_lon"]
            isochrone_center = [isochrone_center_longitude, isochrone_center_latitude]
            envelope_north_point = [isochrone_center_longitude, envelope_max_lat]
            envelope_south_point = [isochrone_center_longitude, envelope_min_lat]
            envelope_east_point =  [envelope_max_lon, isochrone_center_latitude]
            envelope_west_point =  [envelope_min_lon, isochrone_center_latitude]
            envelope_cardinal_point_distances_from_isochrone_center = [
                compute_geodesic_distance(isochrone_center, envelope_north_point, True),
                compute_geodesic_distance(isochrone_center, envelope_south_point, True),
                compute_geodesic_distance(isochrone_center, envelope_east_point, True),
                compute_geodesic_distance(isochrone_center, envelope_west_point, True),
            ]
            isochrone_radius = max(envelope_cardinal_point_distances_from_isochrone_center)
        return isochrone_radius
    
    def extract_isochrone_attributes(self, isochrone_geojson):
        isochrone_polygon = isochrone_geojson["geometry"]["coordinates"]
        isochrone_has_no_geojson = (isochrone_polygon == [[]])
        if self.isochrones_api_to_use == 'open_route_service':
            if isochrone_has_no_geojson:
                return {isochrone_attribute: 0 for isochrone_attribute in self.isochrones_attributes}
            else:
                return {isochrone_attribute: isochrone_geojson["properties"][isochrone_attribute]
                        for isochrone_attribute in self.isochrones_attributes}
        else:
            return None