"use client";

/**
 * Animated number that counts up when it enters the viewport.
 * - The FINAL value is the reliable resting state: it is rendered on the
 *   server / before JS hydrates and under prefers-reduced-motion, so the number
 *   NEVER persists at 0 (no "$0M+" looking-broken state, even with no JS).
 * - When motion is allowed and the element is below the fold, the animation
 *   resets to 0 once and counts up as it scrolls into view. If the element is
 *   already in (or above) the viewport on mount, it animates immediately.
 * - Motion `animate()` drives a raw value; we format with prefix/suffix/decimals.
 * - Real number text stays in the DOM for SEO / screen readers.
 */
import { useEffect, useRef, useState } from "react";
import { animate, useReducedMotion } from "motion/react";
import { EASE_OUT } from "./motion";

export default function CountUp({
  to,
  prefix = "",
  suffix = "",
  decimals = 0,
  duration = 1.8,
}: {
  to: number;
  prefix?: string;
  suffix?: string;
  decimals?: number;
  duration?: number;
}) {
  const ref = useRef<HTMLSpanElement>(null);
  const reduce = useReducedMotion();
  // Resting state = the FINAL value. SSR + no-JS + reduced-motion all show this.
  const [val, setVal] = useState(to);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    // Reduced motion: leave the final value in place, never animate.
    if (reduce) {
      setVal(to);
      return;
    }

    let controls: ReturnType<typeof animate> | null = null;
    let started = false;

    const run = () => {
      if (started) return;
      started = true;
      // Reset to 0 only at the moment we start, so the count-up is visible —
      // but we never SIT at 0 (the value was the final number until now).
      setVal(0);
      controls = animate(0, to, {
        duration,
        ease: EASE_OUT,
        onUpdate: (v) => setVal(v),
      });
    };

    // Trigger on mount if already in or above the viewport (handles refresh
    // mid-page, anchor jumps, and SSR-then-hydrate where the section is visible).
    const rect = el.getBoundingClientRect();
    const vh = window.innerHeight || document.documentElement.clientHeight;
    if (rect.top < vh * 0.85) {
      run();
      return () => controls?.stop();
    }

    // Otherwise wait until it scrolls into view.
    const io = new IntersectionObserver(
      (entries) => {
        if (entries.some((e) => e.isIntersecting)) {
          run();
          io.disconnect();
        }
      },
      { rootMargin: "0px 0px -15% 0px" }
    );
    io.observe(el);

    return () => {
      io.disconnect();
      controls?.stop();
    };
  }, [to, duration, reduce]);

  const display = val.toLocaleString("en-US", {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals,
  });

  return (
    <span ref={ref}>
      {prefix}
      {display}
      {suffix}
    </span>
  );
}
