'use client';

import {useCallback, useEffect, useRef, useState} from 'react';
import {usePathname} from 'next/navigation';

/** Safety net: a navigation that never lands must not leave the bar stuck. */
export const NAVIGATION_TIMEOUT_MS = 8000;

/** Window event that lets programmatic navigations (router.push) light the bar. */
export const NAVIGATION_START_EVENT = 'tk:navigation-start';

/** Call right before a programmatic navigation to show the loader immediately. */
export function startNavigationLoader(): void {
  if (typeof window === 'undefined') return;
  window.dispatchEvent(new Event(NAVIGATION_START_EVENT));
}

/** How long the finished bar stays at 100% before it is removed. */
const COMPLETE_HIDE_DELAY_MS = 200;

/** usePathname() ignores the query string, so the browser URL is polled too. */
const URL_WATCH_INTERVAL_MS = 150;

/** Opt-out marker a link can carry to keep the loader silent. */
const OPT_OUT_ATTRIBUTE = 'data-no-loader';

/** Fake progress: the real duration of a navigation is unknown, so the bar
 *  creeps towards a ceiling and only reaches 100% once the route has changed. */
const INITIAL_PROGRESS_PERCENT = 30;
const PROGRESS_STEPS: ReadonlyArray<{percent: number; afterMs: number}> = [
  {percent: 60, afterMs: 300},
  {percent: 85, afterMs: 900},
];

function currentUrl(): string {
  return `${window.location.pathname}${window.location.search}`;
}

function resolveUrl(href: string | null): URL | null {
  if (!href) return null;
  try {
    return new URL(href, window.location.href);
  } catch {
    return null;
  }
}

/** True when the click will make Next.js navigate to another page of this site. */
function isInternalNavigationClick(event: MouseEvent): boolean {
  if (event.defaultPrevented) return false;
  if (event.button !== 0) return false;
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;

  const eventTarget = event.target;
  if (!(eventTarget instanceof Element)) return false;

  const anchor = eventTarget.closest('a[href]');
  if (!(anchor instanceof HTMLAnchorElement)) return false;

  if (anchor.hasAttribute('download')) return false;
  if (anchor.hasAttribute(OPT_OUT_ATTRIBUTE)) return false;

  const anchorTarget = anchor.getAttribute('target');
  if (anchorTarget && anchorTarget !== '_self') return false;

  const href = anchor.getAttribute('href');
  if (href?.startsWith('#')) return false;

  const destination = resolveUrl(href);
  if (!destination) return false;
  if (destination.origin !== window.location.origin) return false;

  const isSamePage =
    destination.pathname === window.location.pathname && destination.search === window.location.search;
  if (isSamePage) return false;

  return true;
}

export default function NavigationLoader() {
  const pathname = usePathname();
  const [loading, setLoading] = useState(false);
  const [progress, setProgress] = useState(0);
  const loadingRef = useRef(false);
  const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
  const urlWatchRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const startedUrlRef = useRef('');
  const previousPathnameRef = useRef(pathname);

  const clearTimers = useCallback(() => {
    timersRef.current.forEach(clearTimeout);
    timersRef.current = [];
    if (urlWatchRef.current !== null) {
      clearInterval(urlWatchRef.current);
      urlWatchRef.current = null;
    }
  }, []);

  const schedule = useCallback((task: () => void, delay: number) => {
    timersRef.current.push(setTimeout(task, delay));
  }, []);

  const reset = useCallback(() => {
    clearTimers();
    loadingRef.current = false;
    setLoading(false);
    setProgress(0);
  }, [clearTimers]);

  const finish = useCallback(() => {
    clearTimers();
    setProgress(100);
    schedule(reset, COMPLETE_HIDE_DELAY_MS);
  }, [clearTimers, reset, schedule]);

  const start = useCallback(() => {
    clearTimers();
    loadingRef.current = true;
    startedUrlRef.current = currentUrl();
    setLoading(true);
    setProgress(INITIAL_PROGRESS_PERCENT);
    PROGRESS_STEPS.forEach((step) => schedule(() => setProgress(step.percent), step.afterMs));
    schedule(reset, NAVIGATION_TIMEOUT_MS);
    urlWatchRef.current = setInterval(() => {
      if (currentUrl() !== startedUrlRef.current) finish();
    }, URL_WATCH_INTERVAL_MS);
  }, [clearTimers, finish, reset, schedule]);

  useEffect(() => {
    const handleDocumentClick = (event: MouseEvent) => {
      if (!isInternalNavigationClick(event)) return;
      start();
    };

    document.addEventListener('click', handleDocumentClick, true);
    window.addEventListener(NAVIGATION_START_EVENT, start);
    return () => {
      document.removeEventListener('click', handleDocumentClick, true);
      window.removeEventListener(NAVIGATION_START_EVENT, start);
    };
  }, [start]);

  useEffect(() => {
    if (pathname === previousPathnameRef.current) return;
    previousPathnameRef.current = pathname;
    if (!loadingRef.current) return;
    finish();
  }, [pathname, finish]);

  // clearTimers is stable, so this only runs on unmount.
  useEffect(() => clearTimers, [clearTimers]);

  if (!loading) return null;

  return (
    <div
      data-testid="navigation-loader"
      aria-hidden="true"
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        width: `${progress}%`,
        height: '3px',
        backgroundColor: 'var(--tk-orange)',
        transition: 'width 200ms ease',
        zIndex: 9999,
      }}
    />
  );
}
