import swagger_client
from swagger_client.rest import ApiException
import maya
import os
import json
import datetime
import pandas as pd
import glob
import datetime
from loguru import logger
import requests
import socket
import urllib
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer


class StravaIO():

    def __init__(self, access_token=None):
        if access_token is None:
            access_token = os.getenv('STRAVA_ACCESS_TOKEN')
        self.configuration = swagger_client.Configuration()
        self.configuration.access_token = access_token
        self._api_client = swagger_client.ApiClient(self.configuration)
        self.athletes_api = swagger_client.AthletesApi(self._api_client)
        self.activities_api = swagger_client.ActivitiesApi(self._api_client)
        self.streams_api = swagger_client.StreamsApi(self._api_client)

    def get_logged_in_athlete(self):
        """Get logged in athlete
        
        Returns
        -------
        athlete: Athlete object
        """

        try:
            rv = Athlete(self.athletes_api.get_logged_in_athlete())
        except ApiException as e:
            logger.error(f""""
            Error in strava_swagger_client.AthletesApi! 
            STRAVA_ACCESS_TOKEN is likely out of date!
            Check the https://github.com/sladkovm/strava-oauth for help.
            Returning None.
            Original Error:
            {e}""")
            rv = None
        return rv 

    def local_athletes(self):
        """List local athletes
        
        Returns
        -------
        athletes: generator of JSON friendly dicts
        """
        for f_name in glob.glob(os.path.join(dir_stravadata(), 'athlete*.json')):
            with open(f_name) as f:
                yield json.load(f)


    def get_activity_by_id(self, id, include_all_efforts=False):
        """Get activity by ID

        Parameters
        ----------
        id: int
            activity_id
        include_all_efforts: bool (default=False)
            Include all segment efforts in the response
        
        Returns
        -------
        activity: Activity ojbect
        """
        return Activity(self.activities_api.get_activity_by_id(id, include_all_efforts=include_all_efforts))

    def get_logged_in_athlete_activities(self, after=0, list_activities=None):
        """List all activities after a given date
        
        Parameters
        ----------
        after: int, str or datetime object
            If integer, the time since epoch is assumed
            If str, the maya.parse() compatible date string is expected e.g. iso8601 or 2018-01-01 or 20180101
            If datetime, the datetime object is expected

        Returns
        -------
        list_activities: list
            List of SummaryActivity objects
        """
        if list_activities is None:
            list_activities = []
        after = date_to_epoch(after)
        _fetched = self.activities_api.get_logged_in_athlete_activities(after=after)
        if len(_fetched) > 0:
            print(f"Fetched {len(_fetched)}, the latests is on {_fetched[-1].start_date}")
            list_activities.extend(_fetched)
            if len(_fetched) == 30:
                last_after = list_activities[-1].start_date
                return self.get_logged_in_athlete_activities(after=last_after, list_activities=list_activities)
        else:
            print("empty list")
        
        return list_activities
        


    def local_activities(self, athlete_id):
        """List local activities
        
        Parameters
        ----------
        athlete_id: int

        Returns
        -------
        activities: generator of JSON friendly dicts
        """
        dir_activities = os.path.join(dir_stravadata(), f"activities_{athlete_id}")
        for f_name in glob.glob(os.path.join(dir_activities, '*.json')):
            with open(f_name) as f:
                yield json.load(f)


    def local_streams(self, athlete_id):
        """List local streams
        
        Parameters
        ----------
        athlete_id: int

        Returns
        -------
        streams: generator of dataframes
        """
        dir_streams = os.path.join(dir_stravadata(), f"streams_{athlete_id}")
        for f_name in glob.glob(os.path.join(dir_streams, '*.parquet')):
            yield pd.read_parquet(f_name)


    def get_activity_streams(self, id, athlete_id, local=True):
        """Get activity streams by ID
        
        Parameters
        ----------
        id: int
            activity_id
        athlete_id: int
            athlete_id
        local: bool (default=True)
            if the streams is already storred, return the local version

        Returns
        -------
        streams: Streams ojbect (remote) or pd.Dataframe (local)
        """
        if local:
            dir_streams = os.path.join(dir_stravadata(), f"streams_{athlete_id}")
            f_name = f"streams_{id}.parquet"
            f_path = os.path.join(dir_streams, f_name)
            if f_path in glob.glob(f_path):
                return pd.read_parquet(f_path)
        keys = ['time', 'distance', 'latlng', 'altitude', 'velocity_smooth',
        'heartrate', 'cadence', 'watts', 'temp', 'moving', 'grade_smooth']
        api_response = self.streams_api.get_activity_streams(id, keys, key_by_type=True)
        return Streams(api_response, id, athlete_id)


class Athlete():

    def __init__(self, api_response):
        """
        Parameters
        ----------
        api_response: swagger_client.get...() object
            e.g. athletes_api.get_logged_in_athlete()
        """
        self.api_response = api_response
        self.id = self.api_response.id

    def __str__(self):
        return self._stringify()

    def __repr__(self):
        return self._stringify()

    def to_dict(self):
        _dict = self.api_response.to_dict()
        _dict = convert_datetime_to_iso8601(_dict)
        return _dict

    def store_locally(self):
        strava_dir = dir_stravadata()
        f_name = f"athlete_{self.api_response.id}.json"
        with open(os.path.join(strava_dir, f_name), 'w') as fp:
            json.dump(self.to_dict(), fp)

    def _stringify(self):
        return json.dumps(self.to_dict(), indent=2)


class Activity():

    def __init__(self, api_response, client=None):
        self.api_response = api_response
        self.athlete_id = self.api_response.athlete.id
        self.id = self.api_response.id
        if client:
            self.streams_api = client.streams_api
        else:
            client = None

    def __repr__(self):
        return f"Activity: {self.id}, Date: {self.api_response.start_date}, Name: {self.api_response.name}"

    def to_dict(self):
        _dict = self.api_response.to_dict()
        _dict = convert_datetime_to_iso8601(_dict)
        return _dict

    def store_locally(self):
        strava_dir = dir_stravadata()
        athlete_id = self.api_response.athlete.id
        activities_dir = os.path.join(strava_dir, f"activities_{athlete_id}")
        if not os.path.exists(activities_dir):
            os.mkdir(activities_dir)
        f_name = f"activity_{self.api_response.id}.json"
        with open(os.path.join(activities_dir, f_name), 'w') as fp:
            json.dump(self.to_dict(), fp)


class Streams():

    ACCEPTED_KEYS = ['time', 'distance', 'altitude', 'velocity_smooth', 'heartrate', 'cadence', 'watts', 'temp', 'moving', 'grade_smooth', 'lat', 'lng']

    def __init__(self, api_response, activity_id, athlete_id):
        self.api_response = api_response
        self.activity_id = activity_id
        self.athlete_id = athlete_id

    def __repr__(self):
        return f"""Streams for {self.activity_id}\nKeys: {list(self.to_dict().keys())}\nAccess: obj.key or obj.to_dict() to load into a pd.DataFrame()"""

    def to_dict(self):
        _dict = self.api_response.to_dict()
        r = {}
        for k, v in _dict.items():
            if v is not None:
                r.update({k: v['data']})
        if r.get('latlng', None):
            latlng = r.pop('latlng')
            _r = list(zip(*latlng))
            r.update({'lat': list(_r[0])})
            r.update({'lng': list(_r[1])}) 
        return r

    def store_locally(self):
        _df = pd.DataFrame(self.to_dict())
        strava_dir = dir_stravadata()
        streams_dir = os.path.join(strava_dir, f"streams_{self.athlete_id}")
        if not os.path.exists(streams_dir):
            os.mkdir(streams_dir)
        f_name = f"streams_{self.activity_id}.parquet"
        _df.to_parquet(os.path.join(streams_dir, f_name))

    @property
    def time(self):
        return self._get_stream_by_name('time')

    @property
    def distance(self):
        return self._get_stream_by_name('distance')

    @property
    def altitude(self):
        return self._get_stream_by_name('altitude')

    @property
    def velocity_smooth(self):
        return self._get_stream_by_name('velocity_smooth')

    @property
    def heartrate(self):
        return self._get_stream_by_name('heartrate')

    @property
    def cadence(self):
        return self._get_stream_by_name('cadence')

    @property
    def watts(self):
        return self._get_stream_by_name('watts')

    @property
    def grade_smooth(self):
        return self._get_stream_by_name('grade_smooth')

    @property
    def moving(self):
        return self._get_stream_by_name('moving')
    
    @property
    def lat(self):
        return self._get_stream_by_name('lat')

    @property
    def lng(self):
        return self._get_stream_by_name('lng')


    def _get_stream_by_name(self, key):
        
        if key not in self.ACCEPTED_KEYS:
            raise KeyError(f"key must be one of {self.ACCEPTED_KEYS}")
        
        try:
            rv = self.to_dict()[key]
        except KeyError:
            logger.warning(f"Stream does not contain {key}")
            rv = None
        return rv


def strava_oauth2(client_id=None, client_secret=None):
    """Run strava authorization flow. This function will open a default system
    browser alongside starting a local webserver. The authorization procedure will be completed in the browser.

    The access token will be returned in the browser in the format ready to copy to the .env file.
    
    Parameters:
    -----------
    client_id: int, if not provided will be retrieved from the STRAVA_CLIENT_ID env viriable
    client_secret: str, if not provided will be retrieved from the STRAVA_CLIENT_SECRET env viriable
    """
    if client_id is None:
        client_id = os.getenv('STRAVA_CLIENT_ID', None)
        if client_id is None:
            raise ValueError('client_id is None')
    if client_secret is None:
        client_secret = os.getenv('STRAVA_CLIENT_SECRET', None)
        if client_secret is None:
            raise ValueError('client_secret is None')
    
    port = 8000
    _request_strava_authorize(client_id, port)

    logger.info(f"serving at port {port}")

    token = run_server_and_wait_for_token(
        port=port,
        client_id=client_id,
        client_secret=client_secret
    )

    return token


def _request_strava_authorize(client_id, port):
    params_oauth = {
        "client_id": client_id,
        "response_type": "code",
        "redirect_uri": f"http://localhost:{port}/authorization_successful",
        "scope": "read,profile:read_all,activity:read",
        "state": 'https://github.com/sladkovm/strava-http',
        "approval_prompt": "force"
    }
    values_url = urllib.parse.urlencode(params_oauth)
    base_url = 'https://www.strava.com/oauth/authorize'
    rv = base_url + '?' + values_url
    webbrowser.get().open(rv)
    return None


def run_server_and_wait_for_token(port, client_id, client_secret):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(('127.0.0.1', port))
        s.listen()
        conn, addr = s.accept()

        request_bytes = b''
        with conn:
            while True:
                chunk = conn.recv(512)
                request_bytes += chunk

                if request_bytes.endswith(b'\r\n\r\n'):
                    break
            conn.sendall(b'HTTP/1.1 200 OK\r\n\r\nsuccess\r\n')

        request = request_bytes.decode('utf-8')
        status_line = request.split('\n', 1)[0]
        
        method, raw_url, protocol_version = status_line.split(' ')
        
        url = urllib.parse.urlparse(raw_url)
        query_string = url.query
        query_params = urllib.parse.parse_qs(query_string, keep_blank_values=True)

        if url.path == "/authorization_successful":
            code = query_params.get('code')[0]
            logger.debug(f"code: {code}")
            params = {
                "client_id": client_id,
                "client_secret": client_secret,
                "code": code,
                "grant_type": "authorization_code"
            }
            r = requests.post("https://www.strava.com/oauth/token", params)
            data = r.json()
            logger.debug(f"Authorized athlete: {data.get('access_token', 'Oeps something went wrong!')}")
        else:
            data = url.path.encode()
        
        return data


def convert_datetime_to_iso8601(d):
    for k, v in d.items():
        if isinstance(v, dict):
            convert_datetime_to_iso8601(v)
        elif isinstance(v, list):
            for i in v:
                if isinstance(i, dict):
                    convert_datetime_to_iso8601(i)
        else:
            if isinstance(v, datetime.datetime):
                d[k] = maya.parse(v).iso8601()
    return d


def dir_stravadata():
    home_dir = os.path.expanduser('~')
    strava_dir = os.path.join(home_dir, '.stravadata')
    if not os.path.exists(strava_dir):
        os.mkdir(strava_dir)
    return strava_dir


def date_to_epoch(date):
    """Convert a date to epoch representation"""
    rv = None
    if isinstance(date, int):
        rv = date
    if isinstance(date, datetime.datetime):
        _ = maya.parse(date)
        rv = _.epoch
    if isinstance(date, str):
        _ = maya.when(date)
        rv = _.epoch
    if rv is None:
        raise TypeError('date must be epoch int, datetime obj or the string')
    return rv