import { ConnectedPosition, Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { DOCUMENT } from '@angular/common';
import { Directive, ElementRef, inject, input, numberAttribute, OnDestroy, TemplateRef, } from '@angular/core';
import { fromEvent, Subject, Subscription, takeUntil } from 'rxjs';
import { FastTipContainerComponent } from './fast-tip-container.component';
export type TOOLTIP_POSITIONS = 'top' | 'right' | 'bottom' | 'left';

export type TOOLTIP_EVENT = 'hover' | 'focus' | 'none';

export type BG_TYPE = 'opaque' | 'standard';

let tooltipId = 0;

function getTooltipId() {
  return `tooltip-${tooltipId++}`;
}

@Directive({
  selector: '[fast-tip]',
  host: {
    '(focus)': 'onFocus()',
    '(blur)': 'onBlur()',
    '(mouseenter)': 'onMouseEnter()',
    '(mouseleave)': 'onMouseLeave()',
    '(pointerdown)': 'onPointerDown($event)',
    '(pointerup)': 'onPointerUp()',
    '(pointercancel)': 'onPointerCancel()',
    '(contextmenu)': 'onContextMenu($event)',
    '(selectstart)': 'onSelectStart($event)',
    '(dragstart)': 'onDragStart($event)',
    '[attr.aria-describedby]': 'tooltipId'
  },
  exportAs: 'fast-tip'
})
export class FastTipDirective implements OnDestroy {
  message = input.required<string>({ alias: 'fast-tip' });
  tipPosition = input<TOOLTIP_POSITIONS>('right');
  tipEvent = input<TOOLTIP_EVENT>('hover');
  tipDelay = input(0, { transform: numberAttribute });
  tipContent = input<TemplateRef<unknown>>();
  tipContext = input<unknown>();
  tipBackground = input<BG_TYPE>('opaque');
  pushTooltip = input<boolean>(true);
  tooltipId = getTooltipId();

  // track all active overlays, not just the current one
  // so that we can detach them all on hide()
  // including some overlays that may be orphaned in edge cases
  private static activeOverlays: OverlayRef[] = [];

  private overlay = inject(Overlay);
  private el = inject<ElementRef<HTMLElement>>(ElementRef);
  private document = inject(DOCUMENT);
  private timeoutId: unknown | undefined;
  private hideTimeoutId: unknown | undefined;
  private escapeSub: Subscription | undefined;
  private readonly destroyed = new Subject<void>(); // Emits when the component is destroyed. */

  // long-press and outside-dismiss state
  private longPressTimer: unknown | undefined;
  private isPressing = false;
  private readonly longPressThreshold = 500;
  private outsideSub: Subscription | undefined;
  private suppressNextOutsideCheck = false;
  private lastOverlayRef: OverlayRef | undefined;

  // inline style state for press prevention
  private previousUserSelect?: string;
  private previousTouchAction?: string;
  private previousTouchCallout?: string;
  private pressPreventionApplied = false;

  // lightweight mobile detection with caching
  private _isMobile: boolean | undefined;
  private isMobileDevice(): boolean {
    if (this._isMobile === undefined) {
      const win = this.document?.defaultView as (Window & { ontouchstart?: unknown }) | null;
      const hasTouchEvent = !!(win && ('ontouchstart' in win));
      const hasMaxTouch = typeof navigator !== 'undefined' && (navigator as Navigator & { maxTouchPoints?: number }).maxTouchPoints > 0;
      this._isMobile = hasTouchEvent || hasMaxTouch;
    }
    return this._isMobile;
  }

  onFocus() {
    if (this.tipEvent() === 'focus')
      this.show();
  }

  onBlur() {
    if (this.tipEvent() === 'focus')
      this.hide();
  }

  onMouseEnter() {
    if (this.tipEvent() === 'hover' && !this.isMobileDevice()) {
      this.clearHideTimeout();
      this.show();
    }
  }

  onMouseLeave() {
    if (this.tipEvent() === 'hover' && !this.isMobileDevice())
      this.scheduleHide();
  }

  // Prevent default context menu/select/drag while using hover semantics on mobile
  onContextMenu(e: Event) {
    if (this.tipEvent() === 'hover' && this.isMobileDevice()) {
      e.preventDefault();
    }
  }
  onSelectStart(e: Event) {
    if (this.tipEvent() === 'hover' && this.isMobileDevice()) {
      e.preventDefault();
    }
  }
  onDragStart(e: Event) {
    if (this.tipEvent() === 'hover' && this.isMobileDevice()) {
      e.preventDefault();
    }
  }

  // temporarily suppress text selection and long-press callouts on origin
  private applyPressPrevention() {
    if (this.pressPreventionApplied) return;
    const el = this.el.nativeElement;
    this.previousUserSelect = el.style.userSelect;
    this.previousTouchAction = el.style.touchAction;
    this.previousTouchCallout = el.style.getPropertyValue('-webkit-touch-callout');
    el.style.userSelect = 'none';
    el.style.touchAction = 'manipulation';
    el.style.setProperty('-webkit-touch-callout', 'none');
    this.pressPreventionApplied = true;
  }

  private clearPressPrevention() {
    if (!this.pressPreventionApplied) return;
    const el = this.el.nativeElement;
    if (this.previousUserSelect !== undefined) el.style.userSelect = this.previousUserSelect;
    else el.style.removeProperty('user-select');

    if (this.previousTouchAction !== undefined) el.style.touchAction = this.previousTouchAction;
    else el.style.removeProperty('touch-action');

    if (this.previousTouchCallout !== undefined) el.style.setProperty('-webkit-touch-callout', this.previousTouchCallout);
    else el.style.removeProperty('-webkit-touch-callout');

    this.pressPreventionApplied = false;
  }

  // pointer-based long-press handling for mobile
  onPointerDown(e: PointerEvent) {
    if (this.tipEvent() !== 'hover') return;
    if (!this.isMobileDevice()) return;
    if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return;

    // suppress OS text selection/callout during long-press
    e.preventDefault();
    this.applyPressPrevention();

    this.isPressing = true;
    this.clearLongPress();
    this.longPressTimer = setTimeout(() => {
      if (this.isPressing) {
        this.show();
      }
    }, this.longPressThreshold) as unknown;
  }

  onPointerUp() {
    if (this.tipEvent() !== 'hover') return;
    if (!this.isMobileDevice()) return;
    this.isPressing = false;
    this.clearLongPress();
    this.clearPressPrevention();
  }

  onPointerCancel() {
    if (this.tipEvent() !== 'hover') return;
    if (!this.isMobileDevice()) return;
    this.isPressing = false;
    this.clearLongPress();
    this.clearPressPrevention();
  }

  private clearLongPress() {
    if (this.longPressTimer) {
      clearTimeout(this.longPressTimer as number);
      this.longPressTimer = undefined;
    }
  }

  show(anchor?: ElementRef | Element) {
    this.clearSchedule();
    this.clearHideTimeout();
    this.timeoutId = setTimeout(() => {
      const originElement = anchor
        ? (anchor instanceof ElementRef ? anchor.nativeElement : anchor)
        : this.el.nativeElement;
      const tooltipContent = this.tipContent();
      const portal = new ComponentPortal(FastTipContainerComponent);

      const positionStrategy = this.overlay
        .position()
        .flexibleConnectedTo(originElement)
        .withPositions([this.getOverlayPosition(), this.getOverlayFallbackPosition()])
        .withPush(this.pushTooltip());
      ;

      const overlayRef = this.overlay.create({ positionStrategy: positionStrategy });
      FastTipDirective.activeOverlays.push(overlayRef);
      this.lastOverlayRef = overlayRef; // keep reference to latest
      const componentRef = overlayRef.attach(portal);
      componentRef.instance.content.set(tooltipContent);
      componentRef.instance.context.set(this.tipContext());
      componentRef.instance.message.set(this.message());
      componentRef.instance.bg.set(this.tipBackground());
      componentRef.instance.pushTooltip.set(this.pushTooltip());
      componentRef.instance.id.set(this.tooltipId);

      // Subscribe to container mouse events
      componentRef.instance.containerMouseEnter.subscribe(() => {
        this.clearHideTimeout();
      });

      componentRef.instance.containerMouseLeave.subscribe(() => {
        this.hide();
      });

      // Subscribe to position changes to get the actual position used
      positionStrategy.positionChanges.pipe(takeUntil(this.destroyed)).subscribe(change => {
        const actualPosition = this.getPositionFromConnectedPosition(change.connectionPair);
        componentRef.instance.position.set(actualPosition);
      });

      this.listenToEscape();

      // enable outside click/tap dismiss for all devices
      this.suppressNextOutsideCheck = true;
      this.enableOutsideDismiss();
      setTimeout(() => (this.suppressNextOutsideCheck = false), 0);
    }, this.tipDelay()) as unknown;
  }

  hide() {
    this.escapeSub?.unsubscribe();
    this.clearSchedule();
    this.clearHideTimeout();
    this.disableOutsideDismiss();
    this.isPressing = false;
    this.clearLongPress();
    this.clearPressPrevention();

    FastTipDirective.activeOverlays
      .filter(overlay => overlay.hasAttached())
      .forEach(overlay => {
        overlay.detach();
        overlay = undefined;
      });
    FastTipDirective.activeOverlays = [];
    this.lastOverlayRef = undefined;
  }

  private enableOutsideDismiss() {
    this.disableOutsideDismiss();
    this.outsideSub = fromEvent<PointerEvent>(this.document, 'pointerdown').subscribe((event) => {
      if (this.suppressNextOutsideCheck)
        return;

      const target = event.target as Node | null;
      if (!target) {
        this.hide();
        return;
      }
      // If click/tap is inside any active overlay, ignore. Otherwise hide.
      const insideAnyOverlay = FastTipDirective.activeOverlays.some(ref =>
        !!ref && !!ref.overlayElement && ref.overlayElement.contains(target)
      );
      if (!insideAnyOverlay)
        // Clicking the origin counts as outside by requirement; nothing extra to check.
        this.hide();
    });
  }

  private disableOutsideDismiss() {
    this.outsideSub?.unsubscribe();
    this.outsideSub = undefined;
  }

  private scheduleHide() {
    this.clearHideTimeout();
    this.hideTimeoutId = setTimeout(() => {
      this.hide();
    }) as unknown;
  }

  private clearHideTimeout() {
    if (this.hideTimeoutId) {
      clearTimeout(this.hideTimeoutId as number);
      this.hideTimeoutId = undefined;
    }
  }

  private offset = 6;
  private connectedPositions = {
    top: { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -this.offset } as ConnectedPosition,
    right: { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: this.offset } as ConnectedPosition,
    bottom: { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: this.offset } as ConnectedPosition,
    left: { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -this.offset } as ConnectedPosition
  }

  private getOverlayPosition(): ConnectedPosition {
    const tooltipPosition = this.tipPosition();

    switch (tooltipPosition) {
      case 'top':
        return this.connectedPositions.top;
      case 'right':
        return this.connectedPositions.right;
      case 'bottom':
        return this.connectedPositions.bottom;
      case 'left':
        return this.connectedPositions.left;
    }
  }

  private getOverlayFallbackPosition(): ConnectedPosition {
    const tooltipPosition = this.tipPosition();
    switch (tooltipPosition) {
      case 'top':
        return this.connectedPositions.bottom;
      case 'right':
        return this.connectedPositions.left;
      case 'bottom':
        return this.connectedPositions.top;
      case 'left':
        return this.connectedPositions.right;
    }
  }

  private getPositionFromConnectedPosition(connectionPair: ConnectedPosition): TOOLTIP_POSITIONS {
    // Compare the connection pair with our known positions to determine which one was used
    const positions = this.connectedPositions;

    if (this.isPositionMatch(connectionPair, positions.top)) {
      return 'top';
    } else if (this.isPositionMatch(connectionPair, positions.right)) {
      return 'right';
    } else if (this.isPositionMatch(connectionPair, positions.bottom)) {
      return 'bottom';
    } else if (this.isPositionMatch(connectionPair, positions.left)) {
      return 'left';
    }

    // Fallback to original position if no match found
    return this.tipPosition();
  }

  private isPositionMatch(pos1: ConnectedPosition, pos2: ConnectedPosition): boolean {
    return pos1.originX === pos2.originX &&
      pos1.originY === pos2.originY &&
      pos1.overlayX === pos2.overlayX &&
      pos1.overlayY === pos2.overlayY;
  }

  private clearSchedule() {
    if (this.timeoutId) {
      clearTimeout(this.timeoutId as number);
      this.timeoutId = undefined;
    }
  }

  private listenToEscape() {
    this.escapeSub?.unsubscribe();
    this.escapeSub = fromEvent<KeyboardEvent>(
      this.document,
      'keydown'
    ).subscribe((event) => {
      if (event.key === 'Escape') {
        this.hide();
      }
    });
  }

  ngOnDestroy(): void {
    this.clearSchedule();
    this.escapeSub?.unsubscribe();
    this.disableOutsideDismiss();
    this.isPressing = false;
    this.clearLongPress();
    this.clearPressPrevention();
    this.destroyed.next();
    this.destroyed.complete();
  }
}
