/**
 * A lightweight, framework-agnostic pub/sub event bus.
 *
 * Used project-wide to decouple:
 *  - UI layer (floating controls) → logic layer (player controller)
 *  - Logic layer (damage system) → UI layer (HUD / feedback)
 *  - Any manager → Logger / BI埋点
 *
 * Design notes:
 *  - `emit` is synchronous to avoid a frame of latency for combat events.
 *  - Subscribing the same callback twice is a no-op (idempotent) to avoid
 *    double-fire bugs when views are rebuilt.
 *  - `once` unsubscribes itself after the first invocation.
 *  - Handler errors are caught and forwarded to a user-provided error hook
 *    (defaulting to `console.error`) so that one bad listener cannot break
 *    the rest of the fan-out.
 */

export type EventHandler<TPayload = unknown> = (payload: TPayload) => void;
export type ErrorHook = (event: string, err: unknown) => void;

interface HandlerRecord {
    fn: EventHandler<any>;
    once: boolean;
}

export class EventBus {
    private readonly handlers = new Map<string, HandlerRecord[]>();
    private errorHook: ErrorHook = (event, err) => {
        // Fallback error hook; replaced via `setErrorHook` once Logger is
        // available.
        // eslint-disable-next-line no-console
        console.error(`[EventBus] handler for "${event}" threw:`, err);
    };

    /** Override the default error hook (used by Logger integration). */
    public setErrorHook(hook: ErrorHook): void {
        this.errorHook = hook;
    }

    /**
     * Subscribe a handler. Idempotent — the same `fn` cannot be registered
     * twice for the same event.
     */
    public on<T>(event: string, fn: EventHandler<T>): void {
        this.register(event, fn as EventHandler<any>, false);
    }

    /** Subscribe a handler that auto-unsubscribes after one invocation. */
    public once<T>(event: string, fn: EventHandler<T>): void {
        this.register(event, fn as EventHandler<any>, true);
    }

    /**
     * Unsubscribe. If `fn` is omitted, all handlers for `event` are cleared.
     */
    public off<T>(event: string, fn?: EventHandler<T>): void {
        const list = this.handlers.get(event);
        if (!list) {
            return;
        }
        if (!fn) {
            this.handlers.delete(event);
            return;
        }
        const filtered = list.filter((r) => r.fn !== fn);
        if (filtered.length === 0) {
            this.handlers.delete(event);
        } else {
            this.handlers.set(event, filtered);
        }
    }

    /** Synchronously dispatch `payload` to every handler registered for `event`. */
    public emit<T>(event: string, payload?: T): void {
        const list = this.handlers.get(event);
        if (!list || list.length === 0) {
            return;
        }
        // Snapshot first: `once` handlers will mutate `list` via `off`.
        const snapshot = list.slice();
        for (const record of snapshot) {
            try {
                record.fn(payload as unknown);
            } catch (err) {
                this.errorHook(event, err);
            }
            if (record.once) {
                this.off(event, record.fn);
            }
        }
    }

    /** Return the number of handlers registered for `event`. */
    public listenerCount(event: string): number {
        return this.handlers.get(event)?.length ?? 0;
    }

    /** Remove every handler of every event (used in unit tests / scene unload). */
    public clear(): void {
        this.handlers.clear();
    }

    private register(event: string, fn: EventHandler<any>, once: boolean): void {
        const list = this.handlers.get(event) ?? [];
        if (list.some((r) => r.fn === fn)) {
            return;
        }
        list.push({ fn, once });
        this.handlers.set(event, list);
    }
}

/** Shared, process-wide event bus. Tests should create a fresh `new EventBus()`. */
export const globalEventBus = new EventBus();
