// Lightweight auth client for NADIPLAYER custom JWT auth.
// Token is stored in localStorage (role-agnostic).
import { Capacitor, CapacitorHttp } from "@capacitor/core";

const TOKEN_KEY = "nadi_auth_token";
const USER_KEY = "nadi_auth_user";

// ----- Production API base ---------------------------------------------------
// Resolution order:
//   1. Vite build-time env: VITE_BACKEND_URL  (set by GitHub Actions / .env)
//   2. CRA-style env:        REACT_APP_BACKEND_URL  (legacy)
//   3. Hardcoded production fallback so the APK always has a reachable API
//      even if a build accidentally ships without env vars.
//
// IMPORTANT: this is the URL the installed APK calls. There is no localhost
// here - the installed app NEVER hits localhost. Capacitor's androidScheme
// "https://localhost" is only the *origin* of the WebView's bundled assets,
// not the API target.
//
// For the WEB build deployed at prmtly.com we keep this empty so the SPA
// makes same-origin relative calls (`/api/auth/login` → resolves to
// `https://prmtly.com/api/auth/login`). The APK build overrides this via
// `frontend/.env.production` (`VITE_BACKEND_URL=https://prmtly.com`).
const PROD_API_FALLBACK = "https://prmtly.com";

/**
 * Tolerantly extract a clean base URL from whatever the env / secret holds.
 *
 * We've seen GitHub Actions secrets get set to the *markdown* version of
 * the URL (e.g. when a user copy-pastes from a chat that renders links):
 *     `[https://prmtly.com](https://prmtly.com)`
 * OkHttp on Android then chokes with `no protocol: [https://...`.
 *
 * This sanitiser pulls the FIRST `http(s)://...` substring out of the
 * raw value and trims any trailing slash. So both:
 *     `https://prmtly.com/`
 *     `[https://prmtly.com](https://prmtly.com)`
 *     `<https://prmtly.com>`
 * normalise to the same clean string.
 */
function sanitizeBaseUrl(raw: unknown): string | null {
  if (raw == null) return null;
  const s = String(raw).trim();
  if (!s) return null;
  const m = s.match(/https?:\/\/[^\s\])>"']+/i);
  if (!m) return null;
  return m[0].replace(/\/+$/, "");
}

const BASE: string =
  sanitizeBaseUrl((import.meta as any).env?.VITE_BACKEND_URL) ||
  sanitizeBaseUrl((import.meta as any).env?.REACT_APP_BACKEND_URL) ||
  PROD_API_FALLBACK;

// Surface in the dev tools so future malformed secrets are obvious.
if (typeof console !== "undefined") {
  // eslint-disable-next-line no-console
  console.info("[nadiAuth] API base URL =", BASE);
}

/** Resolved API base URL (no trailing slash). Useful for components that
 *  need to call endpoints not yet wrapped in a typed helper, e.g. the
 *  Stream Proxy stats widget. */
export const API_BASE = BASE;

export type NadiUser = {
  id: string;
  username: string;
  role: "admin" | "user";
  created_at?: string;
  expiry_at?: string | null;
  days_remaining?: number | null;
  is_expired?: boolean;
  xtream_mode?: "shared" | "own";
  xtream_server?: string;
  xtream_username?: string;
  xtream_password?: string;
  note?: string;
  /** Admin per-user ad override.
   *  true  → force ads ON for this user (even if active subscriber)
   *  false → force ads OFF (even if expired) — admin discretion
   *  undefined → follow the global rule (entitlement logic)
   */
  ads_enabled?: boolean | null;
  /** BUG-5 FEATURE (2026-05): ISO timestamp until which all ads are
   *  suppressed for this user. Set by the rewarded-ads "watch N ads
   *  → unlock M hours ad-free" flow (`POST /api/user/grant-ad-free`).
   */
  ad_free_until?: string | null;
};

export const getToken = (): string | null => {
  try {
    return localStorage.getItem(TOKEN_KEY);
  } catch {
    return null;
  }
};

export const getStoredUser = (): NadiUser | null => {
  try {
    const raw = localStorage.getItem(USER_KEY);
    return raw ? (JSON.parse(raw) as NadiUser) : null;
  } catch {
    return null;
  }
};

export const setSession = (token: string, user: NadiUser) => {
  localStorage.setItem(TOKEN_KEY, token);
  localStorage.setItem(USER_KEY, JSON.stringify(user));
};

export const clearSession = () => {
  localStorage.removeItem(TOKEN_KEY);
  localStorage.removeItem(USER_KEY);
};

export function formatApiErrorDetail(detail: unknown): string {
  if (detail == null) return "Something went wrong. Please try again.";
  if (typeof detail === "string") return detail;
  if (Array.isArray(detail)) {
    return detail
      .map((e: any) => (e && typeof e.msg === "string" ? e.msg : JSON.stringify(e)))
      .filter(Boolean)
      .join(" ");
  }
  if (detail && typeof (detail as any).msg === "string") return (detail as any).msg;
  return String(detail);
}

async function request<T>(
  method: string,
  path: string,
  body?: unknown,
  auth: boolean = false,
): Promise<T> {
  const headers: Record<string, string> = { "Content-Type": "application/json" };
  if (auth) {
    const t = getToken();
    if (t) headers["Authorization"] = `Bearer ${t}`;
  }
  const url = `${BASE}${path}`;

  // ────────────────────────────────────────────────────────────────────
  // NATIVE (Android / iOS): use CapacitorHttp directly. This bypasses
  // the WebView's fetch (no CORS, no Cloudflare-edge-cache for the
  // WebView's `https://localhost` origin, no SPA-fallback HTML
  // intercept). It uses Android's OkHttp under the hood — exactly the
  // same stack as `curl`, which we've verified works against the
  // production server.
  //
  // NOTE: we DO NOT rely on Capacitor's optional `CapacitorHttp.enabled`
  // patch of window.fetch — calling the plugin directly here means it
  // works whether or not `cap sync` picked up the config flag.
  // ────────────────────────────────────────────────────────────────────
  if (Capacitor && Capacitor.isNativePlatform && Capacitor.isNativePlatform()) {
    let nres: any;
    try {
      nres = await CapacitorHttp.request({
        url,
        method,
        headers,
        // Pass body as object - CapacitorHttp will JSON-encode automatically
        // when Content-Type is application/json.
        data: body ?? undefined,
        // Force JSON parsing on the native side; if the server returns
        // non-JSON the plugin gives us the raw string in `data` instead.
        responseType: "json",
        // Don't auto-cache the request on Android.
        readTimeout: 30000,
        connectTimeout: 30000,
      });
    } catch (networkErr) {
      const msg =
        networkErr instanceof Error ? networkErr.message : String(networkErr);
      throw new Error(`Network error reaching ${url}: ${msg}`);
    }

    const status: number = (nres && nres.status) || 0;
    const nativeData = nres && nres.data;
    // CapacitorHttp returns parsed JSON in `data` when responseType is
    // 'json' AND the server returned JSON. If the server returned a
    // string (HTML, plaintext), `data` is that string.
    let parsed: any = nativeData;
    if (typeof nativeData === "string") {
      try {
        parsed = JSON.parse(nativeData);
      } catch {
        parsed = null;
      }
    }

    if (status < 200 || status >= 300) {
      const detailMsg = formatApiErrorDetail(parsed?.detail);
      const fallback =
        detailMsg ||
        (typeof nativeData === "string" && nativeData
          ? `HTTP ${status}: ${nativeData.slice(0, 160)}`
          : `HTTP ${status} from ${url}`);
      throw new Error(fallback);
    }

    if (parsed === null) {
      const snippet =
        typeof nativeData === "string" ? nativeData.slice(0, 160) : "";
      throw new Error(
        snippet
          ? `Unexpected non-JSON response from ${url}: ${snippet}`
          : `Empty response from ${url}`,
      );
    }
    return parsed as T;
  }

  // ────────────────────────────────────────────────────────────────────
  // WEB browser: use plain fetch.
  // ────────────────────────────────────────────────────────────────────
  let res: Response;
  try {
    res = await fetch(url, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });
  } catch (networkErr) {
    const msg =
      networkErr instanceof Error ? networkErr.message : String(networkErr);
    throw new Error(`Network error reaching ${url}: ${msg}`);
  }

  let raw = "";
  try {
    raw = await res.text();
  } catch {
    /* no body */
  }
  let data: any = null;
  if (raw) {
    try {
      data = JSON.parse(raw);
    } catch {
      /* not JSON */
    }
  }

  if (!res.ok) {
    const detailMsg = formatApiErrorDetail(data?.detail);
    const fallback =
      detailMsg ||
      (raw
        ? `HTTP ${res.status}: ${raw.slice(0, 160)}`
        : `HTTP ${res.status} (empty body) from ${url}`);
    throw new Error(fallback);
  }

  if (data === null && raw) {
    throw new Error(
      `Unexpected non-JSON response from ${url}: ${raw.slice(0, 160)}`,
    );
  }
  return data as T;
}

// ---- Public auth ----
export const apiLogin = async (
  username: string,
  password: string,
): Promise<{ access_token: string; user: NadiUser }> => {
  const res = await request<{ access_token: string; user: NadiUser } | null>(
    "POST",
    "/api/auth/login",
    { username, password },
  );
  // Guard against PHP backends that return 200 with empty/null body
  // or a different shape (e.g. {success:true, data:{...}}).
  if (!res || typeof res !== "object") {
    throw new Error(
      "Login failed: empty response from server. Check that the backend URL is reachable and CORS is allowed.",
    );
  }
  // Some PHP backends wrap the payload in `data` — unwrap it transparently.
  const flat: any = res as any;
  const payload =
    flat && flat.access_token
      ? flat
      : flat && flat.data && flat.data.access_token
        ? flat.data
        : null;
  if (!payload || !payload.access_token || !payload.user) {
    throw new Error(
      "Login failed: server response is missing access_token or user. Verify your backend at /api/auth/login.",
    );
  }
  return { access_token: payload.access_token, user: payload.user as NadiUser };
};

export const apiMe = () => request<NadiUser>("GET", "/api/auth/me", undefined, true);

// ---- User self-service ----
export type UpdateProfileInput = {
  current_password: string;
  new_username?: string;
  new_password?: string;
};

export const apiUpdateProfile = (body: UpdateProfileInput) =>
  request<{ access_token: string; token_type: string; user: NadiUser }>(
    "PUT",
    "/api/user/profile",
    body,
    true,
  );

// ---- Trial status (device fingerprint) ----
export type TrialStatusApi = {
  hardware_id: string;
  first_seen_at: string;
  last_seen_at: string;
  trial_days: number;
  days_used: number;
  days_remaining: number;
  trial_expired: boolean;
  is_premium: boolean;
  premium_expiry: string | null;
};

export const apiTrialStatus = (body: {
  hardware_id: string;
  mac_address?: string;
  model?: string;
  app_version?: string;
}) => request<TrialStatusApi>("POST", "/api/trial/status", body);

// ---- Subscription (authenticated) ----
export type SubscriptionStatus = {
  user: NadiUser;
  xtream: { server: string; username: string; password: string };
  /**
   * BUG-2 FIX (2026-05): the backend now echoes the resolved mode so
   * the client can render an "own server but unconfigured" error
   * instead of silently falling back to shared credentials when the
   * user has selected Own mode but left fields empty.
   */
  xtream_mode?: "own" | "shared";
  /** True if `xtream` has all three of server/username/password set. */
  xtream_configured?: boolean;
  whatsapp_number: string;
};

export const apiSubscriptionStatus = () =>
  request<SubscriptionStatus>("GET", "/api/subscription/status", undefined, true);

// ---- Admin ----
export const apiListUsers = () =>
  request<NadiUser[]>("GET", "/api/admin/users", undefined, true);

export type CreateUserInput = {
  username: string;
  password: string;
  days: number;
  xtream_mode: "shared" | "own";
  xtream_server?: string;
  xtream_username?: string;
  xtream_password?: string;
  note?: string;
};

export const apiCreateUser = (body: CreateUserInput) =>
  request<NadiUser>("POST", "/api/admin/users", body, true);

export type UpdateUserInput = Partial<{
  password: string;
  extend_days: number;
  set_expiry_at: string;
  xtream_mode: "shared" | "own";
  xtream_server: string;
  xtream_username: string;
  xtream_password: string;
  note: string;
  /** Per-user ad override toggle (admin only). */
  ads_enabled: boolean;
}>;

export const apiUpdateUser = (id: string, body: UpdateUserInput) =>
  request<NadiUser>("PUT", `/api/admin/users/${id}`, body, true);

export const apiDeleteUser = (id: string) =>
  request<{ ok: boolean }>("DELETE", `/api/admin/users/${id}`, undefined, true);

export type SharedProfile = {
  id?: string;
  name: string;
  kind: "xtream" | "m3u" | "stalker";
  server?: string;
  username?: string;
  password?: string;
  m3u_url?: string;
  // Raw text content of an uploaded .m3u/.m3u8 file (admin upload).
  m3u_content?: string;
  // Stalker / MAC portal credentials
  portal_url?: string;
  mac_address?: string;
  // Hybrid Source A (Live TV)
  source_a_server?: string;
  source_a_username?: string;
  source_a_password?: string;
  source_a_kind?: string;
  // Hybrid Source B (Movies & Series)
  source_b_server?: string;
  source_b_username?: string;
  source_b_password?: string;
  source_b_kind?: string;
};

export type Settings = {
  whatsapp_number: string;
  shared_xtream_server: string;
  shared_xtream_username: string;
  shared_xtream_password: string;
  shared_profiles: SharedProfile[];
  proxy_enabled?: boolean;
  external_player_enabled?: boolean;
  player_notice_enabled?: boolean;
  inline_preview_enabled?: boolean;
  /** Server kill-switch for FCM push notifications. Default OFF — see
   *  usePushNotifications.ts for the rationale (v2.1.0 crash fix). */
  push_enabled?: boolean;
  // ── In-app update / about / support fields ──
  update_enabled?: boolean;
  app_version?: string;
  min_supported_version?: string;
  update_apk_url?: string;
  update_play_url?: string;
  update_changelog?: string;
  popup_delay_seconds?: number;
  whatsapp_message?: string;
  privacy_policy_text?: string;
  support_email?: string;
  creator_name?: string;
  creator_role?: string;
  creator_avatar_url?: string;
  creator_bio?: string;
  // ── AdMob / Ads configuration ──
  ads_enabled?: boolean;
  /** BUG-2 FIX (2026-05) — admin-controlled toggle for logged-in users. */
  ads_enabled_for_users?: boolean;
  /** BUG-2 FIX (2026-05) — admin-controlled toggle for guests. */
  ads_enabled_for_guests?: boolean;
  ads_testing_mode?: boolean;
  ads_app_id?: string;
  ads_banner_id?: string;
  ads_interstitial_id?: string;
  ads_native_id?: string;
  ads_rewarded_id?: string;
  ads_interstitial_every?: number;
  // ── Privacy / Terms (admin-editable, surfaced by the About menu modal) ──
  /** Admin-editable Terms & Conditions text. Markdown supported. */
  terms_policy_text?: string;
  // ── Smart-TV "House Ad" banner (FEATURE 2026-01) ──
  // AdMob is mobile-only; on Tizen / webOS the React frontend mounts
  // a custom banner driven by these fields. `tv_banner_type` chooses
  // between rendering an <img> wrapped in <a> ("image") or a raw HTML
  // snippet ("html"). All payloads are persisted regardless of type
  // so the admin can flip modes without losing data.
  tv_banner_enabled?: boolean;
  tv_banner_type?: "image" | "html";
  tv_banner_html?: string;
  tv_banner_image_url?: string;
  tv_banner_click_url?: string;
};

export const apiGetSettings = () =>
  request<Settings>("GET", "/api/admin/settings", undefined, true);

export const apiUpdateSettings = (body: Partial<Settings>) =>
  request<Settings>("PUT", "/api/admin/settings", body, true);

// ---- BUG-5 FEATURE (2026-05): rewarded-ads "unlock ad-free" ----
export type AdFreeGrantResponse = {
  ad_free_until: string | null;
  granted_hours: number;
  seconds_remaining: number;
};
export type AdFreeStatusResponse = {
  ad_free_until: string | null;
  seconds_remaining: number;
  active: boolean;
  ads_count?: number;
};

export const apiGrantAdFree = (hours = 2.0) =>
  request<AdFreeGrantResponse>(
    "POST",
    "/api/user/grant-ad-free",
    { hours },
    true,
  );

export type AdWatchedResponse = {
  ads_count: number;
  threshold: number;
  granted: boolean;
  ad_free_until: string | null;
  seconds_remaining: number;
};

/** Record ONE watched rewarded ad. The server increments the persistent
 *  `user_ads_count`, and once it reaches `threshold` it grants `hours`
 *  ad-free and resets the counter (server-authoritative). */
export const apiRecordAdWatched = (threshold = 3, hours = 1.0) =>
  request<AdWatchedResponse>(
    "POST",
    "/api/user/ad-watched",
    { threshold, hours },
    true,
  );

export const apiAdFreeStatus = () =>
  request<AdFreeStatusResponse>(
    "GET",
    "/api/user/ad-free-status",
    undefined,
    true,
  );

export const apiListSharedProfiles = () =>
  request<{ profiles: SharedProfile[] }>(
    "GET",
    "/api/shared-profiles",
    undefined,
    true,
  );

// ---- Public (no auth) ----
export type PublicSharedXtream = {
  server: string;
  username: string;
  password: string;
  configured: boolean;
};

export const apiPublicSharedXtream = () =>
  request<PublicSharedXtream>("GET", "/api/public/shared-xtream");

export type PublicPlaybackMode = {
  external_player_enabled: boolean;
  player_notice_enabled?: boolean;
  inline_preview_enabled?: boolean;
};

export const apiPublicPlaybackMode = () =>
  request<PublicPlaybackMode>("GET", "/api/public/playback-mode");

// ── Push notifications (FCM) ─────────────────────────────────────
export type RegisterTokenInput = {
  token: string;
  device_id: string;
  platform?: "android" | "ios" | "web";
  app_version?: string;
};

export const apiRegisterDeviceToken = (body: RegisterTokenInput) =>
  request<{ registered: boolean; device_id: string }>(
    "POST",
    "/api/devices/register-token",
    {
      ...body,
      app_version:
        body.app_version ||
        ((import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env?.
          VITE_APP_VERSION as string | undefined) ||
        "1.0.1",
    },
    true,
  );

export type BroadcastInput = {
  title: string;
  body: string;
  route?: string | null;
  /** Optional rich-notification image (HTTPS URL, ≤ 1 MB) */
  image_url?: string | null;
  /** Optional CTA label rendered as a "Watch Now" button */
  action_label?: string | null;
  /** Optional specific user — when set, falls back to /api/admin/notify-user */
  target_user_id?: string | null;
  data?: Record<string, string>;
};

export type BroadcastResult = {
  queued: boolean;
  total_tokens: number;
  sent: number;
  failed: number;
  pruned: number;
  error?: string | null;
};

export const apiBroadcastNotification = (body: BroadcastInput) => {
  const path = body.target_user_id
    ? "/api/admin/notify-user"
    : "/api/admin/broadcast-notification";
  return request<BroadcastResult>("POST", path, body, true);
};

// ── Live engagement stats ────────────────────────────────────────
export type LiveStats = {
  live_watchers: number;
  window_seconds: number;
  by_kind: Record<string, number>;
  top_channels: { channel: string; viewers: number }[];
  as_of: string;
};

export type InstallStats = {
  total_installs: number;
  active_installs: number;
  dau: number;
  wau: number;
  total_installs_trial_devices?: number;
  active_installs_trial_devices_30d?: number;
  total_installs_fcm_tokens?: number;
  active_installs_fcm_tokens?: number;
  total_installs_merged_unique?: number;
  by_platform: Record<string, number>;
  as_of: string;
};

export const apiAdminLiveStats = () =>
  request<LiveStats>("GET", "/api/admin/stats/live", undefined, true);

export const apiAdminInstallStats = () =>
  request<InstallStats>("GET", "/api/admin/stats/installs", undefined, true);

export const apiPostHeartbeat = (
  body: {
    device_id?: string;
    channel?: string;
    kind?: "live" | "vod" | "series";
  } = {},
) => request<{ ok: boolean; device_id: string }>(
  "POST",
  "/api/stats/heartbeat",
  body,
  true,
);

export type BroadcastHistoryItem = {
  title: string;
  body: string;
  route?: string | null;
  total_tokens: number;
  sent: number;
  failed: number;
  pruned: number;
  created_at: string;
};

export const apiListBroadcasts = (limit = 20) =>
  request<{ broadcasts: BroadcastHistoryItem[] }>(
    "GET",
    `/api/admin/broadcasts?limit=${limit}`,
    undefined,
    true,
  );


// ---- Admin alerts ----
export type UnconfiguredUser = {
  id: string;
  username: string;
  created_at: string;
  days_remaining: number | null;
};

export type AdminAlerts = {
  unconfigured_own_users: UnconfiguredUser[];
};

export const apiAdminAlerts = () =>
  request<AdminAlerts>("GET", "/api/admin/alerts", undefined, true);
