/**
 * User-Agent + request-context parsing
 * ────────────────────────────────────
 * Small, dependency-free helpers that extract device / browser / OS
 * classifications from a User-Agent string and client IP / geo from
 * the incoming request's headers. Used by /api/collect when writing
 * TrackedSession to so the Session Explorer has something better
 * than "?" to render for the device + location columns.
 *
 * Heuristics are intentionally coarse — we're rendering a chip, not
 * running forensics. If a bot sends a malformed UA we fall back to
 * "Unknown" rather than throwing.
 */
export type DeviceClass = "mobile" | "tablet" | "desktop";

export interface ParsedUserAgent {
  device: DeviceClass;
  browser: string;
  os: string;
}

export function parseUserAgent(ua: string | null | undefined): ParsedUserAgent {
  const raw = (ua ?? "").slice(0, 1000);
  if (!raw) return { device: "desktop", browser: "Unknown", os: "Unknown" };

  // Tablet detection must run before mobile — the iPad UA contains
  // the "Mobile" token on iPadOS, which would otherwise classify as
  // a phone.
  const isTablet = /iPad|Tablet|PlayBook|Kindle|Silk/i.test(raw);
  const isMobile = /Mobile|Android|iPhone|iPod|Opera Mini|IEMobile|BlackBerry/i.test(raw);
  const device: DeviceClass = isTablet ? "tablet" : isMobile ? "mobile" : "desktop";

  // Browser: order matters — Edge/Opera strings contain "Chrome" or
  // "Safari", so match the more-specific tokens first.
  let browser = "Unknown";
  if (/Edg\//i.test(raw) || /Edge\//i.test(raw)) browser = "Edge";
  else if (/OPR\//i.test(raw) || /\bOpera\b/i.test(raw)) browser = "Opera";
  else if (/Firefox\//i.test(raw) || /\bFxiOS\//i.test(raw)) browser = "Firefox";
  else if (/Chrome\//i.test(raw) || /CriOS\//i.test(raw)) browser = "Chrome";
  else if (/Safari\//i.test(raw)) browser = "Safari";
  else if (/MSIE |Trident\//i.test(raw)) browser = "IE";

  // OS.
  let os = "Unknown";
  if (/Windows NT/i.test(raw)) os = "Windows";
  else if (/iPhone|iPad|iPod|iOS/i.test(raw)) os = "iOS";
  else if (/Android/i.test(raw)) os = "Android";
  else if (/Mac OS X|Macintosh/i.test(raw)) os = "macOS";
  else if (/CrOS/i.test(raw)) os = "ChromeOS";
  else if (/Linux/i.test(raw)) os = "Linux";

  return { device, browser, os };
}

export interface RequestGeo {
  country: string | null;
  city: string | null;
  region: string | null;
}

export interface RequestClientContext extends RequestGeo, ParsedUserAgent {
  userAgent: string | null;
  ipAddress: string | null;
}

/** Pulls the caller's IP from the forwarded-for chain. Takes the
 *  first address — that's the original client, the rest are proxies.
 *  Returns null when no forwarded header is present (e.g. local
 *  dev hit directly). */
export function extractClientIp(req: Request): string | null {
  const forwarded = req.headers.get("x-forwarded-for");
  if (forwarded) {
    const first = forwarded.split(",")[0]?.trim();
    if (first) return first.slice(0, 64);
  }
  const real = req.headers.get("x-real-ip");
  return real ? real.slice(0, 64) : null;
}

/** Reads Vercel-injected geo headers. Decodes percent-encoded city
 *  values (x-vercel-ip-city is RFC-3986 encoded for non-ASCII). */
export function extractClientGeo(req: Request): RequestGeo {
  const safe = (v: string | null): string | null => {
    if (!v) return null;
    try { return decodeURIComponent(v).slice(0, 120); }
    catch { return v.slice(0, 120); }
  };
  return {
    country: safe(req.headers.get("x-vercel-ip-country")),
    city: safe(req.headers.get("x-vercel-ip-city")),
    region: safe(req.headers.get("x-vercel-ip-country-region")),
  };
}

/** One-shot extractor for /api/collect. Combines UA parse + IP +
 *  geo headers into the shape TrackedSession.create expects. */
export function extractClientContext(req: Request): RequestClientContext {
  const userAgent = req.headers.get("user-agent");
  const parsed = parseUserAgent(userAgent);
  const geo = extractClientGeo(req);
  return {
    userAgent: userAgent ? userAgent.slice(0, 500) : null,
    ipAddress: extractClientIp(req),
    ...parsed,
    ...geo,
  };
}
