import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter } from 'events';
import { randomUUID } from 'crypto';
import { DomainEventBase, DomainEventType } from '@wms/shared';
import { Tx } from '@/common/db/db.service';
import { outboxEvents } from '@/db/schema';
import { getRequestContext } from '@/common/context/request-context';

export type DomainEvent = DomainEventBase & { type: DomainEventType };
export type EventHandler = (event: DomainEvent) => Promise<void> | void;

/**
 * Transactional outbox + in-process bus.
 *
 * publish(event, tx) writes the event into outbox_events inside the business
 * transaction. The OutboxDispatcher job (jobs module) later reads PENDING rows
 * and fans them out to in-process handlers (billing capture, notifications)
 * and to webhook subscriptions. If the business transaction rolls back, the
 * event never existed. If the process crashes after commit, the event is still
 * delivered. At-least-once semantics: handlers must be idempotent.
 */
@Injectable()
export class EventBus {
  private readonly logger = new Logger(EventBus.name);
  private readonly emitter = new EventEmitter({ captureRejections: false });
  private readonly handlers = new Map<string, EventHandler[]>();

  constructor() {
    this.emitter.setMaxListeners(200);
  }

  async publish(type: DomainEventType, payload: Record<string, unknown>, tx: Tx, scope: { warehouseId?: string; storerId?: string } = {}): Promise<DomainEvent> {
    const ctx = getRequestContext();
    const tenantId = ctx?.tenantId;
    if (!tenantId) throw new Error(`Cannot publish ${type} without tenant context`);
    const event: DomainEvent = {
      id: randomUUID(),
      type,
      tenantId,
      warehouseId: scope.warehouseId,
      storerId: scope.storerId,
      occurredAt: new Date().toISOString(),
      actorId: ctx?.actorId ?? undefined,
      payload,
    };
    await tx.insert(outboxEvents).values({
      id: event.id,
      tenantId,
      type,
      warehouseId: scope.warehouseId ?? null,
      storerId: scope.storerId ?? null,
      actorId: ctx?.userId ?? null,
      payload: { ...payload, _actorName: ctx?.actorName ?? null },
    });
    return event;
  }

  /** Register an in-process handler. Called by modules on init. */
  subscribe(type: DomainEventType | '*', handler: EventHandler): void {
    const list = this.handlers.get(type) ?? [];
    list.push(handler);
    this.handlers.set(type, list);
  }

  /** Invoked by the outbox dispatcher. Runs every handler; collects errors. */
  async dispatch(event: DomainEvent): Promise<{ errors: Error[] }> {
    const errors: Error[] = [];
    const list = [...(this.handlers.get(event.type) ?? []), ...(this.handlers.get('*') ?? [])];
    for (const h of list) {
      try {
        await h(event);
      } catch (e) {
        errors.push(e as Error);
        this.logger.error(`Handler failed for ${event.type} (${event.id}): ${(e as Error).message}`);
      }
    }
    return { errors };
  }
}
