/**
 *  Jawbone Service Manager
 *
 *  Author: Juan Risso
 *  Date: 2013-12-19
 */

include 'asynchttp_v1'

definition(
	name: "Jawbone UP (Connect)",
	namespace: "juano2310",
	author: "Juan Pablo Risso",
	description: "Connect your Jawbone UP to SmartThings",
	category: "SmartThings Labs",
	iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
	iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
	oauth: true,
    usePreferencesForAuthorization: false,
    singleInstance: true
) {
	appSetting "clientId"
	appSetting "clientSecret"
}

preferences {
    page(name: "Credentials", title: "Jawbone UP", content: "authPage", install: false)
}

mappings {
	path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
	path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
	path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
  path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
	path("/oauth/callback") { action: [ GET: "callback" ] }
}

def getServerUrl() { return "https://graph.api.smartthings.com" }
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
def buildRedirectUrl(page) { return buildActionUrl(page) }

def callback() {
	def redirectUrl = null
	if (params.authQueryString) {
		redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
		log.debug "redirectUrl: ${redirectUrl}"
	} else {
		log.warn "No authQueryString"
	}

	if (state.JawboneAccessToken) {
		log.debug "Access token already exists"
		setup()
		success()
	} else {
		def code = params.code
		if (code) {
			if (code.size() > 6) {
				// Jawbone code
				log.debug "Exchanging code for access token"
				receiveToken(redirectUrl)
			} else {
				// SmartThings code, which we ignore, as we don't need to exchange for an access token.
				// Instead, go initiate the Jawbone OAuth flow.
				log.debug "Executing callback redirect to auth page"
			    state.oauthInitState = UUID.randomUUID().toString()
			    def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
				redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
			}
		} else {
			log.debug "This code should be unreachable"
			success()
		}
	}
}

def authPage() {
    log.debug "authPage"
    def description = null
    if (state.JawboneAccessToken == null) {
		if (!state.accessToken) {
			log.debug "About to create access token"
			createAccessToken()
		}
        description = "Click to enter Jawbone Credentials"
        def redirectUrl = buildRedirectUrl
        log.debug "RedirectURL = ${redirectUrl}"
        def donebutton= state.JawboneAccessToken != null
        return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
							 section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
               section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
        }
    } else {
        description = "Jawbone Credentials Already Entered."
        return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
               section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
        }
    }
}

def oauthInitUrl() {
    log.debug "oauthInitUrl"
    state.oauthInitState = UUID.randomUUID().toString()
    def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ]
	redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
}

def receiveToken(redirectUrl = null) {
	log.debug "receiveToken"
    def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ]
    def params = [
      uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
    ]
    httpGet(params) { response ->
    	log.debug "${response.data}"
		log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
    	state.JawboneAccessToken = response.data.access_token
		state.refreshToken = response.data.refresh_token
    }

	setup()
	if (state.JawboneAccessToken) {
		success()
	} else {
		def message = """
			<p>The connection could not be established!</p>
			<p>Click 'Done' to return to the menu.</p>
		"""
		connectionStatus(message)
	}
}

def success() {
	def message = """
		<p>Your Jawbone Account is now connected to SmartThings!</p>
		<p>Click 'Done' to finish setup.</p>
	"""
	connectionStatus(message)
}

def receivedToken() {
	def message = """
		<p>Your Jawbone Account is already connected to SmartThings!</p>
		<p>Click 'Done' to finish setup.</p>
	"""
	connectionStatus(message)
}

def connectionStatus(message, redirectUrl = null) {
	def redirectHtml = ""
	if (redirectUrl) {
		redirectHtml = """
			<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
		"""
	}

    def html = """
        <!DOCTYPE html>
        <html>
        <head>
        <meta name="viewport" content="width=640">
        <title>SmartThings Connection</title>
        <style type="text/css">
            @font-face {
                font-family: 'Swiss 721 W01 Thin';
                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
                font-weight: normal;
                font-style: normal;
            }
            @font-face {
                font-family: 'Swiss 721 W01 Light';
                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
                src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
                     url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
                font-weight: normal;
                font-style: normal;
            }
            .container {
                width: 560px;
                padding: 40px;
                /*background: #eee;*/
                text-align: center;
            }
            img {
                vertical-align: middle;
            }
            img:nth-child(2) {
                margin: 0 30px;
            }
            p {
                font-size: 2.2em;
                font-family: 'Swiss 721 W01 Thin';
                text-align: center;
                color: #666666;
                padding: 0 40px;
                margin-bottom: 0;
            }
        /*
            p:last-child {
                margin-top: 0px;
            }
        */
            span {
                font-family: 'Swiss 721 W01 Light';
            }
        </style>
		${redirectHtml}
        </head>
        <body>
            <div class="container">
                <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSMuoEIQ7gQhFtc02vXkybwmH0o7L1cs5mtbcJye0mgNqop_LOZbg" alt="Jawbone UP icon" />
                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
                <img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
                ${message}
            </div>
        </body>
        </html>
	"""
	render contentType: 'text/html', data: html
}

String toQueryString(Map m) {
	return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
}

def validateCurrentToken() {
	log.debug "validateCurrentToken"
    def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
    def requestBody = "secret=${appSettings.clientSecret}"

	try {
		httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],  body: requestBody) {response ->
	    	if (response.status == 200) {
				log.debug "${response.data}"
				log.debug "Setting refresh token"
	     		state.refreshToken = response.data.data.refresh_token
	        }
	    }
	} catch (groovyx.net.http.HttpResponseException e) {
        if (e.statusCode == 401) { // token is expired
        	log.debug "Access token is expired"
        	if (state.refreshToken) { // if we have this we are okay
    			def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
        		def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
        		def params = [
          			uri: tokenUrl
        		]
        		httpGet(params) { refreshResponse ->
					def data = refreshResponse.data
					log.debug "Status: ${refreshResponse.status}, data: ${data}"
					if (data.error) {
						if (data.error == "access_denied") {
							// User has removed authorization (probably)
							log.warn "Access denied, because: ${data.error_description}"
							state.remove("JawboneAccessToken")
							state.remove("refreshToken")
						}
					} else {
						log.debug "Setting access token"
						state.JawboneAccessToken = data.access_token
						state.refreshToken = data.refresh_token
					}
				}
            }
        }
	} catch (java.net.SocketTimeoutException e) {
		log.warn "Connection timed out, not much we can do here"
	}
}

def initialize() {
    log.debug "Callback URL - Webhook"
	def localServerUrl = getApiServerUrl()
	def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
    def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
	httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
}

def setup() {
	// make sure this is going to work
	validateCurrentToken()

	if (state.JawboneAccessToken) {
		def urlmember = "https://jawbone.com/nudge/api/users/@me/"
		def member = null
		httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
		    member = response.data.data
		}

		if (member) {
			state.member = member
			def externalId = "${app.id}.${member.xid}"

			// find the appropriate child device based on my app id and the device network id
			def deviceWrapper = getChildDevice("${externalId}")

			// invoke the generatePresenceEvent method on the child device
			log.debug "Device $externalId: $deviceWrapper"
			if (!deviceWrapper) {
			  	def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true])
			    if (childDevice) {
			       	log.debug "Child Device Successfully Created"
			        childDevice?.generateSleepingEvent(false)
                    pollChild(childDevice)
			    }
			}
		}

		initialize()
	}
}

def installed() {

	if (!state.accessToken) {
		log.debug "About to create access token"
		createAccessToken()
	}

	if (state.JawboneAccessToken) {
		setup()
	}
}

def updated() {

	if (!state.accessToken) {
		log.debug "About to create access token"
		createAccessToken()
	}

	if (state.JawboneAccessToken) {
		setup()
	}
}

def uninstalled() {
	if (state.JawboneAccessToken) {
		try {
			httpDelete(uri: "https://jawbone.com/nudge/api/v.1.0/users/@me/PartnerAppMembership", headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) { response ->
				log.debug "Success disconnecting Jawbone from SmartThings"
			}
		} catch (groovyx.net.http.HttpResponseException e) {
			log.error "Error disconnecting Jawbone from SmartThings: ${e.statusCode}"
		}
	}
}

def pollChild(childDevice) {
		def childMap = [ value: "$childDevice.device.deviceNetworkId}"]

		def params = [
				uri: 'https://jawbone.com',
				path: '/nudge/api/users/@me/goals',
				headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
				contentType: 'application/json'
		]

		asynchttp_v1.get('responseGoals', params, childMap)

		def params2 = [
				uri: 'https://jawbone.com',
				path: '/nudge/api/users/@me/moves',
				headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
				contentType: 'application/json'
		]

		asynchttp_v1.get('responseMoves', params2, childMap)
}

def responseGoals(response, dni) {
	if (response.hasError()) {
			log.error "response has error: $response.errorMessage"
	} else {
			def goals
			try {
					// json response already parsed into JSONElement object
					goals = response.json.data
			} catch (e) {
					log.error "error parsing json from response: $e"
			}
			if (goals) {
				def childDevice = getChildDevice(dni.value)
				log.debug "Goal = ${goals.move_steps} Steps"
				childDevice?.sendEvent(name:"goal", value: goals.move_steps)
			} else {
					log.debug "did not get json results from response body: $response.data"
			}
	}
}

def responseMoves(response, dni) {
	if (response.hasError()) {
			log.error "response has error: $response.errorMessage"
	} else {
			def moves
			try {
					// json response already parsed into JSONElement object
					moves = response.json.data.items[0]
			} catch (e) {
					log.error "error parsing json from response: $e"
			}
			if (moves) {
				def childDevice = getChildDevice(dni.value)
				log.debug "Moves = ${moves.details.steps} Steps"
				childDevice?.sendEvent(name:"steps", value: moves.details.steps)
			} else {
					log.debug "did not get json results from response body: $response.data"
			}
	}
}

def setColor (steps,goal,childDevice) {
    def result = steps * 100 / goal
    if (result < 25)
    	childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
    else if ((result >= 25) && (result < 50))
        childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
    else if ((result >= 50) && (result < 75))
        childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
    else if (result >= 75)
        childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
}

def hookEventHandler() {
    // log.debug "In hookEventHandler method."
    log.debug "request = ${request}"

    def json = request.JSON

    // get some stuff we need
    def userId = json.events.user_xid[0]
    def	json_type = json.events.type[0]
	  def json_action = json.events.action[0]

    //log.debug json
    log.debug "Userid = ${userId}"
    log.debug "Notification Type: " + json_type
    log.debug "Notification Action: " + json_action

    // find the appropriate child device based on my app id and the device network id
    def externalId = "${app.id}.${userId}"
    def childDevice = getChildDevice("${externalId}")

    if (childDevice) {
    	switch (json_action) {
	        case "enter_sleep_mode":
            	childDevice?.generateSleepingEvent(true)
                break
            case "exit_sleep_mode":
            	childDevice?.generateSleepingEvent(false)
                break
            case "creation":
                childDevice?.sendEvent(name:"steps", value: 0)
          		break
            case "updation":
                def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
                def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
                def goals = null
                def moves = null
                httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
 	               goals = response.data.data
                }
                httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
                   moves = response.data.data.items[0]
                }
                log.debug "Goal = ${goals.move_steps} Steps"
        				log.debug "Steps = ${moves.details.steps} Steps"
                childDevice?.sendEvent(name:"steps", value: moves.details.steps)
                childDevice?.sendEvent(name:"goal", value: goals.move_steps)
                //setColor(moves.details.steps,goals.move_steps,childDevice)
                break
			case "deletion":
				app.delete()
				break
		}
    }
    else {
            log.debug "Couldn't find child device associated with Jawbone."
    }

	def html = """{"code":200,"message":"OK"}"""
	render contentType: 'application/json', data: html
}
