// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: car;
const widgetHeadline = "Smart #1"

let hashes
try {
  hashes = importModule('modules/hashes')
  new hashes.MD5()
  new hashes.SHA1()
} catch (exception) {
  await showError('Hashes file unavailable or unreadable!')
  return
}

let userName
let password
let vin
const param = args.widgetParameter
if (param != null && param.length > 0) {
  const paramArray = param.split(';')
  if (paramArray.length >= 3) {
    userName = paramArray[0].trim()
    password = paramArray[1].trim()
    vin = paramArray[2].trim()
  } else {
    console.log('Error reading user credentials.')
  }
} else {
  // insert credentials for testing inside the app
  // never share this code with your sensible data!
  userName = '***'
  password = '***'
  vin = '***'
}

const deviceId = randomHexString(16)
let apiTokenRefreshed = false
let credentials = {}

let cachedCredentials = await loadCachedCredentials()
const isEmpty = Object.entries(cachedCredentials).length === 0
if (isEmpty) {
  console.log('Cached credentials file is empty. Creating...')
  cachedCredentials = await initialLogin()
  credentials = cachedCredentials
} else {
  credentials = cachedCredentials
}
if (credentials.hasOwnProperty('apiAccessToken')) {
  console.log('Found apiAccessToken.')
} else {
  console.log('apiAccessToken unavailable. Refreshing...')
  await refreshApiAccessToken()
}

let updateSession = await updateSessionForCar(credentials.apiAccessToken, vin)
if (updateSession.code == 1402) {
  if (apiTokenRefreshed) {
    console.log('Api Token expired and could not be refreshed!')
  } else {
    console.log('Api token expired and neees to be refreshed!')
    let refreshedApiAccessToken = await refreshApiAccessToken()
    if (!refreshedApiAccessToken) {
      cachedCredentials = await initialLogin()
      credentials = cachedCredentials
      refreshedApiAccessToken = await refreshApiAccessToken()
    }
    apiTokenRefreshed = true
    updateSession = await updateSessionForCar(credentials.apiAccessToken, vin)
  }
}

let carData = await getCarInfo(credentials.apiAccessToken)
//console.log(carData)
const batteryState = carData.data.vehicleStatus.additionalVehicleStatus.electricVehicleStatus.chargeLevel
const temperatureInt = carData.data.vehicleStatus.additionalVehicleStatus.climateStatus.interiorTemp
const temperatureExt = carData.data.vehicleStatus.additionalVehicleStatus.climateStatus.exteriorTemp
const temperature = Number.parseFloat(temperatureInt).toFixed(1) + ' °C | ' + Number.parseFloat(temperatureExt).toFixed(1) + ' °C'
const remainingKilometer = carData.data.vehicleStatus.additionalVehicleStatus.electricVehicleStatus.distanceToEmptyOnBatteryOnly
console.log('Battery State: ' + batteryState)
console.log('TemperatureInt: ' + temperatureInt)
console.log('TemperatureExt: ' + temperatureExt)
console.log('RemainingKilometer: ' + remainingKilometer)

let widget = new ListWidget()
await getData()
widget.presentAccessoryRectangular()
Script.setWidget(widget)
Script.complete()

// loads a random track from recent shazams
async function getData() {
  
  widget.addSpacer(2)
  widget.url = "hellosmart://"

  let logoStack = widget.addStack()
  logoStack.layoutHorizontally()
  let smartLogo = await getImage("smart-lockscreen.png")
  logoStack.addSpacer(3)
  let image = logoStack.addImage(smartLogo)
  image.imageSize = new Size(16,16)
  logoStack.addSpacer(6)
  let headerStack = logoStack.addStack()
  headerStack.layoutVertically()
  headerStack.addSpacer(1)
  let headerText = headerStack.addText(widgetHeadline)
  headerText.font = Font.mediumRoundedSystemFont(12)

  widget.addSpacer(5)

  let temperatureStack = widget.addStack()
  temperatureStack.layoutHorizontally()
  let tempLogo = await getImage("temperature-icon.png")
  temperatureStack.addSpacer(2)
  let tempImage = temperatureStack.addImage(tempLogo)
  tempImage.imageSize = new Size(16, 16)
  temperatureStack.addSpacer(7)
  let tempTextStack = temperatureStack.addStack()
  tempTextStack.layoutVertically()
  tempTextStack.addSpacer(1)
  let tempTxt = tempTextStack.addText(temperature)
  tempTxt.font = Font.lightRoundedSystemFont(13)

  widget.addSpacer(5)

  let reachStack = widget.addStack()
  reachStack.layoutHorizontally()
  let reachLogo = await getImage('reachability.png')
  let reachImage = reachStack.addImage(reachLogo)
  reachImage.imageSize = new Size(18,18)
  reachStack.addSpacer(7)
  let reachTextStack = reachStack.addStack()
  reachTextStack.layoutVertically()
  reachTextStack.addSpacer(1)
  let reachTxt = reachTextStack.addText(remainingKilometer + ' km')
  reachTxt.font = Font.lightRoundedSystemFont(13)
  reachStack.addSpacer(10)
  let batteryLogo = await getImage('battery.png')
  let batteryImage = reachStack.addImage(batteryLogo)
  batteryImage.imageSize = new Size(16,16)
  reachStack.addSpacer(7)
  let batteryTextStack = reachStack.addStack()
  batteryTextStack.layoutVertically()
  batteryTextStack.addSpacer(1)
  let batteryTxt = batteryTextStack.addText(batteryState + ' %')
  batteryTxt.font = Font.lightRoundedSystemFont(13)

}

// random number, min and max included 
function getRandomNumber(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min)
}

// download an image from a given url
async function loadImage(imgUrl) {
  let req = new Request(imgUrl)
  req.allowInsecureRequest = true
  let image = await req.loadImage()
  return image
}

// get image from local filestore or download it only once
async function getImage(image) {
  let fm = FileManager.local()
  let dir = fm.documentsDirectory()
  let path = fm.joinPath(dir, image)
  if (fm.fileExists(path)) {
    return fm.readImage(path)
  } else {
    // download once
    let imageUrl
    switch (image) {
      case 'smart-lockscreen.png':
        imageUrl = "https://i.imgur.com/1tJMoG1.png";
        break;
      case 'temperature-icon.png':
        imageUrl = "https://i.imgur.com/U8jq3QB.png";
        break;
      case 'reachability.png':
        imageUrl = "https://i.imgur.com/WCx3VWK.png";
        break;
      case 'battery.png':
        imageUrl = "https://i.imgur.com/xz6v8os.png";
        break;
      default:
        console.log(`Sorry, couldn't find ${image}.`);
    }
    let iconImage = await loadImage(imageUrl)
    fm.writeImage(path, iconImage)
    return iconImage
  }
}

// returns a pseudo random value as hex
function randomHexString (len) {
  charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  var randomString = ''
  for (var i = 0; i < len; i++) {
    var randomPoz = Math.floor(Math.random() * charSet.length)
    randomString += charSet.substring(randomPoz, randomPoz + 1)
  }

  var result = ''
  for (var i = 0; i < randomString.length; i++) {
    result += randomString.charCodeAt(i).toString(16)
  }
  return result
}

// returns url query params as json
function getUrlParams (url) {
  var regex = /[?&]([^=#]+)=([^&#]*)/g,
    params = {},
    match
  while ((match = regex.exec(url))) {
    params[match[1]] = match[2]
  }
  return params
}

// load credentials from iCloud Drive
async function loadCachedCredentials () {
  // load existing credentials from iCloud Drive
  const fm = FileManager.iCloud()
  const dir = fm.documentsDirectory()
  const path = fm.joinPath(dir, 'smart-credentials.json')
  const credentials = Data.fromFile(path)
  if (credentials != null) {
    return JSON.parse(credentials.toRawString())
  } else {
    return new Object()
  }
}

// save smart api credentials to iCloud Drive
async function saveCredentials (credentials) {
  const fm = FileManager.iCloud()
  const dir = fm.documentsDirectory()
  const path = fm.joinPath(dir, 'smart-credentials.json')
  fm.writeString(path, JSON.stringify(credentials))
}

// initial login to get credentials for the first time or after login expiration
async function initialLogin () {
  console.log('Starting complete login process from scratch.')
  const url =
    'https://awsapi.future.smart.com/login-app/api/v1/authorize?uiLocales=de-DE&uiLocales=de-DE'
  let req = new Request(url)
  req.headers = {
    'x-app-id': 'SmartAPPEU',
    accept: 'application/json;responseformat=3',
    'x-requested-with': 'com.smart.hellosmart',
    'upgrade-insecure-requests': '1',
    'user-agent':
      'Mozilla/5.0 (Linux; Android 9; ANE-LX1 Build/HUAWEIANE-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/118.0.0.0 Mobile Safari/537.36',
    'content-type': 'application/json; charset=utf-8',
    'sec-fetch-site': 'none',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-user': '?1',
    'sec-fetch-dest': 'document',
    'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-US;q=0.7,en;q=0.6'
  }
  let contextResult = await req.load()
  const urlParams = getUrlParams(req.response.url)
  const context = urlParams.context

  const loginUrl = 'https://auth.smart.com/accounts.login'
  req = new Request(loginUrl)
  req.method = 'POST'
  req.headers = {
    accept: '*/*',
    'accept-language': 'de',
    'content-type': 'application/x-www-form-urlencoded',
    'x-requested-with': 'com.smart.hellosmart',
    cookie:
      'gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4',
    origin: 'https://app.id.smart.com',
    'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)'
  }
  req.body =
    'loginID=' +
    encodeURIComponent(userName) +
    '&password=' +
    encodeURIComponent(password) +
    '&sessionExpiration=2592000&targetEnv=jssdk&include=profile%2Cdata%2Cemails%2Csubscriptions%2Cpreferences%2C&includeUserInfo=true&loginMode=standard&lang=de&APIKey=3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a&source=showScreenSet&sdk=js_latest&authMode=cookie&pageURL=https%3A%2F%2Fapp.id.smart.com%2Flogin%3Fgig_ui_locales%3Dde-DE&sdkBuild=15482&format=json&riskContext=%7B%22b0%22%3A41187%2C%22b1%22%3A%5B0%2C2%2C3%2C1%5D%2C%22b2%22%3A4%2C%22b3%22%3A%5B%22-23%7C0.383%22%2C%22-81.33333587646484%7C0.236%22%5D%2C%22b4%22%3A3%2C%22b5%22%3A1%2C%22b6%22%3A%22Mozilla%2F5.0%20%28Linux%3B%20Android%209%3B%20ANE-LX1%20Build%2FHUAWEIANE-L21%3B%20wv%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Version%2F4.0%20Chrome%2F118.0.0.0%20Mobile%20Safari%2F537.36%22%2C%22b7%22%3A%5B%5D%2C%22b8%22%3A%2216%3A33%3A26%22%2C%22b9%22%3A-60%2C%22b10%22%3Anull%2C%22b11%22%3Afalse%2C%22b12%22%3A%7B%22charging%22%3Atrue%2C%22chargingTime%22%3Anull%2C%22dischargingTime%22%3Anull%2C%22level%22%3A0.58%7D%2C%22b13%22%3A%5B5%2C%22360%7C760%7C24%22%2Cfalse%2Ctrue%5D%7D'
  let loginResult = await req.loadJSON()
  console.log('Login token result http status code: ' + req.response.statusCode)
  const loginToken = loginResult.sessionInfo.login_token

  const authUrl =
    'https://auth.smart.com/oidc/op/v1.0/3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a/authorize/continue?context=' +
    context +
    '&login_token=' +
    loginToken
  const cookieValue =
    'gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4; glt_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=' +
    loginToken
  req = new Request(authUrl)
  req.headers = {
    accept: '*/*',
    cookie: cookieValue,
    'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-US;q=0.7,en;q=0.6',
    'x-requested-with': 'com.smart.hellosmart',
    'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)'
  }
  let authResult = await req.load()
  req = new Request(req.response.url)
  let finalAuthResult = await req.load()
  const tokens = getUrlParams(req.response.url)
  // console.log('Login finished. Saving credentials to iCloud: ' + JSON.stringify(tokens))
  await saveCredentials(tokens)
  return tokens
}

// get credentials for configured car/vin
async function updateSessionForCar (access_token, vin) {
  const timestamp = Date.now().toString()
  const nonce = randomHexString(16)
  const params = {}
  let url = '/device-platform/user/session/update'
  const payload = {
    vin: vin,
    sessionToken: access_token,
    language: ''
  }
  const sign = createSignature(nonce, params, timestamp, 'POST', url, payload)
  url = 'https://api.ecloudeu.com' + url
  let req = new Request(url)
  req.method = 'POST'
  req.headers = {
    'x-app-id': 'SmartAPPEU',
    accept: 'application/json;responseformat=3',
    'x-agent-type': 'iOS',
    'x-device-type': 'mobile',
    'x-operator-code': 'SMART',
    'x-device-identifier': deviceId,
    'x-env-type': 'production',
    'x-version': 'smartNew',
    'accept-language': 'en_US',
    'x-api-signature-version': '1.0',
    'x-api-signature-nonce': nonce,
    'x-device-manufacture': 'Apple',
    'x-device-brand': 'Apple',
    'x-device-model': 'iPhone',
    'x-agent-version': '17.1',
    authorization: access_token,
    'content-type': 'application/json; charset=utf-8',
    'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)',
    'x-signature': sign,
    'x-timestamp': timestamp
  }
  req.body = JSON.stringify(payload)
  const carSessionResult = await req.loadJSON()
  const statusCode = req.response.statusCode
  console.log('CarSession http status code: ' + statusCode)
  console.log('CarSession api status code: ' + carSessionResult.code)
  //console.log(carSessionResult);
  return carSessionResult
}

// refreshes the api access token (valid for a few hours)
async function refreshApiAccessToken () {
  const timestamp = Date.now().toString()
  const nonce = randomHexString(16)
  const params = { identity_type: 'smart' }
  let url = '/auth/account/session/secure'
  let data = { accessToken: credentials.access_token }

  const sign = createSignature(nonce, params, timestamp, 'POST', url, data)
  url =
    'https://api.ecloudeu.com/auth/account/session/secure?identity_type=smart'
  let req = new Request(url)
  req.method = 'POST'
  req.headers = {
    'x-app-id': 'SmartAPPEU',
    accept: 'application/json;responseformat=3',
    'x-agent-type': 'iOS',
    'x-device-type': 'mobile',
    'x-operator-code': 'SMART',
    'x-device-identifier': deviceId,
    'x-env-type': 'production',
    'x-version': 'smartNew',
    'accept-language': 'en_US',
    'x-api-signature-version': '1.0',
    'x-api-signature-nonce': nonce,
    'x-device-manufacture': 'Apple',
    'x-device-brand': 'Apple',
    'x-device-model': 'iPhone',
    'x-agent-version': '17.1',
    'Content-Type': 'application/json',
    'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)',
    'x-signature': sign,
    'x-timestamp': timestamp
  }
  req.body = JSON.stringify({ accessToken: credentials.access_token })
  let result = await req.loadJSON()
  if (result.code == 1501) {
    console.log(
      'Both access and login token expired. Logging in from the beginning.'
    )
    return null
  }
  console.log('currentToken http status code: ' + req.response.statusCode)
  console.log('currentToken api status code: ' + result.code)
  credentials.apiAccessToken = result.data.accessToken
  credentials.userId = result.data.userId
  console.log('Saving new credentials with updated api access token.')
  await saveCredentials(credentials)
  return result.data.accessToken
}

async function getCarInfo (access_token) {
  const timestamp = Date.now().toString()
  const nonce = randomHexString(16)
  let url = '/remote-control/vehicle/status/' + vin
  const params = {
    latest: true,
    target: 'basic%2Cmore',
    userId: credentials.userId
  }
  const sign = createSignature(nonce, params, timestamp, 'GET', url)
  url =
    'https://api.ecloudeu.com' +
    url +
    '?latest=true&target=basic%2Cmore&userId=' +
    credentials.userId
  let req = new Request(url)
  req.method = 'GET'
  req.headers = {
    'x-app-id': 'SmartAPPEU',
    accept: 'application/json;responseformat=3',
    'x-agent-type': 'iOS',
    'x-device-type': 'mobile',
    'x-operator-code': 'SMART',
    'x-device-identifier': deviceId,
    'x-env-type': 'production',
    'x-version': 'smartNew',
    'accept-language': 'en_US',
    'x-api-signature-version': '1.0',
    'x-api-signature-nonce': nonce,
    'x-device-manufacture': 'Apple',
    'x-device-brand': 'Apple',
    'x-device-model': 'iPhone',
    'x-agent-version': '17.1',
    authorization: access_token,
    'content-type': 'application/json; charset=utf-8',
    'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)',
    'x-signature': sign,
    'x-timestamp': timestamp
  }

  let carData = await req.loadJSON()
  const statusCode = req.response.statusCode
  console.log('carInfo http status code: ' + statusCode)
  console.log('carInfo api status code: ' + carData.code)
  // console.log(carData);
  return carData
}

// sign http requests for smart api
function createSignature (nonce, urlParams, timestamp, method, url, body) {
  var MD5 = new hashes.MD5()
  const md5Hash = body
    ? MD5.b64(JSON.stringify(body))
    : '1B2M2Y8AsgTpgAmY7PhCfg=='
  let urlParameters = Object.entries(urlParams)
    .map(e => e.join('='))
    .join('&')
  const payload = `application/json;responseformat=3
x-api-signature-nonce:${nonce}
x-api-signature-version:1.0

${urlParameters}
${md5Hash}
${timestamp}
${method}
${url}`
  const secret = atob('NzRlNzQ2OWFmZjUwNDJiYmJlZDdiYmIxYjM2YzE1ZTk=')
  const result = new hashes.SHA1().b64_hmac(secret, payload)
  return result
}


//
//  make sure to copy this script until the end!
//