// VK ID web login for linking a VK identity to the current account from a browser.
// Unlike the VK Mini App (whose signed launch params authenticate offline), a browser
// has no launch signature, so it runs the VK ID raw OAuth 2.1 flow (PKCE, no @vkid/sdk):
// a full-page redirect to VK's hosted login, then a callback to our redirect URL carrying
// the authorization code. The gateway completes the confidential code exchange. This is
// web-only — a full-page redirect would strand a native Mini App webview, where the
// platform identity is already linked anyway.

import { insideTelegram } from './telegram';
import { insideVK } from './vk';

function isMock(): boolean {
  return import.meta.env.MODE === 'mock';
}

function vkAppId(): string {
  return ((import.meta.env.VITE_VK_APP_ID as string | undefined) ?? '').trim();
}

function vkRedirectUrl(): string {
  return ((import.meta.env.VITE_VK_ID_REDIRECT_URL as string | undefined) ?? '').trim();
}

/**
 * vkWebLinkAvailable reports whether the "Link VK" control should be shown: on the plain
 * web (not inside a Telegram/VK Mini App, where a redirect would break the webview) and
 * either the mock build or a configured VK ID app id and redirect URL.
 */
export function vkWebLinkAvailable(): boolean {
  if (insideTelegram() || insideVK()) return false;
  if (isMock()) return true;
  return vkAppId() !== '' && vkRedirectUrl() !== '';
}

const STORAGE_KEY = 'vkid.link';

// PendingState is stashed before the redirect and consumed on the callback: the PKCE
// verifier (needed for the gateway exchange), the CSRF state, and whether this is an
// initial link or the re-authorization for a merge (VK codes are single-use, so a merge
// cannot reuse the link's code — it re-authorizes for a fresh one).
type PendingState = { verifier: string; state: string; mode: 'link' | 'merge' };

/** VKLinkCallback carries a verified VK ID authorization callback for the gateway exchange. */
export type VKLinkCallback = { code: string; deviceId: string; verifier: string; mode: 'link' | 'merge' };

// b64url encodes bytes as base64url without padding (the PKCE / VK ID alphabet).
function b64url(bytes: Uint8Array): string {
  let s = '';
  for (const b of bytes) s += String.fromCharCode(b);
  return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function randomToken(bytes: number): string {
  const a = new Uint8Array(bytes);
  crypto.getRandomValues(a);
  return b64url(a);
}

async function challengeFrom(verifier: string): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  return b64url(new Uint8Array(digest));
}

/**
 * startVKLink begins VK ID web authorization by redirecting the whole tab to VK's hosted
 * login. The PKCE verifier and CSRF state survive the redirect in sessionStorage; VK
 * returns to VITE_VK_ID_REDIRECT_URL with ?code&device_id&state, processed on the next
 * boot via pendingVKLink. mode distinguishes an initial link from a merge re-auth. The
 * mock build never leaves the SPA.
 */
export async function startVKLink(mode: 'link' | 'merge'): Promise<void> {
  if (isMock()) return;
  const verifier = randomToken(48); // 64 base64url chars (VK ID requires 43..128)
  const state = randomToken(24); // 32 base64url chars (VK ID requires >= 32)
  const challenge = await challengeFrom(verifier);
  const pending: PendingState = { verifier, state, mode };
  sessionStorage.setItem(STORAGE_KEY, JSON.stringify(pending));
  const url = new URL('https://id.vk.com/authorize');
  url.searchParams.set('response_type', 'code');
  url.searchParams.set('client_id', vkAppId());
  url.searchParams.set('redirect_uri', vkRedirectUrl());
  url.searchParams.set('state', state);
  url.searchParams.set('code_challenge', challenge);
  url.searchParams.set('code_challenge_method', 'S256');
  url.searchParams.set('scope', 'vkid.personal_info');
  location.assign(url.toString());
}

// stripCallbackQuery removes the OAuth query params from the URL so a refresh does not
// re-process the callback, keeping the path and any hash route intact.
function stripCallbackQuery(): void {
  if (typeof history !== 'undefined' && history.replaceState) {
    history.replaceState(null, '', location.pathname + location.hash);
  }
}

/**
 * pendingVKLink extracts and clears a VK ID authorization callback from the current URL,
 * verifying the returned state against the one stored before the redirect (CSRF). It
 * returns null on a normal load or when the state does not match, and always strips the
 * query so a refresh does not re-run it.
 */
export function pendingVKLink(): VKLinkCallback | null {
  if (typeof location === 'undefined') return null;
  const q = new URLSearchParams(location.search);
  const code = q.get('code');
  const deviceId = q.get('device_id');
  const returnedState = q.get('state');
  if (!code || !deviceId || !returnedState) return null;
  const raw = sessionStorage.getItem(STORAGE_KEY);
  sessionStorage.removeItem(STORAGE_KEY);
  stripCallbackQuery();
  if (!raw) return null;
  let pending: PendingState;
  try {
    pending = JSON.parse(raw) as PendingState;
  } catch {
    return null;
  }
  if (pending.state !== returnedState) return null;
  return { code, deviceId, verifier: pending.verifier, mode: pending.mode };
}
