/**
 * useAdFreeUnlock — drives the "Watch N rewarded ads → unlock M hours
 * ad-free" feature.
 *
 * Wires together three sources of truth:
 *
 *  1. AdMob rewarded lifecycle events (`onRewardedEvent`) — counts a
 *     click / reward earned. Each one bumps the in-memory + localStorage
 *     counter by 1. The default threshold is 2 (`adsToWatch=2`).
 *
 *  2. localStorage (`nadi_ad_free_until` ISO timestamp) — fast read so
 *     guests and offline app starts can immediately suppress ads.
 *
 *  3. The backend (`/api/user/grant-ad-free` POST) — for logged-in
 *     users, the server is authoritative. The hook calls
 *     `apiGrantAdFree(hours)` once the counter hits the threshold, then
 *     refreshes the auth context so `user.ad_free_until` propagates to
 *     every component that reads `useEntitlement().adsEnabled`.
 *
 * BUG-5 FEATURE (2026-05).
 */
import { useCallback, useEffect, useRef, useState } from "react";
import { onRewardedEvent, showRewarded } from "@/lib/admob";
import { useAuth } from "@/hooks/useAuth";
import { apiGrantAdFree, apiRecordAdWatched, apiAdFreeStatus } from "@/lib/nadiAuth";
import { useToast } from "@/hooks/use-toast";

import { Capacitor } from "@capacitor/core";

const isNative = Capacitor.isNativePlatform();
const LS_KEY = "nadi_ad_free_until";

function apiBase(): string {
  // @ts-expect-error Vite inlines import.meta.env at build time
  return import.meta.env.VITE_BACKEND_URL
    // @ts-expect-error
    || import.meta.env.REACT_APP_BACKEND_URL
    || (globalThis as { REACT_APP_BACKEND_URL?: string }).REACT_APP_BACKEND_URL
    || "";
}

function readLocalAdFreeUntil(): string | null {
  try {
    const v = localStorage.getItem(LS_KEY);
    if (!v) return null;
    // Auto-purge expired so we don't pollute storage.
    const t = Date.parse(v);
    if (!Number.isFinite(t)) return null;
    if (t - Date.now() <= 0) {
      localStorage.removeItem(LS_KEY);
      return null;
    }
    return v;
  } catch {
    return null;
  }
}

function writeLocalAdFreeUntil(iso: string): void {
  try { localStorage.setItem(LS_KEY, iso); } catch { /* quota / SSR */ }
}

/**
 * The hook return surface.
 *
 *  • `clicks`         — current progress (0..adsToWatch).
 *  • `adsToWatch`     — threshold (2 by default).
 *  • `unlocked`       — true while the granted window is still active.
 *  • `secondsRemaining` — ad-free time left, in seconds. Updates every
 *    1s while > 0.
 *  • `watchAd()`      — fires a rewarded video; resolves to the new
 *    click count.
 *  • `reset()`        — manually clears progress + active window. Used
 *    by admin tools / dev menus.
 */
export interface AdFreeUnlockState {
  clicks: number;
  adsToWatch: number;
  unlocked: boolean;
  secondsRemaining: number;
  watchAd: () => Promise<number>;
  reset: () => void;
  loading: boolean;
}

export function useAdFreeUnlock(
  opts: { adsToWatch?: number; hoursPerUnlock?: number } = {},
): AdFreeUnlockState {
  const adsToWatch    = Number.isFinite(opts.adsToWatch) && (opts.adsToWatch as number) > 0
    ? (opts.adsToWatch as number)
    : 3;
  const hoursPerUnlock = Number.isFinite(opts.hoursPerUnlock) && (opts.hoursPerUnlock as number) > 0
    ? (opts.hoursPerUnlock as number)
    : 1;
  const { user, refresh } = useAuth();
  const { toast } = useToast();

  // BUG FIX (2026-01): explicit `useState<number>(0)` ensures the
  // counter NEVER lands on undefined / NaN on first paint. Tracked
  // a report of the UI rendering "(NaN/3)" on cold start; defensive
  // typing here guarantees the initial value is a finite integer.
  const [clicks, setClicks] = useState<number>(0);
  const [adFreeUntil, setAdFreeUntil] = useState<string | null>(
    () => user?.ad_free_until || readLocalAdFreeUntil(),
  );
  const [now, setNow] = useState<number>(() => Date.now());
  const [loading, setLoading] = useState(false);
  const grantingRef = useRef(false);

  // Sync from auth context whenever the user record changes (login,
  // /auth/me refresh, etc.). Server is authoritative for logged-in
  // users; we mirror it to localStorage so the next cold start is
  // instant.
  useEffect(() => {
    const server = user?.ad_free_until;
    if (server) {
      setAdFreeUntil(server);
      writeLocalAdFreeUntil(server);
    } else {
      // Fall back to local (covers guests).
      setAdFreeUntil(readLocalAdFreeUntil());
    }
  }, [user?.ad_free_until]);

  // Hydrate the persisted ad counter on login / cold start so the
  // progress bar (e.g. 2/3) reflects the server-side `user_ads_count`.
  useEffect(() => {
    if (!user) { setClicks(0); return; }
    let cancelled = false;
    void apiAdFreeStatus()
      .then((s) => {
        if (!cancelled && typeof s.ads_count === "number") {
          setClicks(Math.min(adsToWatch, Math.max(0, s.ads_count)));
        }
      })
      .catch(() => undefined);
    return () => { cancelled = true; };
  }, [user?.id, adsToWatch]);

  // Tick "now" every second while a window is active — drives the
  // countdown UI without re-rendering the whole tree.
  useEffect(() => {
    if (!adFreeUntil) return;
    const tEnd = Date.parse(adFreeUntil);
    if (!Number.isFinite(tEnd) || tEnd <= Date.now()) {
      setAdFreeUntil(null);
      try { localStorage.removeItem(LS_KEY); } catch {}
      return;
    }
    const id = window.setInterval(() => setNow(Date.now()), 1000);
    return () => window.clearInterval(id);
  }, [adFreeUntil]);

  const secondsRemaining = adFreeUntil
    ? Math.max(0, Math.floor((Date.parse(adFreeUntil) - now) / 1000))
    : 0;
  const unlocked = secondsRemaining > 0;

  // Internal: persist a grant. For logged-in users hit the backend;
  // for guests, write to localStorage only.
  const grantAdFree = useCallback(async () => {
    if (grantingRef.current) return;
    grantingRef.current = true;
    setLoading(true);
    try {
      if (user) {
        const resp = await apiGrantAdFree(hoursPerUnlock);
        const iso = resp?.ad_free_until || null;
        if (iso) {
          setAdFreeUntil(iso);
          writeLocalAdFreeUntil(iso);
        }
        // Refresh the auth context so every consumer of
        // useEntitlement() sees the new ad_free_until.
        try { await refresh(); } catch { /* network */ }
      } else {
        // Guest fallback — local only.
        const until = new Date(Date.now() + hoursPerUnlock * 3600 * 1000).toISOString();
        setAdFreeUntil(until);
        writeLocalAdFreeUntil(until);
      }
      toast({
        title: `🎉 ${hoursPerUnlock} hours ad-free unlocked!`,
        description: "Enjoy uninterrupted streaming.",
      });
      setClicks(0);
    } catch (e) {
      console.warn("[useAdFreeUnlock] grantAdFree failed:", e);
      toast({
        title: "Could not save your reward",
        description: "Please check your connection and try again.",
        variant: "destructive",
      });
    } finally {
      grantingRef.current = false;
      setLoading(false);
    }
  }, [user, hoursPerUnlock, refresh, toast]);

  // Dedupe guard — AdMob can emit BOTH `clicked` and `rewarded` for a
  // single ad. Without this they'd double-count (and, server-side,
  // double-increment). Ignore a second tick within 2s of the first.
  const lastTickRef = useRef(0);

  /** Record ONE watched ad. Logged-in → server-authoritative counter
   *  (`/api/user/ad-watched`, persists + grants at threshold). Guest →
   *  local counter + local grant. */
  const recordTick = useCallback(async () => {
    if (user) {
      try {
        const resp = await apiRecordAdWatched(adsToWatch, hoursPerUnlock);
        // Server owns the count: after a grant it resets to 0.
        setClicks(resp.granted ? 0 : resp.ads_count);
        if (resp.granted && resp.ad_free_until) {
          setAdFreeUntil(resp.ad_free_until);
          writeLocalAdFreeUntil(resp.ad_free_until);
          await refresh().catch(() => undefined);
          toast({
            title: `🎉 ${hoursPerUnlock} ${hoursPerUnlock === 1 ? "hour" : "hours"} ad-free unlocked!`,
            description: "Enjoy uninterrupted streaming.",
          });
        }
      } catch (e) {
        // Offline / server error — fall back to a local increment so
        // the user still sees progress; the server reconciles next tick.
        console.warn("[useAdFreeUnlock] ad-watched failed, local fallback:", e);
        setClicks((prev) => Math.min(adsToWatch, (Number.isFinite(prev) ? prev : 0) + 1));
      }
    } else {
      // Guest — local only.
      setClicks((prev) => {
        const safePrev = Number.isFinite(prev) ? prev : 0;
        const next = Math.min(adsToWatch, safePrev + 1);
        if (next >= adsToWatch) {
          queueMicrotask(() => { void grantAdFree(); });
        }
        return next;
      });
    }
  }, [user, adsToWatch, hoursPerUnlock, refresh, toast, grantAdFree]);

  // Subscribe to the AdMob rewarded event bus — count every click /
  // reward as a tick of progress (deduped).
  useEffect(() => {
    const off = onRewardedEvent((evt) => {
      if (evt !== "clicked" && evt !== "rewarded") return;
      const ts = Date.now();
      if (ts - lastTickRef.current < 2000) return; // same ad → ignore 2nd event
      lastTickRef.current = ts;
      console.log(`[useAdFreeUnlock] reward tick (${evt})`);
      void recordTick();
    });
    return off;
  }, [recordTick]);

  // Public action: watch a rewarded ad. If the ad doesn't fill / the
  // user dismisses early, the click count stays put — we ONLY tick on
  // a genuine `clicked` or `rewarded` event from the SDK.
  //
  // BUG FIX (iter-5): the previous version called showRewarded() and
  // silently swallowed the null return path — meaning the user tapped
  // the button, the SDK said "no ad available / no rewarded_id / not
  // native runtime / load failed", and the UI gave ZERO feedback.
  // The button looked unresponsive. We now surface every failure mode
  // with a clear toast so the user knows whether to wait, switch to
  // mobile, or contact support.
  const watchAd = useCallback(async (): Promise<number> => {
    if (unlocked) return clicks;

    // Non-native runtime (web / desktop preview) — the AdMob SDK is a
    // no-op here. Tell the user explicitly instead of looking broken.
    if (!isNative) {
      toast({
        title: "Available in the mobile app",
        description:
          "Rewarded ads only run on the Android app. Open NadiPlayer on your phone to unlock ad-free hours.",
      });
      return clicks;
    }

    try {
      setLoading(true);
      const result = await showRewarded(apiBase());
      // Either a genuine reward came back, or one of the SDK lifecycle
      // events (clicked / rewarded) already ticked the counter via the
      // listener above. If we got back null AND no listener event has
      // fired in the meantime, surface a toast so the tap isn't silent.
      if (result === null) {
        toast({
          title: "Ad not available right now",
          description:
            "Please try again in a moment. If this keeps happening, contact support.",
          variant: "destructive",
        });
      }
    } catch (e) {
      console.warn("[useAdFreeUnlock] watchAd failed:", e);
      toast({
        title: "Could not show the ad",
        description: e instanceof Error ? e.message : "Unknown error.",
        variant: "destructive",
      });
    } finally {
      setLoading(false);
    }
    return clicks;
  }, [unlocked, clicks, toast]);

  const reset = useCallback(() => {
    setClicks(0);
    setAdFreeUntil(null);
    try { localStorage.removeItem(LS_KEY); } catch {}
  }, []);

  return { clicks, adsToWatch, unlocked, secondsRemaining, watchAd, reset, loading };
}

/** Format a "Xh Ym" countdown string. */
export function formatRemaining(seconds: number): string {
  if (seconds <= 0) return "0m";
  const h = Math.floor(seconds / 3600);
  const m = Math.floor((seconds % 3600) / 60);
  const s = seconds % 60;
  if (h > 0) return `${h}h ${m}m`;
  if (m > 0) return `${m}m ${s}s`;
  return `${s}s`;
}
