// VK Mini App SDK access via @vkontakte/vk-bridge. The bridge is imported lazily inside the
// functions that need it — not at module top level — because the SDK reads browser globals on
// import: the lazy import keeps the pure URL helpers below importable in the node test environment,
// and code-splits the bridge into a chunk loaded only on the /vk/ entry. This wraps the subset the
// app uses: launch detection, the signed launch parameters (for auth.vk) and the user's display name
// (VKWebAppGetUserInfo, since VK omits it from the signed launch params). Every helper is safe to
// call outside VK.

import { clientEnvLines, queryFieldNames, type LaunchDiag } from './launchdiag';
import { isExternalHttpUrl, type Haptic } from './telegram';

async function bridge() {
  return (await import('@vkontakte/vk-bridge')).default;
}

/**
 * onVKPath reports whether the app is served under the dedicated VK entry path (/vk/).
 */
export function onVKPath(): boolean {
  if (typeof location === 'undefined') return false;
  return location.pathname.startsWith('/vk/');
}

/**
 * vkLaunchParams returns the raw signed VK launch query string (the vk_* parameters plus sign) from
 * the page URL — the exact form the gateway verifies — or '' when the URL carries no signed launch
 * (an ordinary browser tab, or the /vk/ path opened directly).
 */
export function vkLaunchParams(): string {
  if (typeof location === 'undefined') return '';
  const query = location.search.replace(/^\?/, '');
  return new URLSearchParams(query).has('sign') ? query : '';
}

/**
 * insideVK reports whether the app launched as a VK Mini App — the URL carries signed launch
 * parameters (an ordinary browser tab has none).
 */
export function insideVK(): boolean {
  return vkLaunchParams() !== '';
}

/**
 * vkInit signals to the VK client that the Mini App has loaded (VKWebAppInit), dismissing VK's own
 * loading cover. Best-effort: it resolves even if the bridge is unavailable (outside VK), so the
 * caller can await it unconditionally.
 */
export async function vkInit(): Promise<void> {
  try {
    await (await bridge()).send('VKWebAppInit', {});
  } catch {
    // Outside VK there is no client to receive it; the launch continues regardless.
  }
}

/**
 * vkClose closes the VK Mini App (VKWebAppClose), used on the terminal account-deleted
 * screen. Best-effort: a no-op outside VK.
 */
export async function vkClose(): Promise<void> {
  try {
    await (await bridge()).send('VKWebAppClose', { status: 'success' });
  } catch {
    // Outside VK there is no client to receive it.
  }
}

/**
 * vkStorageGet reads a value from the user's VK Bridge storage (VKWebAppStorageGet), resolving null
 * when the key is unset, VK is unavailable (outside a VK launch / the bridge cannot answer) or the
 * read errors — so the caller can fall back to the local value. VK Bridge storage travels over the
 * postMessage channel to vk.com rather than browser storage, so it survives the desktop iframe's
 * partitioned or blocked IndexedDB and localStorage (notably in Firefox), unlike the local store.
 */
export async function vkStorageGet(key: string): Promise<string | null> {
  if (!insideVK()) return null;
  try {
    const r = (await (await bridge()).send('VKWebAppStorageGet', { keys: [key] })) as {
      keys?: { key: string; value: string }[];
    };
    const item = r.keys?.find((k) => k.key === key);
    return item && item.value !== '' ? item.value : null;
  } catch {
    return null;
  }
}

/**
 * vkStorageSet writes value to the user's VK Bridge storage under key (VKWebAppStorageSet). It is
 * best-effort: a no-op outside VK, and it swallows write errors, since the browser-local store stays
 * the in-session source of truth.
 */
export async function vkStorageSet(key: string, value: string): Promise<void> {
  if (!insideVK()) return;
  try {
    await (await bridge()).send('VKWebAppStorageSet', { key, value });
  } catch {
    // Outside VK / unsupported / over quota: the local store keeps the value for this session.
  }
}

/**
 * vkUserName fetches the launching user's display name via VKWebAppGetUserInfo, since VK omits it
 * from the signed launch params. Returns '' on any failure or outside VK, so the backend falls back
 * to a generated placeholder. The value is a cosmetic seed only — being unsigned, the gateway never
 * trusts it for identity.
 */
export async function vkUserName(): Promise<string> {
  try {
    const u = await (await bridge()).send('VKWebAppGetUserInfo', {});
    return [u.first_name, u.last_name].filter(Boolean).join(' ').trim();
  } catch {
    return '';
  }
}

/**
 * vkAppId returns the launching VK app id (vk_app_id) from the signed launch parameters in the URL —
 * so the app can build its own vk.com/app<id> links without a VK API call — or '' outside a VK launch.
 */
export function vkAppId(): string {
  if (typeof location === 'undefined') return '';
  return new URLSearchParams(location.search.replace(/^\?/, '')).get('vk_app_id') ?? '';
}

/**
 * vkStartParam returns the VK direct-link deep-link payload. VK passes everything after the '#' in a
 * vk.com/app<id>#<payload> link to the app as the `hash` query parameter (it is also in location.hash,
 * but that collides with the app's hash router, so the query parameter is the safe source). Empty when
 * the launch carried no deep link.
 */
export function vkStartParam(): string {
  if (typeof location === 'undefined') return '';
  return new URLSearchParams(location.search.replace(/^\?/, '')).get('hash') ?? '';
}

/**
 * vkPlatform returns the VK client platform from the launch parameters (vk_platform:
 * mobile_android, mobile_iphone, desktop_web, mobile_web, …), or '' outside a VK launch.
 */
export function vkPlatform(): string {
  if (typeof location === 'undefined') return '';
  return new URLSearchParams(location.search.replace(/^\?/, '')).get('vk_platform') ?? '';
}

// --- Launch diagnostics (the /vk/ entry without a signed launch) ---

/** The launch parameters a valid VK Mini App launch is expected to carry in the URL; their absence —
 *  notably `sign` — is the signal the launch-error screen reports. Only names are inspected. */
const expectedVKParams = ['vk_app_id', 'vk_user_id', 'vk_ts', 'vk_platform', 'sign'];

/**
 * vkDiagLines formats the URL-derived lines of the VK launch diagnostic from a query string (no
 * leading '?') and two runtime flags. It reports only the launch-parameter NAMES present and the
 * expected ones missing — never a value, since `sign` is auth material — plus the non-secret
 * `vk_platform` and whether the URL carried a signature at all. Pure, so it is unit-tested.
 */
export function vkDiagLines(search: string, inIframe: boolean, referrerHost: string): string[] {
  const names = queryFieldNames(search);
  const platform = new URLSearchParams(search).get('vk_platform') ?? '';
  const missing = expectedVKParams.filter((p) => !names.includes(p));
  return [
    `launch: ${names.includes('sign') ? 'signed' : 'unsigned'}   iframe: ${inIframe ? 'yes' : 'no'}`,
    `vk-platform: ${platform || '—'}`,
    `params: ${names.join(',') || '—'}`,
    `missing: ${missing.join(',') || '—'}`,
    `referrer: ${referrerHost || '—'}`,
  ];
}

/** vkInIframe reports whether the app runs inside an iframe (VK desktop frames the Mini App; a plain
 *  browser tab is top-level). A cross-origin top frame denies property access, which itself means we
 *  are framed, so that is caught as true. */
function vkInIframe(): boolean {
  if (typeof window === 'undefined') return false;
  try {
    return window.self !== window.top;
  } catch {
    return true;
  }
}

/** vkReferrerHost returns the host of document.referrer (a real VK launch is referred from vk.com; a
 *  direct type-in has none), or '' when there is no referrer or it does not parse. */
function vkReferrerHost(): string {
  if (typeof document === 'undefined' || !document.referrer) return '';
  try {
    return new URL(document.referrer).host;
  } catch {
    return '';
  }
}

/**
 * vkLaunchDiag captures the current VK launch state as a neutral LaunchDiag for the shared LaunchError
 * screen — a passive read of the URL launch parameters (names only), the iframe / referrer context and
 * the shared client environment. retry is false: VK delivers its signed parameters in the launch URL at
 * load, so — unlike Telegram's initData — they never arrive late, and a Retry could not recover them.
 */
export function vkLaunchDiag(): LaunchDiag {
  const search = typeof location === 'undefined' ? '' : location.search.replace(/^\?/, '');
  const report = [...vkDiagLines(search, vkInIframe(), vkReferrerHost()), ...clientEnvLines()].join('\n');
  return { platform: 'vk', report, retry: false };
}

/**
 * vkAndroidWebView reports whether the app runs inside the Android VK client's WebView
 * (vk_platform mobile_android / mobile_android_messenger) — the one environment whose WebView
 * ignores target=_blank and navigates the app's own window instead of opening a browser.
 */
export function vkAndroidWebView(): boolean {
  return insideVK() && vkPlatform().startsWith('mobile_android');
}

/**
 * vkExternalBrowserUrl wraps url in VK's own leave-VK redirect (vk.com/away.php). The VK mobile
 * client intercepts vk.com navigations natively and hands the redirect target to the system
 * browser, so an external link escapes the Mini App WebView instead of replacing the game.
 * vk-bridge (3.x) offers no method to open an external URL, so this redirect is the supported
 * escape hatch. Pure, so it is unit-tested.
 */
export function vkExternalBrowserUrl(url: string): string {
  return `https://vk.com/away.php?to=${encodeURIComponent(url)}`;
}

/**
 * vkOpenExternalUrl opens url in the system browser from inside the Android VK client by routing
 * it through vkExternalBrowserUrl. Returns false outside the Android VK WebView — iOS and the
 * desktop iframe open target=_blank correctly and are left alone — so the caller falls back to
 * the anchor's own navigation or a plain window.open.
 */
export function vkOpenExternalUrl(url: string): boolean {
  if (typeof window === 'undefined' || !vkAndroidWebView()) return false;
  window.open(vkExternalBrowserUrl(url), '_blank');
  return true;
}

/**
 * routeExternalLinkInVK decides whether a clicked anchor must be opened through VK's external
 * redirect rather than the WebView's default navigation: only inside the Android VK client, and
 * only for an external http(s) link opened in a new tab (target=_blank). Returns true when it
 * opened the link — the caller should then preventDefault — and false to let the browser handle
 * the click normally.
 */
export function routeExternalLinkInVK(anchor: { href: string; target: string }): boolean {
  if (anchor.target !== '_blank') return false;
  if (!isExternalHttpUrl(anchor.href)) return false;
  return vkOpenExternalUrl(anchor.href);
}

/**
 * vkShare opens VK's native share dialog for link (VKWebAppShare) — the in-iframe replacement for
 * navigator.share, which is unavailable in the desktop VK iframe. Resolves true when the share was
 * handled, false on any failure or outside VK.
 */
export async function vkShare(link: string): Promise<boolean> {
  try {
    await (await bridge()).send('VKWebAppShare', { link });
    return true;
  } catch {
    return false;
  }
}

/**
 * vkShowImages opens VK's native image viewer on url — on the Android client this is the
 * PNG delivery: the viewer's own "save" works there, while VKWebAppDownloadFile hangs
 * indefinitely (on-device finding). Resolves false on any failure, so the caller can
 * fall back.
 */
export async function vkShowImages(url: string): Promise<boolean> {
  try {
    await (await bridge()).send('VKWebAppShowImages', { images: [url] });
    return true;
  } catch {
    return false;
  }
}

/**
 * vkShowOrderBox opens VK's payment box for a "голоса" item purchase; item is the order id the
 * backend created (VK echoes it to our payment callback, which credits). Resolves true when the
 * payment completes, false on cancel/failure or outside VK. The chips are credited by the server
 * callback, so the wallet refreshes from the payment push regardless of this result.
 */
export async function vkShowOrderBox(item: string): Promise<boolean> {
  try {
    await (await bridge()).send('VKWebAppShowOrderBox', { type: 'item', item });
    return true;
  } catch {
    return false;
  }
}

/**
 * vkRewardedReady reports whether a rewarded ad is preloaded and ready to show
 * (VKWebAppCheckNativeAds, required before a rewarded view). A false — or an unavailable method —
 * hides the "watch for chips" button, since tapping it would show nothing.
 */
export async function vkRewardedReady(): Promise<boolean> {
  try {
    const data = await (await bridge()).send('VKWebAppCheckNativeAds', { ad_format: 'reward' });
    return !!(data as { result?: boolean }).result;
  } catch {
    return false;
  }
}

/**
 * vkShowRewarded shows a rewarded ad (VKWebAppShowNativeAds) and reports whether it was watched
 * (data.result — VK returns only this boolean, no server-verifiable token). A rejected call reports
 * not-watched.
 */
export async function vkShowRewarded(): Promise<boolean> {
  try {
    const data = await (await bridge()).send('VKWebAppShowNativeAds', { ad_format: 'reward' });
    return !!(data as { result?: boolean }).result;
  } catch {
    return false;
  }
}

/**
 * vkShowInterstitial shows a between-screens interstitial ad (VKWebAppShowNativeAds, interstitial
 * format) and reports whether it was shown. It carries no reward — it is the post-move fullscreen ad.
 * A rejected or unavailable call reports false.
 */
export async function vkShowInterstitial(): Promise<boolean> {
  try {
    const data = await (await bridge()).send('VKWebAppShowNativeAds', { ad_format: 'interstitial' });
    return !!(data as { result?: boolean }).result;
  } catch {
    return false;
  }
}

/**
 * vkDownloadFile downloads url as filename through the VK client (VKWebAppDownloadFile) —
 * the mobile in-app file delivery, where the webview ignores <a download>. Resolves false
 * on any failure or where the method is unavailable (notably the desktop iframe), so the
 * caller falls back to a plain browser download of the same URL.
 */
export async function vkDownloadFile(url: string, filename: string): Promise<boolean> {
  try {
    await (await bridge()).send('VKWebAppDownloadFile', { url, filename });
    return true;
  } catch {
    return false;
  }
}

/**
 * vkCopyText copies text to the clipboard via VKWebAppCopyText — which works inside the VK iframe,
 * where navigator.clipboard is blocked. Resolves true on success, false on any failure or outside VK.
 */
export async function vkCopyText(text: string): Promise<boolean> {
  try {
    await (await bridge()).send('VKWebAppCopyText', { text });
    return true;
  } catch {
    return false;
  }
}

/**
 * vkOnScheme subscribes to VK's appearance (VKWebAppUpdateConfig) and calls handler with the mapped
 * 'light' | 'dark' scheme on launch and whenever the user switches the VK client theme — so the app's
 * "auto" theme can follow the VK client instead of the (often wrong) webview prefers-color-scheme.
 * A no-op outside VK.
 */
export async function vkOnScheme(handler: (scheme: 'light' | 'dark') => void): Promise<void> {
  try {
    const b = await bridge();
    b.subscribe((e) => {
      const detail = (e as { detail?: { type?: string; data?: { scheme?: string; appearance?: string } } }).detail;
      if (detail?.type !== 'VKWebAppUpdateConfig') return;
      const scheme = detail.data?.scheme ?? detail.data?.appearance ?? '';
      handler(/dark|space_gray/i.test(scheme) ? 'dark' : 'light');
    });
  } catch {
    // Outside VK / bridge unavailable: leave the app on its own theme.
  }
}

/** VKInsets is the device safe-area the VK client reports (px). */
export interface VKInsets {
  top: number;
  bottom: number;
  left: number;
  right: number;
}

/**
 * vkOnInsets subscribes to VK's safe-area insets and calls handler with them on launch and on change.
 * VK reports them via VKWebAppUpdateConfig (iOS) and the dedicated VKWebAppUpdateInsets event; the VK
 * mobile webview does not expose them through CSS env() the way iOS Safari does, so the app reads them
 * from the bridge to clear the home bar (notably on Android). A no-op outside VK.
 */
export async function vkOnInsets(handler: (insets: VKInsets) => void): Promise<void> {
  try {
    const b = await bridge();
    b.subscribe((e) => {
      const d = (e as { detail?: { type?: string; data?: { insets?: Partial<VKInsets> } } }).detail;
      if (d?.type !== 'VKWebAppUpdateConfig' && d?.type !== 'VKWebAppUpdateInsets') return;
      const i = d.data?.insets;
      if (!i) return;
      handler({ top: i.top ?? 0, bottom: i.bottom ?? 0, left: i.left ?? 0, right: i.right ?? 0 });
    });
  } catch {
    // Outside VK / bridge unavailable: the CSS env() safe-area fallback applies.
  }
}

/**
 * vkHaptic fires a VK Bridge haptic mirroring the Telegram set: a selection tick
 * (VKWebAppTapticSelectionChanged), a success/warning/error notification
 * (VKWebAppTapticNotificationOccurred) or a light/medium/heavy impact (VKWebAppTapticImpactOccurred).
 * Best-effort and a no-op outside VK or on a client without taptic support (the send rejects and is
 * swallowed), so callers fire it unconditionally.
 */
export async function vkHaptic(kind: Haptic): Promise<void> {
  try {
    const b = await bridge();
    if (kind === 'select') {
      await b.send('VKWebAppTapticSelectionChanged', {});
    } else if (kind === 'success' || kind === 'warning' || kind === 'error') {
      await b.send('VKWebAppTapticNotificationOccurred', { type: kind });
    } else {
      await b.send('VKWebAppTapticImpactOccurred', { style: kind });
    }
  } catch {
    // Outside VK / no taptic support: silent, like the Telegram haptic off-platform.
  }
}

/**
 * vkDisableSwipeBack turns off VK's horizontal swipe-back gesture (VKWebAppSetSwipeSettings with
 * history:false) so it does not fight the app's own edge-swipe-back and on-board tile drag — the
 * app owns navigation through its back chevron, as on Telegram (telegramDisableVerticalSwipes).
 * Best-effort; a no-op outside VK or on a client without the method.
 */
export async function vkDisableSwipeBack(): Promise<void> {
  try {
    await (await bridge()).send('VKWebAppSetSwipeSettings', { history: false });
  } catch {
    // Outside VK / unsupported: VK's default gesture handling stays as-is.
  }
}

/**
 * appearanceForBg maps a background colour to the VK view appearance: a dark background wants the
 * 'dark' appearance (light status-bar icons), a light one 'light'. It accepts a `#rgb` / `#rrggbb`
 * string (the app's `--bg` token); anything unparseable defaults to 'light'. Pure, so it is
 * unit-tested.
 */
export function appearanceForBg(bg: string): 'light' | 'dark' {
  const hex = bg.trim().replace(/^#/, '');
  const full = hex.length === 3 ? [...hex].map((c) => c + c).join('') : hex;
  if (full.length !== 6 || /[^0-9a-fA-F]/.test(full)) return 'light';
  const r = parseInt(full.slice(0, 2), 16);
  const g = parseInt(full.slice(2, 4), 16);
  const b = parseInt(full.slice(4, 6), 16);
  // Rec. 601 luma; below the midpoint is a dark background.
  return 0.299 * r + 0.587 * g + 0.114 * b < 128 ? 'dark' : 'light';
}

/**
 * vkSetViewSettings paints VK's status bar — and, on Android, the action and navigation bars — to
 * match the app. appearance is the VK status-bar appearance ('light' | 'dark'); actionBarColor and
 * navigationBarColor are Android-only hex colours (ignored elsewhere). Best-effort; a no-op outside
 * VK or on a client without the method.
 */
export async function vkSetViewSettings(
  appearance: 'light' | 'dark',
  actionBarColor: string,
  navigationBarColor: string,
): Promise<void> {
  try {
    await (await bridge()).send('VKWebAppSetViewSettings', {
      status_bar_style: appearance,
      action_bar_color: actionBarColor,
      navigation_bar_color: navigationBarColor,
    });
  } catch {
    // Outside VK / unsupported: the client keeps its default bars.
  }
}
