/**
 * Browser-only helpers for the phone: device id reset, nickname memory, haptics and waiting for a socket.
 * Every storage/API access is guarded: private mode, blocked storage and old browsers must
 * never break the game.
 */
import type { GameConnection } from '../game/connection';
import { getDeviceId, lastNickname, loadTokens, saveToken } from '../game/storage';

const NICK_PREFIX = 'pintrush:nick:';
/** Same key lib/game/storage.ts reads in getDeviceId(). */
const DEVICE_KEY = 'pintrush:device';

/** Set only when storage refused the reset: this tab keeps the fresh id in memory instead. */
let deviceOverride: string | null = null;

/** The device id to send with room.join (honours a reset even when storage is unavailable). */
export function deviceId(): string {
  return deviceOverride ?? getDeviceId();
}

/**
 * "Not you? Start over": the server hands a known device its existing player, so a new person on
 * this phone needs a new device id to become a new player.
 */
export function resetDeviceId(): void {
  const id = crypto.randomUUID();
  try {
    window.localStorage.setItem(DEVICE_KEY, id);
    if (window.localStorage.getItem(DEVICE_KEY) === id) {
      deviceOverride = null;
      return;
    }
  } catch {
    // storage blocked (private mode): fall through to the in-memory id
  }
  deviceOverride = id;
}

/** Forget the nickname remembered for a room (so a new player starts from an empty field). */
export function forgetNickname(roomId: string): void {
  try {
    window.localStorage.removeItem(NICK_PREFIX + roomId);
  } catch {
    // nothing stored
  }
}

/**
 * The server can move this phone into another session on its own (back to tonight's always-on after
 * a championship); that token arrives without a nickname. Fill it in once the snapshot names the
 * player, so the join page can still say "Welcome back <name>".
 */
export function syncTokenNickname(roomId: string, gameSessionId: string, nickname: string): void {
  const stored = loadTokens(roomId).find((t) => t.gameSessionId === gameSessionId);
  if (stored && stored.nickname !== nickname) saveToken(roomId, { ...stored, nickname });
  rememberNickname(roomId, nickname);
}

/**
 * Remember the nickname used in a room separately from the tokens, so it can still be prefilled
 * after tokens were cleared (game over, link expired). Only the nickname is kept — no other data.
 */
export function rememberNickname(roomId: string, nickname: string): void {
  try {
    window.localStorage.setItem(NICK_PREFIX + roomId, nickname);
  } catch {
    // storage unavailable: the token list still carries the nickname for this tab
  }
}

export function recallNickname(roomId: string): string | null {
  const fromToken = lastNickname(roomId);
  if (fromToken) return fromToken;
  try {
    return window.localStorage.getItem(NICK_PREFIX + roomId);
  } catch {
    return null;
  }
}

/**
 * Short haptic cue where supported (Android Chrome); silently ignored elsewhere (iOS Safari).
 * Chrome refuses (and logs an intervention) until the page has had a user tap, e.g. a reveal
 * right after a reload, so only buzz once the user has interacted.
 */
export function vibrate(pattern: number | number[]): void {
  try {
    if (typeof navigator === 'undefined' || typeof navigator.vibrate !== 'function') return;
    if (navigator.userActivation && !navigator.userActivation.hasBeenActive) return;
    navigator.vibrate(pattern);
  } catch {
    // some browsers throw without a user gesture
  }
}

/** Resolves true once the connection is live, false after `timeoutMs` (bad bar Wi-Fi). */
export function waitForConnected(conn: GameConnection, timeoutMs: number): Promise<boolean> {
  if (conn.getState().status === 'connected') return Promise.resolve(true);
  return new Promise((resolve) => {
    let unsubscribe: () => void = () => undefined;
    const timer = setTimeout(() => {
      unsubscribe();
      resolve(false);
    }, timeoutMs);
    unsubscribe = conn.subscribe(() => {
      const status = conn.getState().status;
      if (status === 'connected' || status === 'denied') {
        clearTimeout(timer);
        unsubscribe();
        resolve(status === 'connected');
      }
    });
  });
}

/**
 * KeyRing scope for an answer. Long always-on nights repeat questions in later rounds, and the
 * database keeps answer keys unique, so the round is part of the scope (a reused key would be dropped).
 */
export function answerScope(roundId: string, questionId: string): string {
  return `answer:${roundId}:${questionId}`;
}

/** One idempotency key per logical action (e.g. answerScope(...)), stable across retries and re-renders. */
export class KeyRing {
  private readonly keys = new Map<string, string>();

  for(scope: string): string {
    let key = this.keys.get(scope);
    if (!key) {
      key = crypto.randomUUID();
      this.keys.set(scope, key);
      // Bounded: a long always-on night asks hundreds of questions.
      if (this.keys.size > 200) this.keys.delete(this.keys.keys().next().value as string);
    }
    return key;
  }
}
