// https://khalilstemmler.com/articles/typescript-domain-driven-design/chain-business-logic-domain-events/

import { AggregateRoot } from "../aggregate-root";
import { UniqueID } from "../value-objects";
import { IDomainEvent } from "./domain-event.interface";

// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
export class DomainEvents {
  private static handlersMap: { [key: string]: Array<(event: IDomainEvent) => void> } = {};
  private static markedAggregates: AggregateRoot<any>[] = [];

  /**
   * @method markAggregateForDispatch
   * @static
   * @desc Called by aggregate root objects that have created domain
   * events to eventually be dispatched when the infrastructure commits
   * the unit of work.
   */

  public static markAggregateForDispatch(aggregate: AggregateRoot<any>): void {
    const aggregateFound = !!DomainEvents.findMarkedAggregateByID(aggregate.id);

    if (!aggregateFound) {
      DomainEvents.markedAggregates.push(aggregate);
    }
  }

  /**
   * @method dispatchAggregateEvents
   * @static
   * @private
   * @desc Call all of the handlers for any domain events on this aggregate.
   */

  private static dispatchAggregateEvents(aggregate: AggregateRoot<any>): void {
    aggregate.domainEvents.forEach((event: IDomainEvent) => DomainEvents.dispatch(event));
  }

  /**
   * @method removeAggregateFromMarkedDispatchList
   * @static
   * @desc Removes an aggregate from the marked list.
   */

  private static removeAggregateFromMarkedDispatchList(aggregate: AggregateRoot<any>): void {
    const index = DomainEvents.markedAggregates.findIndex((a) => a.equals(aggregate));

    DomainEvents.markedAggregates.splice(index, 1);
  }

  /**
   * @method findMarkedAggregateByID
   * @static
   * @desc Finds an aggregate within the list of marked aggregates.
   */

  private static findMarkedAggregateByID(id: UniqueID): AggregateRoot<any> {
    let found!: AggregateRoot<any>;

    for (const aggregate of DomainEvents.markedAggregates) {
      if (aggregate.id.equals(id)) {
        found = aggregate;
      }
    }

    return found;
  }

  /**
   * @method dispatchEventsForAggregate
   * @static
   * @desc When all we know is the ID of the aggregate, call this
   * in order to dispatch any handlers subscribed to events on the
   * aggregate.
   */

  public static dispatchEventsForAggregate(id: UniqueID): void {
    const aggregate = DomainEvents.findMarkedAggregateByID(id);

    if (aggregate) {
      DomainEvents.dispatchAggregateEvents(aggregate);
      aggregate.clearDomainEvents();
      DomainEvents.removeAggregateFromMarkedDispatchList(aggregate);
    }
  }

  /**
   * @method register
   * @static
   * @desc Register a handler to a domain event.
   */

  public static register(callback: (event: IDomainEvent) => void, eventClassName: string): void {
    if (!Object.prototype.hasOwnProperty.call(DomainEvents.handlersMap, eventClassName)) {
      DomainEvents.handlersMap[eventClassName] = [];
    }
    DomainEvents.handlersMap[eventClassName].push(callback);
  }

  /**
   * @method clearHandlers
   * @static
   * @desc Useful for testing.
   */

  public static clearHandlers(): void {
    DomainEvents.handlersMap = {};
  }

  /**
   * @method clearMarkedAggregates
   * @static
   * @desc Useful for testing.
   */

  public static clearMarkedAggregates(): void {
    DomainEvents.markedAggregates = [];
  }

  /**
   * @method dispatch
   * @static
   * @desc Invokes all of the subscribers to a particular domain event.
   */

  private static dispatch(event: IDomainEvent): void {
    const eventClassName: string = event.constructor.name;

    if (Object.hasOwn(DomainEvents.handlersMap, eventClassName)) {
      const handlers: any[] = DomainEvents.handlersMap[eventClassName];
      for (const handler of handlers) {
        handler(event);
      }
    }
  }
}
