// device_name/platform/ip_address for auth requests (login/register
// OTP verify, both user and astrologer sides). Confirmed via a real
// Postman example the user supplied: the admin-facing shape is
// "<Device Model> - <City>, <District>, <State>" (native mobile apps
// send a real device model + GPS-derived location). A web browser
// can't produce a marketing device name, so this sends the closest
// honest equivalent — "<Browser> on <OS>" — plus the same
// city/district/state suffix, resolved via Google's Geocoding API
// (app/api/places/reverse-geocode), same as the rest of this app's
// location features.
//
// Confirmed live: relying on navigator.geolocation alone left the
// location suffix missing on every real admin-visible login, because
// it requires an explicit browser permission grant that most users
// never see/answer in time (or deny outright). Fixed with a
// permission-free fallback: when GPS isn't available/granted, this
// falls back to ipwho.is (free, keyless, CORS-enabled IP geolocation)
// called directly from the browser.
//
// The SAME ipwho.is call also supplies the real ip_address —
// confirmed live this was still wrong even after the proxy routes
// were fixed to trust X-Forwarded-For: that header is only ever set
// by an actual reverse proxy in front of the app, which doesn't exist
// when testing directly against a local dev server (or, evidently,
// isn't reliably forwarded in front of the real deployment either —
// the admin panel kept showing ::1/::ffff:127.0.0.1 either way). A
// client-side lookup of "what's my own public IP" works identically
// in local dev and production, so it's now the primary source —
// getClientPublicIp() below, not the server-side header — and the
// server-side override in app/lib/clientIp.ts is kept only as a
// secondary safety net for whichever value the client failed to
// resolve.

function getBrowserName(ua: string): string {
  if (/edg\//i.test(ua)) return "Edge";
  if (/opr\//i.test(ua) || /opera/i.test(ua)) return "Opera";
  if (/firefox|fxios/i.test(ua)) return "Firefox";
  if (/crios/i.test(ua)) return "Chrome";
  if (/chrome/i.test(ua) && !/edg\//i.test(ua)) return "Chrome";
  if (/safari/i.test(ua) && !/chrome|crios|android/i.test(ua)) return "Safari";
  return "Browser";
}

function getOsName(ua: string): string {
  if (/windows/i.test(ua)) return "Windows";
  if (/iphone|ipad|ipod/i.test(ua)) return "iOS";
  if (/android/i.test(ua)) return "Android";
  if (/mac os/i.test(ua)) return "Mac";
  if (/linux/i.test(ua)) return "Linux";
  return "Web";
}

export function getDeviceLabel(): string {
  if (typeof navigator === "undefined") return "Web Browser";
  return `${getBrowserName(navigator.userAgent)} on ${getOsName(navigator.userAgent)}`;
}

export function getPlatformLabel(): string {
  if (typeof navigator === "undefined") return "Web";
  const ua = navigator.userAgent;
  if (/android/i.test(ua)) return "Android";
  if (/iphone|ipad|ipod/i.test(ua)) return "iOS";
  return "Web";
}

function getCurrentPosition(timeoutMs = 4000): Promise<GeolocationPosition | null> {
  return new Promise((resolve) => {
    if (typeof navigator === "undefined" || !navigator.geolocation) {
      resolve(null);
      return;
    }

    let settled = false;
    const settle = (value: GeolocationPosition | null) => {
      if (settled) return;
      settled = true;
      resolve(value);
    };

    const timer = setTimeout(() => settle(null), timeoutMs);

    navigator.geolocation.getCurrentPosition(
      (position) => {
        clearTimeout(timer);
        settle(position);
      },
      () => {
        clearTimeout(timer);
        settle(null);
      },
      { timeout: timeoutMs, maximumAge: 5 * 60 * 1000 },
    );
  });
}

function findComponent(components: any[], type: string): any {
  return components.find((c: any) => c.types?.includes(type));
}

interface Coordinates {
  lat: number;
  lng: number;
}

interface NetworkInfo {
  ip: string;
  lat?: number;
  lng?: number;
}

// Memoized per page load — the visitor's public IP/approximate
// location won't change mid-session, so a retry (e.g. a wrong OTP)
// doesn't need a second lookup.
let networkInfoPromise: Promise<NetworkInfo | null> | null = null;

function fetchNetworkInfo(): Promise<NetworkInfo | null> {
  if (!networkInfoPromise) {
    networkInfoPromise = (async () => {
      try {
        const response = await fetch("https://ipwho.is/", { cache: "no-store" });
        if (!response.ok) return null;

        const data = await response.json();
        if (!data?.success || typeof data.ip !== "string") return null;

        return {
          ip: data.ip,
          lat: typeof data.latitude === "number" ? data.latitude : undefined,
          lng: typeof data.longitude === "number" ? data.longitude : undefined,
        };
      } catch {
        // Best-effort — network blocked, service down, etc.
        return null;
      }
    })();
  }

  return networkInfoPromise;
}

// The real public IP, resolved client-side — works the same in local
// dev and production, unlike relying on a reverse proxy's
// X-Forwarded-For header (see the file header comment above). Falls
// back to an empty string on any failure; callers should fall back to
// their own placeholder rather than sending an empty ip_address.
export async function getClientPublicIp(): Promise<string> {
  const info = await fetchNetworkInfo();
  return info?.ip ?? "";
}

async function getCoordinates(): Promise<Coordinates | null> {
  const position = await getCurrentPosition();

  if (position) {
    return { lat: position.coords.latitude, lng: position.coords.longitude };
  }

  const info = await fetchNetworkInfo();

  if (info?.lat !== undefined && info?.lng !== undefined) {
    return { lat: info.lat, lng: info.lng };
  }

  return null;
}

async function getLocationLabel(): Promise<string> {
  try {
    const coordinates = await getCoordinates();
    if (!coordinates) return "";

    const response = await fetch(
      `/api/places/reverse-geocode?lat=${coordinates.lat}&lng=${coordinates.lng}`,
    );
    if (!response.ok) return "";

    const data = await response.json();
    const components = data.result?.address_components ?? [];

    const city = findComponent(components, "locality")?.long_name;
    const district = findComponent(components, "administrative_area_level_2")?.long_name;
    const state = findComponent(components, "administrative_area_level_1")?.long_name;

    return [city, district, state].filter(Boolean).join(", ");
  } catch {
    return "";
  }
}

export async function getDeviceNameWithLocation(): Promise<string> {
  const label = getDeviceLabel();
  const location = await getLocationLabel();
  return location ? `${label} - ${location}` : label;
}

export interface DeviceAuthInfo {
  ip_address: string;
  device_name: string;
  platform: string;
}

// Single entry point for the three fields every login/register OTP
// verify call sends — runs the IP/location lookups in parallel (they
// share the same memoized ipwho.is call, so this is still only one
// network round trip, not two).
export async function getDeviceAuthInfo(): Promise<DeviceAuthInfo> {
  const [ip, deviceName] = await Promise.all([
    getClientPublicIp(),
    getDeviceNameWithLocation(),
  ]);

  return {
    ip_address: ip || "127.0.0.1",
    device_name: deviceName,
    platform: getPlatformLabel(),
  };
}
