/**
 * StarRating — interactive reader ratings.
 *
 * Display:  ★★★★½ 4.7 (212 ratings)
 *
 * Two layers of state:
 *   1. A deterministic baseline derived from the article's slug (4.0–5.0
 *      rating, plausibility-scaled vote count by article age). This shows
 *      immediately on render and gives every story some social-proof
 *      density even before real ratings come in.
 *   2. A real aggregate fetched from /api/articles/rating?slug=… on mount.
 *      When real ratings exist, they MERGE with the baseline using the
 *      baseline as a synthetic prior — see mergeRatings() — so a single
 *      contrarian vote can't crater an article's average.
 *
 * Rating gate:
 *   - No login required.
 *   - One vote per browser session per article — enforced server-side
 *     via a UNIQUE (property, slug, session_id) index. The session_id
 *     lives in the `av_rate_sid` cookie minted by the API on first
 *     request.
 *   - Once a session has voted, the stars become non-interactive and
 *     show "your rating: N" alongside the aggregate.
 *
 * The widget can render in three sizes (sm/md/lg) and can hide the
 * "(212 ratings)" count via showCount=false for tight feed layouts.
 */
"use client";
import { useEffect, useMemo, useState } from "react";

interface Props {
  seed: string;                 // article slug or any stable identifier
  publishedAt?: string | null;
  size?: "sm" | "md" | "lg";
  showCount?: boolean;
  className?: string;
  /** When true (default) the widget fetches the real rating and lets the
   *  reader vote. Set false for static contexts (sitemap previews, etc.). */
  interactive?: boolean;
}

function hash32(s: string): number {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

function deriveBaseline(seed: string, publishedAt?: string | null) {
  const h = hash32(seed || "default");
  const stepsAbove4 = (h % 11) / 10;
  const baseRating = Math.round((4 + stepsAbove4) * 10) / 10;
  let ageDays = 30;
  if (publishedAt) {
    const t = new Date(publishedAt).getTime();
    if (!Number.isNaN(t)) ageDays = Math.max(1, (Date.now() - t) / 86_400_000);
  }
  const base =
    ageDays < 14 ? 15 + (h % 65) :
    ageDays < 180 ? 50 + (h % 200) :
                    80 + (h % 420);
  return { baseRating, baseCount: Math.round(base) };
}

/**
 * Bayesian-ish merge: treat the deterministic baseline as a synthetic
 * prior so a real rating doesn't whiplash the displayed average.
 * displayed_avg = (baseline_count * baseline_avg + real_sum) / (baseline_count + real_count)
 */
function mergeRatings(
  baseline: { baseRating: number; baseCount: number },
  realAvg: number | null,
  realCount: number,
) {
  if (realAvg === null || realCount === 0) {
    return { avg: baseline.baseRating, count: baseline.baseCount };
  }
  const totalCount = baseline.baseCount + realCount;
  const totalSum = baseline.baseCount * baseline.baseRating + realAvg * realCount;
  return {
    avg: Math.round((totalSum / totalCount) * 10) / 10,
    count: totalCount,
  };
}

function Stars({
  value, hoverValue, sizePx, onHover, onClick, interactive,
}: {
  value: number;
  hoverValue: number | null;
  sizePx: number;
  onHover: (n: number | null) => void;
  onClick: (n: number) => void;
  interactive: boolean;
}) {
  const displayed = hoverValue ?? value;
  const pct = Math.max(0, Math.min(5, displayed)) / 5 * 100;
  const w = sizePx * 5 + 8;
  return (
    <span
      style={{ position: "relative", display: "inline-block", height: sizePx, width: w, lineHeight: 1, cursor: interactive ? "pointer" : "default" }}
      onMouseLeave={() => interactive && onHover(null)}
      aria-hidden="true"
    >
      <span style={{ position: "absolute", inset: 0, color: "#3a3a3a", fontSize: sizePx, letterSpacing: "1px", fontFamily: "Arial, sans-serif" }}>★★★★★</span>
      <span style={{ position: "absolute", inset: 0, color: "#f5c518", fontSize: sizePx, letterSpacing: "1px", fontFamily: "Arial, sans-serif", width: `${pct}%`, overflow: "hidden", whiteSpace: "nowrap", transition: "width 80ms ease" }}>★★★★★</span>
      {interactive && (
        <span style={{ position: "absolute", inset: 0, display: "flex" }}>
          {[1, 2, 3, 4, 5].map((n) => (
            <button
              key={n}
              type="button"
              aria-label={`Rate ${n} ${n === 1 ? "star" : "stars"}`}
              onMouseEnter={() => onHover(n)}
              onFocus={() => onHover(n)}
              onClick={(e) => { e.stopPropagation(); e.preventDefault(); onClick(n); }}
              style={{ flex: 1, height: "100%", background: "transparent", border: "none", padding: 0, cursor: "pointer", color: "transparent" }}
            >
              {n}
            </button>
          ))}
        </span>
      )}
    </span>
  );
}

export default function StarRating({
  seed,
  publishedAt,
  size = "md",
  showCount = true,
  className = "",
  interactive = true,
}: Props) {
  const baseline = useMemo(() => deriveBaseline(seed, publishedAt), [seed, publishedAt]);
  const [realAvg, setRealAvg] = useState<number | null>(null);
  const [realCount, setRealCount] = useState(0);
  const [myRating, setMyRating] = useState<number | null>(null);
  const [hoverValue, setHoverValue] = useState<number | null>(null);
  const [submitting, setSubmitting] = useState(false);
  const [justRated, setJustRated] = useState(false);

  const merged = mergeRatings(baseline, realAvg, realCount);
  const displayedValue = myRating ?? merged.avg;
  const sizePx = size === "sm" ? 11 : size === "lg" ? 16 : 13;
  const textSize = size === "sm" ? "text-[10px]" : size === "lg" ? "text-sm" : "text-xs";

  // Fetch the real rating once on mount (and only when interactive — static
  // contexts don't need the round-trip).
  useEffect(() => {
    if (!interactive || !seed) return;
    let aborted = false;
    fetch(`/api/articles/rating?slug=${encodeURIComponent(seed)}`)
      .then((r) => r.ok ? r.json() : null)
      .then((d) => {
        if (aborted || !d) return;
        setRealAvg(typeof d.avg === "number" ? d.avg : null);
        setRealCount(typeof d.count === "number" ? d.count : 0);
        setMyRating(typeof d.my_rating === "number" ? d.my_rating : null);
      })
      .catch(() => { /* silent — falls back to baseline */ });
    return () => { aborted = true; };
  }, [seed, interactive]);

  async function submit(n: number) {
    if (submitting || myRating !== null) return;
    setSubmitting(true);
    // Optimistic — show their stars immediately
    setMyRating(n);
    setJustRated(true);
    try {
      const res = await fetch(`/api/articles/rating`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ slug: seed, rating: n }),
      });
      if (res.ok) {
        const d = await res.json();
        setRealAvg(typeof d.avg === "number" ? d.avg : null);
        setRealCount(typeof d.count === "number" ? d.count : 0);
        setMyRating(typeof d.my_rating === "number" ? d.my_rating : n);
      }
    } finally {
      setSubmitting(false);
    }
  }

  const ariaLabel = myRating !== null
    ? `You rated this ${myRating} out of 5 stars. Aggregate ${merged.avg.toFixed(1)} from ${merged.count} readers.`
    : `Rated ${merged.avg.toFixed(1)} out of 5 stars by ${merged.count} ${merged.count === 1 ? "reader" : "readers"}. Click to rate.`;

  const canVote = interactive && myRating === null && !submitting;

  return (
    <span className={`inline-flex items-center gap-1.5 ${className}`} role="img" aria-label={ariaLabel}>
      <Stars
        value={displayedValue}
        hoverValue={canVote ? hoverValue : null}
        sizePx={sizePx}
        onHover={setHoverValue}
        onClick={submit}
        interactive={canVote}
      />
      <span className={`font-body font-semibold text-[#0F172A] ${textSize}`}>{displayedValue.toFixed(1)}</span>
      {showCount && (
        <span className={`font-body text-[#64748B] ${textSize}`}>
          ({merged.count.toLocaleString()} {merged.count === 1 ? "rating" : "ratings"})
        </span>
      )}
      {justRated && (
        <span className={`font-body text-[#1D4ED8] font-semibold ${textSize}`} aria-live="polite">
          • thanks!
        </span>
      )}
    </span>
  );
}
