import { useLayoutEffect, useState } from 'react'

/**
 * Options for useDynamicTabWidths hook
 */
export interface UseDynamicTabWidthsOptions {
  /** Ref to the container element holding the tab elements */
  containerRef: React.RefObject<HTMLElement | null>
  /** CSS class name used to identify tab elements */
  tabClassName: string
  /** Whether the measurement system is enabled. Defaults to true. */
  enabled?: boolean
}

/**
 * Return value from useDynamicTabWidths hook
 */
export interface UseDynamicTabWidthsReturn {
  /**
   * A counter that increments whenever tab widths are recalculated.
   * Use this in dependency arrays to trigger effects when measurements change.
   */
  measurementKey: number
}

/**
 * Safely parse CSS padding values, returning 0 for invalid values.
 * This prevents NaN errors from malformed CSS.
 */
function parsePaddingValue(value: string): number {
  const parsedValue = Number.parseFloat(value)
  return Number.isNaN(parsedValue) ? 0 : parsedValue
}

/**
 * Calculates tab width with bold font compensation.
 *
 * The 0.28px constant compensates for font-weight difference:
 * - Empirically measured: average bold glyph is ~0.28px wider per character
 * - Prevents visual jump when tab becomes active (normal → bold font)
 * - Applied as: originalWidth + (characterCount × 0.28px)
 *
 * @param element - The tab element to measure
 * @param originalWidth - The current rendered width of the element
 * @returns The calculated width including bold font compensation
 */
function calculateBoldCompensatedWidth(
  element: HTMLElement,
  originalWidth: number,
): number {
  const text = element.innerText ?? element.textContent ?? ''
  const BOLD_COMPENSATION_PER_CHAR = 0.28

  let width = originalWidth + text.length * BOLD_COMPENSATION_PER_CHAR

  // Adjust width if using content-box (subtract padding from measurement)
  const computedStyles = window.getComputedStyle(element)
  if (computedStyles.getPropertyValue('box-sizing') === 'content-box') {
    width -=
      parsePaddingValue(computedStyles.getPropertyValue('padding-left')) +
      parsePaddingValue(computedStyles.getPropertyValue('padding-right'))
  }

  return width
}

/**
 * Custom hook for dynamic tab width measurement.
 *
 * Manages tab widths to prevent layout shifts when content changes (e.g., tab counts updating).
 * Implements a dual-observer system to detect and respond to content changes.
 *
 * @example
 * ```tsx
 * const { measurementKey } = useDynamicTabWidths({
 *   containerRef: tabsContainerRef,
 *   tabClassName: 'tab-link',
 *   enabled: true,
 * })
 *
 * useLayoutEffect(() => {
 *   // Realign active bar when measurements change
 *   alignActiveBar()
 * }, [measurementKey])
 * ```
 *
 * ## Problem Solved
 * Tab labels can change dynamically (e.g., "Images (5)" → "Images (125)"),
 * causing the active highlight bar to become misaligned because tab widths change.
 *
 * ## Solution
 * This hook implements a comprehensive measurement system that:
 * 1. Measures each tab's width accounting for bold font weight when active
 * 2. Watches for any changes using ResizeObserver and MutationObserver
 * 3. Triggers realignment by incrementing measurementKey when widths change
 *
 * ## Performance
 * Uses requestAnimationFrame to batch measurements and avoid layout thrashing.
 * Includes guard against infinite loops when setting width to 'auto' for measurement.
 */
export function useDynamicTabWidths({
  containerRef,
  tabClassName,
  enabled = true,
}: UseDynamicTabWidthsOptions): UseDynamicTabWidthsReturn {
  const [measurementKey, setMeasurementKey] = useState(0)

  useLayoutEffect(() => {
    // Skip if disabled or no container
    if (!enabled) {
      return undefined
    }

    const container = containerRef.current

    // Early exit if container doesn't exist or we're in SSR
    if (!container || typeof window === 'undefined') {
      return undefined
    }

    // Track pending animation frame to allow cancellation
    let scheduledFrameId: number | null = null
    // Flag to prevent infinite measurement loops when ResizeObserver fires during measurement
    let isMeasurementInProgress = false

    /**
     * Measures and applies fixed widths to all tab elements.
     *
     * Algorithm:
     * 1. Check if already measuring to prevent infinite loops
     * 2. Reset to 'auto' width to get accurate natural measurement
     * 3. Get current rendered width via getBoundingClientRect
     * 4. Add buffer for bold font (0.28px per character)
     * 5. Adjust for box-sizing: content-box if needed (subtract padding)
     * 6. Apply width only if changed (prevents unnecessary style updates)
     * 7. Increment measurement key to trigger active bar realignment
     */
    const applyWidths = () => {
      // Prevent infinite loop: ResizeObserver can trigger when we set width to 'auto'
      if (isMeasurementInProgress) {
        return
      }

      isMeasurementInProgress = true

      const tabElements = Array.from(
        container.getElementsByClassName(tabClassName),
      )
      let anyWidthsChanged = false

      tabElements.forEach((element) => {
        // Skip non-HTML elements (shouldn't happen, but type-safe)
        if (!(element instanceof HTMLElement)) {
          return
        }

        // Reset to 'auto' to measure natural width accurately
        element.style.width = 'auto'
        element.style.maxWidth = 'none'

        // Get the current rendered width of the tab
        const { width: originalWidth } = element.getBoundingClientRect()

        // Calculate width with bold font compensation
        const width = calculateBoldCompensatedWidth(element, originalWidth)

        // Only update styles if they've changed (prevents unnecessary reflows)
        const widthString = `${width}px`
        if (
          element.style.width !== widthString ||
          element.style.maxWidth !== widthString
        ) {
          element.style.width = widthString
          element.style.maxWidth = widthString
          anyWidthsChanged = true
        }
      })

      // Trigger active bar realignment if any widths changed
      if (anyWidthsChanged) {
        setMeasurementKey((value) => value + 1)
      }

      isMeasurementInProgress = false
    }

    /**
     * Schedules a width measurement on the next animation frame.
     * Uses requestAnimationFrame to batch multiple measurement requests
     * and prevent layout thrashing from rapid consecutive calls.
     */
    const scheduleWidthsMeasurement = () => {
      // Cancel any pending measurement to avoid duplicate work
      if (scheduledFrameId) {
        cancelAnimationFrame(scheduledFrameId)
      }

      // Schedule measurement on next animation frame
      scheduledFrameId = requestAnimationFrame(() => {
        scheduledFrameId = null
        applyWidths()
      })
    }

    // Perform initial measurement
    scheduleWidthsMeasurement()

    /**
     * DUAL OBSERVER SYSTEM
     *
     * ResizeObserver (Primary):
     * - Detects tab content changes (e.g., count updates "5" → "125")
     * - Optimized for layout changes by the browser
     * - May miss certain DOM mutations that don't immediately trigger resize
     *
     * MutationObserver (Safety Net):
     * - Catches tabs added/removed (childList)
     * - Detects text content changes (characterData)
     * - Handles structural changes (subtree)
     * - Catches edge cases where ResizeObserver fires inconsistently
     *
     * Why not just MutationObserver?
     * - Less efficient (requires manual measurement on every mutation)
     * - ResizeObserver is optimized for layout changes
     *
     * Why not just ResizeObserver?
     * - Certain mutations don't trigger resize immediately
     * - Text changes may not fire ResizeObserver consistently
     * - Need explicit detection for structural changes
     *
     * Together: Comprehensive coverage with minimal redundancy
     * RAF batching prevents duplicate measurements even with both observers
     */

    /**
     * ResizeObserver: Watches for size changes on individual tab elements.
     * Triggers when tab content changes (e.g., count updates in tab labels).
     * Gracefully degrades if ResizeObserver is not supported (older browsers).
     */
    const resizeObserver =
      typeof ResizeObserver !== 'undefined'
        ? new ResizeObserver(() => {
            scheduleWidthsMeasurement()
          })
        : null

    /**
     * Attaches ResizeObserver to all current tab link elements.
     * Called initially and whenever DOM structure changes.
     */
    const observeCurrentLinks = () => {
      if (!resizeObserver) {
        return
      }

      Array.from(container.getElementsByClassName(tabClassName)).forEach(
        (link) => resizeObserver.observe(link),
      )
    }

    // Start observing initial set of tabs
    observeCurrentLinks()

    /**
     * MutationObserver: Watches for DOM changes within the tabs container.
     * Detects when:
     * - Tabs are added/removed (childList)
     * - Tab structure changes (subtree)
     * - Tab text content changes (characterData)
     *
     * When changes occur, re-attach observers to new elements and remeasure.
     */
    const mutationObserver =
      typeof MutationObserver !== 'undefined'
        ? new MutationObserver(() => {
            observeCurrentLinks() // Re-observe any new tab elements
            scheduleWidthsMeasurement() // Recalculate widths
          })
        : null

    // Start watching for DOM mutations
    mutationObserver?.observe(container, {
      childList: true, // Watch for added/removed nodes
      subtree: true, // Watch entire subtree, not just direct children
      characterData: true, // Watch for text content changes
    })

    // Also watch for window resize (viewport size changes)
    window.addEventListener('resize', scheduleWidthsMeasurement)

    /**
     * Cleanup function: Runs when component unmounts or dependencies change.
     * Prevents memory leaks by disconnecting all observers and event listeners.
     */
    return () => {
      if (scheduledFrameId) {
        cancelAnimationFrame(scheduledFrameId)
      }

      resizeObserver?.disconnect()
      mutationObserver?.disconnect()
      window.removeEventListener('resize', scheduleWidthsMeasurement)
    }
  }, [containerRef, tabClassName, enabled])

  return { measurementKey }
}
