#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Aug  2 12:51:22 2022.

@author: Nishad Mandlik
"""

from enum import IntFlag, auto
import requests
import warnings
import xml.etree.ElementTree as ET

_LOGIN_URL = "https://api.komoot.de/v006/account/email/%s/"
_TOURS_URL = "https://api.komoot.de/v007/users/%s/tours/"
_TOUR_DL_GPX_URL = "https://api.komoot.de/v007/tours/%s.gpx"
_TOUR_UL_GPX_URL = "https://api.komoot.de/v007/tours/"


class AutoFlag(IntFlag):
    """Flag Enum with auto value generation."""

    def __new__(cls, flag_name):
        """
        Create custom element with Flag-Enum value.

        Parameters
        ----------
        flag_name : str
            Flag name.

        Returns
        -------
        obj : instance of AutoFlag
            New Flag Enum element.

        """
        val = 2 ** len(cls.__members__)
        obj = int.__new__(cls, val)
        obj._value_ = val
        obj.flag_name = flag_name
        return obj


class TourType(AutoFlag):
    """Flag Enum for Tour Type (Planned, Recorded)."""

    PLANNED = "tour_planned"
    RECORDED = "tour_recorded"


class TourStatus(AutoFlag):
    """Flag Enum for Tour Status (Public, Private)."""

    PUBLIC = "public"
    PRIVATE = "private"


class Sport(AutoFlag):
    """
    Flag Enum for Sports.

    Obtained from https://static.komoot.de/doc/external-api/v007/sports.html
    """

    BIKING = "citybike"
    E_BIKE_TOURING = "e_touringbicycle"
    BIKE_TOURING = "touringbicycle"
    E_ROAD_CYCLING = "e_racebike"
    ROAD_CYCLING = "racebike"
    E_GRAVEL_BIKING = "e_mtb_easy"
    GRAVEL_BIKING = "mtb_easy"
    E_MT_BIKING = "e_mtb"
    MT_BIKING = "mtb"
    MT_BIKING_DOWNHILL = "downhillbike"
    E_MT_BIKING_ENDURO = "e_mtb_advanced"
    MT_BIKING_ENDURO = "mtb_advanced"
    UNICYCLING = "unicycle"
    HIKING = "hike"
    RUNNING = "jogging"
    SKATING = "skaten"
    MOUNTAINEERING = "mountaineering"
    ROCK_CLIMBING = "climbing"
    NORDIC_WALKING = "nordicwalking"
    SKI_TOURING = "skitour"
    CROSS_COUNTRY_SKIING = "nordic"
    ALPINE_SKIING = "skialpin"
    SLEDGING = "sled"
    SNOWBOARDING = "snowboard"
    SNOWSHOEING = "snowshoe"
    OTHER = "other"


class TourOwner(IntFlag):
    SELF = auto()
    OTHER = auto()


class API():
    """Class for interfacing with the Komoot API."""

    def __init__(self):
        """
        Initialize a Komoot API object.

        Returns
        -------
        None.

        """
        self.user_details = {}

    def login(self, email_id, password):
        """
        Authenticate user credentials and generate access token for the
        Komoot API.

        Parameters
        ----------
        email_id : str
            Email ID for Komoot account.
        password : str
            Password for Komoot account.

        Returns
        -------
        bool
            True if login is successful, False otherwise.

        """
        self.user_details = {}
        resp = requests.get(_LOGIN_URL % email_id, auth=(email_id, password))
        if (resp.status_code != 200):
            return False
        details = resp.json()
        self.user_details["email"] = email_id
        self.user_details["user_id"] = details["username"]
        self.user_details["disp_name"] = details["user"]["displayname"]
        self.user_details["dp_url"] = (
            details["user"]["imageUrl"]
            if details["user"]["content"]["hasImage"] else None)
        self.user_details["token"] = details["password"]
        return True

    def get_user_email(self):
        """
        Return the email ID of the logged-in user.

        Raises
        ------
        RuntimeError
            If no user is logged-in.

        Returns
        -------
        str.

        """
        if (self.user_details == {}):
            raise RuntimeError("User Details Not Available. Please Sign In.")
        return self.user_details["email"]

    def get_user_id(self):
        """
        Return the user ID of the logged-in user.

        Raises
        ------
        RuntimeError
            If no user is logged-in.

        Returns
        -------
        str.

        """
        if (self.user_details == {}):
            raise RuntimeError("User Details Not Available. Please Sign In.")
        return self.user_details["user_id"]

    def get_user_disp_name(self):
        """
        Return the display name of the logged-in user.

        Raises
        ------
        RuntimeError
            If no user is logged-in.

        Returns
        -------
        str.

        """
        if (self.user_details == {}):
            raise RuntimeError("User Details Not Available. Please Sign In.")
        return self.user_details["disp_name"]

    def get_user_pic_url(self):
        """
        Return the display picture URL of the logged-in user. If picture is
        not set, None is returned.

        Raises
        ------
        RuntimeError
            If no user is logged-in.

        Returns
        -------
        str or None.

        """
        if (self.user_details == {}):
            raise RuntimeError("User Details Not Available. Please Sign In.")
        return self.user_details["dp_url"]

    def _parse_flags(self, user_flags, flag_cls):
        flag_names = []
        if (user_flags is not None):
            for flag in flag_cls:
                if (flag & user_flags):
                    flag_names.append(flag.flag_name)
        return (",").join(flag_names)

    def _add_flags_to_req_params(self, params_dict, param_name,
                                 user_flags, flag_cls):
        param_val = self._parse_flags(user_flags, flag_cls)
        if param_val:
            params_dict[param_name] = param_val

    def _filt_tours_status(self, tours, user_flags):
        if (user_flags is None):
            return tours
        tours_filt = []
        for flag in TourStatus:
            if (flag & user_flags):
                tours_filt += [tour for tour in tours
                               if (tour["status"] == flag.flag_name)]
        return tours_filt

    def _filt_tours_owner(self, tours, user_flags):
        if ((user_flags is None) or
                (user_flags == (TourOwner.SELF | TourOwner.OTHER))):
            return tours
        tours_filt = []
        if (TourOwner.SELF & user_flags):
            tours_filt += [tour for tour in tours
                           if (tour["_embedded"]["creator"]["username"] ==
                               self.get_user_id())]

        if (TourOwner.OTHER & user_flags):
            tours_filt += [tour for tour in tours
                           if (tour["_embedded"]["creator"]["username"] !=
                               self.get_user_id())]

        return tours_filt

    def get_user_tours_list(self, tour_type=None, tour_status=None,
                            sport=None, tour_owner=None):
        """
        Get the list of tours for the logged-in user, according to the
        user-defined filters.

        Parameters
        ----------
        tour_type : TourType or None, optional
            Bitwise OR-ed flags for filtering multiple tour types.
            The default is None.
        tour_status : TourStatus or None, optional
            Bitwise OR-ed flags for filtering multiple tour statuses.
            The default is None.
        sport : Sport or None, optional
            Bitwise OR-ed flags for filtering multiple sports.
            The default is None.
        tour_owner: TourOwner or None, optional
            Bitwise OR-ed flags for filtering activities according to creator
            (self or others).
            The default is None.

        Raises
        ------
        RuntimeError
            If no user is logged-in.

        Returns
        -------
        tours : list
            List of dictionaries containing details of tours.

        """

        if (self.user_details == {}):
            raise RuntimeError("User Details Not Available. Please Sign In.")
        params = {}
        self._add_flags_to_req_params(params, "type", tour_type, TourType)
        self._add_flags_to_req_params(params, "sport_types", sport, Sport)
        params["page"] = 0

        tours = []
        while True:
            resp = requests.get(_TOURS_URL % self.user_details["user_id"],
                                params=params,
                                auth=(self.user_details["user_id"],
                                      self.user_details["token"]))
            if (resp.status_code != 200):
                warnings.warn("Request Failed. Tour List May Be Incomplete")
                break

            content = resp.json()
            if (content["page"]["totalElements"] == 0):
                break

            tours += content["_embedded"]["tours"]

            params["page"] += 1
            if (content["page"]["totalPages"] == params["page"]):
                break

        tours = self._filt_tours_status(tours, tour_status)
        tours = self._filt_tours_owner(tours, tour_owner)

        return tours

    def download_tour_gpx(self, tour_id, download_dir):
        """
        Download tour in the GPX format.

        Parameters
        ----------
        tour_id : str
            Tour ID.
        download_dir : str
            Path of the directory where the downloaded tour will be saved.

        Raises
        ------
        RuntimeError
            If no user is logged-in.

        Returns
        -------
        str or None
            file_name if download is successful, None otherwise.

        """
        if (self.user_details == {}):
            raise RuntimeError("User Details Not Available. Please Sign In.")
        resp = requests.get(_TOUR_DL_GPX_URL % tour_id,
                            auth=(self.user_details["user_id"],
                                  self.user_details["token"]))
        if (resp.status_code != 200):
            warnings.warn("Download failed.\nError Code: %d" %
                          resp.status_code)
            return None
        gpx_txt = resp.text
        gpx_tree = ET.fromstring(gpx_txt)
        file_name = gpx_tree[0][0].text + ".gpx"
        with open(download_dir + "/" + file_name, "w") as f:
            f.write(resp.text)
        return file_name

    def upload_tour_gpx(self, sport, file_path):
        """
        Upload a GPX file as a recorded activity.

        Parameters
        ----------
        sport : Sport
            Type of sport for the tour.
        file_path : str
            Path of the GPX file.

        Raises
        ------
        RuntimeError
            If no user is logged-in.

        Returns
        -------
        resp : TYPE
            DESCRIPTION.

        """
        if (self.user_details == {}):
            raise RuntimeError("User Details Not Available. Please Sign In.")
        headers = {"User-Agent": "komPYoot"}
        params = {"data_type": "gpx", "sport": sport.flag_name}
        with open(file_path, "rb") as f:
            data = f.read()
        resp = requests.post(_TOUR_UL_GPX_URL, params=params,
                             headers=headers, data=data,
                             auth=(self.user_details["user_id"],
                                   self.user_details["token"]))
        if (resp.status_code != 201):
            warnings.warn("Upload Failed.\nError Code: %d" %
                          resp.status_code)
            return False
        return True
