// Retry policy + error classification for the gateway transport. When a unary call
// fails at the transport level the app retries it with capped exponential backoff while showing
// the "Connecting…" indicator, instead of flashing a red toast each time.
//
// Retry policy: a transport 'unavailable' is ambiguous for a mutation (its response could have been
// lost after the backend applied it), so only **read-only** ops are auto-retried on 'unavailable'; a
// mutation is surfaced instead (its button is disabled while offline and re-enables on reconnect, so
// the player re-issues it deliberately). A rate-limit rejection (ResourceExhausted) is NOT retried and
// is NOT a connectivity code: it is a deliberate "slow down", so auto-retrying cannot succeed until the
// bucket refills — it only freezes the calling button for the whole backoff and feeds the gateway's ban
// tripwire with more rejections. It is surfaced as its own message (error.rate_limited) and never flips
// the offline chrome, which the session-less login screen has no session-bearing probe to recover from.

import { Code, ConnectError } from '@connectrpc/connect';
import { GatewayError } from './client';

/**
 * toGatewayError normalises a thrown Connect/transport error to a GatewayError with a stable code.
 * Connection-level failures — the server is unreachable, the request timed out, was reset or
 * cancelled, or a raw network error — all collapse to **'unavailable'**, so they are handled as
 * connectivity (the indicator + retry), never as a red error toast. A genuine server-side
 * 'internal' or a domain code is preserved.
 */
export function toGatewayError(e: unknown): GatewayError {
  if (e instanceof ConnectError) {
    switch (e.code) {
      case Code.Unauthenticated:
        return new GatewayError('session_invalid', e.message);
      case Code.ResourceExhausted:
        return new GatewayError('rate_limited', e.message);
      case Code.Unavailable:
      case Code.DeadlineExceeded:
      case Code.Canceled:
      case Code.Aborted:
      case Code.Unknown:
        return new GatewayError('unavailable', e.message);
      case Code.NotFound:
        return new GatewayError('not_found', e.message);
      case Code.FailedPrecondition:
        // The Subscribe stream's counterpart of the update_required envelope: the client is too old.
        return new GatewayError('update_required', e.message);
      default:
        return new GatewayError('internal', e.message);
    }
  }
  return new GatewayError('unavailable', String(e));
}

/** READ_OPS is the set of side-effect-free message types (safe to auto-retry on any failure). */
export const READ_OPS: ReadonlySet<string> = new Set([
  'profile.get',
  'games.list',
  'game.state',
  'game.history',
  'game.gcg',
  'game.export_url',
  'game.evaluate',
  'game.check_word',
  'stats.get',
  'lobby.poll',
  'chat.list',
  'draft.get',
  'friends.list',
  'friends.incoming',
  'friends.outgoing',
  'blocks.list',
  'invitation.list',
]);

/**
 * retryable reports whether a failed op should be auto-retried. A transport 'unavailable' is retried
 * only for read-only ops, never a mutation; every other code — a rate-limit (a deliberate throttle,
 * surfaced not spun), a domain rejection, not-found, … — is final.
 */
export function retryable(code: string, op: string): boolean {
  if (code === 'unavailable') return READ_OPS.has(op);
  return false;
}

/** isConnectionCode reports whether a code is a transport/connectivity failure the Connecting
 *  indicator covers — so the UI suppresses its red toast and reportOffline may flip the offline
 *  chrome. A rate-limit is deliberately NOT one: it is a genuine server signal, surfaced as its own
 *  "slow down" message, never the offline chrome (which the session-less login screen, whose
 *  reachability probe needs a bearer token, cannot recover from). */
export function isConnectionCode(code: string): boolean {
  return code === 'unavailable';
}

/** backoffMs is the delay before retry attempt n (1-based): capped exponential growth plus a
 *  little jitter, so a fleet of clients does not retry in lockstep after an outage. */
export function backoffMs(attempt: number): number {
  const base = Math.min(8000, 500 * 2 ** Math.max(0, attempt - 1));
  return base + Math.floor(Math.random() * 250);
}
