type Utm = {
  source?: string;
  medium?: string;
  campaign?: string;
  term?: string;
  content?: string;
};

type SourceContext = {
  referrer_host?: string | "";
  utm?: Utm;
};

type PageContext = {
  path: string;
  title?: string;
};

type ClickOutbound = {
  target_host: string;
  target_path?: string;
  placement?: string;
};

type ClickInternal = {
  target: string;
  placement?: string;
};

type EventBase = {
  occurred_at: string;
  page: PageContext;
  source?: SourceContext;
};

type EventPageview = EventBase & {
  event_name: "pageview";
};

type EventClickOutbound = EventBase & {
  event_name: "click_outbound";
  click: ClickOutbound;
};

type EventClickInternal = EventBase & {
  event_name: "click_internal";
  click: ClickInternal;
};

type Event = EventPageview | EventClickOutbound | EventClickInternal;

type EventBatch = {
  schema_version: "1.0";
  site_id: string;
  sent_at: string;
  events: Event[];
};

const SITE_ID = "nab2b";
const ENDPOINT = "/.netlify/functions/stats";

const MAX_BATCH_SIZE = 10;
const MAX_BATCH_EVENTS = 50;

const queue: Event[] = [];
let flushing = false;

function nowIso(): string {
  return new Date().toISOString();
}

function getPathname(): string {
  // Privacy-first: path only (no query string).
  return window.location.pathname || "/";
}

function tryGetReferrerHost(): string | "" {
  const ref = document.referrer;
  if (!ref) return "";
  try {
    return new URL(ref).hostname || "";
  } catch {
    return "";
  }
}

function readUtmFromUrl(): Utm | undefined {
  const sp = new URLSearchParams(window.location.search);
  const utm: Utm = {};
  const source = sp.get("utm_source");
  const medium = sp.get("utm_medium");
  const campaign = sp.get("utm_campaign");
  const term = sp.get("utm_term");
  const content = sp.get("utm_content");

  if (source) utm.source = source.slice(0, 200);
  if (medium) utm.medium = medium.slice(0, 200);
  if (campaign) utm.campaign = campaign.slice(0, 200);
  if (term) utm.term = term.slice(0, 200);
  if (content) utm.content = content.slice(0, 200);

  return Object.keys(utm).length ? utm : undefined;
}

function getSourceContext(): SourceContext {
  const referrer_host = tryGetReferrerHost();
  const utm = readUtmFromUrl();
  const source: SourceContext = {};
  if (referrer_host !== undefined) source.referrer_host = referrer_host;
  if (utm) source.utm = utm;
  return source;
}

function getPlacement(target: Element | null): string | undefined {
  if (!target) return undefined;
  const el = target.closest("[data-analytics-placement],[data-placement]");
  if (!el) return undefined;
  const raw =
    (el.getAttribute("data-analytics-placement") || el.getAttribute("data-placement") || "")
      .trim()
      .toLowerCase();

  // Match `Placement.schema.json` pattern.
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(raw)) return undefined;
  return raw;
}

function pushEvent(ev: Event): void {
  queue.push(ev);
  if (queue.length >= MAX_BATCH_SIZE) {
    void flush({ reason: "batch_size" });
  }
}

async function postBatch(events: Event[]): Promise<void> {
  const batch: EventBatch = {
    schema_version: "1.0",
    site_id: SITE_ID,
    sent_at: nowIso(),
    events,
  };

  const body = JSON.stringify(batch);

  // Prefer beacon on page hide.
  if (typeof navigator.sendBeacon === "function") {
    try {
      const ok = navigator.sendBeacon(ENDPOINT, new Blob([body], { type: "application/json" }));
      if (ok) return;
    } catch {
      // fall through to fetch
    }
  }

  await fetch(ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body,
    keepalive: true,
    credentials: "omit",
  });
}

async function flush({ reason }: { reason: string }): Promise<void> {
  if (flushing) return;
  if (!queue.length) return;
  flushing = true;

  try {
    // Collector contract allows max 50 events per batch.
    const toSend = queue.splice(0, Math.min(queue.length, MAX_BATCH_EVENTS));
    await postBatch(toSend);
  } catch {
    // Best-effort: drop failures silently (no retries to avoid background churn).
  } finally {
    flushing = false;
    if (queue.length) {
      // Yield to avoid blocking UI if many events were queued.
      setTimeout(() => void flush({ reason: `${reason}_drain` }), 0);
    }
  }
}

function trackPageview(): void {
  const event: EventPageview = {
    event_name: "pageview",
    occurred_at: nowIso(),
    page: {
      path: getPathname(),
      title: (document.title || "").slice(0, 200) || undefined,
    },
    source: getSourceContext(),
  };

  pushEvent(event);
}

function trackClick(a: HTMLAnchorElement, placement?: string): void {
  const href = a.getAttribute("href") || "";
  if (!href) return;
  if (href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("javascript:")) return;

  let url: URL;
  try {
    url = new URL(href, window.location.href);
  } catch {
    return;
  }

  const page: PageContext = { path: getPathname() };
  const source = getSourceContext();
  const occurred_at = nowIso();

  const isSameHost = url.hostname === window.location.hostname;
  if (isSameHost) {
    const target = `${url.pathname || "/"}${url.hash || ""}`;
    const ev: EventClickInternal = {
      event_name: "click_internal",
      occurred_at,
      page,
      source,
      click: {
        target,
        placement,
      },
    };
    pushEvent(ev);
    return;
  }

  const ev: EventClickOutbound = {
    event_name: "click_outbound",
    occurred_at,
    page,
    source,
    click: {
      target_host: url.hostname,
      // For privacy-first aggregation you may omit `target_path`, but it's allowed by contract.
      target_path: url.pathname || "/",
      placement,
    },
  };
  pushEvent(ev);
}

function installClickListener(): void {
  document.addEventListener(
    "click",
    (e) => {
      const t = e.target;
      if (!(t instanceof Element)) return;
      const a = t.closest("a");
      if (!(a instanceof HTMLAnchorElement)) return;
      if (a.hasAttribute("download")) return;
      if (a.target === "_blank") {
        // Still track best-effort; flushing happens on visibility/pagehide too.
      }
      const placement = getPlacement(t);
      trackClick(a, placement);
    },
    { capture: true },
  );
}

function installVisibilityFlush(): void {
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") {
      void flush({ reason: "visibility_hidden" });
    }
  });
  window.addEventListener("pagehide", () => void flush({ reason: "pagehide" }));
}

function init(): void {
  // Disable in local dev unless explicitly enabled.
  const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
  const enabled = !isLocalhost || window.localStorage.getItem("nab2b_stats") === "1";
  if (!enabled) return;

  trackPageview();
  installClickListener();
  installVisibilityFlush();
}

init();

