#!/bin/env python
# vim: fileencoding=utf-8
from datetime import datetime, timedelta
import json
import logging
import os
import re
import sys

import timezonefinder
from pytz import timezone

from constants import WWO_CODE

logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
logger = logging.getLogger(__name__)


def metno_request(path, query_string):
    # We'll need to sanitize the inbound request - ideally the
    # premium/v1/weather.ashx portion would have always been here, though
    # it seems as though the proxy was built after the majority of the app
    # and not refactored. For WAPI we'll strip this and the API key out,
    # then manage it on our own.
    logger.debug("Original path: " + path)
    logger.debug("Original query: " + query_string)

    path = path.replace(
        "premium/v1/weather.ashx", "weatherapi/locationforecast/2.0/complete"
    )
    query_string = re.sub(r"key=[^&]*&", "", query_string)
    query_string = re.sub(r"format=[^&]*&", "", query_string)
    days = int(re.search(r"num_of_days=([0-9]+)&", query_string).group(1))
    query_string = re.sub(r"num_of_days=[0-9]+&", "", query_string)
    # query_string = query_string.replace('key=', '?key=' + WAPI_KEY)
    # TP is for hourly forecasting, which isn't available in the free api.
    query_string = re.sub(r"tp=[0-9]*&", "", query_string)
    # This assumes lang=... is at the end. Also note that the API doesn't
    # localize, and we're not either. TODO: add language support
    query_string = re.sub(r"lang=[^&]*$", "", query_string)
    query_string = re.sub(r"&$", "", query_string)

    logger.debug("qs: " + query_string)
    # Deal with coordinates. Need to be rounded to 4 decimals for metno ToC
    # and in a different query string format
    coord_match = re.search(r"q=[^&]*", query_string)
    coords_str = coord_match.group(0)
    coords = re.findall(r"[-0-9.]+", coords_str)
    lat = str(round(float(coords[0]), 4))
    lng = str(round(float(coords[1]), 4))
    logger.debug("lat: " + lat)
    logger.debug("lng: " + lng)
    query_string = re.sub(r"q=[^&]*", "lat=" + lat + "&lon=" + lng + "&", query_string)
    logger.debug("Return path: " + path)
    logger.debug("Return query: " + query_string)

    return path, query_string, days


def celsius_to_f(celsius):
    return round((1.8 * celsius) + 32, 1)


def to_weather_code(symbol_code):
    logger.debug(symbol_code)
    code = re.sub(r"_.*", "", symbol_code)
    logger.debug(code)
    # symbol codes: https://api.met.no/weatherapi/weathericon/2.0/documentation
    # they also have _day, _night and _polartwilight variants
    # See json from https://api.met.no/weatherapi/weathericon/2.0/legends
    # WWO codes: https://github.com/chubin/wttr.in/blob/master/lib/constants.py
    #            http://www.worldweatheronline.com/feed/wwoConditionCodes.txt
    weather_code_map = {
        "clearsky": 113,
        "cloudy": 119,
        "fair": 116,
        "fog": 143,
        "heavyrain": 302,
        "heavyrainandthunder": 389,
        "heavyrainshowers": 305,
        "heavyrainshowersandthunder": 386,
        "heavysleet": 314,  # There's a ton of 'LightSleet' in WWO_CODE...
        "heavysleetandthunder": 377,
        "heavysleetshowers": 362,
        "heavysleetshowersandthunder": 374,
        "heavysnow": 230,
        "heavysnowandthunder": 392,
        "heavysnowshowers": 371,
        "heavysnowshowersandthunder": 392,
        "lightrain": 266,
        "lightrainandthunder": 200,
        "lightrainshowers": 176,
        "lightrainshowersandthunder": 386,
        "lightsleet": 281,
        "lightsleetandthunder": 377,
        "lightsleetshowers": 284,
        "lightsnow": 320,
        "lightsnowandthunder": 392,
        "lightsnowshowers": 368,
        "lightssleetshowersandthunder": 365,
        "lightssnowshowersandthunder": 392,
        "partlycloudy": 116,
        "rain": 293,
        "rainandthunder": 389,
        "rainshowers": 299,
        "rainshowersandthunder": 386,
        "sleet": 185,
        "sleetandthunder": 392,
        "sleetshowers": 263,
        "sleetshowersandthunder": 392,
        "snow": 329,
        "snowandthunder": 392,
        "snowshowers": 230,
        "snowshowersandthunder": 392,
    }
    if code not in weather_code_map:
        logger.debug("not found")
        return -1  # not found
    logger.debug(weather_code_map[code])
    return weather_code_map[code]


def to_description(symbol_code):
    desc = WWO_CODE[str(to_weather_code(symbol_code))]
    logger.debug(desc)
    return desc


def to_16_point(degrees):
    # 360 degrees / 16 = 22.5 degrees of arc or 11.25 degrees around the point
    if degrees > (360 - 11.25) or degrees <= 11.25:
        return "N"
    if degrees > 11.25 and degrees <= (11.25 + 22.5):
        return "NNE"
    if degrees > (11.25 + (22.5 * 1)) and degrees <= (11.25 + (22.5 * 2)):
        return "NE"
    if degrees > (11.25 + (22.5 * 2)) and degrees <= (11.25 + (22.5 * 3)):
        return "ENE"
    if degrees > (11.25 + (22.5 * 3)) and degrees <= (11.25 + (22.5 * 4)):
        return "E"
    if degrees > (11.25 + (22.5 * 4)) and degrees <= (11.25 + (22.5 * 5)):
        return "ESE"
    if degrees > (11.25 + (22.5 * 5)) and degrees <= (11.25 + (22.5 * 6)):
        return "SE"
    if degrees > (11.25 + (22.5 * 6)) and degrees <= (11.25 + (22.5 * 7)):
        return "SSE"
    if degrees > (11.25 + (22.5 * 7)) and degrees <= (11.25 + (22.5 * 8)):
        return "S"
    if degrees > (11.25 + (22.5 * 8)) and degrees <= (11.25 + (22.5 * 9)):
        return "SSW"
    if degrees > (11.25 + (22.5 * 9)) and degrees <= (11.25 + (22.5 * 10)):
        return "SW"
    if degrees > (11.25 + (22.5 * 10)) and degrees <= (11.25 + (22.5 * 11)):
        return "WSW"
    if degrees > (11.25 + (22.5 * 11)) and degrees <= (11.25 + (22.5 * 12)):
        return "W"
    if degrees > (11.25 + (22.5 * 12)) and degrees <= (11.25 + (22.5 * 13)):
        return "WNW"
    if degrees > (11.25 + (22.5 * 13)) and degrees <= (11.25 + (22.5 * 14)):
        return "NW"
    if degrees > (11.25 + (22.5 * 14)) and degrees <= (11.25 + (22.5 * 15)):
        return "NNW"


def meters_to_miles(meters):
    return round(meters * 0.00062137, 2)


def mm_to_inches(mm):
    return round(mm / 25.4, 2)


def hpa_to_mb(hpa):
    return hpa


def hpa_to_in(hpa):
    return round(hpa * 0.02953, 2)


def hpa_to_mmHg(hpa):
    return round(hpa * 0.75006157584566, 3)


def group_hours_to_days(lat, lng, hourlies, days_to_return):
    tf = timezonefinder.TimezoneFinder()
    timezone_str = tf.certain_timezone_at(lat=lat, lng=lng)
    logger.debug("got TZ: " + timezone_str)
    tz = timezone(timezone_str)
    start_day_gmt = datetime.fromisoformat(hourlies[0]["time"].replace("Z", "+00:00"))
    start_day_local = start_day_gmt.astimezone(tz)
    end_day_local = (start_day_local + timedelta(days=days_to_return - 1)).date()
    logger.debug("series starts at gmt time: " + str(start_day_gmt))
    logger.debug("series starts at local time: " + str(start_day_local))
    logger.debug("series ends on day: " + str(end_day_local))
    days = {}

    for hour in hourlies:
        current_day_gmt = datetime.fromisoformat(hour["time"].replace("Z", "+00:00"))
        current_local = current_day_gmt.astimezone(tz)
        current_day_local = current_local.date()
        if current_day_local > end_day_local:
            continue
        if current_day_local not in days:
            days[current_day_local] = {"hourly": []}
        hour["localtime"] = current_local.time()
        days[current_day_local]["hourly"].append(hour)

    # Need a second pass to build the min/max/avg data
    for date, day in days.items():
        minTempC = -999
        maxTempC = 1000
        avgTempC = None
        n = 0
        maxUvIndex = 0
        for hour in day["hourly"]:
            temp = hour["data"]["instant"]["details"]["air_temperature"]
            if temp > minTempC:
                minTempC = temp
            if temp < maxTempC:
                maxTempC = temp
            if avgTempC is None:
                avgTempC = temp
                n = 1
            else:
                avgTempC = ((avgTempC * n) + temp) / (n + 1)
                n = n + 1

            uv = hour["data"]["instant"]["details"]
            if "ultraviolet_index_clear_sky" in uv:
                if uv["ultraviolet_index_clear_sky"] > maxUvIndex:
                    maxUvIndex = uv["ultraviolet_index_clear_sky"]
        day["maxtempC"] = str(maxTempC)
        day["maxtempF"] = str(celsius_to_f(maxTempC))
        day["mintempC"] = str(minTempC)
        day["mintempF"] = str(celsius_to_f(minTempC))
        day["avgtempC"] = str(round(avgTempC, 1))
        day["avgtempF"] = str(celsius_to_f(avgTempC))
        # day["totalSnow_cm": "not implemented",
        # day["sunHour": "12",  # This would come from astonomy data
        day["uvIndex"] = str(maxUvIndex)

    return days


def _convert_hour(hour):
    # Whatever is upstream is expecting data in the shape of WWO. This method will
    # morph from metno to hourly WWO response format.
    # Note that WWO is providing data every 3 hours. Metno provides every hour
    #   {
    # "time": "0",
    # "tempC": "19",
    # "tempF": "66",
    # "windspeedMiles": "6",
    # "windspeedKmph": "9",
    # "winddirDegree": "276",
    # "winddir16Point": "W",
    # "weatherCode": "119",
    # "weatherIconUrl": [
    #   {
    #     "value": "http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0003_white_cloud.png"
    #   }
    # ],
    # "weatherDesc": [
    #   {
    #     "value": "Cloudy"
    #   }
    # ],
    # "precipMM": "0.0",
    # "precipInches": "0.0",
    # "humidity": "62",
    # "visibility": "10",
    # "visibilityMiles": "6",
    # "pressure": "1017",
    # "pressureInches": "31",
    # "cloudcover": "66",
    # "HeatIndexC": "19",
    # "HeatIndexF": "66",
    # "DewPointC": "12",
    # "DewPointF": "53",
    # "WindChillC": "19",
    # "WindChillF": "66",
    # "WindGustMiles": "8",
    # "WindGustKmph": "13",
    # "FeelsLikeC": "19",
    # "FeelsLikeF": "66",
    # "chanceofrain": "0",
    # "chanceofremdry": "93",
    # "chanceofwindy": "0",
    # "chanceofovercast": "89",
    # "chanceofsunshine": "18",
    # "chanceoffrost": "0",
    # "chanceofhightemp": "0",
    # "chanceoffog": "0",
    # "chanceofsnow": "0",
    # "chanceofthunder": "0",
    # "uvIndex": "1"
    details = hour["data"]["instant"]["details"]
    if "next_1_hours" in hour["data"]:
        next_hour = hour["data"]["next_1_hours"]
    elif "next_6_hours" in hour["data"]:
        next_hour = hour["data"]["next_6_hours"]
    elif "next_12_hours" in hour["data"]:
        next_hour = hour["data"]["next_12_hours"]
    else:
        next_hour = {}

    # Need to dig out symbol_code and precipitation_amount
    symbol_code = "clearsky_day"  # Default to sunny
    if "summary" in next_hour and "symbol_code" in next_hour["summary"]:
        symbol_code = next_hour["summary"]["symbol_code"]
    precipitation_amount = 0  # Default to no rain
    if "details" in next_hour and "precipitation_amount" in next_hour["details"]:
        precipitation_amount = next_hour["details"]["precipitation_amount"]

    uvIndex = 0  # default to 0 index
    if "ultraviolet_index_clear_sky" in details:
        uvIndex = details["ultraviolet_index_clear_sky"]
    localtime = ""
    if "localtime" in hour:
        localtime = "{h:02.0f}".format(h=hour["localtime"].hour) + "{m:02.0f}".format(
            m=hour["localtime"].minute
        )
        logger.debug(str(hour["localtime"]))
    # time property is local time, 4 digit 24 hour, with no :, e.g. 2100
    return {
        "time": localtime,
        "observation_time": hour["time"],  # Need to figure out WWO TZ
        # temp_C is used in we-lang.go calcs in such a way
        # as to expect a whole number
        "temp_C": str(int(round(details["air_temperature"], 0))),
        # temp_F can be more precise - not used in we-lang.go calcs
        "temp_F": str(celsius_to_f(details["air_temperature"])),
        "weatherCode": str(to_weather_code(symbol_code)),
        "weatherIconUrl": [
            {
                "value": "not yet implemented",
            }
        ],
        "weatherDesc": [
            {
                "value": to_description(symbol_code),
            }
        ],
        # similiarly, windspeedMiles is not used by we-lang.go, but kmph is
        "windspeedMiles": str(meters_to_miles(details["wind_speed"])),
        "windspeedKmph": str(int(round(details["wind_speed"], 0))),
        "winddirDegree": str(details["wind_from_direction"]),
        "winddir16Point": to_16_point(details["wind_from_direction"]),
        "precipMM": str(precipitation_amount),
        "precipInches": str(mm_to_inches(precipitation_amount)),
        "humidity": str(details["relative_humidity"]),
        "visibility": "not yet implemented",  # str(details['vis_km']),
        "visibilityMiles": "not yet implemented",  # str(details['vis_miles']),
        "pressure": str(hpa_to_mb(details["air_pressure_at_sea_level"])),
        "pressure_mmHg": str(hpa_to_mmHg(details["air_pressure_at_sea_level"])),
        "pressureInches": str(hpa_to_in(details["air_pressure_at_sea_level"])),
        "cloudcover": "not yet implemented",  # Convert from cloud_area_fraction?? str(details['cloud']),
        # metno doesn't have FeelsLikeC, but we-lang.go is using it in calcs,
        # so we shall set it to temp_C
        "FeelsLikeC": str(int(round(details["air_temperature"], 0))),
        "FeelsLikeF": "not yet implemented",  # str(details['feelslike_f']),
        "uvIndex": str(uvIndex),
    }


def _convert_hourly(hours):
    converted_hours = []
    for hour in hours:
        converted_hours.append(_convert_hour(hour))
    return converted_hours


# Whatever is upstream is expecting data in the shape of WWO. This method will
# morph from metno to WWO response format.
def create_standard_json_from_metno(content, days_to_return):
    try:
        forecast = json.loads(content)  # pylint: disable=invalid-name
    except (ValueError, TypeError) as exception:
        logger.error("---")
        logger.error(exception)
        logger.error("---")
        return {}, ""
    hourlies = forecast["properties"]["timeseries"]
    current = hourlies[0]
    # We are assuming these units:
    # "units": {
    #   "air_pressure_at_sea_level": "hPa",
    #   "air_temperature": "celsius",
    #   "air_temperature_max": "celsius",
    #   "air_temperature_min": "celsius",
    #   "cloud_area_fraction": "%",
    #   "cloud_area_fraction_high": "%",
    #   "cloud_area_fraction_low": "%",
    #   "cloud_area_fraction_medium": "%",
    #   "dew_point_temperature": "celsius",
    #   "fog_area_fraction": "%",
    #   "precipitation_amount": "mm",
    #   "relative_humidity": "%",
    #   "ultraviolet_index_clear_sky": "1",
    #   "wind_from_direction": "degrees",
    #   "wind_speed": "m/s"
    # }
    content = {
        "data": {
            "request": [
                {
                    "type": "feature",
                    "query": str(forecast["geometry"]["coordinates"][1])
                    + ","
                    + str(forecast["geometry"]["coordinates"][0]),
                }
            ],
            "current_condition": [_convert_hour(current)],
            "weather": [],
        }
    }

    days = group_hours_to_days(
        forecast["geometry"]["coordinates"][1],
        forecast["geometry"]["coordinates"][0],
        hourlies,
        days_to_return,
    )

    # TODO: Astronomy needs to come from this:
    # https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-10-07&offset=-05:00
    # and obviously can be cached for a while
    # https://api.met.no/weatherapi/sunrise/2.0/documentation
    # Note that full moon/new moon/first quarter/last quarter aren't returned
    # and the moonphase value should match these from WWO:
    # New Moon
    # Waxing Crescent
    # First Quarter
    # Waxing Gibbous
    # Full Moon
    # Waning Gibbous
    # Last Quarter
    # Waning Crescent

    for date, day in days.items():
        content["data"]["weather"].append(
            {
                "date": str(date),
                "astronomy": [],
                "maxtempC": day["maxtempC"],
                "maxtempF": day["maxtempF"],
                "mintempC": day["mintempC"],
                "mintempF": day["mintempF"],
                "avgtempC": day["avgtempC"],
                "avgtempF": day["avgtempF"],
                "totalSnow_cm": "not implemented",
                "sunHour": "12",  # This would come from astonomy data
                "uvIndex": day["uvIndex"],
                "hourly": _convert_hourly(day["hourly"]),
            }
        )

    # for day in forecast.
    return json.dumps(content)


if __name__ == "__main__":
    # if len(sys.argv) == 1:
    #     for deg in range(0, 360):
    #         print('deg: ' + str(deg) + '; 16point: ' + to_16_point(deg))
    if len(sys.argv) == 2:
        req = sys.argv[1].split("?")
        # to_description(sys.argv[1])
        metno_request(req[0], req[1])
    elif len(sys.argv) == 3:
        with open(sys.argv[1], "r") as contentf:
            content = create_standard_json_from_metno(contentf.read(), int(sys.argv[2]))
        print(content)
    else:
        print("usage: metno <content file> <days>")