import * as plugins from './logcontext.plugins.js';

export class AsyncStore {
  private static idCounter = 0;
  private id: number;
  private parentStore?: AsyncStore;
  private deletedKeys: string[] = [];
  private dataObject: { [key: string]: any } = {};

  constructor(parentStore?: AsyncStore) {
    this.parentStore = parentStore;
    this.id = AsyncStore.idCounter++;
  }

  /**
   * Logs debug info if process.env.DEBUG is set.
   */
  private logDebug(functionName: string, before: Record<string, any>, after: Record<string, any>) {
    if (typeof process !== 'undefined' && process.env && process.env.DEBUG) {
      console.log(`Store ID: ${this.id}`);
      console.log(`Function: ${functionName}`);
      console.log('--- Before ---');
      console.log(before);
      console.log('--- After ---');
      console.log(after);
      console.log('-----------------------');
    }
  }

  /**
   * Cleans up the deleted keys if they no longer exist in any parent store.
   */
  private cleanUp() {
    for (const key of this.deletedKeys) {
      if (this.parentStore && this.parentStore.get(key)) {
        // Parent still has it, so keep in deletedKeys
      } else {
        const index = this.deletedKeys.indexOf(key);
        if (index !== -1) {
          this.deletedKeys.splice(index, 1);
        }
      }
    }
  }

  /**
   * Adds or updates a value under a specific key in this store.
   */
  public add(keyArg: string, objectArg: any) {
    // capture the before state
    const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };

    this.cleanUp();
    // If this key was previously deleted, remove it from deletedKeys.
    if (this.deletedKeys.includes(keyArg)) {
      this.deletedKeys = this.deletedKeys.filter((key) => key !== keyArg);
    }
    this.dataObject[keyArg] = objectArg;

    // capture the after state
    const after = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };
    this.logDebug('add', before, after);
  }

  /**
   * Deletes a key from the current store.
   * If a parent store has the key, we record it in `deletedKeys` so the child store "shadows" it.
   */
  public delete(paramName: string) {
    // capture the before state
    const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };

    this.cleanUp();
    if (this.parentStore?.get(paramName)) {
      // The parent store has this key; let's mark it as deleted in the child
      this.deletedKeys.push(paramName);
    }
    delete this.dataObject[paramName];

    // capture the after state
    const after = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };
    this.logDebug('delete', before, after);
  }

  /**
   * Gets the value of a key, checking this store first, then the parent store if necessary.
   * Will log the store state before/after for debugging.
   */
  public get(paramName: string) {
    // capture the before state
    const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };

    this.cleanUp();
    // figure out if paramName is deleted or present
    let result: any;
    if (this.deletedKeys.includes(paramName)) {
      result = undefined;
    } else {
      result = this.dataObject[paramName] ?? this.parentStore?.get(paramName);
    }

    // capture the after state; we can also show the `result` in the log
    const after = {
      ...this.dataObject,
      deletedKeys: [...this.deletedKeys],
      retrievedKey: paramName,
      result
    };
    this.logDebug('get', before, after);

    return result;
  }

  /**
   * Returns all keys and values, merged with the parent store, but
   * does NOT include keys that are "deleted" in the child.
   * Child store should override parent if the same key exists in both.
   */
  public getAll() {
    this.cleanUp();
    // first, get parent's data as a shallow copy
    const parentData = { ...(this.parentStore?.getAll() || {}) };

    // remove keys from parent data that this child has deleted
    for (const key of this.deletedKeys) {
      delete parentData[key];
    }

    // child's data overrides parent data for any matching keys
    return {
      ...parentData,
      ...this.dataObject
    };
  }
}