/**
 * Author: Ben West
 * https://github.com/bewest
 * Advisor: Scott Hanselman
 * http://www.hanselman.com/blog/BridgingDexcomShareCGMReceiversAndNightscout.aspx
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 *
 * @description: Allows user to store their Dexcom data in their own
 * Nightscout server by facilitating the transfer of latest records
 * from Dexcom's server into theirs.
 */
var request = require('request');
var qs = require('querystring');
var crypto = require('crypto');
var meta = require('./package.json');


// Defaults
var server = "share2.dexcom.com";
var bridge = readENV('BRIDGE_SERVER')
    if (bridge && bridge.indexOf(".") > 1) {
    server = bridge;
   } 
    else if (bridge && bridge === 'EU') {
        server = "shareous1.dexcom.com";
    } 


var Defaults = {
  "applicationId":"d89443d2-327c-4a6f-89e5-496bbb0317db"
, "agent": [meta.name, meta.version].join('/')
, login: 'https://' + server + '/ShareWebServices/Services/General/LoginPublisherAccountByName'
, accept: 'application/json'
, 'content-type': 'application/json'
, LatestGlucose: 'https://' + server + '/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues'
// ?sessionID=e59c836f-5aeb-4b95-afa2-39cf2769fede&minutes=1440&maxCount=1"
, nightscout_upload: '/api/v1/entries.json'
, nightscout_battery: '/api/v1/devicestatus.json'
, MIN_PASSPHRASE_LENGTH: 12
};

var DIRECTIONS = {
  NONE: 0
, DoubleUp: 1
, SingleUp: 2
, FortyFiveUp: 3
, Flat: 4
, FortyFiveDown: 5
, SingleDown: 6
, DoubleDown: 7
, 'NOT COMPUTABLE': 8
, 'RATE OUT OF RANGE': 9
};
var Trends = (function ( ) {
  var keys = Object.keys(DIRECTIONS);
  var trends = keys.sort(function (a, b) {
    return DIRECTIONS[a] - DIRECTIONS[b];
  });
  return trends;
})( );
function directionToTrend (direction) {
  var trend = 8;
  if (direction in DIRECTIONS) {
    trend = DIRECTIONS[direction];
  }
  return trend;
}
function trendToDirection (trend) {
  return Trends[trend] || Trends[0];
}

// assemble the POST body for the login endpoint
function login_payload (opts) {
  var body = {
    "password": opts.password
  , "applicationId" : opts.applicationId || Defaults.applicationId
  , "accountName": opts.accountName
  };
  return body;
}

// Login to Dexcom's server.
function authorize (opts, then) {
  var url = opts.login || Defaults.login;
  var body = login_payload(opts);
  var headers = { 'User-Agent': opts.agent || Defaults.agent
                , 'Content-Type': Defaults['content-type']
                , 'Accept': Defaults.accept };
  var req ={ uri: url, body: body, json: true, headers: headers, method: 'POST'
           , rejectUnauthorized: false };
  // Asynchronously calls the `then` function when the request's I/O
  // is done.
  return request(req, then);
}

// Assemble query string for fetching data.
function fetch_query (opts) {
  // ?sessionID=e59c836f-5aeb-4b95-afa2-39cf2769fede&minutes=1440&maxCount=1"
  var q = {
    sessionID: opts.sessionID
  , minutes: opts.minutes || 1440
  , maxCount: opts.maxCount || 1
  };
  var url = (opts.LatestGlucose || Defaults.LatestGlucose) + '?' + qs.stringify(q);
  return url;
}

// Asynchronously fetch data from Dexcom's server.
// Will fetch `minutes` and `maxCount` records.
function fetch (opts, then) {
  var url = fetch_query(opts);
  var body = "";
  var headers = { 'User-Agent': Defaults.agent
                , 'Content-Type': Defaults['content-type']
                , 'Content-Length': 0
                , 'Accept': Defaults.accept };

  var req ={ uri: url, body: body, json: true, headers: headers, method: 'POST'
           , rejectUnauthorized: false };
  return request(req, then);
}

// Authenticate and fetch data from Dexcom.
function do_everything (opts, then) {
  var login_opts = opts.login;
  var fetch_opts = opts.fetch;
  authorize(login_opts, function (err, res, body) {

    fetch_opts.sessionID = body;
    fetch(fetch_opts, function (err, res, glucose) {
      then(err, glucose);

    });
  });

}

// Map Dexcom's property values to Nightscout's.
function dex_to_entry (d) {
/*
[ { DT: '/Date(1426292016000-0700)/',
    ST: '/Date(1426295616000)/',
    Trend: 4,
    Value: 101,
    WT: '/Date(1426292039000)/' } ]
*/
  var regex = /\((.*)\)/;
  var wall = parseInt(d.WT.match(regex)[1]);
  var date = new Date(wall);
  var entry = {
    sgv: d.Value
  , date: wall
  , dateString: date.toISOString( )
  , trend: d.Trend
  , direction: trendToDirection(d.Trend)
  , device: 'share2'
  , type: 'sgv'
  };
  return entry;
}

// Record data into Nightscout.
function report_to_nightscout (opts, then) {
  var shasum = crypto.createHash('sha1');
  var hash = shasum.update(opts.API_SECRET);
  var headers = { 'api-secret': shasum.digest('hex')
                , 'Content-Type': Defaults['content-type']
                , 'Accept': Defaults.accept };
  var url = opts.endpoint + Defaults.nightscout_upload;
  var req = { uri: url, body: opts.entries, json: true, headers: headers, method: 'POST'
            , rejectUnauthorized: false };
  return request(req, then);

}

function nullify_battery_status (opts, then) {
  var shasum = crypto.createHash('sha1');
  var hash = shasum.update(opts.API_SECRET);
  var headers = { 'api-secret': shasum.digest('hex')
                , 'Content-Type': Defaults['content-type']
                , 'Accept': Defaults.accept };
  var url = opts.endpoint + Defaults.nightscout_battery;
  var body = { uploaderBattery: false };
  var req = { uri: url, body: body, json: true, headers: headers, method: 'POST'
            , rejectUnauthorized: false };
  return request(req, then);
}

function engine (opts) {

  var runs = 0;
  var failures = 0;
  function my ( ) {
    console.log('RUNNING', runs, 'failures', failures);
    if (my.sessionID) {
      var fetch_opts = Object.create(opts.fetch);
      if (runs === 0) {
        console.log('First run, fetching', opts.firstFetchCount);
        fetch_opts.maxCount = opts.firstFetchCount;
      }
      fetch_opts.sessionID = my.sessionID;
      fetch(fetch_opts, function (err, res, glucose) {
        if (res && res.statusCode < 400) {
          to_nightscout(glucose);
        } else {
          my.sessionID = null;
          refresh_token( );
        }
      });
    } else {
      failures++;
      refresh_token( );
    }
  }

  function refresh_token ( ) {
    console.log('Fetching new token');
    authorize(opts.login, function (err, res, body) {
      if (!err && body && res && res.statusCode == 200) {
        my.sessionID = body;
        failures = 0;
        my( );
      } else {
        failures++;
        var responseStatus = res ? res.statusCode : "response not found";
        console.log("Error refreshing token", err, responseStatus, body);
        if (failures >= opts.maxFailures) {
          throw "Too many login failures, check DEXCOM_ACCOUNT_NAME and DEXCOM_PASSWORD";
        }
      }
    });
  }

  function to_nightscout (glucose) {
    var ns_config = Object.create(opts.nightscout);
    if (glucose) {
      runs++;
      // Translate to Nightscout data.
      var entries = glucose.map(dex_to_entry);
      console.log('Entries', entries);
      if (opts && opts.callback && opts.callback.call) {
        opts.callback(null, entries);
      }
      if (ns_config.endpoint) {
        if (runs === 0) {
          nullify_battery_status(ns_config, function (err, resp) {
            if (err) {
              console.warn('Problem reporting battery', arguments);
            } else {
              console.log('Battery status hidden');
            }
          });
        }
        ns_config.entries = entries;
        // Send data to Nightscout.
        report_to_nightscout(ns_config, function (err, response, body) {
          console.log("Nightscout upload", 'error', err, 'status', response.statusCode, body);

        });
      }
    }
  }

  my( );
  return my;
}

// Provide public, testable API
engine.fetch = fetch;
engine.authorize = authorize;
engine.authorize_fetch = do_everything;
engine.Defaults = Defaults;
module.exports = engine;

function readENV(varName, defaultValue) {
    //for some reason Azure uses this prefix, maybe there is a good reason
    var value = process.env['CUSTOMCONNSTR_' + varName]
        || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()]
        || process.env[varName]
        || process.env[varName.toLowerCase()];

    return value || defaultValue;
}

// If run from commandline, run the whole program.
if (!module.parent) {
  if (readENV('API_SECRET').length < Defaults.MIN_PASSPHRASE_LENGTH) {
    var msg = [ "API_SECRET environment variable should be at least"
              , Defaults.MIN_PASSPHRASE_LENGTH, "characters" ];
    var err = new Error(msg.join(' '));
    throw err;
    process.exit(1);
  }
  if (readENV('DEXCOM_ACCOUNT_NAME', '@').match(/\@/)) {
    var msg = [ "environment variable"
              , "DEXCOM_ACCOUNT_NAME should be"
              , "Dexcom Share user name, not an email address"];
    var err = new Error(msg.join(' '));
    throw err;
    process.exit(1);
  }
  var args = process.argv.slice(2);
  var config = {
    accountName: readENV('DEXCOM_ACCOUNT_NAME')
  , password: readENV('DEXCOM_PASSWORD')
  };
  var ns_config = {
    API_SECRET: readENV('API_SECRET')
  , endpoint: readENV('NS', 'https://' + readENV('WEBSITE_HOSTNAME'))
  };
  var interval = readENV('SHARE_INTERVAL', 60000 * 2.5);
  interval = Math.max(60000, interval);
  var fetch_config = { maxCount: readENV('maxCount', 1)
    , minutes: readENV('minutes', 1440)
  };
  var meta = {
    login: config
  , fetch: fetch_config
  , nightscout: ns_config
  , maxFailures: readENV('maxFailures', 3)
  , firstFetchCount: readENV('firstFetchCount', 3)
  };
  switch (args[0]) {
    case 'login':
      authorize(config, console.log.bind(console, 'login'));
      break;
    case 'fetch':
      config = { sessionID: args[1] };
      fetch(config, console.log.bind(console, 'fetched'));
      break;
    case 'testdaemon':
      setInterval(engine(meta), 2500);
      break;
    case 'run':
      // Authorize and fetch from Dexcom.
      do_everything(meta, function (err, glucose) {
        console.log('From Dexcom', err, glucose);
        if (glucose) {
          // Translate to Nightscout data.
          var entries = glucose.map(dex_to_entry);
          console.log('Entries', entries);
          if (ns_config.endpoint) {
            ns_config.entries = entries;
            // Send data to Nightscout.
            report_to_nightscout(ns_config, function (err, response, body) {
              console.log("Nightscout upload", 'error', err, 'status', response.statusCode, body);

            });
          }
        }
      });
      break;
    default:
      setInterval(engine(meta), interval);
      break;
      break;
  }
}