import {
  ConnectedPosition,
  Overlay,
  OverlayPositionBuilder,
  OverlayRef,
} from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import {
  Directive,
  effect,
  ElementRef,
  inject,
  input,
  OnDestroy,
  signal,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import { TooltipComponent } from './tooltip.component';
import { CloseOnScrollDirective } from '@isa/ui/layout';

/**
 * Defines the trigger events for the tooltip.
 */
export const TooltipTrigger = {
  Click: 'click',
  Hover: 'hover',
  Focus: 'focus',
} as const;

/**
 * Type representing the possible tooltip trigger events.
 */
export type TooltipTrigger =
  (typeof TooltipTrigger)[keyof typeof TooltipTrigger];

/**
 * Directive to attach a tooltip to a host element.
 * The tooltip can be triggered by click, hover, or focus events.
 *
 * @example
 * ```html
 * <button
 *   uiTooltip
 *   content="This is a tooltip"
 *   title="Tooltip Title"
 *   [triggerOn]="['hover']"
 * >
 *   Hover me
 * </button>
 *
 * <div uiTooltip [content]="templateRef" title="Template Tooltip">
 *  Show tooltip with template
 * </div>
 * <ng-template #templateRef>
 *  <p>This is content from a template!</p>
 * </ng-template>
 * ```
 */
@Directive({
  selector: '[uiTooltip]',
  hostDirectives: [
    {
      directive: CloseOnScrollDirective,
      outputs: ['closeOnScroll'],
    },
  ],
  host: {
    '(click)': 'onClickEvent($event)',
    '(mouseenter)': 'onMouseEnter()',
    '(mouseleave)': 'onMouseLeave()',
    '(focus)': 'onFocusEvent()',
    '(blur)': 'onBlurEvent()',
    '(closeOnScroll)': 'onScrollClose()',
    'tabindex': '0',
  },
  standalone: true,
  exportAs: 'uiTooltip',
})
export class TooltipDirective implements OnDestroy {
  #overlay = inject(Overlay);
  #elementRef = inject(ElementRef);
  #viewContainerRef = inject(ViewContainerRef);
  #positionBuilder = inject(OverlayPositionBuilder);
  #closeOnScroll = inject(CloseOnScrollDirective, { self: true });

  #overlayRef: OverlayRef | null = null;
  #tooltipInstance: TooltipComponent | null = null;
  #openTrigger: TooltipTrigger | null = null; // Tracks which trigger opened the tooltip
  #isOpen = signal(false); // Internal signal to track open state for CloseOnScrollDirective
  #isHiding = false; // Tracks if tooltip is currently animating out

  // Distance between tooltip and anchor element
  readonly #offset = 8; // 0.5rem = 8px
  // Animation duration in ms (must match CSS)
  readonly #animationDuration = 150;

  /** Optional title for the tooltip. */
  title = input<string>();
  /** Content for the tooltip. Can be a string or a TemplateRef. */
  content = input<string | TemplateRef<unknown>>();

  /**
   * Array of triggers that will cause the tooltip to show.
   * Defaults to `['click', 'hover', 'focus']`.
   */
  triggerOn = input<TooltipTrigger[]>([
    TooltipTrigger.Click,
    TooltipTrigger.Hover,
    TooltipTrigger.Focus,
  ]);

  /**
   * Visual variant of the tooltip.
   * Defaults to 'default'.
   */
  variant = input<'default' | 'warning' | 'success'>('default');

  /**
   * Preferred position of the tooltip relative to the host element.
   * Defaults to 'left'. Falls back to other positions if preferred doesn't fit.
   */
  position = input<'left' | 'right' | 'top' | 'bottom'>('left');

  constructor() {
    // Set up effects to update tooltip instance when inputs change
    effect(() => {
      const titleValue = this.title();
      if (this.#tooltipInstance && titleValue !== undefined) {
        this.#tooltipInstance.title.set(titleValue);
      }
    });

    effect(() => {
      const contentValue = this.content();
      if (this.#tooltipInstance && contentValue !== undefined) {
        this.#tooltipInstance.content.set(contentValue);
      }
    });

    effect(() => {
      const variantValue = this.variant();
      if (this.#tooltipInstance && variantValue !== undefined) {
        this.#tooltipInstance.variant.set(variantValue);
      }
    });

    // Configure CloseOnScrollDirective: activate when tooltip is open (any trigger)
    effect(() => {
      const isOpen = this.#isOpen();
      // Activate scroll-close behavior whenever tooltip is open
      this.#closeOnScroll.closeOnScrollWhen.set(isOpen);
    });
  }

  /**
   * Determines if the tooltip should be shown for a given trigger event.
   * @param trigger The trigger event to check.
   * @returns True if the tooltip should show for the trigger, false otherwise.
   */
  #shouldForTrigger(trigger: TooltipTrigger): boolean {
    return this.triggerOn().includes(trigger);
  }

  /**
   * Calculates the connected positions for the tooltip overlay.
   * Orders positions based on the preferred position input.
   * @returns An array of `ConnectedPosition` objects.
   */
  #getPositions(): ConnectedPosition[] {
    const left: ConnectedPosition = {
      originX: 'start',
      originY: 'center',
      overlayX: 'end',
      overlayY: 'center',
      offsetX: -this.#offset,
    };

    const right: ConnectedPosition = {
      originX: 'end',
      originY: 'center',
      overlayX: 'start',
      overlayY: 'center',
      offsetX: this.#offset,
    };

    const bottom: ConnectedPosition = {
      originX: 'center',
      originY: 'bottom',
      overlayX: 'center',
      overlayY: 'top',
      offsetY: this.#offset,
    };

    const top: ConnectedPosition = {
      originX: 'center',
      originY: 'top',
      overlayX: 'center',
      overlayY: 'bottom',
      offsetY: -this.#offset,
    };

    // Order positions based on preferred position
    switch (this.position()) {
      case 'right':
        return [right, left, bottom, top];
      case 'top':
        return [top, bottom, left, right];
      case 'bottom':
        return [bottom, top, left, right];
      case 'left':
      default:
        return [left, right, bottom, top];
    }
  }
  /**
   * Shows the tooltip.
   * Creates an overlay and attaches the `TooltipComponent` to it.
   * Sets the initial title and content of the tooltip.
   * If the tooltip is already visible, this method does nothing.
   */
  show(trigger?: TooltipTrigger) {
    // Don't create multiple tooltips
    if (this.#overlayRef) {
      return;
    }
    // Set the open trigger
    this.#openTrigger = trigger ?? null;

    // Create overlay positioned relative to the host element
    const positionStrategy = this.#positionBuilder
      .flexibleConnectedTo(this.#elementRef)
      .withPositions(this.#getPositions())
      .withFlexibleDimensions(false);

    // Create the overlay
    this.#overlayRef = this.#overlay.create({
      positionStrategy,
      scrollStrategy: this.#overlay.scrollStrategies.reposition(),
      hasBackdrop: false,
      panelClass: 'ui-tooltip-panel',
    });

    // Create the portal for the tooltip component
    const portal = new ComponentPortal(
      TooltipComponent,
      this.#viewContainerRef,
    );

    // Attach the portal to the overlay and initialize content
    // Effects will automatically update the instance when inputs change
    this.#tooltipInstance = this.#overlayRef.attach(portal).instance;

    // Set initial values (effects will handle subsequent updates)
    const titleValue = this.title();
    if (titleValue !== undefined) {
      this.#tooltipInstance.title.set(titleValue);
    }

    const contentValue = this.content();
    if (contentValue !== undefined) {
      this.#tooltipInstance.content.set(contentValue);
    }

    const variantValue = this.variant();
    if (variantValue !== undefined) {
      this.#tooltipInstance.variant.set(variantValue);
    }

    // Update open state for CloseOnScrollDirective
    // Set exclude element first, then activate
    this.#closeOnScroll.closeOnScrollExclude.set(
      this.#overlayRef.overlayElement,
    );
    this.#isOpen.set(true);
  }

  /**
   * Hides the tooltip only if the trigger matches the open trigger.
   * Plays fade-out animation before detaching.
   */
  hide(trigger?: TooltipTrigger) {
    if (!this.#overlayRef || this.#isHiding) {
      return;
    }

    if (
      trigger === TooltipTrigger.Click &&
      this.#openTrigger !== TooltipTrigger.Click
    ) {
      this.#openTrigger = TooltipTrigger.Click;
      return; // Do not close if the click trigger is not the one that opened it
    }

    // Treat null and undefined as equivalent for programmatic control
    const normalizedTrigger = trigger ?? null;
    const normalizedOpenTrigger = this.#openTrigger ?? null;

    if (trigger !== TooltipTrigger.Click) {
      if (normalizedOpenTrigger !== normalizedTrigger) {
        // If the tooltip is not opened by the same trigger, do not close
        return;
      }
    }

    // Start exit animation
    this.#isHiding = true;
    if (this.#tooltipInstance) {
      this.#tooltipInstance.leaving.set(true);
    }

    // Wait for animation to complete before detaching
    setTimeout(() => {
      if (this.#overlayRef) {
        this.#overlayRef.detach();
        this.#overlayRef.dispose();
        this.#overlayRef = null;
      }
      this.#tooltipInstance = null;
      this.#openTrigger = null;
      this.#isHiding = false;

      // Update open state for CloseOnScrollDirective
      this.#isOpen.set(false);
    }, this.#animationDuration);
  }

  /**
   * Toggles the visibility of the tooltip for a given trigger.
   * If the tooltip is visible, it will be hidden by the same trigger.
   * If the tooltip is hidden, it will be shown by the trigger.
   */
  toggle(trigger: TooltipTrigger) {
    if (this.#overlayRef) {
      this.hide(trigger);
    } else {
      this.show(trigger);
    }
  }

  /**
   * Handles the click event on the host element.
   * Toggles the tooltip visibility if 'click' is an active trigger.
   * @param event The mouse event.
   * @internal
   */
  onClickEvent(event: MouseEvent) {
    if (this.#shouldForTrigger(TooltipTrigger.Click)) {
      event.preventDefault();
      event.stopPropagation();
      this.toggle(TooltipTrigger.Click);
    }
  }

  /**
   * Handles the mouseenter event on the host element.
   * Shows the tooltip if 'hover' is an active trigger.
   * @internal
   */
  onMouseEnter() {
    if (this.#shouldForTrigger(TooltipTrigger.Hover)) {
      if (!this.#overlayRef) {
        this.show(TooltipTrigger.Hover);
      }
    }
  }

  /**
   * Handles the mouseleave event on the host element.
   * Hides the tooltip if 'hover' is an active trigger and it was opened by hover.
   * @internal
   */
  onMouseLeave() {
    if (this.#shouldForTrigger(TooltipTrigger.Hover)) {
      this.hide(TooltipTrigger.Hover);
    }
  }

  /**
   * Handles the focus event on the host element.
   * Shows the tooltip if 'focus' is an active trigger.
   * @internal
   */
  onFocusEvent() {
    if (this.#shouldForTrigger(TooltipTrigger.Focus)) {
      if (!this.#overlayRef) {
        this.show(TooltipTrigger.Focus);
      }
    }
  }

  /**
   * Handles the blur event on the host element.
   * Hides the tooltip if 'focus' is an active trigger and it was opened by focus.
   * @internal
   */
  onBlurEvent() {
    if (this.#shouldForTrigger(TooltipTrigger.Focus)) {
      this.hide(TooltipTrigger.Focus);
    }
  }

  /**
   * Handles the scroll close event from CloseOnScrollDirective.
   * Closes the tooltip when user scrolls while tooltip is open.
   * @internal
   */
  onScrollClose() {
    // Force close regardless of trigger since scroll should always close
    if (this.#overlayRef) {
      this.#isOpen.set(false);
      this.#closeOnScroll.closeOnScrollExclude.set(undefined);
      this.#overlayRef.detach();
      this.#overlayRef.dispose();
      this.#overlayRef = null;
      this.#tooltipInstance = null;
      this.#openTrigger = null;
    }
  }

  /**
   * Cleans up the tooltip when the directive is destroyed.
   * Ensures the tooltip is hidden to prevent memory leaks.
   */
  ngOnDestroy() {
    this.hide();
  }
}
