import fs from 'node:fs';
import { chain } from 'stream-chain';
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
import { streamObject } from 'stream-json/streamers/StreamObject';
import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 3600 });

/**
 * Queries a large JSON array file for an item matching the provided query function.
 * This function streams the file to avoid loading the entire content into memory.
 *
 * @param filePath - The path to the JSON file containing an array of items.
 * @param query - A function that takes an item and returns true if it matches the criteria.
 * @returns A promise that resolves to the first matching item or null if no match is found.
 */
export function queryJsonArray<T>(
  filePath: string,
  query: (item: T) => boolean,
  cacheKey?: string,
  cacheExpiry?: number,
): Promise<T | null> {
  if (cacheKey) {
    const cached = cache.get<T>(cacheKey);
    if (cached) {
      return Promise.resolve(cached);
    }
  }

  return new Promise((resolve, reject) => {
    const pipeline = chain([fs.createReadStream(filePath), parser(), streamArray(), (data) => (query(data.value) ? data.value : null)]);

    pipeline.on('data', (value) => {
      if (value) {
        if (cacheKey) {
          cache.set(cacheKey, value, cacheExpiry || 3600);
        }
        resolve(value);
      }
    });

    pipeline.on('end', () => {
      resolve(null); // No match found
    });

    pipeline.on('error', (err) => {
      reject(err);
    });
  });
}

/**
 * Queries a large JSON object file for a value matching the provided query function.
 * This function streams the file to avoid loading the entire content into memory.
 *
 * @param filePath - The path to the JSON file containing an object of key-value pairs.
 * @param query - A function that takes a key-value pair and returns true if it matches the criteria.
 * @returns A promise that resolves to the first matching value or null if no match is found.
 */
export function queryJsonObject<T, K = { key: string; value: T }>(
  filePath: string,
  query: (item: K) => boolean,
  cacheKey?: string,
  cacheExpiry?: number,
): Promise<T | null> {
  if (cacheKey) {
    const cached = cache.get<T>(cacheKey);
    if (cached) {
      return Promise.resolve(cached);
    }
  }

  return new Promise((resolve, reject) => {
    const pipeline = chain([fs.createReadStream(filePath), parser(), streamObject(), (data) => (query(data) ? data.value : null)]);

    pipeline.on('data', (value) => {
      if (value) {
        if (cacheKey) {
          cache.set(cacheKey, value, cacheExpiry || 3600);
        }
        resolve(value);
      }
    });

    pipeline.on('end', () => {
      resolve(null); // No match found
    });

    pipeline.on('error', (err) => {
      reject(err);
    });
  });
}
