// Banner serving — source of truth is bb.banner_creatives in Supabase.
// The DB query is date-gated (status=active AND start<=NOW<=end) and the
// result is cached in-process for 5 minutes. A hardcoded fallback keeps
// the site rendering banners if Supabase is unreachable during a deploy
// or during the initial module load.
//
// Public helpers (rotateAll, pickAds) are SYNCHRONOUS so existing client
// component callsites keep working. They read from a module-level cache
// that is primed on first import and refreshed asynchronously after the
// TTL expires.

import { createClient } from "@supabase/supabase-js";

export interface Ad {
  slug?: string;
  campaign_id?: string;
  label: string;
  size: "300x250" | "300x600" | "728x90";
  src: string;
  alt: string;
  click_url?: string;
}

const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "av";

// 30s in-process cache so admin uploads show on the site within ~30s.
// The /api/banners endpoint also caps at 30s, so worst-case from upload
// to live is ~60s. Was 5min, which combined with the API + CDN layers
// could push banner updates out to 15 minutes.
const CACHE_TTL_MS = 30 * 1000;
type CacheRow = { rows: Ad[]; loadedAt: number };
let CACHE: CacheRow | null = null;
let REFRESH_IN_FLIGHT: Promise<void> | null = null;

const FALLBACK: Ad[] = [
  { slug: "liveu-728x90",
    campaign_id: "6031b0e3-2eee-4974-834f-c5c71b339aae",
    label: "LiveU 728x90", size: "728x90",
    src: "/legacy/ads/728-x-90-pxEN.gif",
    alt: "LiveU 728x90",
    click_url: "https://advertising.relevantmediaproperties.com/r/6031b0e3-2eee-4974-834f-c5c71b339aae" },
  { slug: "blackmagic-davinci-resolve-300x600",
    campaign_id: "1d7aecbc-39ec-4e2f-a10f-b5394a9eaf27",
    label: "Blackmagic DaVinci Resolve 20", size: "300x600",
    src: "/legacy/ads/Davinci-Resolve-20_300x600.jpg",
    alt: "Blackmagic Design DaVinci Resolve 20",
    click_url: "https://advertising.relevantmediaproperties.com/r/1d7aecbc-39ec-4e2f-a10f-b5394a9eaf27" },
  { slug: "magewell-pro-convert-300x250",
    campaign_id: "47f19e71-fc97-4aa5-998d-97e780b57729",
    label: "Magewell Pro-Convert", size: "300x250",
    src: "/legacy/ads/Magewell_Pro-Convert-IP-to-HDMI_Sports_NAB2026.gif",
    alt: "Magewell Pro-Convert IP to HDMI",
    click_url: "https://advertising.relevantmediaproperties.com/r/47f19e71-fc97-4aa5-998d-97e780b57729" },
  { slug: "liveu-payg-300x250", label: "LiveU PAYG", size: "300x250",
    src: "/legacy/ads/PAYG-300x250-1.gif",
    alt: "LiveU Pay-As-You-Go", click_url: "https://bit.ly/4ghXVeq" },
  { slug: "aja-colorbox-300x250",
    campaign_id: "75157edc-df21-460e-b08c-e0c8d8cfdf5b",
    label: "AJA ColorBox", size: "300x250",
    src: "/legacy/ads/aja_2025_colorbox_og_colorbox_300x250_en-2.gif",
    alt: "AJA ColorBox",
    click_url: "https://advertising.relevantmediaproperties.com/r/75157edc-df21-460e-b08c-e0c8d8cfdf5b" },
  { slug: "zixi-300x250", label: "Zixi", size: "300x250",
    src: "/legacy/ads/Zixi_Ads_300x250.png",
    alt: "Zixi", click_url: "https://zixi.com/" },
  { slug: "telycam-300x250",
    campaign_id: "d4fda1cd-1ffc-4ae5-bec3-1b38cf62a5e9",
    label: "Telycam MixOne / ExploreXE", size: "300x250",
    src: "/legacy/ads/Telycam_BroadcastBeat_300x250_MixOne-ExploreXE_with_NAB2026_2f30157f.gif",
    alt: "Telycam MixOne / ExploreXE — 2026 NAB Show",
    click_url: "https://advertising.relevantmediaproperties.com/r/d4fda1cd-1ffc-4ae5-bec3-1b38cf62a5e9" },
  { slug: "lectrosonics-300x250",
    campaign_id: "e69cb845-5526-4165-bca1-964104b70c6b",
    label: "Lectrosonics", size: "300x250",
    src: "/legacy/ads/300x250-banner-Our-Story-film_resize.jpg",
    alt: "Lectrosonics Our Story",
    click_url: "https://advertising.relevantmediaproperties.com/r/e69cb845-5526-4165-bca1-964104b70c6b" },
];

interface DbRow {
  slug: string;
  name: string;
  image_path: string;
  click_url: string | null;
  size: "300x250" | "300x600" | "728x90";
}

// Centralized banner source: advertising.relevantmediaproperties.com
// serves /api/banners/<property>/<placement> with CORS + 5min cache.
// We fetch all three sizes once and merge.
const RMP_API = process.env.NEXT_PUBLIC_RMP_ADS_API
  || "https://advertising.relevantmediaproperties.com";
const PROPERTY = process.env.NEXT_PUBLIC_RMP_PROPERTY || "avbeat";

interface RmpBanner {
  campaign_id: string;
  name: string;
  image_url: string;
  image_webp_url?: string | null;
  click_url: string;
  width: number;
  height: number;
  placement: "300x250" | "300x600" | "728x90";
  property: string;
  slug?: string;
}

async function fetchPlacement(size: Ad["size"]): Promise<Ad[]> {
  try {
    const res = await fetch(`${RMP_API}/api/banners/${PROPERTY}/${size}`, {
      next: { revalidate: 30 },
    });
    if (!res.ok) return [];
    const data = await res.json() as { banners?: RmpBanner[] };
    return (data.banners || []).map((b): Ad => ({
      slug: b.slug || b.campaign_id,
      campaign_id: b.campaign_id,
      label: b.name,
      size: b.placement,
      src: b.image_url,
      alt: b.name,
      // All clicks route through advertising.RMP.com/r/<campaign_id> so they
      // land in bb.ad_clicks — the single table the RMP analytics dashboard
      // reads. Do not change to a different host or path; the legacy
      // www.RMP.com/<property>/<slug> path writes to adv.banner_clicks which
      // the dashboard does not query.
      click_url: `${RMP_API}/r/${b.campaign_id}`,
    }));
  } catch {
    return [];
  }
}

async function loadFromDb(): Promise<Ad[]> {
  // 1) Try centralized RMP source first (preferred path).
  try {
    const [a, b, c] = await Promise.all([
      fetchPlacement("300x250"),
      fetchPlacement("300x600"),
      fetchPlacement("728x90"),
    ]);
    const all = [...a, ...b, ...c];
    if (all.length > 0) return all;
  } catch {
    /* fall through */
  }

  // 2) Legacy fallback: query BB's own bb.banner_creatives (kept until the
  //    centralized RMP source is fully verified in prod).
  if (!SUPABASE_URL || !SUPABASE_ANON) return FALLBACK;
  try {
    const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
      db: { schema: SCHEMA as "public" },
      auth: { persistSession: false },
    });
    const nowIso = new Date().toISOString();
    const { data, error } = await sb
      .from("banner_creatives")
      .select("slug,name,image_path,click_url,size,start_date,end_date,status")
      .eq("status", "active")
      .or(`start_date.is.null,start_date.lte.${nowIso}`)
      .or(`end_date.is.null,end_date.gte.${nowIso}`);
    if (error || !data || data.length === 0) return FALLBACK;
    return (data as DbRow[]).map((r) => ({
      slug: r.slug,
      label: r.name,
      size: r.size,
      src: r.image_path,
      alt: r.name,
      click_url: r.click_url || undefined,
    }));
  } catch {
    return FALLBACK;
  }
}

function triggerRefresh(): void {
  if (REFRESH_IN_FLIGHT) return;
  REFRESH_IN_FLIGHT = loadFromDb()
    .then((rows) => {
      CACHE = { rows, loadedAt: Date.now() };
    })
    .catch(() => {
      // Failures stay logged via loadFromDb; keep CACHE as-is.
    })
    .finally(() => {
      REFRESH_IN_FLIGHT = null;
    });
}

function readCachedAds(): Ad[] {
  if (!CACHE) {
    triggerRefresh();
    return FALLBACK;
  }
  if (Date.now() - CACHE.loadedAt > CACHE_TTL_MS) {
    // Stale — kick off a background refresh but serve the stale snapshot.
    triggerRefresh();
  }
  return CACHE.rows;
}

export function shuffle<T>(arr: T[]): T[] {
  const out = arr.slice();
  for (let i = out.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [out[i], out[j]] = [out[j], out[i]];
  }
  return out;
}

// Slugs (or campaign_ids) that must be the FIRST banner of their size in
// every rotation, ahead of the shuffled pool. AJA 2026 sits at the top of
// every 300x250 stack per Ryan 2026-06-16.
const PINNED_SLUGS_BY_SIZE: Partial<Record<Ad["size"], string[]>> = {
  "300x250": ["aja-colorbox-300x250"],
};

export function rotateAll(size: Ad["size"], exclude: Set<string> = new Set()): Ad[] {
  const pool = readCachedAds().filter((a) => a.size === size);
  const key = (a: Ad) => a.src ?? a.label;
  const remaining = pool.filter((a) => !exclude.has(key(a)));

  const pinnedSlugs = PINNED_SLUGS_BY_SIZE[size] || [];
  if (pinnedSlugs.length === 0) return shuffle(remaining);

  const pinned: Ad[] = [];
  const rest: Ad[] = [];
  for (const ad of remaining) {
    const slugMatch = ad.slug && pinnedSlugs.includes(ad.slug);
    const idMatch = ad.campaign_id && pinnedSlugs.includes(ad.campaign_id);
    if (slugMatch || idMatch) {
      pinned.push(ad);
    } else {
      rest.push(ad);
    }
  }
  pinned.sort((a, b) => {
    const ai = pinnedSlugs.indexOf((a.slug || a.campaign_id) ?? "");
    const bi = pinnedSlugs.indexOf((b.slug || b.campaign_id) ?? "");
    return ai - bi;
  });
  return [...pinned, ...shuffle(rest)];
}

export function pickAds(
  size: Ad["size"],
  n: number,
  exclude: Set<string> = new Set()
): Ad[] {
  const all = rotateAll(size, exclude);
  return all.slice(0, Math.min(n, all.length));
}

// Prime the cache once at module load. Callers must use rotateAll() or
// pickAds() so the live cache (which fills async after import) is read at
// render time — the previous module-level IIFE constants captured the
// FALLBACK array forever and silently bypassed the centralized click logger.
triggerRefresh();
