// Контракт с бэкендом Netrunner (control-plane).
//
// Базовый URL берётся из VITE_API_BASE_URL (см. .env). По умолчанию — прод.
// Эндпоинт списка серверов закрыт auth_guard (JWT), поэтому клиент анонимно
// бутстрапит токен через Ghost Protocol (/auth/seed/*): при первом запуске
// генерирует seed, дальше переиспользует его для логина. JWT короткоживущий
// (~10 мин), поэтому кешируется и автоматически обновляется по истечении.

const API_BASE = (
  import.meta.env.VITE_API_BASE_URL ?? "https://netrunner-vpn.com"
).replace(/\/$/, "");

const API_V1 = `${API_BASE}/api/v1`;

const SEED_KEY = "nrxp_seed";
const TOKEN_KEY = "nrxp_token";

// --- Ретраи с backoff для сетевых сбоев ---
// Раньше любой fetch() к бэкенду падал с первой же сетевой ошибки/5xx без
// единой попытки повторить — временная просадка сети/перезапуск бэкенда сразу
// приводили к нечитаемой ошибке в UI ("anon_failed" и т.п.). Ретраим только
// сетевые сбои (fetch throws) и 5xx (backend сам сломан) — 4xx-ответы (401 на
// просроченный код, 410 и т.д.) НЕ ретраим, это осмысленный ответ сервера,
// а не транзиентный сбой, повторять его бессмысленно.
const RETRY_ATTEMPTS = 2; // + первая попытка = до 3 всего
const RETRY_BASE_DELAY_MS = 400;

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export class NetworkUnavailableError extends Error {
  constructor(cause?: unknown) {
    super("NETWORK_UNAVAILABLE");
    this.name = "NetworkUnavailableError";
    if (cause) this.cause = cause;
  }
}

async function fetchWithRetry(
  input: string,
  init?: RequestInit,
): Promise<Response> {
  let lastError: unknown;
  for (let attempt = 0; attempt <= RETRY_ATTEMPTS; attempt++) {
    try {
      const res = await fetch(input, init);
      if (res.status >= 500 && attempt < RETRY_ATTEMPTS) {
        await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
        continue;
      }
      return res;
    } catch (e) {
      lastError = e;
      if (attempt < RETRY_ATTEMPTS) {
        await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt);
        continue;
      }
    }
  }
  throw new NetworkUnavailableError(lastError);
}

// --- Доменная модель сервера (камелкейс-зеркало VpnNode из бэкенда) ---
export interface ServerNode {
  id: string;
  name: string;
  countryCode: string;
  ipAddress: string;
  port: number;
  // Домен-декой (SNI) — под него маскируется ClientHello при подключении
  // именно к этому серверу. Должен совпадать с --decoy-host на сервере.
  sni: string;
  status: string;
  currentLoad: number;
  tunnelAddress: string;
  tunnelPrefix: number;
  tunnelDns: string;
  tunnelMtu: number;
}

// Сырой ответ бэкенда (snake_case, сериализация VpnNode по умолчанию).
interface RawVpnNode {
  id: string;
  name: string;
  country_code: string;
  ip_address: string;
  port: number;
  sni: string;
  status: string;
  current_load: number;
  tunnel_address: string;
  tunnel_prefix: number;
  tunnel_dns: string;
  tunnel_mtu: number;
}

function mapNode(n: RawVpnNode): ServerNode {
  return {
    id: n.id,
    name: n.name,
    countryCode: n.country_code,
    ipAddress: n.ip_address,
    port: n.port,
    sni: n.sni,
    status: n.status,
    currentLoad: n.current_load,
    tunnelAddress: n.tunnel_address,
    tunnelPrefix: n.tunnel_prefix,
    tunnelDns: n.tunnel_dns,
    tunnelMtu: n.tunnel_mtu,
  };
}

// --- Работа с токеном ---

function decodeJwtExp(token: string): number | null {
  try {
    const payload = token.split(".")[1];
    const json = JSON.parse(
      atob(payload.replace(/-/g, "+").replace(/_/g, "/")),
    );
    return typeof json.exp === "number" ? json.exp : null;
  } catch {
    return null;
  }
}

function isTokenValid(token: string | null): token is string {
  if (!token) return false;
  const exp = decodeJwtExp(token);
  if (exp === null) return false;
  // 30 секунд запаса, чтобы не ловить протухание прямо в полёте.
  return exp * 1000 > Date.now() + 30_000;
}

async function generateSeed(): Promise<string> {
  const res = await fetchWithRetry(`${API_V1}/auth/seed/generate`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
  });
  if (!res.ok) throw new Error(`seed/generate failed: ${res.status}`);
  const data = (await res.json()) as { seed: string; token: string };
  localStorage.setItem(SEED_KEY, data.seed);
  localStorage.setItem(TOKEN_KEY, data.token);
  return data.token;
}

async function loginSeed(seed: string): Promise<string> {
  const res = await fetchWithRetry(`${API_V1}/auth/seed/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ seed }),
  });
  if (!res.ok) throw new Error(`seed/login failed: ${res.status}`);
  const data = (await res.json()) as { token: string };
  localStorage.setItem(TOKEN_KEY, data.token);
  return data.token;
}

// Возвращает валидный JWT: из кеша, через логин по сохранённому seed
// либо генерируя новый seed при первом запуске.
async function ensureToken(): Promise<string> {
  const cached = localStorage.getItem(TOKEN_KEY);
  if (isTokenValid(cached)) return cached;

  const seed = localStorage.getItem(SEED_KEY);
  if (seed) {
    try {
      return await loginSeed(seed);
    } catch {
      // Seed мог протухнуть/быть удалён на сервере — перегенерируем.
    }
  }
  return generateSeed();
}

// --- Публичный API ---

export async function fetchNodes(): Promise<ServerNode[]> {
  const token = await ensureToken();
  const res = await fetchWithRetry(`${API_V1}/nodes`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) throw new Error(`GET /nodes failed: ${res.status}`);
  const data = (await res.json()) as RawVpnNode[];
  return data.map(mapNode);
}

/** Текущий JWT, если уже есть (для передачи прокси на `--require-auth` инстансах). */
export function getCachedToken(): string | null {
  const cached = localStorage.getItem(TOKEN_KEY);
  return isTokenValid(cached) ? cached : null;
}

/** Гарантирует, что seed/токен существуют (Ghost Protocol), и возвращает seed для бэкапа. */
export async function getCurrentSeed(): Promise<string> {
  await ensureToken();
  const seed = localStorage.getItem(SEED_KEY);
  if (!seed) {
    // Юзер залогинен через Telegram (seed никогда не выдавался) — сгенерировать
    // отдельный seed поверх текущей сессии сервер сейчас не умеет, только через
    // новую анонимную регистрацию, что разлогинило бы текущий аккаунт.
    throw new Error("NO_SEED_FOR_ACCOUNT");
  }
  return seed;
}

/** Вход по чужому/сохранённому seed (например, с другого устройства). */
export async function loginWithSeed(seed: string): Promise<void> {
  await loginSeed(seed);
  localStorage.setItem(SEED_KEY, seed);
}

/**
 * Обменивает код на сессию. Код теперь генерирует сам бот по кнопке
 * "Код для входа" (уже подтверждённым — см. AuthService::create_bot_login_code),
 * поэтому это одноразовый обмен, а не поллинг: `true` при первом же успешном вызове.
 */
export async function pollTelegramLogin(authCode: string): Promise<boolean> {
  const res = await fetchWithRetry(`${API_V1}/auth/telegram/session/${authCode}`);
  if (res.status === 401 || res.status === 410) {
    throw new Error("TELEGRAM_LOGIN_EXPIRED");
  }
  if (!res.ok) throw new Error(`telegram/session poll failed: ${res.status}`);
  const data = (await res.json()) as { status: string; token?: string };
  if (data.status === "success" && data.token) {
    localStorage.setItem(TOKEN_KEY, data.token);
    localStorage.removeItem(SEED_KEY);
    return true;
  }
  return false;
}

export interface UsageInfo {
  usedBytes: number;
  limitBytes: number | null;
}

/** Текущий расход/лимит трафика юзера — для индикатора на `VpnStats`. */
export async function fetchUsage(): Promise<UsageInfo> {
  const token = await ensureToken();
  const res = await fetchWithRetry(`${API_V1}/users/me`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) throw new Error(`GET /users/me failed: ${res.status}`);
  const data = (await res.json()) as {
    data_used_bytes: number;
    data_limit_bytes: number | null;
  };
  return { usedBytes: data.data_used_bytes, limitBytes: data.data_limit_bytes };
}

let cachedBotUsername: string | null = null;

/** Юзернейм бота для ссылки "открыть чат" на экране входа (без `?start=` —
 * код теперь вводится вручную, см. pollTelegramLogin). */
export async function getBotUsername(): Promise<string | null> {
  if (cachedBotUsername) return cachedBotUsername;
  try {
    const res = await fetchWithRetry(`${API_V1}/config`);
    if (!res.ok) return null;
    const data = (await res.json()) as { bot_username: string };
    cachedBotUsername = data.bot_username;
    return cachedBotUsername;
  } catch {
    return null;
  }
}
