'use strict';

var _ = require('lodash');
var async = require('async');
var moment = require('moment');
var find_options = require('./query');

function storage (env, ctx) {
  var ObjectID = require('mongodb-legacy').ObjectId;
  var OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;

  function create (objOrArray, fn) {

    function done (err, result) {
      ctx.bus.emit('data-received');
      fn(err, result);
    }

    if (_.isArray(objOrArray)) {
      if (objOrArray.length === 0) {
        return done(null, []);
      }

      // Check if any docs have preBolus (need special handling with upsert)
      // Don't call prepareData yet - that happens in upsert or before bulkWrite
      var hasPreBolus = objOrArray.some(function(obj) {
        // preBolus may be a string from API, so check truthiness and non-zero
        var preBolus = Number(obj.preBolus);
        return preBolus && preBolus !== 0;
      });

      // If any preBolus docs exist, fall back to sequential processing
      // because preBolus creates additional treatment records
      if (hasPreBolus) {
        var allDocs = [];
        var errs = [];
        async.eachSeries(objOrArray, function (obj, callback) {
          upsert(obj, function upserted (err, docs) {
            allDocs = allDocs.concat(docs);
            errs.push(err);
            callback(err, docs);
          });
        }, function () {
          errs = _.compact(errs);
          done(errs.length > 0 ? errs : null, allDocs);
        });
        return;
      }

      // Build bulkWrite operations for regular docs (no preBolus)
      // Prepare data and build bulk ops together
      var bulkOps = objOrArray.map(function(obj) {
        normalizeTreatmentId(obj);
        var results = prepareData(obj);
        return {
          replaceOne: {
            filter: upsertQueryFor(obj, results),
            replacement: obj,
            upsert: true
          }
        };
      });

      api().bulkWrite(bulkOps, { ordered: true }, function(err, bulkResult) {
        if (err) {
          console.error('Problem upserting treatments batch', err);
          return done(err, []);
        }

        // Assign _ids from upserted results
        if (bulkResult && bulkResult.upsertedIds) {
          Object.keys(bulkResult.upsertedIds).forEach(function(index) {
            objOrArray[index]._id = bulkResult.upsertedIds[index];
          });
        }
        
        // REQ-SYNC-072: For docs that were updated (not inserted) via identifier,
        // fetch their _id from the database (only identifier field, not others)
        var docsNeedingId = objOrArray.filter(function(obj) {
          return !obj._id && obj.identifier;
        });

        if (docsNeedingId.length > 0) {
          var identifiers = docsNeedingId.map(function(obj) { return obj.identifier; });
          
          api().find({ identifier: { $in: identifiers } }).toArray(function(findErr, existing) {
            if (!findErr && existing) {
              var idMap = {};
              existing.forEach(function(doc) {
                if (doc.identifier) idMap[doc.identifier] = doc._id;
              });
              docsNeedingId.forEach(function(obj) {
                if (obj.identifier && idMap[obj.identifier]) {
                  obj._id = idMap[obj.identifier];
                }
              });
            }

            ctx.bus.emit('data-update', {
              type: 'treatments',
              op: 'update',
              changes: ctx.ddata.processRawDataForRuntime(objOrArray)
            });

            done(null, objOrArray);
          });
          return;
        }

        ctx.bus.emit('data-update', {
          type: 'treatments',
          op: 'update',
          changes: ctx.ddata.processRawDataForRuntime(objOrArray)
        });

        done(null, objOrArray);
      });
    } else {
      upsert(objOrArray, function upserted (err, docs) {
        done(err, docs);
      });
    }


  }

  function upsert (obj, fn) {
    normalizeTreatmentId(obj);

    var results = prepareData(obj);
    var query = upsertQueryFor(obj, results);

    api( ).replaceOne(query, obj, {upsert: true}, function complete (err, updateResults) {

      if (err) console.error('Problem upserting treatment', err);

      if (updateResults) {
        if (updateResults.upsertedCount == 1) {
          obj._id = updateResults.upsertedId;
        } else if (updateResults.matchedCount >= 1 && obj.identifier && !obj._id) {
          // REQ-SYNC-072: On update by identifier, fetch the existing _id
          api().findOne(query, function(findErr, existing) {
            if (!findErr && existing) {
              obj._id = existing._id;
            }
            finishUpsert(err, obj, results);
          });
          return;
        }
      }

      finishUpsert(err, obj, results);
    });

    function finishUpsert(err, obj, results) {
      // TODO document this feature
      if (!err && obj.preBolus) {
        //create a new object to insert copying only the needed fields
        var pbTreat = {
          created_at: (new Date(new Date(results.created_at).getTime() + (obj.preBolus * 60000))).toISOString(),
          eventType: obj.eventType,
          carbs: results.preBolusCarbs
        };

        if (obj.notes) {
          pbTreat.notes = obj.notes;
        }

        var pbQuery = {
          created_at: pbTreat.created_at,
          eventType: pbTreat.eventType
        };
        api( ).replaceOne(pbQuery, pbTreat, {upsert: true}, function pbComplete (err, updateResults) {

          if (updateResults) {
            if (updateResults.upsertedCount == 1) {
              pbTreat._id = updateResults.upsertedId
            }
          }

          var treatments = _.compact([obj, pbTreat]);

          ctx.bus.emit('data-update', {
            type: 'treatments',
            op: 'update',
            changes: ctx.ddata.processRawDataForRuntime(treatments)
          });

          fn(err, treatments);
        });
      } else {

        ctx.bus.emit('data-update', {
          type: 'treatments',
          op: 'update',
          changes: ctx.ddata.processRawDataForRuntime([obj])
        });

        fn(err, [obj]);
      }
    }
  }

  function list (opts, fn) {

    function limit ( ) {
      if (opts && opts.count) {
        return this.limit(parseInt(opts.count));
      }
      return this;
    }

    return limit.call(api()
      .find(query_for(opts))
      .sort(opts && opts.sort || {created_at: -1}), opts)
      .toArray(fn);
  }

  function query_for (opts) {
    // Build queryOpts inside function to access env.uuidHandling
    var queryOpts = {
      walker: {
        insulin: parseInt
        , carbs: parseInt
        , glucose: parseInt
        , notes: find_options.parseRegEx
        , eventType: find_options.parseRegEx
        , enteredBy: find_options.parseRegEx
      }
      , dateField: 'created_at'
      , uuidHandling: env.uuidHandling
    };
    return find_options(opts, queryOpts);
  }

  function remove (opts, fn) {
    return api( ).deleteMany(query_for(opts), {}, function (err, stat) {
        //TODO: this is triggering a read from Mongo, we can do better
        //console.log('Treatment removed', opts); // , stat);

        ctx.bus.emit('data-update', {
          type: 'treatments',
          op: 'remove',
          count: stat.deletedCount,
          changes: opts.find._id
        });

        ctx.bus.emit('data-received');
        fn(err, stat);
      });
  }

  function save (obj, fn) {
    normalizeTreatmentId(obj);
    prepareData(obj);

    var query = upsertQueryFor(obj, { created_at: obj.created_at });

    function saved (err, updateResults) {
      if (!err) {
        if (updateResults && updateResults.upsertedCount == 1) {
          obj._id = updateResults.upsertedId;
        } else if (updateResults && updateResults.matchedCount >= 1 && obj.identifier && !obj._id) {
          // REQ-SYNC-072: On update by identifier, fetch the existing _id
          api().findOne(query, function(findErr, existing) {
            if (!findErr && existing) {
              obj._id = existing._id;
            }
            finishSave(err, obj);
          });
          return;
        }
        // console.log('Treatment updated', created);

        ctx.ddata.processRawDataForRuntime(obj);

        ctx.bus.emit('data-update', {
          type: 'treatments',
          op: 'update',
          changes: ctx.ddata.processRawDataForRuntime([obj])
        });

      }
      if (err) console.error('Problem saving treating', err);

      fn(err, obj);
    }

    function finishSave(err, obj) {
      if (!err) {
        ctx.ddata.processRawDataForRuntime(obj);
        ctx.bus.emit('data-update', {
          type: 'treatments',
          op: 'update',
          changes: ctx.ddata.processRawDataForRuntime([obj])
        });
      }
      if (err) console.error('Problem saving treating', err);
      fn(err, obj);
    }

    api().replaceOne(query, obj, {upsert: true}, saved);

    ctx.bus.emit('data-received');
  }

  function api ( ) {
    return ctx.store.collection(env.treatments_collection);
  }

  /**
   * Build upsert query - REQ-SYNC-072: identifier-first lookup
   * Priority: identifier > _id > time+type
   * 
   * IMPORTANT: When returning identifier-based query, also removes _id from obj
   * because MongoDB doesn't allow changing _id on upsert update.
   */
  function upsertQueryFor (obj, results) {
    // 1. Prefer identifier for dedup (AAPS, Loop UUID _id normalized)
    if (obj.identifier) {
      // Remove _id from replacement - MongoDB will use existing _id on update,
      // or generate new one on insert
      delete obj._id;
      return { identifier: obj.identifier };
    }
    // 2. Fall back to _id if present and valid
    if (Object.prototype.hasOwnProperty.call(obj, '_id') && obj._id !== null && obj._id !== '') {
      return { _id: obj._id };
    }
    // 3. Last resort: time + eventType
    return {
      created_at: results.created_at
      , eventType: obj.eventType
    };
  }

  /**
   * Normalize treatment ID - REQ-SYNC-072: Server-Controlled ID
   * 
   * Scope: ONLY handles UUID values in _id field
   * - Loop overrides: UUID in _id → moved to identifier
   * 
   * Does NOT touch other client fields (syncIdentifier, uuid, etc.)
   * Those fields are preserved as-is and used for dedup in upsertQueryFor().
   * 
   * Note: _id handling is done here to properly handle update vs insert
   */
  function normalizeTreatmentId (obj) {
    // REQ-SYNC-072: Only handle UUID values in _id field
    // Scope: ONLY the _id field when value is a valid UUID
    // Does NOT touch syncIdentifier, uuid, or other client fields
    if (typeof obj._id === 'string' && !OBJECT_ID_HEX_RE.test(obj._id)) {
      // Non-ObjectId string in _id (UUID format)
      // Only move to identifier when UUID_HANDLING is enabled
      if (env.uuidHandling && !obj.identifier) {
        obj.identifier = obj._id;
      }
      // Always delete invalid _id so server generates ObjectId
      delete obj._id;
    } else if (Object.prototype.hasOwnProperty.call(obj, '_id') && obj._id !== null && obj._id !== '') {
      // Convert valid ObjectId strings to ObjectId objects
      if (typeof obj._id === 'string' && OBJECT_ID_HEX_RE.test(obj._id)) {
        obj._id = new ObjectID(obj._id);
      }
    }
  }

  api.list = list;
  api.create = create;
  api.query_for = query_for;
  api.indexedFields = [
    'created_at'
    , 'eventType'
    , 'insulin'
    , 'carbs'
    , 'glucose'
    , 'enteredBy'
    , 'boluscalc.foods._id'
    , 'notes'
    , 'NSCLIENT_ID'
    , 'percent'
    , 'absolute'
    , 'duration'
    , 'identifier'  // REQ-SYNC-072: Client sync identity (UUID from _id field)
    , { 'eventType' : 1, 'duration' : 1, 'created_at' : 1 }
  ];

  api.remove = remove;
  api.save = save;
  api.aggregate = require('./aggregate')({ }, api);

  return api;
}

function prepareData(obj) {

  // Convert all dates to UTC dates

  // TODO remove this -> must not create new date if missing
  const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment();
  obj.created_at = d.toISOString();

  var results = {
    created_at: obj.created_at
    , preBolusCarbs: ''
  };

  const offset = d.utcOffset();
  obj.utcOffset = offset;
  results.offset = offset;

  obj.glucose = Number(obj.glucose);
  obj.targetTop = Number(obj.targetTop);
  obj.targetBottom = Number(obj.targetBottom);
  obj.carbs = Number(obj.carbs);
  obj.insulin = Number(obj.insulin);
  obj.duration = Number(obj.duration);
  obj.percent = Number(obj.percent);
  obj.absolute = Number(obj.absolute);
  obj.relative = Number(obj.relative);
  obj.preBolus = Number(obj.preBolus);

  //NOTE: the eventTime is sent by the client, but deleted, we only store created_at
  var eventTime;
  if (obj.eventTime) {
    eventTime = new Date(obj.eventTime).toISOString();
    results.created_at = eventTime;
  }

  obj.created_at = results.created_at;
  if (obj.preBolus && obj.preBolus !== 0 && obj.carbs) {
    results.preBolusCarbs = obj.carbs;
    delete obj.carbs;
  }

  if (obj.eventType === 'Announcement') {
    obj.isAnnouncement = true;
  }

  // clean data
  delete obj.eventTime;

  function deleteIfEmpty (field) {
    if (!obj[field] || obj[field] === 0) {
      delete obj[field];
    }
  }

  function deleteIfNaN (field) {
    if (isNaN(obj[field])) {
      delete obj[field];
    }
  }

  deleteIfEmpty('targetTop');
  deleteIfEmpty('targetBottom');
  deleteIfEmpty('carbs');
  deleteIfEmpty('insulin');
  deleteIfEmpty('percent');
  deleteIfEmpty('relative');
  deleteIfEmpty('notes');
  deleteIfEmpty('preBolus');

  deleteIfNaN('absolute');
  deleteIfNaN('duration');

  if (obj.glucose === 0 || isNaN(obj.glucose)) {
    delete obj.glucose;
    delete obj.glucoseType;
    delete obj.units;
  }

  return results;
}

module.exports = storage;
