export type AsukaEventMeta = { instanceId?: string; [k: string]: any };
export type AsukaEventHandler = (payload?: any) => void | boolean;

/**
 * Global event bus for AsukaDesigner.
 *
 * Event naming convention:
 *  - before:namespace.action (for example before:layout.openModule)
 *  - after:namespace.action  (for example after:layout.openModule)
 *  - free-form events are also allowed (for example layout.moveBlock)
 */
const listeners = new Map<string, Set<AsukaEventHandler>>();

export function onAsukaEvent(event: string, handler: AsukaEventHandler): () => void {
  const set = listeners.get(event) ?? new Set<AsukaEventHandler>();
  set.add(handler);
  listeners.set(event, set);
  return () => {
    const current = listeners.get(event);
    if (!current) return;
    current.delete(handler);
    if (current.size === 0) {
      listeners.delete(event);
    }
  };
}

export function offAsukaEvent(event: string, handler: AsukaEventHandler): void {
  const set = listeners.get(event);
  if (!set) return;
  set.delete(handler);
  if (set.size === 0) {
    listeners.delete(event);
  }
}

export function emitAsukaEvent(event: string, payload?: any, meta?: AsukaEventMeta): void {
  const set = listeners.get(event);
  if (!set || set.size === 0) return;

  // Attach meta in a non-breaking way.
  // Handlers that don't care about meta can ignore it.
  const p = (payload && typeof payload === 'object') ? payload : { value: payload };
  if (meta && typeof meta === 'object') {
    (p as any).__meta = { ...(p as any).__meta, ...meta };
  }

  for (const handler of Array.from(set)) {
    try {
      handler(p);
    } catch {
      // Never let an external listener break the application
    }
  }
}

/** Emit a non-cancellable before:<event> notification. */
export function emitBefore(event: string, payload?: any, meta?: AsukaEventMeta): void {
  emitAsukaEvent(`before:${event}`, payload, meta);
}

export function emitAfter(event: string, payload?: any, meta?: AsukaEventMeta): void {
  emitAsukaEvent(`after:${event}`, payload, meta);
}
/**
 * Emit an event with a cancellation mechanism.
 *
 * Convention:
 *  - If ANY handler returns `false`, the event is considered cancelled.
 *  - Handlers may also set `payload.cancelled = true`.
 *
 * Returns `true` if NOT cancelled, `false` if cancelled.
 */
export function emitAsukaEventCancelable<TPayload = any>(event: string, payload: TPayload = {} as any, meta?: AsukaEventMeta): boolean {
  const set = listeners.get(event);
  if (!set || set.size === 0) return true;

  // Ensure a mutable object payload
  const p = (payload && typeof payload === 'object') ? payload : { value: payload };
  if (meta && typeof meta === 'object') {
    (p as any).__meta = { ...(p as any).__meta, ...meta };
  }
  if (typeof (p as any).cancelled !== 'boolean') {
    (p as any).cancelled = false;
  }

  for (const handler of Array.from(set)) {
    try {
      const res = handler(p);
      if (res === false) {
        (p as any).cancelled = true;
      }
    } catch (_e) {
      void 0;
    }
    if ((p as any).cancelled) return false;
  }
  return !(p as any).cancelled;
}
