'use strict';

var es = require('event-stream');
var find_options = require('./query');
var ObjectId = require('mongodb-legacy').ObjectId;
var moment = require('moment');

// REQ-SYNC-072: Pattern to match valid MongoDB ObjectId hex strings
var OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;

/**********\
 * Entries
 * Encapsulate persistent storage of sgv entries.
\**********/

function storage (env, ctx) {

  // TODO: Code is a little redundant.

  // query for entries from storage
  function list (opts, fn) {
    // these functions, find, sort, and limit, are used to
    // dynamically configure the request, based on the options we've
    // been given

    // determine sort options
    function sort () {
      return opts && opts.sort || { date: -1 };
    }

    // configure the limit portion of the current query
    function limit () {
      if (opts && opts.count) {
        return this.limit(parseInt(opts.count));
      }
      return this;
    }

    // handle all the results
    function toArray (err, entries) {
      fn(err, entries);
    }

    // now just stitch them all together
    limit.call(api()
      .find(query_for(opts))
      .sort(sort())
    ).toArray(toArray);
  }

  function remove (opts, fn) {
    api().deleteMany(query_for(opts), function(err, stat) {

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

      //TODO: this is triggering a read from Mongo, we can do better
      ctx.bus.emit('data-received');
      fn(err, stat);
    });
  }

  // return writable stream to lint each sgv record passing through it
  // TODO: get rid of this? not doing anything now
  function map () {
    return es.map(function iter (item, next) {
      return next(null, item);
    });
  }

  // writable stream that persists all records
  // takes function to call when done
  function persist (fn) {
    // receives entire list at end of stream
    function done (err, result) {
      // report any errors
      if (err) { return fn(err, result); }
      // batch insert a list of records
      create(result, fn);
    }
    // lint and store the entire list
    return es.pipeline(map(), es.writeArray(done));
  }

  //TODO: implement
  //function update (fn) {
  //}
  //

  // store new documents using the storage mechanism
  function create (docs, fn) {
    // Handle empty array case - call callback immediately
    if (docs.length === 0) {
      return fn(null, docs);
    }

    // Prepare all documents and build bulk operations
    var bulkOps = docs.map(function(doc) {
      // REQ-SYNC-072: Normalize entry ID - extract UUID to identifier, handle _id
      normalizeEntryId(doc);
      
      // Normalize dates to be in UTC, store offset in utcOffset
      var _sysTime;

      if (doc.dateString) { _sysTime = moment.parseZone(doc.dateString); }
      if (!_sysTime && doc.date) { _sysTime = moment(doc.date); }
      if (!_sysTime) _sysTime = moment();

      doc.utcOffset = _sysTime.utcOffset();
      doc.sysTime = _sysTime.toISOString();
      if (doc.dateString) doc.dateString = doc.sysTime;

      // Build upsert query - prefer identifier, fall back to sysTime+type
      var query = upsertQueryFor(doc);

      return {
        updateOne: {
          filter: query,
          update: { $set: doc },
          upsert: true
        }
      };
    });

    // Use bulkWrite for batch upsert
    api().bulkWrite(bulkOps, { ordered: true }, function(err, bulkResult) {
      if (err) {
        console.error('Problem upserting entries batch', err);
        fn(err, docs);
        return;
      }

      // Assign _ids from upserted results
      if (bulkResult && bulkResult.upsertedIds) {
        Object.keys(bulkResult.upsertedIds).forEach(function(index) {
          docs[index]._id = bulkResult.upsertedIds[index];
        });
      }

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

      ctx.bus.emit('data-received');
      fn(null, docs);
    });
  }

  function getEntry (id, fn) {
    api().findOne({ "_id": new ObjectId(id) }, function(err, entry) {
      if (err) {
        fn(err);
      } else {
        fn(null, entry);
      }
    });
  }

  function query_for (opts) {
    // Build queryOpts inside function to access env.uuidHandling
    var queryOpts = {
      walker: {
        date: parseInt
        , sgv: parseInt
        , filtered: parseInt
        , unfiltered: parseInt
        , rssi: parseInt
        , noise: parseInt
        , mbg: parseInt
      }
      , useEpoch: true
      , uuidHandling: env.uuidHandling
    };
    return find_options(opts, queryOpts);
  }

  // closure to represent the API
  function api () {
    // obtain handle usable for querying the collection associated
    // with these records
    return ctx.store.collection(env.entries_collection);
  }

  // Expose all the useful functions
  api.list = list;
  api.map = map;
  api.create = create;
  api.remove = remove;
  api.persist = persist;
  api.query_for = query_for;
  api.getEntry = getEntry;
  api.aggregate = require('./aggregate')({}, api);
  api.indexedFields = [
    'date'
    , 'type'
    , 'sgv'
    , 'mbg'
    , 'sysTime'
    , 'dateString'
    , 'identifier'  // REQ-SYNC-072: Client sync identity (Trio/Loop syncIdentifier)
    , { 'type': 1, 'date': -1, 'dateString': 1 }
 ];
 
  /**
   * Build upsert query for entry - GAP-SYNC-045 fix
   * 
   * For CGM entries, sysTime+type is ALWAYS the primary dedup key.
   * This ensures only one SGV reading per timestamp, regardless of source UUID.
   * 
   * The fix: strip non-ObjectId _id before $set to avoid "immutable field '_id'" error.
   * UUID is preserved in identifier field for reference.
   */
  function upsertQueryFor (doc) {
    // Always strip non-ObjectId _id to avoid "immutable field '_id'" error
    // The UUID has already been preserved in doc.identifier by normalizeEntryId()
    if (doc._id && typeof doc._id === 'string' && !OBJECT_ID_HEX_RE.test(doc._id)) {
      delete doc._id;
    }
    
    // Standard CGM dedup: sysTime + type (one reading per timestamp per type)
    if (doc.sysTime && doc.type) {
      return { sysTime: doc.sysTime, type: doc.type };
    }
    // Last resort
    return doc;
  }

  /**
   * Normalize entry ID - REQ-SYNC-072: Server-Controlled ID
   * 
   * Extracts client sync identity from _id if UUID:
   * - Trio: UUID in _id → moved to identifier
   * - Loop: syncIdentifier in _id → moved to identifier
   * 
   * Note: _id is stripped in upsertQueryFor to avoid MongoDB errors
   */
  function normalizeEntryId (doc) {
    // REQ-SYNC-072: Only handle UUID values in _id field
    // Scope: ONLY the _id field when value is a valid UUID
    if (typeof doc._id === 'string' && !OBJECT_ID_HEX_RE.test(doc._id)) {
      // Non-ObjectId string in _id (UUID format)
      // Only move to identifier when UUID_HANDLING is enabled
      if (env.uuidHandling && !doc.identifier) {
        doc.identifier = doc._id;
      }
      // Always delete invalid _id so server generates ObjectId
      delete doc._id;
    } else if (Object.prototype.hasOwnProperty.call(doc, '_id') && doc._id !== null && doc._id !== '') {
      // Convert valid ObjectId strings to ObjectId objects
      if (typeof doc._id === 'string' && OBJECT_ID_HEX_RE.test(doc._id)) {
        doc._id = new ObjectId(doc._id);
      }
    }
  }

  return api;
}

// expose module
storage.storage = storage;
module.exports = storage;
