# coding=utf-8
# Copyright 2008, Sean B. Palmer, inamidst.com
# Copyright 2012, Elsie Powell, embolalia.com
# Copyright 2018, Rusty Bower, rustybower.com
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals, absolute_import, print_function, division

import requests

from datetime import datetime

import pytz

from sopel.config.types import NO_DEFAULT, ChoiceAttribute, StaticSection, ValidatedAttribute
from sopel.module import commands, example, NOLIMIT
from sopel.modules.units import c_to_f
from sopel.tools import Identifier
from sopel.tools.time import format_time

from .providers.weather.darksky import darksky_forecast, darksky_weather
from .providers.weather.openmeteo import openmeteo_forecast, openmeteo_weather
from .providers.weather.openweathermap import openweathermap_forecast, openweathermap_weather
from .providers.weather.pirateweather import pirateweather_forecast, pirateweather_weather

WEATHER_PROVIDERS = [
    'darksky',
    'openmeteo',
    'openweathermap',
    'pirateweather',
]

GEOCOORDS_PROVIDERS = {
    'locationiq_eu': 'https://eu1.locationiq.com/v1/search.php',
    'locationiq_us': 'https://us1.locationiq.com/v1/search.php',
    # for backward compatibility with previous `geocoords_provider` default value
    'locationiq': 'https://us1.locationiq.com/v1/search.php',
}


# Define our sopel weather configuration
class WeatherSection(StaticSection):
    geocoords_provider = ChoiceAttribute('geocoords_provider', GEOCOORDS_PROVIDERS.keys(), default='locationiq_us')
    geocoords_api_key = ValidatedAttribute('geocoords_api_key', str, default='')
    weather_provider = ChoiceAttribute('weather_provider', WEATHER_PROVIDERS, default=NO_DEFAULT)
    weather_api_key = ValidatedAttribute('weather_api_key', str, default='')
    sunrise_sunset = ValidatedAttribute('sunrise_sunset', bool, default=False)
    nick_lookup = ValidatedAttribute('nick_lookup', bool, default=True)


def setup(bot):
    bot.config.define_section('weather', WeatherSection)


# Walk the user through defining variables required
def configure(config):
    config.define_section('weather', WeatherSection, validate=False)
    config.weather.configure_setting(
        'geocoords_provider',
        'Enter GeoCoords API Provider:',
        default=NO_DEFAULT
    )
    config.weather.configure_setting(
        'geocoords_api_key',
        'Enter GeoCoords API Key:',
        default=NO_DEFAULT
    )
    config.weather.configure_setting(
        'weather_provider',
        'Enter Weather API Provider: ({}):'.format(', '.join(WEATHER_PROVIDERS)),
        default=NO_DEFAULT
    )
    config.weather.configure_setting(
        'weather_api_key',
        'Enter Weather API Key:',
        default=NO_DEFAULT
    )
    config.weather.configure_setting(
        'sunrise_sunset',
        'Enable sunrise/sunset:',
        default=False
    )
    config.weather.configure_setting(
        'nick_lookup',
        'Enable looking up weather in a nick\'s location:',
        default=True
    )


def get_temp(temp):
    try:
        temp = float(temp)
    except (KeyError, TypeError, ValueError):
        return 'unknown'
    return u'%d\u00B0C (%d\u00B0F)' % (round(temp), round(c_to_f(temp)))


def get_humidity(humidity):
    try:
        humidity = int(humidity * 100)
    except (KeyError, TypeError, ValueError):
        return 'unknown'
    return "Humidity: %s%%" % humidity


def get_wind(speed, bearing):
    m_s = float(round(speed, 1))
    kph = int(round(speed * 3.6))
    mph = int(round(speed * 2.236936))
    speed = int(round(m_s * 1.94384))
    bearing = int(bearing)

    if speed < 1:
        description = 'Calm'
    elif speed < 4:
        description = 'Light air'
    elif speed < 7:
        description = 'Light breeze'
    elif speed < 11:
        description = 'Gentle breeze'
    elif speed < 16:
        description = 'Moderate breeze'
    elif speed < 22:
        description = 'Fresh breeze'
    elif speed < 28:
        description = 'Strong breeze'
    elif speed < 34:
        description = 'Near gale'
    elif speed < 41:
        description = 'Gale'
    elif speed < 48:
        description = 'Strong gale'
    elif speed < 56:
        description = 'Storm'
    elif speed < 64:
        description = 'Violent storm'
    else:
        description = 'Hurricane'

    if (bearing <= 22.5) or (bearing > 337.5):
        bearing = u'\u2193'
    elif (bearing > 22.5) and (bearing <= 67.5):
        bearing = u'\u2199'
    elif (bearing > 67.5) and (bearing <= 112.5):
        bearing = u'\u2190'
    elif (bearing > 112.5) and (bearing <= 157.5):
        bearing = u'\u2196'
    elif (bearing > 157.5) and (bearing <= 202.5):
        bearing = u'\u2191'
    elif (bearing > 202.5) and (bearing <= 247.5):
        bearing = u'\u2197'
    elif (bearing > 247.5) and (bearing <= 292.5):
        bearing = u'\u2192'
    elif (bearing > 292.5) and (bearing <= 337.5):
        bearing = u'\u2198'

    return description + ': ' + str(kph) + 'km/h ' + '(' + str(mph) + 'mph) ' + '(' + bearing + ')'

def convert_timestamp(timestamp, tz):
    # Partial logic from sopel/tools/time.format_time
    time = datetime.fromtimestamp(timestamp, pytz.timezone('UTC'))
    # We only return the time, without a date or timezone.
    tz = pytz.timezone(tz)
    return time.astimezone(tz).strftime('%H:%M')

def get_geocoords(bot, trigger):
    target = trigger.group(2)
    if not target:
        latitude = bot.db.get_nick_value(trigger.nick, 'latitude')
        longitude = bot.db.get_nick_value(trigger.nick, 'longitude')
        location = bot.db.get_nick_value(trigger.nick, 'location')

        if latitude and longitude and location:
            return latitude, longitude, location
        else:
            raise ValueError

    if bot.config.weather.nick_lookup and ' ' not in target:
        # Try to look up nickname in DB, if enabled
        nick = Identifier(target)
        latitude = bot.db.get_nick_value(nick, 'latitude')
        longitude = bot.db.get_nick_value(nick, 'longitude')
        location = bot.db.get_nick_value(nick, 'location')

        if latitude and longitude and location:
            return latitude, longitude, location

    # geocode location if not a nick or not found in DB
    url = GEOCOORDS_PROVIDERS[bot.config.weather.geocoords_provider]
    data = {
        'key': bot.config.weather.geocoords_api_key,
        'q': trigger.group(2),
        'format': 'json',
        'addressdetails': 1,
        'limit': 1
    }

    try:
        r = requests.get(url, params=data)
    except requests.exceptions.RequestException:
        # requests likes to include the full URL in its exceptions, which would
        # mean the API key gets printed to the channel
        raise Exception("Could not geocode location. See logs for details.")

    if r.status_code != 200:
        raise Exception(r.json()['error'])

    latitude = r.json()[0]['lat']
    longitude = r.json()[0]['lon']
    address = r.json()[0]['address']

    parts = []

    # Zip codes give us town versus city
    if 'city' in address:
        parts.append(address['city'])
    elif 'town' in address:
        parts.append(address['town'])
    elif 'county' in address:
        parts.append(address['county'])
    elif 'city_district' in address:
        parts.append(address['city_district'])

    if 'state' in address:
        parts.append(address['state'])

    if parts:
        parts.append(address['country_code'].upper())
        location = ', '.join(parts)
    else:
        location = 'Unknown'

    return latitude, longitude, location


def get_forecast(bot, trigger):
    try:
        latitude, longitude, location = get_geocoords(bot, trigger)
    except ValueError as e:
        bot.reply(str(e))
        return NOLIMIT

    # DarkSky
    if bot.config.weather.weather_provider == 'darksky':
        return darksky_forecast(bot, latitude, longitude, location)
    # Open-Meteo
    if bot.config.weather.weather_provider == 'openmeteo':
        return openmeteo_forecast(bot, latitude, longitude, location)
    # OpenWeatherMap
    elif bot.config.weather.weather_provider == 'openweathermap':
        return openweathermap_forecast(bot, latitude, longitude, location)
    # Pirate Weather
    elif bot.config.weather.weather_provider == 'pirateweather':
        return pirateweather_forecast(bot, latitude, longitude, location)
    # Unsupported Provider
    else:
        raise Exception('Error: Unsupported Provider')


def get_weather(bot, trigger):
    try:
        latitude, longitude, location = get_geocoords(bot, trigger)
    except ValueError as e:
        bot.reply(str(e))
        return NOLIMIT

    # DarkSky
    if bot.config.weather.weather_provider == 'darksky':
        return darksky_weather(bot, latitude, longitude, location)
    # Open-Meteo
    if bot.config.weather.weather_provider == 'openmeteo':
        return openmeteo_weather(bot, latitude, longitude, location)
    # OpenWeatherMap
    elif bot.config.weather.weather_provider == 'openweathermap':
        return openweathermap_weather(bot, latitude, longitude, location)
    # Pirate Weather
    elif bot.config.weather.weather_provider == 'pirateweather':
        return pirateweather_weather(bot, latitude, longitude, location)
    # Unsupported Provider
    else:
        raise Exception('Error: Unsupported Provider')


@commands('weather', 'wea')
@example('.weather')
@example('.weather London')
@example('.weather Seattle, US')
@example('.weather 90210')
def weather_command(bot, trigger):
    """.weather location - Show the weather at the given location."""
    if bot.config.weather.weather_api_key is None or bot.config.weather.weather_api_key == '':
        return bot.reply("Weather API key missing. Please configure this module.")
    if bot.config.weather.geocoords_api_key is None or bot.config.weather.geocoords_api_key == '':
        return bot.reply("GeoCoords API key missing. Please configure this module.")

    # Ensure we have a location for the user
    location = trigger.group(2)
    if not location:
        latitude = bot.db.get_nick_value(trigger.nick, 'latitude')
        longitude = bot.db.get_nick_value(trigger.nick, 'longitude')
        if not latitude or not longitude:
            return bot.say("I don't know where you live. "
                           "Give me a location, like {pfx}{command} London, "
                           "or tell me where you live by saying {pfx}setlocation "
                           "London, for example.".format(command=trigger.group(1),
                                                         pfx=bot.config.core.help_prefix))

    try:
        data = get_weather(bot, trigger)
    except Exception as err:
        bot.reply("Could not get weather: " + str(err))
        return

    weather = u'{location}: {temp}, {condition}, {humidity}'.format(
        location=data['location'],
        temp=get_temp(data['temp']),
        condition=data['condition'],
        humidity=get_humidity(data['humidity'])
    )
    # Some providers don't give us UV Index
    if 'uvindex' in data.keys():
        weather += ', UV Index: {uvindex}'.format(uvindex=data['uvindex'])
    # User wants sunrise/sunset information
    if bot.config.weather.sunrise_sunset:
        tz = data['timezone']
        sr = convert_timestamp(data['sunrise'], tz)
        ss = convert_timestamp(data['sunset'], tz)
        weather += ', Sunrise: {sunrise} Sunset: {sunset}'.format(sunrise=sr, sunset=ss)
    weather += ', {wind}'.format(wind=get_wind(data['wind']['speed'], data['wind']['bearing']))
    return bot.say(weather)


@commands('forecast', 'fc')
@example('.forecast')
@example('.forecast London')
@example('.forecast Seattle, US')
@example('.forecast 90210')
def forecast_command(bot, trigger):
    """.forecast location - Show the weather forecast for the next 4 days at the given location."""
    if bot.config.weather.weather_api_key is None or bot.config.weather.weather_api_key == '':
        return bot.reply("Weather API key missing. Please configure this module.")
    if bot.config.weather.geocoords_api_key is None or bot.config.weather.geocoords_api_key == '':
        return bot.reply("GeoCoords API key missing. Please configure this module.")

    # Ensure we have a location for the user
    location = trigger.group(2)
    if not location:
        latitude = bot.db.get_nick_value(trigger.nick, 'latitude')
        longitude = bot.db.get_nick_value(trigger.nick, 'longitude')
        if not latitude or not longitude:
            return bot.say("I don't know where you live. "
                           "Give me a location, like {pfx}{command} London, "
                           "or tell me where you live by saying {pfx}setlocation "
                           "London, for example.".format(command=trigger.group(1),
                                                         pfx=bot.config.core.help_prefix))

    try:
        data = get_forecast(bot, trigger)
    except Exception as err:
        bot.reply("Could not get forecast: " + str(err))
        return

    forecast = '{location}'.format(location=data['location'])
    for day in data['data']:
        forecast += ' :: {dow} - {summary} - {high_temp} / {low_temp}'.format(
            dow=day.get('dow'),
            summary=day.get('summary'),
            high_temp=get_temp(day.get('high_temp')),
            low_temp=get_temp(day.get('low_temp'))
        )
    return bot.say(forecast)


@commands('setlocation')
@example('.setlocation London')
@example('.setlocation Seattle, US')
@example('.setlocation 90210')
@example('.setlocation w7174408')
def update_location(bot, trigger):
    """Set your location for fetching weather."""
    if bot.config.weather.geocoords_api_key is None or bot.config.weather.geocoords_api_key == '':
        return bot.reply("GeoCoords API key missing. Please configure this module.")

    # Return an error if no location is provided
    if not trigger.group(2):
        bot.reply('Give me a location, like "London" or "90210".')
        return NOLIMIT

    # Get GeoCoords
    try:
        latitude, longitude, location = get_geocoords(bot, trigger)
    except Exception as err:
        # Reply with the error message if geocoding fails
        bot.reply("Could not find location details: " + str(err))
        return

    # Assign Latitude & Longitude to user
    bot.db.set_nick_value(trigger.nick, 'latitude', latitude)
    bot.db.set_nick_value(trigger.nick, 'longitude', longitude)
    bot.db.set_nick_value(trigger.nick, 'location', location)

    return bot.reply('I now have you at {}'.format(location))
