// TODO: Consider moving to ui/common (don't exist) or ui/overlay (don't exist) - this directive handles overlay scroll behavior, not layout
import {
  Directive,
  effect,
  inject,
  model,
  OnDestroy,
  output,
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { logger } from '@isa/core/logging';

/**
 * Directive that emits an event when scrolling occurs outside a specified element.
 *
 * This directive listens to all scroll events in capture phase and emits `closeOnScroll`
 * when scrolling happens anywhere except within the excluded element (e.g., dropdown overlay panel).
 *
 * Use case: Close dropdown when user scrolls the page, but keep it open when scrolling within dropdown options.
 *
 * @example
 * ```typescript
 * // As hostDirective with programmatic configuration:
 * hostDirectives: [{ directive: CloseOnScrollDirective, outputs: ['closeOnScroll'] }]
 *
 * // In component:
 * readonly #closeOnScroll = inject(CloseOnScrollDirective, { self: true });
 * readonly panel = viewChild<ElementRef<HTMLElement>>('panel');
 *
 * constructor() {
 *   effect(() => {
 *     this.#closeOnScroll.closeOnScrollWhen.set(this.isOpen());
 *     this.#closeOnScroll.closeOnScrollExclude.set(this.panel()?.nativeElement);
 *   });
 * }
 *
 * // In host:
 * host: { '(closeOnScroll)': 'close()' }
 * ```
 */
@Directive({
  selector: '[uiCloseOnScroll]',
  standalone: true,
})
export class CloseOnScrollDirective implements OnDestroy {
  readonly #logger = logger(() => ({ directive: 'CloseOnScrollDirective' }));
  #document = inject(DOCUMENT);
  #scrollListener?: (event: Event) => void;
  #isActive = false;
  #pendingActivation?: number;

  /**
   * When true, the directive listens for scroll events.
   * Bind this to your open state (e.g., `closeOnScrollWhen.set(isOpen())`).
   */
  closeOnScrollWhen = model<boolean>(false);

  /**
   * Element to exclude from scroll detection.
   * If scroll occurs within this element or its children, the closeOnScroll event will NOT be emitted.
   */
  closeOnScrollExclude = model<HTMLElement | undefined>(undefined);

  /**
   * Emitted when scrolling occurs outside the excluded element.
   */
  closeOnScroll = output<void>();

  constructor() {
    // Auto-activate/deactivate based on closeOnScrollWhen input
    effect(() => {
      const shouldBeActive = this.closeOnScrollWhen();
      if (shouldBeActive && !this.#isActive) {
        this.#activate();
      } else if (!shouldBeActive && this.#isActive) {
        this.#deactivate();
      }
    });
  }

  #activate(): void {
    if (this.#isActive) {
      return;
    }
    this.#isActive = true;

    // Delay listener registration to skip scroll events caused by:
    // 1. Stale scroll events from before activation
    // 2. iOS Safari's automatic "scroll into view" when focusing elements
    // Using setTimeout with 100ms to ensure iOS scroll-into-view completes
    this.#pendingActivation = window.setTimeout(() => {
      this.#scrollListener = (event: Event) => {
        const excludeElement = this.closeOnScrollExclude();
        if (excludeElement?.contains(event.target as HTMLElement)) {
          return;
        }
        this.#logger.debug('Scroll detected outside panel, emitting close');
        this.closeOnScroll.emit();
      };

      this.#document.defaultView?.addEventListener(
        'scroll',
        this.#scrollListener,
        { capture: true, passive: true },
      );
      this.#logger.debug('Activated scroll listener');
    }, 100);
  }

  #deactivate(): void {
    if (this.#pendingActivation) {
      clearTimeout(this.#pendingActivation);
      this.#pendingActivation = undefined;
    }

    if (this.#scrollListener) {
      this.#document.defaultView?.removeEventListener(
        'scroll',
        this.#scrollListener,
        { capture: true },
      );
      this.#scrollListener = undefined;
    }
    this.#logger.debug('Deactivated scroll listener');
    this.#isActive = false;
  }

  ngOnDestroy(): void {
    this.#deactivate();
  }
}
