from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import unicode_literals, absolute_import

# All the functionality needed to connect to Strava
from scipy import optimize
from scipy.signal import savgol_filter


from django_mailbox.models import Message,Mailbox,MessageAttachment

import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')

from rowers.dataprep import columndict

from rowers.rower_rules import is_workout_user,ispromember

import stravalib
from stravalib.exc import ActivityUploadFailed,TimeoutExceeded

from iso8601 import ParseError
from rowers.utils import myqueue

import rowers.mytypes as mytypes
import gzip

from rowers.tasks import handle_strava_sync

from rowsandall_app.settings import (
    C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
    STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET
    )

try:
    from json.decoder import JSONDecodeError
except ImportError:
    JSONDecodeError = ValueError

from rowers.imports import *

headers = {'Accept': 'application/json',
           'Api-Key': STRAVA_CLIENT_ID,
           'Content-Type': 'application/json',
           'user-agent': 'sanderroosendaal'}


oauth_data = {
    'client_id': STRAVA_CLIENT_ID,
    'client_secret': STRAVA_CLIENT_SECRET,
    'redirect_uri': STRAVA_REDIRECT_URI,
    'autorization_uri': "https://www.strava.com/oauth/authorize",
    'content_type': 'application/json',
    'tokenname': 'stravatoken',
    'refreshtokenname': 'stravarefreshtoken',
    'expirydatename': 'stravatokenexpirydate',
    'bearer_auth': True,
    'base_url': "https://www.strava.com/oauth/token",
    'grant_type': 'refresh_token',
    'headers': headers,
    'scope':'activity:write,activity:read_all',
    }


# Exchange access code for long-lived access token
def get_token(code):
    return imports_get_token(code, oauth_data)

def strava_open(user):
    return imports_open(user, oauth_data)

def do_refresh_token(refreshtoken):
    return imports_do_refresh_token(refreshtoken, oauth_data)

def rower_strava_token_refresh(user):
    r = Rower.objects.get(user=user)
    res = do_refresh_token(r.stravarefreshtoken)
    access_token = res[0]
    expires_in = res[1]
    refresh_token = res[2]
    expirydatetime = timezone.now()+timedelta(seconds=expires_in)

    r.stravatoken = access_token
    r.stravatokenexpirydate = expirydatetime
    r.stravarefreshtoken = refresh_token
    r.save()

    return r.stravatoken

# Make authorization URL including random string
def make_authorization_url(request):
    return imports_make_authorization_url(oauth_data)

# Get list of workouts available on Strava
def get_strava_workout_list(user,limit_n=0):
    r = Rower.objects.get(user=user)
    if (r.stravatoken == '') or (r.stravatoken is None):
        s = "Token doesn't exist. Need to authorize"
        return custom_exception_handler(401,s)
    elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599)>r.stravatokenexpirydate):
        s = "Token expired. Needs to refresh."
        return custom_exception_handler(401,s)
    else:
        # ready to fetch. Hurray
        authorizationstring = str('Bearer ' + r.stravatoken)
        headers = {'Authorization': authorizationstring,
                   'user-agent': 'sanderroosendaal',
                   'Content-Type': 'application/json'}

        url = "https://www.strava.com/api/v3/athlete/activities"

        if limit_n==0:
            params = {}
        else:
            params = {'per_page':limit_n}

        s = requests.get(url,headers=headers,params=params)


    return s


# gets all new Strava workouts for a rower
def get_strava_workouts(rower):

    if not ispromember(rower.user):
        return 0

    try:
        thetoken = strava_open(rower.user)
    except NoTokenError:
        return 0

    res = get_strava_workout_list(rower.user,limit_n=10)

    if (res.status_code != 200):
        return 0
    else:
        stravaids = [int(item['id']) for item in res.json()]
        stravadata = [{
            'id':int(item['id']),
            'elapsed_time':item['elapsed_time'],
            'start_date':item['start_date'],
            } for item in res.json()]

        alldata = {}
        for item in res.json():
            alldata[item['id']] = item


        wfailed = Workout.objects.filter(user=rower,uploadedtostrava=-1)

        for w in wfailed:
            for item in stravadata:
                elapsed_time = item['elapsed_time']
                start_date = item['start_date']
                stravaid = item['id']

                if arrow.get(start_date) == arrow.get(w.startdatetime):
                    dd = datetime.min + timedelta(
                        seconds=int(elapsed_time)
                        )


                    delta = datetime.combine(datetime.min,datetime.time(dd))-datetime.combine(datetime.min,w.duration)

                    if delta < timedelta(minutes=2):
                        w.uploadedtostrava = int(stravaid)
                        w.save()

        knownstravaids = [
            w.uploadedtostrava for w in Workout.objects.filter(user=rower)
            ]

        tombstones = [
            t.uploadedtostrava for t in TombStone.objects.filter(user=rower)
            ]

        knownstravaids = uniqify(knownstravaids+tombstones)

        newids = [stravaid for stravaid in stravaids if not stravaid in knownstravaids]

        for stravaid in newids:
            result = create_async_workout(alldata,rower.user,stravaid)

    return 1

def create_async_workout(alldata,user,stravaid,debug=False):
    data = alldata[stravaid]
    r = Rower.objects.get(user=user)
    distance = data['distance']
    stravaid = data['id']
    try:
        workouttype = mytypes.stravamappinginv[data['type']]
    except:
        workouttype = 'other'

    if workouttype not in [x[0] for x in Workout.workouttypes]:
        workouttype = 'other'

    if workouttype.lower() == 'rowing':
        workouttype = 'rower'
        if 'summary_polyline' in data['map']:
            workouttype = 'water'


    try:
        comments = data['comments']
    except:
        comments = ' '

    try:
        thetimezone = tz(data['timezone'])
    except:
        thetimezone = 'UTC'

    try:
        rowdatetime = iso8601.parse_date(data['date_utc'])
    except KeyError:
        rowdatetime = iso8601.parse_date(data['start_date'])
    except ParseError:
        rowdatetime = iso8601.parse_date(data['date'])

    try:
        c2intervaltype = data['workout_type']

    except KeyError:
        c2intervaltype = ''

    try:
        title = data['name']
    except KeyError:
        title = ""
        try:
            t = data['comments'].split('\n', 1)[0]
            title += t[:20]
        except:
            title = 'Imported'

    workoutdate = rowdatetime.astimezone(
        pytz.timezone(thetimezone)
    ).strftime('%Y-%m-%d')

    starttime = rowdatetime.astimezone(
        pytz.timezone(thetimezone)
    ).strftime('%H:%m:%S')

    totaltime = data['elapsed_time']
    duration = dataprep.totaltime_sec_to_string(totaltime)

    weightcategory = 'hwt'

    # Create CSV file name and save data to CSV file
    csvfilename ='media/mailbox_attachments/{code}_{importid}.csv'.format(
        importid=stravaid,
        code = uuid4().hex[:16]
        )



    # Check if workout has stroke data, and get the stroke data

    starttimeunix = arrow.get(rowdatetime).timestamp

    result = handle_strava_import_stroke_data(
        title,
        user.email,
        r.stravatoken,
        stravaid,
        starttimeunix,
        csvfilename,
        workouttype = workouttype,
        boattype = '1x'
    )

    return 1

from rowers.utils import get_strava_stream



# Get a Strava workout summary data and stroke data by ID
def get_workout(user,stravaid):
    try:
        thetoken = strava_open(user)
    except NoTokenError:
        s = "Token error"
        return custom_exception_handler(401,s)

    r = Rower.objects.get(user=user)
    if (r.stravatoken == '') or (r.stravatoken is None):
        s = "Token doesn't exist. Need to authorize"
        return custom_exception_handler(401,s)
    elif (r.stravatokenexpirydate is not None and timezone.now()>r.stravatokenexpirydate):
        s = "Token expired. Needs to refresh."
        return custom_exception_handler(401,s)
    else:
        # ready to fetch. Hurray
        fetchresolution = 'high'
        series_type = 'time'
        authorizationstring = str('Bearer ' + r.stravatoken)
        headers = {'Authorization': authorizationstring,
                   'user-agent': 'sanderroosendaal',
                   'Content-Type': 'application/json',
                   'resolution': 'medium',}
        url = "https://www.strava.com/api/v3/activities/"+str(stravaid)
        workoutsummary = requests.get(url,headers=headers).json()

        workoutsummary['timezone'] = "Etc/UTC"
        try:
            startdatetime = workoutsummary['start_date']
        except KeyError:
            startdatetime = timezone.now()

        spm = get_strava_stream(r,'cadence',stravaid)
        hr = get_strava_stream(r,'heartrate',stravaid)
        t = get_strava_stream(r,'time',stravaid)
        velo = get_strava_stream(r,'velocity_smooth',stravaid)
        d = get_strava_stream(r,'distance',stravaid)
        coords = get_strava_stream(r,'latlng',stravaid)
        power = get_strava_stream(r,'watts',stravaid)

        if t is not None:
            nr_rows = len(t)
        else:
            duration = int(workoutsummary['elapsed_time'])
            t = pd.Series(range(duration+1))

            nr_rows = len(t)


        if nr_rows == 0:
            return (0,"Error: Time data had zero length")

        if d is None:
            d = 0*t

        if spm is None:
            spm = np.zeros(nr_rows)

        if power is None:
            power = np.zeros(nr_rows)

        if hr is None:
            hr = np.zeros(nr_rows)

        if velo is None:
            velo = np.zeros(nr_rows)

        dt = np.diff(t).mean()
        wsize = round(5./dt)

        velo2 = ewmovingaverage(velo,wsize)

        if coords is not None:
            try:
                lat = coords[:,0]
                lon = coords[:,1]
            except IndexError:
                lat = np.zeros(len(t))
                lon = np.zeros(len(t))
        else:
            lat = np.zeros(len(t))
            lon = np.zeros(len(t))




        strokelength = velo*60./(spm)
        strokelength[np.isinf(strokelength)] = 0.0


        pace = 500./(1.0*velo2)
        pace[np.isinf(pace)] = 0.0

        df = pd.DataFrame({'t':10*t,
                           'd':10*d,
                           'p':10*pace,
                           'spm':spm,
                           'hr':hr,
                           'lat':lat,
                           'lon':lon,
                           'power':power,
                           'strokelength':strokelength,
                           })



        # startdatetime = datetime.datetime.strptime(startdatetime,"%Y-%m-%d-%H:%M:%S")

        return [workoutsummary,df]

# Generate Workout data for Strava (a TCX file)
def createstravaworkoutdata(w,dozip=True):
    filename = w.csvfilename
    try:
        row = rowingdata(csvfile=filename)
    except IOError:
        data = dataprep.read_df_sql(w.id)
        try:
            datalength = len(data)
        except AttributeError:
            datalength = 0

        if datalength != 0:
            data.rename(columns = columndict,inplace=True)
            res = data.to_csv(w.csvfilename+'.gz',
                              index_label='index',
                              compression='gzip')
            try:
                row = rowingdata(csvfile=filename)
            except IOError:
                return '','Error - could not find rowing data'
        else:
            return '','Error - could not find rowing data'

    tcxfilename = filename[:-4]+'.tcx'
    try:
        newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
    except TypeError:
        newnotes = 'from '+w.workoutsource+' via rowsandall.com'

    row.exporttotcx(tcxfilename,notes=newnotes)
    if dozip:
        gzfilename = tcxfilename+'.gz'
        with open(tcxfilename,'rb') as inF:
            s = inF.read()
            with gzip.GzipFile(gzfilename,'wb') as outF:
                outF.write(s)

        try:
            os.remove(tcxfilename)
        except WindowError:
            pass

        return gzfilename,""

    else:
        return tcxfilename,""


# Upload the TCX file to Strava and set the workout activity type
# to rowing on Strava
def handle_stravaexport(f2,workoutname,stravatoken,description='',
                        activity_type='Rowing',quick=False,asynchron=False):
    # w = Workout.objects.get(id=workoutid)
    client = stravalib.Client(access_token=stravatoken)

    act = client.upload_activity(f2,'tcx.gz',name=workoutname)

    try:
        if quick:
            res = act.wait(poll_interval=1.0, timeout=10)
            message = 'Workout successfully synchronized to Strava'
        else:
            res = act.wait(poll_interval=5.0,timeout=30)
            message = 'Workout successfully synchronized to Strava'
    except:
        res = 0
        message = 'Strava upload timed out'


    # description doesn't work yet. Have to wait for stravalib to update
    if res:
        try:
            act = client.update_activity(res.id,activity_type=activity_type,description=description,device_name='Rowsandall.com')
        except TypeError:
            act = client.update_activity(res.id,activity_type=activity_type,description=description)
    else:
        message = 'Strava activity update timed out.'
        return (0,message)

    return (res.id,message)

# Create workout data from Strava or Concept2
# data and create the associated Workout object and save it
def add_workout_from_data(user,importid,data,strokedata,
                          source='strava',splitdata=None,
                          workoutsource='strava'):
    try:
        workouttype = mytypes.stravamappinginv[data['type']]
    except KeyError:
        workouttype = 'other'

    if workouttype.lower() == 'rowing':
        workouttype = 'rower'

    if 'summary_polyline' in data['map'] and workouttype=='rower':
        workouttype = 'water'

    if workouttype not in [x[0] for x in Workout.workouttypes]:
        workouttype = 'other'
    try:
        comments = data['comments']
    except:
        comments = ' '

    try:
        thetimezone = tz(data['timezone'])
    except:
        thetimezone = 'UTC'

    r = Rower.objects.get(user=user)
    try:
        rowdatetime = iso8601.parse_date(data['date_utc'])
    except KeyError:
        rowdatetime = iso8601.parse_date(data['start_date'])
    except ParseError:
        rowdatetime = iso8601.parse_date(data['date'])


    try:
        intervaltype = data['workout_type']

    except KeyError:
        intervaltype = ''

    try:
        title = data['name']
    except KeyError:
        title = ""
        try:
            t = data['comments'].split('\n', 1)[0]
            title += t[:20]
        except:
            title = 'Imported'

    starttimeunix = arrow.get(rowdatetime).timestamp

    res = make_cumvalues(0.1*strokedata['t'])
    cum_time = res[0]
    lapidx = res[1]

    unixtime = cum_time+starttimeunix
    seconds = 0.1*strokedata.loc[:,'t']

    nr_rows = len(unixtime)

    try:
        latcoord = strokedata.loc[:,'lat']
        loncoord = strokedata.loc[:,'lon']
        if latcoord.std() == 0 and loncoord.std() == 0 and workouttype == 'water':
            workouttype = 'rower'
    except:
        latcoord = np.zeros(nr_rows)
        loncoord = np.zeros(nr_rows)
        if workouttype == 'water':
            workouttype = 'rower'



    try:
        strokelength = strokedata.loc[:,'strokelength']
    except:
        strokelength = np.zeros(nr_rows)

    dist2 = 0.1*strokedata.loc[:,'d']

    try:
        spm = strokedata.loc[:,'spm']
    except KeyError:
        spm = 0*dist2

    try:
        hr = strokedata.loc[:,'hr']
    except KeyError:
        hr = 0*spm
    pace = strokedata.loc[:,'p']/10.
    pace = np.clip(pace,0,1e4)
    pace = pace.replace(0,300)

    velo = 500./pace

    try:
        power = strokedata.loc[:,'power']
    except KeyError:
        power = 2.8*velo**3

    #if power.std() == 0 and power.mean() == 0:
    #    power = 2.8*velo**3

    # save csv
    # Create data frame with all necessary data to write to csv
    df = pd.DataFrame({'TimeStamp (sec)':unixtime,
                       ' Horizontal (meters)': dist2,
                       ' Cadence (stokes/min)':spm,
                       ' HRCur (bpm)':hr,
                       ' longitude':loncoord,
                       ' latitude':latcoord,
                       ' Stroke500mPace (sec/500m)':pace,
                       ' Power (watts)':power,
                       ' DragFactor':np.zeros(nr_rows),
                       ' DriveLength (meters)':np.zeros(nr_rows),
                       ' StrokeDistance (meters)':strokelength,
                       ' DriveTime (ms)':np.zeros(nr_rows),
                       ' StrokeRecoveryTime (ms)':np.zeros(nr_rows),
                       ' AverageDriveForce (lbs)':np.zeros(nr_rows),
                       ' PeakDriveForce (lbs)':np.zeros(nr_rows),
                       ' lapIdx':lapidx,
                       ' ElapsedTime (sec)':seconds
                       })


    df.sort_values(by='TimeStamp (sec)',ascending=True)


    timestr = strftime("%Y%m%d-%H%M%S")


    # Create CSV file name and save data to CSV file
    csvfilename ='media/{code}_{importid}.csv'.format(
        importid=importid,
        code = uuid4().hex[:16]
        )

    res =  df.to_csv(csvfilename+'.gz',index_label='index',
                     compression='gzip')


    id,message = dataprep.save_workout_database(
        csvfilename,r,
        workouttype=workouttype,
        title=title,notes=comments,
        workoutsource=workoutsource,
        dosummary=True
    )


    return id,message

def workout_strava_upload(user,w, quick=False,asynchron=True):
    try:
        thetoken = strava_open(user)
    except NoTokenError:
        return "Please connect to Strava first",0

    message = "Uploading to Strava"
    stravaid=-1
    r = Rower.objects.get(user=user)
    res = -1
    if (r.stravatoken == '') or (r.stravatoken is None):
        s = "Token doesn't exist. Need to authorize"
        raise NoTokenError("Your hovercraft is full of eels")

    if (is_workout_user(user,w)):
        if asynchron:
            tcxfile, tcxmesg = createstravaworkoutdata(w)
            if not tcxfile:
                return "Failed to create workout data",0
            job = myqueue(queue,
                          handle_strava_sync,
                          r.stravatoken,
                          w.id,
                          tcxfile,w.name,r.stravaexportas,
                          w.notes
                          )
            return "Asynchronous sync",-1
        try:
            tcxfile,tcxmesg = createstravaworkoutdata(w)
            if tcxfile:
                with open(tcxfile,'rb') as f:
                    res,mes = handle_stravaexport(
                        f,w.name,
                        r.stravatoken,
                        description=w.notes+'\n from '+w.workoutsource+' via rowsandall.com',
                        activity_type=r.stravaexportas,quick=quick,asynchron=asynchron)
                    if res==0:
                        message = mes
                        w.uploadedtostrava = -1
                        stravaid = -1
                        w.save()
                        try:
                            os.remove(tcxfile)
                        except WindowsError:
                            pass
                        return message,stravaid

                    w.uploadedtostrava = res
                    w.save()
                    try:
                        os.remove(tcxfile)
                    except WindowsError:
                        pass
                    message = mes
                    stravaid = res
                    return message,stravaid
            else:
                message = "Strava TCX data error "+tcxmesg
                w.uploadedtostrava = -1
                stravaid = -1
                w.save()
                return message, stravaid

        except ActivityUploadFailed as e:
            message = "Strava Upload error: %s" % e
            w.uploadedtostrava = -1
            stravaid = -1
            w.save()
            os.remove(tcxfile)
            return message,stravaid
    return message,stravaid



def handle_strava_import_stroke_data(title,
                                     useremail,
                                     stravatoken,
                                     stravaid,
                                     starttimeunix,
                                     csvfilename,debug=True,
                                     workouttype = 'rower',
                                     boattype = '1x',
                                     **kwargs):
    # ready to fetch. Hurray

    fetchresolution = 'high'
    series_type = 'time'
    authorizationstring = str('Bearer ' + stravatoken)
    headers = {'Authorization': authorizationstring,
               'user-agent': 'sanderroosendaal',
               'Content-Type': 'application/json',
               'resolution': 'medium',}
    url = "https://www.strava.com/api/v3/activities/"+str(stravaid)
    workoutsummary = requests.get(url,headers=headers).json()

    workoutsummary['timezone'] = "Etc/UTC"
    startdatetime = workoutsummary['start_date']

    r = type('Rower', (object,), {"stravatoken": stravatoken})

    spm = get_strava_stream(r,'cadence',stravaid)
    hr = get_strava_stream(r,'heartrate',stravaid)
    t = get_strava_stream(r,'time',stravaid)
    velo = get_strava_stream(r,'velocity_smooth',stravaid)
    d = get_strava_stream(r,'distance',stravaid)
    coords = get_strava_stream(r,'latlng',stravaid)
    power = get_strava_stream(r,'watts',stravaid)

    if t is not None:
        nr_rows = len(t)
    else:
        return 0

    if nr_rows == 0:
        return 0

    if d is None:
        d = 0*t

    if spm is None:
        spm = np.zeros(nr_rows)

    if power is None:
        power = np.zeros(nr_rows)

    if hr is None:
        hr = np.zeros(nr_rows)

    if velo is None:
        velo = np.zeros(nr_rows)


    f = np.diff(t).mean()
    if f != 0:
        windowsize = 2*(int(10./(f)))+1
    else:
        windowsize = 1

    if windowsize > 3 and windowsize < len(velo):
        velo2 = savgol_filter(velo,windowsize,3)
    else:
        velo2 = velo

    if coords is not None:
        try:
            lat = coords[:,0]
            lon = coords[:,1]
            if lat.std() == 0 and lon.std() == 0 and workouttype == 'water':
                workouttype = 'rower'
        except IndexError:
            lat = np.zeros(len(t))
            lon = np.zeros(len(t))
            if workouttype == 'water':
                workouttype = 'rower'
    else:
        lat = np.zeros(len(t))
        lon = np.zeros(len(t))
        if workouttype == 'water':
            workouttype = 'rower'

    strokelength = velo*60./(spm)
    strokelength[np.isinf(strokelength)] = 0.0

    pace = 500./(1.0*velo2)
    pace[np.isinf(pace)] = 0.0

    unixtime = starttimeunix+t

    strokedistance = 60.*velo2/spm

    if workouttype == 'rower' and pd.Series(power).mean() == 0:
        power = 2.8*(velo2**3)

    nr_strokes = len(t)

    df = pd.DataFrame({'TimeStamp (sec)':unixtime,
                       ' ElapsedTime (sec)':t,
                       ' Horizontal (meters)':d,
                       ' Stroke500mPace (sec/500m)':pace,
                       ' Cadence (stokes/min)':spm,
                       ' HRCur (bpm)':hr,
                       ' latitude':lat,
                       ' longitude':lon,
                       ' StrokeDistance (meters)':strokelength,
                       'cum_dist':d,
                       ' DragFactor':np.zeros(nr_strokes),
                       ' DriveLength (meters)':np.zeros(nr_strokes),
                       ' StrokeDistance (meters)':strokedistance,
                       ' DriveTime (ms)':np.zeros(nr_strokes),
                       ' StrokeRecoveryTime (ms)':np.zeros(nr_strokes),
                       ' AverageDriveForce (lbs)':np.zeros(nr_strokes),
                       ' PeakDriveForce (lbs)':np.zeros(nr_strokes),
                       ' lapIdx':np.zeros(nr_strokes),
                       ' Power (watts)':power,
    })




    df.sort_values(by='TimeStamp (sec)',ascending=True)

    res = df.to_csv(csvfilename,index_label='index')

    workoutsbox = Mailbox.objects.filter(name='workouts')[0]

    body = """stravaid {stravaid}
workouttype {workouttype}
boattype {boattype}""".format(
    stravaid=stravaid,
    workouttype=workouttype,
    boattype=boattype
)

    msg = Message(mailbox=workoutsbox,
                  from_header=useremail,
                  subject=title,
                  body=body)
    msg.save()

    a = MessageAttachment(message=msg,document=csvfilename[6:])
    a.save()

    return res
