# flake8: noqa: E501
import json
import os
from typing import Optional, Type

import aiohttp
import requests

from lagent.actions.base_action import AsyncActionMixin, BaseAction, tool_api
from lagent.actions.parser import BaseParser, JsonParser


class BINGMap(BaseAction):
    """BING Map plugin for looking up map information."""

    def __init__(
        self,
        key: Optional[str] = None,
        description: Optional[dict] = None,
        parser: Type[BaseParser] = JsonParser,
    ) -> None:
        super().__init__(description, parser)
        key = os.environ.get('BING_MAP_KEY', key)
        if key is None:
            raise ValueError(
                'Please set BING Map API key either in the environment '
                'as BING_MAP_KEY or pass it as `key` parameter.')
        self.key = key
        self.base_url = 'http://dev.virtualearth.net/REST/V1/'

    @tool_api(explode_return=True)
    def get_distance(self, start: str, end: str) -> dict:
        """Get the distance between two locations in km.

        Args:
            start (:class:`str`): The start location
            end (:class:`str`): The end location

        Returns:
            :class:`dict`: distance information
                * distance (str): the distance in km.
        """
        # Request URL
        url = self.base_url + 'Routes/Driving?o=json&wp.0=' + start + '&wp.1=' + end + '&key=' + self.key
        # GET request
        r = requests.get(url)
        # TODO check request status?
        data = json.loads(r.text)
        # Extract route information
        route = data['resourceSets'][0]['resources'][0]
        # Extract distance in miles
        distance = route['travelDistance']
        return dict(distance=distance)

    @tool_api(explode_return=True)
    def get_route(self, start: str, end: str) -> dict:
        """Get the route between two locations in km.

        Args:
            start (:class:`str`): The start location
            end (:class:`str`): The end location

        Returns:
            :class:`dict`: route information
                * route (list): the route, a list of actions.
        """
        # Request URL
        url = self.base_url + 'Routes/Driving?o=json&wp.0=' + start + '&wp.1=' + end + '&key=' + self.key
        # GET request
        r = requests.get(url)
        data = json.loads(r.text)
        # Extract route information
        route = data['resourceSets'][0]['resources'][0]
        itinerary = route['routeLegs'][0]['itineraryItems']
        # Extract route text information
        route_text = []
        for item in itinerary:
            if 'instruction' in item:
                route_text.append(item['instruction']['text'])
        return dict(route=route_text)

    @tool_api(explode_return=True)
    def get_coordinates(self, location: str) -> dict:
        """Get the coordinates of a location.

        Args:
            location (:class:`str`): the location need to get coordinates.

        Returns:
            :class:`dict`: coordinates information
                * latitude (float): the latitude of the location.
                * longitude (float): the longitude of the location.
        """
        url = self.base_url + 'Locations'
        params = {'query': location, 'key': self.key}
        response = requests.get(url, params=params)
        json_data = response.json()
        coordinates = json_data['resourceSets'][0]['resources'][0]['point'][
            'coordinates']
        return dict(latitude=coordinates[0], longitude=coordinates[1])

    @tool_api(explode_return=True)
    def search_nearby(self,
                      search_term: str,
                      places: str = 'unknown',
                      latitude: float = 0.0,
                      longitude: float = 0.0,
                      radius: int = 5000) -> dict:
        """Search for places nearby a location, within a given radius, and return the results into a list. You can use either the places name or the latitude and longitude.

        Args:
            search_term (:class:`str`): the place name.
            places (:class:`str`): the name of the location. Defaults to ``'unknown'``.
            latitude (:class:`float`): the latitude of the location. Defaults to ``0.0``.
            longitude (:class:`float`): the longitude of the location. Defaults to ``0.0``.
            radius (:class:`int`): radius in meters. Defaults to ``5000``.

        Returns:
            :class:`dict`: places information
                * places (list): the list of places, each place is a dict with name and address, at most 5 places.
        """
        url = self.base_url + 'LocalSearch'
        if places != 'unknown':
            pos = self.get_coordinates(**{'location': places})
            latitude, longitude = pos[1]['latitude'], pos[1]['longitude']
        # Build the request query string
        params = {
            'query': search_term,
            'userLocation': f'{latitude},{longitude}',
            'radius': radius,
            'key': self.key
        }
        # Make the request
        response = requests.get(url, params=params)
        # Parse the response
        response_data = json.loads(response.content)
        # Get the results
        results = response_data['resourceSets'][0]['resources']
        addresses = []
        for result in results:
            name = result['name']
            address = result['Address']['formattedAddress']
            addresses.append(dict(name=name, address=address))
            if len(addresses) == 5:
                break
        return dict(place=addresses)


class AsyncBINGMap(AsyncActionMixin, BINGMap):
    """BING Map plugin for looking up map information."""

    @tool_api(explode_return=True)
    async def get_distance(self, start: str, end: str) -> dict:
        """Get the distance between two locations in km.

        Args:
            start (:class:`str`): The start location
            end (:class:`str`): The end location

        Returns:
            :class:`dict`: distance information
                * distance (str): the distance in km.
        """
        # Request URL
        url = self.base_url + 'Routes/Driving?o=json&wp.0=' + start + '&wp.1=' + end + '&key=' + self.key
        # GET request
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                # TODO check request status?
                data = await resp.json()
        # Extract route information
        route = data['resourceSets'][0]['resources'][0]
        # Extract distance in miles
        distance = route['travelDistance']
        return dict(distance=distance)

    @tool_api(explode_return=True)
    async def get_route(self, start: str, end: str) -> dict:
        """Get the route between two locations in km.

        Args:
            start (:class:`str`): The start location
            end (:class:`str`): The end location

        Returns:
            :class:`dict`: route information
                * route (list): the route, a list of actions.
        """
        # Request URL
        url = self.base_url + 'Routes/Driving?o=json&wp.0=' + start + '&wp.1=' + end + '&key=' + self.key
        # GET request
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                data = await resp.json()
        # Extract route information
        route = data['resourceSets'][0]['resources'][0]
        itinerary = route['routeLegs'][0]['itineraryItems']
        # Extract route text information
        route_text = []
        for item in itinerary:
            if 'instruction' in item:
                route_text.append(item['instruction']['text'])
        return dict(route=route_text)

    @tool_api(explode_return=True)
    async def get_coordinates(self, location: str) -> dict:
        """Get the coordinates of a location.

        Args:
            location (:class:`str`): the location need to get coordinates.

        Returns:
            :class:`dict`: coordinates information
                * latitude (float): the latitude of the location.
                * longitude (float): the longitude of the location.
        """
        url = self.base_url + 'Locations'
        params = {'query': location, 'key': self.key}
        async with aiohttp.ClientSession() as session:
            async with session.get(url, params=params) as resp:
                data = await resp.json()
        coordinates = data['resourceSets'][0]['resources'][0]['point'][
            'coordinates']
        return dict(latitude=coordinates[0], longitude=coordinates[1])

    @tool_api(explode_return=True)
    async def search_nearby(self,
                            search_term: str,
                            places: str = 'unknown',
                            latitude: float = 0.0,
                            longitude: float = 0.0,
                            radius: int = 5000) -> dict:
        """Search for places nearby a location, within a given radius, and return the results into a list. You can use either the places name or the latitude and longitude.

        Args:
            search_term (:class:`str`): the place name.
            places (:class:`str`): the name of the location. Defaults to ``'unknown'``.
            latitude (:class:`float`): the latitude of the location. Defaults to ``0.0``.
            longitude (:class:`float`): the longitude of the location. Defaults to ``0.0``.
            radius (:class:`int`): radius in meters. Defaults to ``5000``.

        Returns:
            :class:`dict`: places information
                * places (list): the list of places, each place is a dict with name and address, at most 5 places.
        """
        url = self.base_url + 'LocalSearch'
        if places != 'unknown':
            pos = self.get_coordinates(**{'location': places})
            latitude, longitude = pos[1]['latitude'], pos[1]['longitude']
        # Build the request query string
        params = {
            'query': search_term,
            'userLocation': f'{latitude},{longitude}',
            'radius': radius,
            'key': self.key
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(url, params=params) as resp:
                data = await resp.json()
        results = data['resourceSets'][0]['resources']
        addresses = []
        for result in results:
            name = result['name']
            address = result['Address']['formattedAddress']
            addresses.append(dict(name=name, address=address))
            if len(addresses) == 5:
                break
        return dict(place=addresses)
