/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2024, United States Government
 * as represented by the Administrator of the National Aeronautics and Space
 * Administration. All rights reserved.
 *
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 * Open MCT includes source code licensed under additional open source
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 * this source code distribution or the Licensing information page available
 * at runtime from the About dialog for additional information.
 *****************************************************************************/

// This provider exists because due to legacy reasons, we need to install
// two plugins for two namespaces for CouchDB: one for "mct", and one for "".
// Because of this, we need to separate out the search provider from the object
// provider so we don't return two results for each found object.
// If the above namespace is ever resolved, we can fold this search provider
// back into the object provider.

const BATCH_ANNOTATION_DEBOUNCE_MS = 100;

class CouchSearchProvider {
  #bulkPromise;
  #batchIds;
  #lastAbortSignal;

  constructor(couchObjectProvider) {
    this.couchObjectProvider = couchObjectProvider;
    this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
    this.useDesignDocuments = couchObjectProvider.useDesignDocuments;
    this.supportedSearchTypes = [
      this.searchTypes.OBJECTS,
      this.searchTypes.ANNOTATIONS,
      this.searchTypes.TAGS
    ];
    this.#batchIds = [];
    this.#bulkPromise = null;
  }

  supportsSearchType(searchType) {
    return this.supportedSearchTypes.includes(searchType);
  }

  isReadOnly() {
    return true;
  }

  search(query, abortSignal, searchType) {
    if (searchType === this.searchTypes.OBJECTS) {
      return this.searchForObjects(query, abortSignal);
    } else if (searchType === this.searchTypes.ANNOTATIONS) {
      return this.searchForAnnotations(query, abortSignal);
    } else if (searchType === this.searchTypes.TAGS) {
      return this.searchForTags(query, abortSignal);
    } else {
      throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`);
    }
  }

  searchForObjects(query, abortSignal) {
    const filter = {
      selector: {
        model: {
          name: {
            $regex: `(?i)${query}`
          }
        }
      }
    };

    return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
  }

  async #deferBatchAnnotationSearch() {
    // We until the next event loop cycle to "collect" all of the get
    // requests triggered in this iteration of the event loop
    await this.#waitForDebounce();
    const batchIdsToSearch = [...this.#batchIds];
    this.#clearBatch();
    return this.#bulkAnnotationSearch(batchIdsToSearch);
  }

  #clearBatch() {
    this.#batchIds = [];
    this.#bulkPromise = undefined;
  }

  #waitForDebounce() {
    let timeoutID;
    clearTimeout(timeoutID);

    return new Promise((resolve) => {
      timeoutID = setTimeout(() => {
        resolve();
      }, BATCH_ANNOTATION_DEBOUNCE_MS);
    });
  }

  #bulkAnnotationSearch(batchIdsToSearch) {
    if (!batchIdsToSearch?.length) {
      // nothing to search
      return;
    }

    let lastAbortSignal = batchIdsToSearch[batchIdsToSearch.length - 1].abortSignal;

    if (this.useDesignDocuments) {
      const keysToSearch = batchIdsToSearch.map(({ keyString }) => keyString);
      return this.couchObjectProvider.getObjectsByView(
        {
          designDoc: 'annotation_keystring_index',
          viewName: 'by_keystring',
          keysToSearch
        },
        lastAbortSignal
      );
    }

    const filter = {
      selector: {
        $and: [
          {
            'model.type': {
              $eq: 'annotation'
            }
          },
          {
            'model.targets': {
              $elemMatch: {
                keyString: {
                  $in: []
                }
              }
            }
          }
        ]
      }
    };
    // TODO: should remove duplicates from batchIds
    batchIdsToSearch.forEach(({ keyString, abortSignal }) => {
      filter.selector.$and[1]['model.targets'].$elemMatch.keyString.$in.push(keyString);
    });

    return this.couchObjectProvider.getObjectsByFilter(filter, lastAbortSignal);
  }

  async searchForAnnotations(keyString, abortSignal) {
    this.#batchIds.push({ keyString, abortSignal });
    if (!this.#bulkPromise) {
      this.#bulkPromise = this.#deferBatchAnnotationSearch();
    }

    const returnedData = await this.#bulkPromise;
    return returnedData;
  }

  searchForTags(tagsArray, abortSignal) {
    if (!tagsArray || !tagsArray.length) {
      return [];
    }

    if (this.useDesignDocuments) {
      return this.couchObjectProvider.getObjectsByView(
        { designDoc: 'annotation_tags_index', viewName: 'by_tags', keysToSearch: tagsArray },
        abortSignal
      );
    }

    const filter = {
      selector: {
        $and: [
          {
            'model.type': {
              $eq: 'annotation'
            }
          },
          {
            'model.tags': {
              $elemMatch: {
                $in: []
              }
            }
          }
        ]
      }
    };
    tagsArray.forEach((tag) => {
      filter.selector.$and[1]['model.tags'].$elemMatch.$in.push(tag);
    });

    return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
  }
}
export default CouchSearchProvider;
