/**
 * AdMob service (Capacitor). A thin wrapper over
 * @capacitor-community/admob that:
 *
 *   - is safe to import on web (all methods become no-ops when the
 *     Capacitor native runtime is absent)
 *   - loads the live config from /api/public/ad-config before doing
 *     ANYTHING, so Ad Unit IDs and on/off toggle come from the admin
 *     panel — never hardcoded
 *   - exposes just the surface the app actually uses: initBanner,
 *     hideBanner, resumeBanner, prepareInterstitial, showInterstitial
 */

import { Capacitor } from "@capacitor/core";
import {
  AdMob,
  BannerAdPosition,
  BannerAdSize,
  type BannerAdOptions,
  type AdOptions,
  type RewardAdOptions,
  type AdMobRewardItem,
} from "@capacitor-community/admob";

export interface AdConfig {
  enabled: boolean;
  testing: boolean;
  app_id: string;
  banner_id: string;
  interstitial_id: string;
  native_id: string;
  rewarded_id: string;
  interstitial_every: number;
  house_ad?: HouseAd;
}

/** Admin-controlled "Nadiplayer Premium" sponsored popup — INDEPENDENT
 *  of AdMob. Returned by /api/public/ad-config. */
export interface HouseAd {
  enabled: boolean;
  title: string;
  body: string;
  /** Default / phone background image (full-bleed cover). */
  image_url: string;
  /** Optional tablet-specific background image. Falls back to image_url. */
  image_url_tablet?: string;
  /** Optional TV-specific background image. Falls back to image_url. */
  image_url_tv?: string;
  link_url: string;
  cta: string;
  skip_secs: number;
}

let houseAdCache: HouseAd | null = null;
let houseAdFetched = false;

/** Fetch the admin-controlled house-ad config (works on web + native).
 *  Cached for the session. Returns null if unavailable. */
export async function getHouseAdConfig(apiBase: string): Promise<HouseAd | null> {
  if (houseAdFetched) return houseAdCache;
  try {
    const res = await fetch(`${apiBase}/api/public/ad-config`, { cache: "no-cache" });
    if (res.ok) {
      const cfg = (await res.json()) as AdConfig;
      houseAdCache = (cfg.house_ad ?? null) as HouseAd | null;
    }
  } catch {
    houseAdCache = null;
  }
  houseAdFetched = true;
  return houseAdCache;
}

const isNative = Capacitor.isNativePlatform();
const isAndroid = Capacitor.getPlatform() === "android";

let cached: AdConfig | null = null;
let initialised = false;
let bannerVisible = false;
let interstitialReady = false;

// BUG-4 FIX (2026-05): track when a REAL AdMob interstitial is on
// screen. The PeriodicInterstitial fallback overlay subscribes to this
// so it can auto-dismiss itself if AdMob serves a real ad late (i.e.
// after the 4s timeout fired the fallback). Without this, the user
// briefly sees both the custom "Skip in 3s" panel AND the real ad,
// or — worse — the real ad behind the fallback.
type InterstitialEvent =
  | "loaded"
  | "failed"
  | "showed"
  | "dismissed"
  | "impression";
const interstitialListeners = new Set<(e: InterstitialEvent) => void>();
function emitInterstitial(e: InterstitialEvent) {
  for (const fn of interstitialListeners) {
    try { fn(e); } catch (err) { console.warn("[AdMob] interstitial listener threw:", err); }
  }
}
export function onInterstitialEvent(fn: (e: InterstitialEvent) => void): () => void {
  interstitialListeners.add(fn);
  return () => { interstitialListeners.delete(fn); };
}

async function fetchConfig(apiBase: string): Promise<AdConfig> {
  const res = await fetch(`${apiBase}/api/public/ad-config`, { cache: "no-cache" });
  if (!res.ok) throw new Error(`ad-config HTTP ${res.status}`);
  const cfg = (await res.json()) as AdConfig;

  // BUG FIX (2026-01) — Rewarded ad stuck on "Ad not available right now".
  //
  // Backstory:
  //   Iter-2: when `testing=true`, force-override every Ad Unit ID
  //           with Google universal test units → real rewarded creative
  //           never played even when admin entered a real one.
  //   Iter-3: REMOVED that override. But the production PHP backend
  //           at prmtly.com is the OLD `upgrade-kit` build that does
  //           NOT return `rewarded_id` in the JSON at all. So
  //           `cfg.rewarded_id` is `undefined` → showRewarded() bails
  //           silently → the red "Ad not available right now" toast.
  //
  // Compromise that satisfies both Iter-2 and Iter-3 use-cases:
  //   • testing = true  AND backend OMITS rewarded_id entirely →
  //     fall back to Google's universal TEST rewarded unit. This is
  //     safe because we're already in test mode, and it keeps the
  //     Watch-Ad flow functional on older backend deployments that
  //     haven't been upgraded with the new rewarded_id field yet.
  //   • testing = true  AND backend RETURNS rewarded_id (any value,
  //     including empty string) → use that value as-is. Admin made
  //     a conscious choice (empty = "disable the Watch-Ad button").
  //   • testing = false → strictly use whatever the backend says.
  //     NO fallback. If the field is missing/empty, the button
  //     no-ops with a clear console.warn — production users never
  //     see a Google test creative.
  if (cfg.testing && cfg.rewarded_id === undefined) {
    console.warn(
      "[AdMob] /api/public/ad-config did NOT return a rewarded_id field — falling back to the Google universal TEST rewarded unit so the Watch-Ad button still works while you redeploy the PHP backend. Once the new backend is deployed, this fallback will stop firing.",
    );
    cfg.rewarded_id = "ca-app-pub-3940256099942544/5224354917";
  } else if (!cfg.testing && !cfg.rewarded_id) {
    console.warn(
      "[AdMob] testing=false but rewarded_id is empty — admin needs to set Rewarded Ad Unit ID in admin panel → Ads. The Watch-Ad button will be inactive until then.",
    );
  }
  return cfg;
}

async function initOnce(apiBase: string): Promise<AdConfig | null> {
  if (!isNative || !isAndroid) return null;
  if (cached) return cached;
  try {
    cached = await fetchConfig(apiBase);
  } catch (e) {
    console.warn("[AdMob] Could not load /api/public/ad-config:", e);
    return null;
  }
  if (!cached.enabled) {
    console.log("[AdMob] Ads disabled in admin panel.");
    return cached;
  }
  if (!initialised) {
    try {
      await AdMob.initialize({
        // BUG FIX (2026-01) — "Rewarded video Ad Not Available".
        //
        // When `cached.testing === true`, we pass BOTH:
        //   • initializeForTesting=true — marks every ad request
        //     this device makes as a test request, so AdMob never
        //     counts it against your account / policy,
        //   • testingDevices=["EMULATOR"] — tells the SDK to treat
        //     all running emulators as test devices (their device
        //     hash is always "EMULATOR"). Combined with the
        //     Google universal test Ad Unit IDs we swap in inside
        //     fetchConfig(), this guarantees 100% ad fill in dev.
        //
        // For your PHYSICAL device, watch `adb logcat` after the
        // first ad request — AdMob logs a line like:
        //   "Use AdRequest.Builder.addTestDevice(\"ABCDEF…\")"
        // Copy that hash and paste it into your AdMob Console's
        // Test Devices section to also treat that device as test
        // when running release builds.
        initializeForTesting: cached.testing,
        testingDevices: cached.testing ? ["EMULATOR"] : [],
      });
      initialised = true;
      // Listen for interstitial readiness events so showInterstitial()
      // can early-return when the slot isn't prepared yet.
      AdMob.addListener("onInterstitialAdLoaded" as any, () => {
        interstitialReady = true;
        emitInterstitial("loaded");
      });
      AdMob.addListener("onInterstitialAdFailedToLoad" as any, (err: any) => {
        interstitialReady = false;
        emitInterstitial("failed");
        // BUG-3 FIX (2026-05): surface the exact AdMob error code/message
        // so the admin can diagnose. Error code 1 = ERROR_CODE_INVALID_REQUEST
        // (almost always a malformed / wrong-type Ad Unit ID). Error code 2
        // = network. Error code 3 = no fill.
        const code = err?.code ?? err?.errorCode ?? "?";
        const msg = err?.message ?? err?.errorMessage ?? "";
        console.warn(
          `[AdMob] Interstitial onAdFailedToLoad — code=${code} message="${msg}". ` +
            `If code=1: verify the Ad Unit ID in the admin panel is a real INTERSTITIAL unit ` +
            `(ca-app-pub-XXXX/YYYY) and not a rewarded / banner ID.`,
          err,
        );
      });
      // BUG-4 FIX (2026-05): hook the fullscreen lifecycle. `Showed` =
      // the real AdMob view just took over the screen → the React
      // fallback overlay MUST dismiss itself immediately to avoid the
      // "ghost behind real ad" UX. `Dismissed` = user closed it →
      // re-prime the next slot. `Impression` is logged for ops only.
      AdMob.addListener("adImpression" as any, () => {
        emitInterstitial("impression");
      });
      AdMob.addListener("interstitialAdShowed" as any, () => {
        emitInterstitial("showed");
      });
      AdMob.addListener("onInterstitialAdDismissed" as any, () => {
        interstitialReady = false;
        emitInterstitial("dismissed");
        // Eagerly prepare the next one so rapid channel-switching feels snappy.
        void prepareInterstitial();
      });
      console.log("[AdMob] Initialised (testing =", cached.testing, ")");
      // BUG FIX (2026-01): explicit diagnostic dump so the admin can
      // confirm in `adb logcat -s Capacitor` exactly which Ad Unit IDs
      // the device is asking AdMob to fill. A repeated "No Fill"
      // (error code 3) when these are the REAL publisher IDs almost
      // always means the AdMob account hasn't approved this app yet,
      // or the bundle/package mismatch between AndroidManifest and
      // AdMob console. Easy to debug once we can see the IDs.
      console.log(
        "[AdMob] active ad units →",
        JSON.stringify({
          testing_mode: cached.testing,
          app_id: cached.app_id,
          banner_id: cached.banner_id,
          interstitial_id: cached.interstitial_id,
          rewarded_id: cached.rewarded_id,
          native_id: cached.native_id,
          interstitial_every: cached.interstitial_every,
        }),
      );
    } catch (e) {
      console.warn("[AdMob] initialize() failed:", e);
    }
  }
  return cached;
}

/** Show a sticky adaptive banner at the bottom of the screen. */
export async function showBanner(apiBase: string): Promise<void> {
  const cfg = await initOnce(apiBase);
  if (!cfg?.enabled || !cfg.banner_id) return;
  if (bannerVisible) {
    try { await AdMob.resumeBanner(); } catch { /* ignore resume errors */ }
    return;
  }
  const options: BannerAdOptions = {
    adId: cfg.banner_id,
    // ADAPTIVE_BANNER + BOTTOM_CENTER = full-width centered banner.
    //
    // BUG FIX (2026-01) — user reported the banner appeared "forced to
    // the bottom-left corner". The contract we WANT is:
    //   • adSize ADAPTIVE_BANNER → the SDK asks AdMob for a creative
    //     whose pixel width matches `screen.width`. Modern AdMob fills
    //     this with a true-width creative (e.g. 393×50 on a Pixel 5),
    //     so the banner spans edge-to-edge with no side-bands.
    //   • position BOTTOM_CENTER → the native Android plugin maps this
    //     to `Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL`, so even
    //     when a legacy IAB creative (320×50, 468×60) is served and
    //     the SDK letterboxes it INSIDE the adaptive slot, the visible
    //     pixels are centred along the bottom edge — never left-pinned.
    //   • margin: 0 → no inset; banner sits flush with the screen
    //     chin so it cannot be visually mistaken for floating UI.
    // If the user still sees a left-pinned banner after this fix
    // lands, the most common cause is a stale @capacitor-community/
    // admob version (< 6.x) on the device — verify via
    // `grep capacitor-community/admob frontend/package.json`.
    adSize: BannerAdSize.ADAPTIVE_BANNER,
    position: BannerAdPosition.BOTTOM_CENTER,
    margin: 0,
    isTesting: cfg.testing,
  };
  try {
    await AdMob.showBanner(options);
    bannerVisible = true;
    // FEATURE (2026-01): tell the React layer to reserve space AT
    // THE BOTTOM (not the top). PageLayout reads --ad-banner-bottom-h
    // to add bottom-padding to the scroll area so the banner never
    // covers the last row of content. Adaptive banners on phones are
    // 50–64 dp tall — 72 px is a safe comfort margin that also
    // covers the device's gesture-area on Android 12+.
    try {
      document.documentElement.style.setProperty("--ad-banner-bottom-h", "72px");
      // Reset the top reservation (the previous "banner at top" build
      // left it at 72px; clear it explicitly so old WebView state
      // doesn't leave a phantom gap at the top of the page).
      document.documentElement.style.setProperty("--ad-banner-top-h", "0px");
    } catch { /* SSR / no DOM */ }
  } catch (e) {
    console.warn("[AdMob] showBanner failed:", e);
  }
}

/** Hide the banner (keeps memory allocated — cheap to resume). */
export async function hideBanner(): Promise<void> {
  if (!isNative || !bannerVisible) return;
  try {
    await AdMob.hideBanner();
    bannerVisible = false;
    try {
      // FEATURE (2026-01): zero out BOTH custom-property heights so
      // any layout consuming either var (top OR bottom) releases the
      // reserved space. Old builds may still read --ad-banner-top-h.
      document.documentElement.style.setProperty("--ad-banner-bottom-h", "0px");
      document.documentElement.style.setProperty("--ad-banner-top-h", "0px");
    } catch { /* SSR / no DOM */ }
  } catch (e) {
    console.warn("[AdMob] hideBanner failed:", e);
  }
}

/** Pre-load the next interstitial so showInterstitial() is instant. */
export async function prepareInterstitial(apiBase?: string): Promise<void> {
  if (!isNative) return;
  // If first call, make sure we've loaded config + initialised.
  if (!cached && apiBase) await initOnce(apiBase);
  if (!cached?.enabled || !cached.interstitial_id) return;
  try {
    const opts: AdOptions = {
      adId: cached.interstitial_id,
      isTesting: cached.testing,
    };
    await AdMob.prepareInterstitial(opts);
  } catch (e) {
    interstitialReady = false;
    console.warn("[AdMob] prepareInterstitial failed:", e);
  }
}

/** Show the already-prepared interstitial (or no-op if not ready). */
export async function showInterstitial(): Promise<boolean> {
  if (!isNative || !cached?.enabled || !interstitialReady) return false;
  try {
    await AdMob.showInterstitial();
    return true;
  } catch (e) {
    console.warn("[AdMob] showInterstitial failed:", e);
    interstitialReady = false;
    return false;
  }
}

/**
 * Force-show an interstitial NOW. If the slot is already prepared we
 * fire it immediately; otherwise we lazily prepare it (without an
 * apiBase — relies on `cached` being already populated by a previous
 * `showBanner` / `prepareInterstitial` call). Returns whether the ad
 * was actually displayed by the AdMob runtime — callers that want a
 * web fallback overlay should react to a `false` return.
 *
 * BUG-3 FIX (2026-05): the previous version returned `false` the moment
 * `interstitialReady === false`, which raced against the SDK's async
 * load callback and made the custom "NADIPLAYER PREMIUM" fallback fire
 * even though a real ad was about to be ready. We now:
 *   1. ALWAYS lazily call `prepareInterstitial()` if not ready,
 *   2. wait up to `READY_TIMEOUT_MS` for the SDK's
 *      `onInterstitialAdLoaded` event to flip `interstitialReady=true`,
 *   3. log the exact reason if we still bail (no native runtime / no
 *      cached config / no unit ID / load timed out).
 *
 * Callers can override the wait via opts.maxWaitMs (defaults to 4s).
 */
export async function showInterstitialNow(opts?: { maxWaitMs?: number }): Promise<boolean> {
  const READY_TIMEOUT_MS = Math.max(500, opts?.maxWaitMs ?? 4000);
  if (!isNative) {
    console.log("[AdMob] showInterstitialNow: skipped (not native runtime)");
    return false;
  }
  if (!cached?.enabled) {
    console.log("[AdMob] showInterstitialNow: skipped (ads disabled / config not loaded)");
    return false;
  }
  if (!cached.interstitial_id) {
    console.warn("[AdMob] showInterstitialNow: skipped (no interstitial_id configured in admin panel)");
    return false;
  }

  // (1) If not yet ready, kick off a prepare and (2) wait for the load
  // callback up to READY_TIMEOUT_MS before declaring failure.
  if (!interstitialReady) {
    try {
      await prepareInterstitial();
    } catch (e) {
      console.warn("[AdMob] showInterstitialNow: prepareInterstitial threw:", e);
      return false;
    }
    if (!interstitialReady) {
      const start = Date.now();
      while (!interstitialReady && Date.now() - start < READY_TIMEOUT_MS) {
        await new Promise((r) => setTimeout(r, 150));
      }
    }
    if (!interstitialReady) {
      console.warn(
        `[AdMob] showInterstitialNow: timed out after ${READY_TIMEOUT_MS}ms waiting for onInterstitialAdLoaded. ` +
          `Falling back to overlay. Check Ad Unit ID format and network.`,
      );
      return false;
    }
  }

  try {
    await AdMob.showInterstitial();
    return true;
  } catch (e) {
    console.warn("[AdMob] showInterstitialNow: AdMob.showInterstitial() failed:", e);
    interstitialReady = false;
    return false;
  }
}

export function getInterstitialEvery(): number {
  return cached?.interstitial_every ?? 0;
}

export function isAdsEnabled(): boolean {
  return !!cached?.enabled;
}

/**
 * Prepare + show a TRUE Rewarded Video ad (NOT an interstitial).
 *
 * Returns the reward item if the user watched it to completion,
 * or `null` if they dismissed early / the slot wasn't available /
 * native runtime is missing.
 *
 * CRITICAL: This function NEVER falls back to the interstitial unit.
 * The "SKIP AD" interstitial layout is unacceptable for rewarded
 * flows — if `rewarded_id` is empty in the admin panel, the rewarded
 * call simply no-ops. Admin must configure a real Rewarded Video Ad
 * Unit ID (e.g. `ca-app-pub-XXXX/YYYY` of format type "Rewarded").
 *
 * Under the hood `prepareRewardVideoAd` / `showRewardVideoAd` map to
 * Android's `RewardedAd.load()` / `RewardedAd.show()` which guarantee
 * the genuine Rewarded lifecycle (unskippable video → reward callback).
 */
export async function showRewarded(
  apiBase?: string,
): Promise<AdMobRewardItem | null> {
  if (!isNative) {
    console.log("[AdMob] showRewarded: skipped (not native runtime)");
    return null;
  }
  if (!cached && apiBase) await initOnce(apiBase);
  if (!cached?.enabled) {
    console.log("[AdMob] showRewarded: skipped (ads disabled or config not loaded)");
    return null;
  }
  // No fallback to interstitial — must be a real Rewarded unit.
  const adId = cached.rewarded_id;
  if (!adId) {
    console.warn("[AdMob] showRewarded: no rewarded_id configured in admin panel; skipping (interstitial fallback intentionally disabled).");
    return null;
  }
  try {
    attachRewardedListeners();
    const opts: RewardAdOptions = {
      adId,
      isTesting: cached.testing,
    };
    // BUG-3 FIX (2026-05): if the SDK signals onAdFailedToLoad here it
    // throws — we log the exact error code so the admin can diagnose.
    // Error code 1 = ERROR_CODE_INVALID_REQUEST (wrong-type unit ID,
    // typically a banner/interstitial ID pasted into the rewarded slot).
    await AdMob.prepareRewardVideoAd(opts);
    const result = await AdMob.showRewardVideoAd();
    return result ?? null;
  } catch (e: any) {
    const code = e?.code ?? e?.errorCode ?? "?";
    const msg = e?.message ?? e?.errorMessage ?? String(e);
    console.warn(
      `[AdMob] showRewarded failed — code=${code} message="${msg}". ` +
        `If code=1: verify the Rewarded Ad Unit ID in the admin panel is a real ` +
        `REWARDED unit (ca-app-pub-XXXX/YYYY) and not a banner / interstitial / native ID. ` +
        `If code=3 (no fill): try again later.`,
      e,
    );
    return null;
  }
}

/** Reset the in-memory cache so a subsequent call reloads /ad-config. */
export function resetAdConfigCache(): void {
  cached = null;
}

// ── Rewarded-ads "click counter → unlock ad-free" event bus ─────────
// BUG-5 FEATURE (2026-05): a tiny pub-sub mirror of the interstitial
// event bus, dedicated to rewarded video clicks. The RewardAdCard
// component subscribes here and increments its (n / 2) counter on every
// click. We listen to BOTH the SDK's `onAdClick` (preferred — fires the
// instant the user taps the in-ad CTA) AND `onRewardedVideoAdReward`
// (fallback — fires when the watch completes). Either signal counts.
type RewardedEvent =
  | "loaded"
  | "failed"
  | "showed"
  | "dismissed"
  | "clicked"
  | "rewarded";
const rewardedListeners = new Set<(e: RewardedEvent) => void>();
function emitRewarded(e: RewardedEvent) {
  for (const fn of rewardedListeners) {
    try { fn(e); } catch (err) { console.warn("[AdMob] rewarded listener threw:", err); }
  }
}
export function onRewardedEvent(fn: (e: RewardedEvent) => void): () => void {
  rewardedListeners.add(fn);
  return () => { rewardedListeners.delete(fn); };
}

// Wire the Capacitor AdMob plugin's rewarded lifecycle events through
// the bus. We only register the listeners the FIRST time `showRewarded`
// is called so we don't spam AdMob.addListener on cold starts.
let rewardedListenersAttached = false;
function attachRewardedListeners() {
  if (rewardedListenersAttached) return;
  rewardedListenersAttached = true;
  try {
    AdMob.addListener("rewardedVideoAdLoaded" as any, () => emitRewarded("loaded"));
    AdMob.addListener("rewardedVideoAdFailedToLoad" as any, (err: any) => {
      emitRewarded("failed");
      const code = err?.code ?? err?.errorCode ?? "?";
      const msg = err?.message ?? err?.errorMessage ?? "";
      console.warn(`[AdMob] Rewarded onAdFailedToLoad — code=${code} message="${msg}"`);
    });
    AdMob.addListener("rewardedVideoAdShowed" as any, () => emitRewarded("showed"));
    AdMob.addListener("rewardedVideoAdDismissed" as any, () => emitRewarded("dismissed"));
    AdMob.addListener("onRewardedVideoAdReward" as any, () => emitRewarded("rewarded"));
    // Click events — fired the moment the user taps the in-ad CTA.
    // The plugin may emit one of two names depending on its version;
    // we register both and de-dupe on the consumer side.
    AdMob.addListener("rewardedVideoAdClick" as any, () => emitRewarded("clicked"));
    AdMob.addListener("adClick" as any, () => emitRewarded("clicked"));
  } catch (e) {
    console.warn("[AdMob] attachRewardedListeners failed:", e);
  }
}
