# The interactions with the Concept2 logbook API
# All C2 related functions should be defined here
# (There is still some stuff defined directly in views.py. Need to
# move that here.)

# Python
import oauth2 as oauth
import cgi
import requests
import arrow
import requests.auth
import json
import iso8601
from django.utils import timezone
from datetime import datetime
from datetime import timedelta
import time

# Django
from django.shortcuts import render_to_response
from django.http import HttpResponseRedirect, HttpResponse,JsonResponse
from django.conf import settings
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
import dataprep
import pytz
from rowingdata import rowingdata
import pandas as pd
import numpy as np
from rowers.models import Rower,Workout
from rowers.models import checkworkoutuser
import sys
import urllib
from requests import Request, Session

from utils import myqueue,uniqify,isprorower

from rowers.types import otwtypes

from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET

from rowers.tasks import handle_c2_import_stroke_data
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')

# Custom error class - to raise a NoTokenError
class C2NoTokenError(Exception):
    def __init__(self,value):
	self.value=value

    def __str__(self):
	return repr(self.value)
    
# Custom exception handler, returns a 401 HTTP message
# with exception details in the json data
def custom_exception_handler(exc,message):

    response = {
	"errors": [
	    {
		"code": str(exc),
		"detail": message,
		}
	    ]
	}

    res = HttpResponse(message)
    res.status_code = 401
    res.json = json.dumps(response)

    return res

# Checks if user has Concept2 tokens, resets tokens if they are
# expired.
def c2_open(user):
    r = Rower.objects.get(user=user)
    if (r.c2token == '') or (r.c2token is None):
	s = "Token doesn't exist. Need to authorize"
	raise C2NoTokenError("User has no token")
    else:
	if (timezone.now()>r.tokenexpirydate):
	    res = rower_c2_token_refresh(user)
            if res == None:
                raise C2NoTokenError("User has no token")                
            if res[0] != None:
                thetoken = res[0]
            else:
                raise C2NoTokenError("User has no token")
	else:
	    thetoken = r.c2token

    return thetoken

def add_stroke_data(user,c2id,workoutid,startdatetime,csvfilename):
    r = Rower.objects.get(user=user)
    if (r.c2token == '') or (r.c2token is None):
	return custom_exception_handler(401,s)
	s = "Token doesn't exist. Need to authorize"
    elif (timezone.now()>r.tokenexpirydate):
	s = "Token expired. Needs to refresh."
	return custom_exception_handler(401,s)
    else:
        starttimeunix = arrow.get(startdatetime).timestamp

        job = myqueue(queue,
                      handle_c2_import_stroke_data,
                      r.c2token,
                      c2id,
                      workoutid,
                      starttimeunix,
                      csvfilename)

    return 1

def get_c2_workouts(rower):

    if not isprorower(rower):
        return 0
    
    try:
        thetoken = c2_open(rower.user)
    except C2NoTokenError:
        return 0

    res = get_c2_workout_list(rower.user,page=1)

    if (res.status_code != 200):
        return 0
    else:
        c2ids = [item['id'] for item in res.json()['data']]
        alldata = {}
        for item in res.json()['data']:
            alldata[item['id']] = item

        knownc2ids = uniqify([
            w.uploadedtoc2 for w in Workout.objects.filter(user=rower)
        ])
        newids = [c2id for c2id in c2ids if not c2id in knownc2ids]

        for c2id in newids:
            workoutid = create_async_workout(alldata,
                                             rower.user,c2id)

    return 1

# get workout metrics, then relay stroke data to an asynchronous task
def create_async_workout(alldata,user,c2id):
    data = alldata[c2id]
    splitdata = None

    distance = data['distance']
    c2id = data['id']
    workouttype = data['type']
    verified = data['verified']
    startdatetime = iso8601.parse_date(data['date'])
    weightclass = data['weight_class']
    weightcategory = 'hwt'
    if weightclass == "L":
        weightcategory = 'lwt'

    # Create CSV file name and save data to CSV file
    csvfilename ='media/Import_'+str(c2id)+'.csv.gz'

    totaltime = data['time']/10.
    duration = dataprep.totaltime_sec_to_string(totaltime)

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

    workoutdate = startdatetime.astimezone(
        pytz.timezone(timezone_str)
    ).strftime('%Y-%m-%d')
    starttime = startdatetime.astimezone(
        pytz.timezone(timezone_str)
    ).strftime('%H:%M:%S')

    r = Rower.objects.get(user=user)


    w = Workout(
        user=r,
        workouttype = workouttype,
        name = 'C2 Import Workout from {startdatetime}'.format(startdatetime=startdatetime),
        date = workoutdate,
        starttime = starttime,
        startdatetime = startdatetime,
        timezone = timezone_str,
        duration = duration,
        distance=distance,
        weightcategory = weightcategory,
        uploadedtoc2 = c2id,
        csvfilename = csvfilename,
        notes = 'imported from Concept2 log'
    )

    w.save()

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

    result = add_stroke_data(user,c2id,w.id,startdatetime,csvfilename)

    return w.id

# convert datetime object to seconds
def makeseconds(t):
    seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5)
    return seconds

# convert our weight class code to Concept2 weight class code
def c2wc(weightclass):
    if (weightclass=="lwt"):
	res = "L"
    else:
	res = "H"

    return res

# Concept2 logbook sends over split data for each interval
# We use it here to generate a custom summary
# Some users complained about small differences 
def summaryfromsplitdata(splitdata,data,filename,sep='|'):

    totaldist = data['distance']
    totaltime = data['time']/10.
    spm = data['stroke_rate']
    try:
        resttime = data['rest_time']/10.
    except KeyError:
        resttime = 0
    try:
        restdistance = data['rest_distance']
    except keyError:
        restdistance = 0
    try:
        avghr = data['heart_rate']['average']
    except KeyError:
        avghr = 0
    try:
        maxhr = data['heart_rate']['max']
    except KeyError:
        maxhr = 0
        
    avgpace = 500.*totaltime/totaldist
    restpace = 500.*resttime/restdistance

    velo = totaldist/totaltime
    avgpower = 2.8*velo**(3.0)

    restvelo = restdistance/resttime
    restpower = 2.8*restvelo**(3.0)
    
    avgdps = totaldist/data['stroke_count']
    
    from rowingdata import summarystring,workstring,interval_string

    
    sums = summarystring(totaldist,totaltime,avgpace,spm,avghr,maxhr,
                         avgdps,avgpower,readFile=filename,
                         separator=sep)

    sums += workstring(totaldist,totaltime,avgpace,spm,avghr,maxhr,
                       avgdps,avgpower,separator=sep,symbol='W')

    sums += workstring(restdistance,resttime,restpace,0,0,0,0,restpower,
                       separator=sep,
                       symbol='R')

    sums += '\nWorkout Details\n'
    sums += '#-{sep}SDist{sep}-Split-{sep}-SPace-{sep}-Pwr-{sep}SPM-{sep}AvgHR{sep}MaxHR{sep}DPS-\n'.format(
        sep=sep
        )
    
    intervalnr=0
    sa = []
    results = []
    
    try:
        timebased = data['workout_type'] in ['FixedTimeSplits','FixedTimeInterval']
    except KeyError:
        timebased = False
        
    for interval in splitdata:
        idist = interval['distance']
        itime = interval['time']/10.
        ipace = 500.*itime/idist
        ispm = interval['stroke_rate']
        try:
            irest_time = interval['rest_time']/10.
        except KeyError:
            irest_time = 0
        try:
            iavghr = interval['heart_rate']['average']
        except KeyError:
            iavghr = 0
        try:
            imaxhr = interval['heart_rate']['average']
        except KeyError:
            imaxhr = 0

        # create interval values
        iarr = [idist,'meters','work']
        resarr = [itime]
        if timebased:
            iarr = [itime,'seconds','work']
            resarr = [idist]
            
        if irest_time > 0:
            iarr += [irest_time,'seconds','rest']
            try:
                resarr += [interval['rest_distance']]
            except KeyError:
                resarr += [np.nan]

        sa += iarr
        results += resarr
            
        ivelo = idist/itime
        ipower = 2.8*ivelo**(3.0)
        
        sums += interval_string(intervalnr,idist,itime,ipace,ispm,
                                iavghr,imaxhr,0,ipower,separator=sep)
        intervalnr+=1

    return sums,sa,results

# Not used now. Could be used to add workout split data to Concept2
# logbook but needs to be reviewed. 
def createc2workoutdata_as_splits(w):
    filename = w.csvfilename
    row = rowingdata(filename)

    # resize per minute
    df = row.df.groupby(lambda x:x/60).mean()
    
    averagehr = int(df[' HRCur (bpm)'].mean())
    maxhr = int(df[' HRCur (bpm)'].max())

    # adding diff, trying to see if this is valid
    t = 10*df.ix[:,' ElapsedTime (sec)'].diff().values
    t[0] = t[1]
    d = df.ix[:,' Horizontal (meters)'].diff().values
    d[0] = d[1]
    p = 10*df.ix[:,' Stroke500mPace (sec/500m)'].values
    t = t.astype(int)
    d = d.astype(int)
    p = p.astype(int)
    spm = df[' Cadence (stokes/min)'].astype(int)
    spm[0] = spm[1]
    hr = df[' HRCur (bpm)'].astype(int)
    split_data = []
    for i in range(len(t)):
	thisrecord = {"time":t[i],"distance":d[i],"stroke_rate":spm[i],
		      "heart_rate":{
			  "average:":hr[i]
			  }
		      }
	split_data.append(thisrecord)

    try:
	durationstr = datetime.strptime(str(w.duration),"%H:%M:%S.%f")
    except ValueError:
	durationstr = datetime.strptime(str(w.duration),"%H:%M:%S")

    try:
        newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
    except TypeError:
        newnotes = 'from '+w.workoutsource+' via rowsandall.com'

    wtype = w.workouttype
    if wtype in otwtypes:
        wtype = 'water'
        
    data = {
	"type": wtype,
	"date": w.startdatetime.isoformat(),
	"distance": int(w.distance),
	"time": int(10*makeseconds(durationstr)),
	"timezone": w.timezone,
	"weight_class": c2wc(w.weightcategory),
	"comments": newnotes,
	"heart_rate": {
	    "average": averagehr,
	    "max": maxhr,
	    },
	"splits": split_data,
	}


    return data

# Create the Data object for the stroke data to be sent to Concept2 logbook
# API
def createc2workoutdata(w):
    filename = w.csvfilename
    try:
        row = rowingdata(filename)
    except IOError:
        return 0

    try:
        averagehr = int(row.df[' HRCur (bpm)'].mean())
        maxhr = int(row.df[' HRCur (bpm)'].max())
    except ValueError:
        averagehr = 0
        maxhr = 0

    # adding diff, trying to see if this is valid
    t = 10*row.df.ix[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)']
    t[0] = t[1]
    d = 10*row.df.ix[:,' Horizontal (meters)'].values
    d[0] = d[1]
    p = abs(10*row.df.ix[:,' Stroke500mPace (sec/500m)'].values)
    p = np.clip(p,0,3600)
    t = t.astype(int)
    d = d.astype(int)
    p = p.astype(int)
    spm = row.df[' Cadence (stokes/min)'].astype(int)
    spm[0] = spm[1]
    try:
        hr = row.df[' HRCur (bpm)'].astype(int)
    except ValueError:
        hr = 0*d
    stroke_data = []
    for i in range(len(t)):
	thisrecord = {"t":t[i],"d":d[i],"p":p[i],"spm":spm[i],"hr":hr[i]}
	stroke_data.append(thisrecord)

    try:
	durationstr = datetime.strptime(str(w.duration),"%H:%M:%S.%f")
    except ValueError:
	durationstr = datetime.strptime(str(w.duration),"%H:%M:%S")

    workouttype = w.workouttype
    if workouttype in otwtypes:
        workouttype = 'water'
        
    data = {
	"type": workouttype,
	"date": w.startdatetime.isoformat(),
	"timezone": w.timezone,
	"distance": int(w.distance),
	"time": int(10*makeseconds(durationstr)),
	"weight_class": c2wc(w.weightcategory),
	"comments": w.notes,
	"heart_rate": {
	    "average": averagehr,
	    "max": maxhr,
	    },
	"stroke_data": stroke_data,
	}


    return data

# Refresh Concept2 authorization token
def do_refresh_token(refreshtoken):
    scope = "results:write,user:read"
    client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET)
    post_data = {"grant_type": "refresh_token",
		 "client_secret": C2_CLIENT_SECRET,
		 "client_id":C2_CLIENT_ID,
		 "refresh_token": refreshtoken,
		 }
    headers = {'user-agent': 'sanderroosendaal'}
    url = "https://log.concept2.com/oauth/access_token"
    s = Session()
    req = Request('POST',url, data=post_data, headers=headers)
    
    prepped = req.prepare()
    prepped.body+="&scope="
    prepped.body+=scope

    response = s.send(prepped)
    token_json = response.json()
    try:
        thetoken = token_json['access_token']
        expires_in = token_json['expires_in']
        refresh_token = token_json['refresh_token']
    except:
        with open("media/c2errors.log","a") as errorlog:
            errorstring = str(sys.exc_info()[0])
            timestr = time.strftime("%Y%m%d-%H%M%S")
            errorlog.write(timestr+errorstring+"\r\n")
            errorlog.write(str(token_json)+"\r\n")
        thetoken = None
        expires_in = None
        refresh_token = None

    return [thetoken,expires_in,refresh_token]

# Exchange authorization code for authorization token
def get_token(code):
    messg=''
    scope = "user:read,results:write"
    client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET)
    post_data = {"grant_type": "authorization_code",
                 "code": code,
                 "redirect_uri": C2_REDIRECT_URI,
		 "client_secret": C2_CLIENT_SECRET,
		 "client_id":C2_CLIENT_ID,
		 }
    headers = {'user-agent': 'sanderroosendaal'}
    url = "https://log.concept2.com/oauth/access_token"
    s = Session()
    req = Request('POST',url, data=post_data, headers=headers)

    prepped = req.prepare()
    prepped.body+="&scope="
    prepped.body+=scope

    print prepped.body

    response = s.send(prepped)

    with open("media/c2authorize.log","a") as f:
        try:
            f.write(reponse.status_code+"\n")
            f.write(reponse.text+"\n")
            f.write(response.json+"\n\n")
        except:
            pass

    
    token_json = response.json()

    try:
        status_code = response.status_code
        # status_code = token_json['status_code']
    except AttributeError:
    # except KeyError:
        return (0,response.text)
        try:
            status_code = token_json.status_code
        except AttributeError:
            return (0,'Attribute Error on c2_get_token')

    if status_code == 200:
        thetoken = token_json['access_token']
        expires_in = token_json['expires_in']
        refresh_token = token_json['refresh_token']
    else:
        return (0,token_json['message'])

    return (thetoken,expires_in,refresh_token,messg)

# Make URL for authorization and load it
def make_authorization_url(request):
    # Generate a random string for the state parameter
    # Save it for use later to prevent xsrf attacks
    from uuid import uuid4
    state = str(uuid4())
    scope = "user:read,results:write"

    params = {"client_id": C2_CLIENT_ID,
              "response_type": "code",
              "redirect_uri": C2_REDIRECT_URI}
    url = "https://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params)
    url += "&scope="+scope

    return HttpResponseRedirect(url)

# Get workout from C2 ID
def get_c2_workout(user,c2id):
    r = Rower.objects.get(user=user)
    if (r.c2token == '') or (r.c2token is None):
	s = "Token doesn't exist. Need to authorize"
	return custom_exception_handler(401,s)	
    elif (timezone.now()>r.tokenexpirydate):
	s = "Token expired. Needs to refresh."
	return custom_exception_handler(401,s)
    else:
	# ready to fetch. Hurray
	authorizationstring = str('Bearer ' + r.c2token)
	headers = {'Authorization': authorizationstring,
		   'user-agent': 'sanderroosendaal',
		   'Content-Type': 'application/json'}
	url = "https://log.concept2.com/api/users/me/results/"+str(c2id)
	s = requests.get(url,headers=headers)

    return s

# Get stroke data belonging to C2 ID
def get_c2_workout_strokes(user,c2id):
    r = Rower.objects.get(user=user)
    if (r.c2token == '') or (r.c2token is None):
	return custom_exception_handler(401,s)
	s = "Token doesn't exist. Need to authorize"
    elif (timezone.now()>r.tokenexpirydate):
	s = "Token expired. Needs to refresh."
	return custom_exception_handler(401,s)
    else:
	# ready to fetch. Hurray
	authorizationstring = str('Bearer ' + r.c2token)
	headers = {'Authorization': authorizationstring,
		   'user-agent': 'sanderroosendaal',
		   'Content-Type': 'application/json'}
	url = "https://log.concept2.com/api/users/me/results/"+str(c2id)+"/strokes"
	s = requests.get(url,headers=headers)

    return s

# Get list of C2 workouts. We load only the first page,
# assuming that users don't want to import their old workouts
def get_c2_workout_list(user,page=1):
    r = Rower.objects.get(user=user)
    if (r.c2token == '') or (r.c2token is None):
	s = "Token doesn't exist. Need to authorize"
	return custom_exception_handler(401,s)
    elif (timezone.now()>r.tokenexpirydate):
	s = "Token expired. Needs to refresh."
	return custom_exception_handler(401,s)
    else:
	# ready to fetch. Hurray
	authorizationstring = str('Bearer ' + r.c2token)
	headers = {'Authorization': authorizationstring,
		   'user-agent': 'sanderroosendaal',
		   'Content-Type': 'application/json'}
	url = "https://log.concept2.com/api/users/me/results"
        url += "?page={page}".format(page=page)

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

    return s

    
# Get username, having access token.
# Handy for checking if the API access is working
def get_username(access_token):
    authorizationstring = str('Bearer ' + access_token)
    headers = {'Authorization': authorizationstring,
	       'user-agent': 'sanderroosendaal',
	       'Content-Type': 'application/json'}
    import urllib
    url = "https://log.concept2.com/api/users/me"
    response = requests.get(url,headers=headers)
    

    me_json = response.json()

    try:
        res =  me_json['data']['username']
        id = me_json['data']['id']
    except KeyError:
        res = None

    return res

# Get user id, having access token
# Handy for checking if the API access is working
def get_userid(access_token):
    authorizationstring = str('Bearer ' + access_token)
    headers = {'Authorization': authorizationstring,
	       'user-agent': 'sanderroosendaal',
	       'Content-Type': 'application/json'}
    import urllib
    url = "https://log.concept2.com/api/users/me"
    response = requests.get(url,headers=headers)
    

    me_json = response.json()
    try:
        res = me_json['data']['id']
    except KeyError:
        res = 0
        
    return res

# For debugging purposes
def process_callback(request):
    # need error handling

    code = request.GET['code']

    access_token = get_token(code)

    username,id = get_username(access_token)

    return HttpResponse("got a user name: %s" % username)

# Uploading workout
def workout_c2_upload(user,w):
    message = 'trying C2 upload'
    thetoken = c2_open(user)

    r = Rower.objects.get(user=user)

    # ready to upload. Hurray
    if (checkworkoutuser(user,w)):
	c2userid = get_userid(r.c2token)
        if not c2userid:
            raise C2NoTokenError

	data = createc2workoutdata(w)
        if data == 0:
            return "Error: No data file. Contact info@rowsandall.com if the problem persists",0
            
	authorizationstring = str('Bearer ' + r.c2token)
	headers = {'Authorization': authorizationstring,
	           'user-agent': 'sanderroosendaal',
	           'Content-Type': 'application/json'}
	import urllib
	url = "https://log.concept2.com/api/users/%s/results" % (c2userid)
	response = requests.post(url,headers=headers,data=json.dumps(data))

        if (response.status_code == 409 ):
	    message = "Concept2 Duplicate error"
	    w.uploadedtoc2 = -1
            c2id = -1
	    w.save()
        elif (response.status_code == 201 or response.status_code == 200):
            try:
	        s= json.loads(response.text)
	        c2id = s['data']['id']
	        w.uploadedtoc2 = c2id
	        w.save()
                message = "Upload to Concept2 was successful"
            except:
                message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text
                c2id = 0

	else:
            print response.status_code
            message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text
            c2id = 0

    return message,c2id

# This is token refresh. Looks for tokens in our database, then refreshes
def rower_c2_token_refresh(user):
    r = Rower.objects.get(user=user)
    res = do_refresh_token(r.c2refreshtoken)
    if res[0]:
        access_token = res[0]
        expires_in = res[1]
        refresh_token = res[2]
        expirydatetime = timezone.now()+timedelta(seconds=expires_in)

        r = Rower.objects.get(user=user)
        r.c2token = access_token
        r.tokenexpirydate = expirydatetime
        r.c2refreshtoken = refresh_token

        r.save()
        return r.c2token
    else:
        return None

