import { Injectable, NgZone, OnDestroy, ElementRef } from '@angular/core';
import { Observable, Subject, fromEvent, merge } from 'rxjs';
import { takeUntil, map, distinctUntilChanged } from 'rxjs/operators';

export type FocusOrigin = 'keyboard' | 'mouse' | 'touch' | 'program' | null;

export interface FocusMonitorOptions {
  detectSubtleFocusChanges?: boolean;
  checkChildren?: boolean;
}

export interface FocusEvent {
  origin: FocusOrigin;
  event: Event | null;
}

@Injectable({
  providedIn: 'root'
})
export class FocusMonitorService implements OnDestroy {
  private destroy$ = new Subject<void>();
  private currentFocusOrigin: FocusOrigin = null;
  private lastFocusOrigin: FocusOrigin = null;
  private lastTouchTarget: EventTarget | null = null;
  
  private monitoredElements = new Map<Element, {
    subject: Subject<FocusEvent>;
    unlisten: () => void;
  }>();

  constructor(private ngZone: NgZone) {
    this.setupGlobalListeners();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    
    // Clean up all monitored elements
    this.monitoredElements.forEach((info) => info.unlisten());
    this.monitoredElements.clear();
  }

  /**
   * Monitor focus changes on an element
   */
  monitor(
    element: Element | ElementRef<Element>,
    options: FocusMonitorOptions = {}
  ): Observable<FocusEvent> {
    const elementToMonitor = element instanceof ElementRef ? element.nativeElement : element;
    
    if (this.monitoredElements.has(elementToMonitor)) {
      return this.monitoredElements.get(elementToMonitor)!.subject.asObservable();
    }

    const subject = new Subject<FocusEvent>();
    
    const focusHandler = (event: FocusEvent) => {
      subject.next(event);
    };

    const blurHandler = () => {
      subject.next({ origin: null, event: null });
    };

    // Set up event listeners
    const focusListener = this.ngZone.runOutsideAngular(() => {
      return fromEvent(elementToMonitor, 'focus', { capture: true }).pipe(
        takeUntil(this.destroy$),
        map((event: Event) => ({
          origin: this.currentFocusOrigin,
          event
        }))
      ).subscribe(focusHandler);
    });

    const blurListener = this.ngZone.runOutsideAngular(() => {
      return fromEvent(elementToMonitor, 'blur', { capture: true }).pipe(
        takeUntil(this.destroy$)
      ).subscribe(blurHandler);
    });

    const unlisten = () => {
      focusListener.unsubscribe();
      blurListener.unsubscribe();
      subject.complete();
    };

    this.monitoredElements.set(elementToMonitor, { subject, unlisten });

    return subject.asObservable().pipe(
      distinctUntilChanged((x, y) => x.origin === y.origin)
    );
  }

  /**
   * Stop monitoring focus changes on an element
   */
  stopMonitoring(element: Element | ElementRef<Element>): void {
    const elementToStop = element instanceof ElementRef ? element.nativeElement : element;
    const info = this.monitoredElements.get(elementToStop);
    
    if (info) {
      info.unlisten();
      this.monitoredElements.delete(elementToStop);
    }
  }

  /**
   * Focus an element and mark it as programmatically focused
   */
  focusVia(element: Element | ElementRef<Element>, origin: FocusOrigin): void {
    const elementToFocus = element instanceof ElementRef ? element.nativeElement : element;
    
    this.setOrigin(origin);
    
    if (typeof (elementToFocus as any).focus === 'function') {
      (elementToFocus as HTMLElement).focus();
    }
  }

  /**
   * Get the current focus origin
   */
  getCurrentFocusOrigin(): FocusOrigin {
    return this.currentFocusOrigin;
  }

  /**
   * Set the focus origin for the next focus event
   */
  private setOrigin(origin: FocusOrigin): void {
    this.currentFocusOrigin = origin;
    this.lastFocusOrigin = origin;
  }

  /**
   * Set up global event listeners to detect focus origin
   */
  private setupGlobalListeners(): void {
    if (typeof document === 'undefined') return;

    this.ngZone.runOutsideAngular(() => {
      // Mouse events
      merge(
        fromEvent(document, 'mousedown', { capture: true }),
        fromEvent(document, 'mouseup', { capture: true })
      ).pipe(
        takeUntil(this.destroy$)
      ).subscribe(() => {
        this.setOrigin('mouse');
      });

      // Keyboard events
      fromEvent(document, 'keydown', { capture: true }).pipe(
        takeUntil(this.destroy$)
      ).subscribe((event) => {
        const keyEvent = event as KeyboardEvent;
        // Only set keyboard origin for navigation keys
        if (this.isNavigationKey(keyEvent.key)) {
          this.setOrigin('keyboard');
        }
      });

      // Touch events
      fromEvent(document, 'touchstart', { capture: true }).pipe(
        takeUntil(this.destroy$)
      ).subscribe((event) => {
        const touchEvent = event as TouchEvent;
        this.lastTouchTarget = touchEvent.target;
        this.setOrigin('touch');
      });

      // Focus events that might be programmatic
      fromEvent(document, 'focus', { capture: true }).pipe(
        takeUntil(this.destroy$)
      ).subscribe((event) => {
        // If we don't have an origin, assume it's programmatic
        if (this.currentFocusOrigin === null) {
          this.setOrigin('program');
        }
        
        // Reset origin after a short delay
        setTimeout(() => {
          this.currentFocusOrigin = null;
        }, 1);
      });
    });
  }

  /**
   * Check if a key is a navigation key
   */
  private isNavigationKey(key: string): boolean {
    const navigationKeys = [
      'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
      'Home', 'End', 'PageUp', 'PageDown', 'Enter', ' ', 'Escape'
    ];
    return navigationKeys.includes(key);
  }
}