import bcrypt from 'bcryptjs';

/**
 * Offline JWT secret.
 *
 * The secret MUST match the server's JWT_SECRET or the offline-minted token
 * will be rejected when the server comes back online. We source it from
 * `localStorage`, which `auth.ts` populates on every successful `/auth/login`
 * from the response's `offline_jwt_secret` field. The server only includes
 * that field when `ALLOW_CLIENT_OFFLINE_MINT=true` (default in dev, false
 * in prod) — see `server/src/config/index.js`.
 *
 * If the secret is unavailable (never logged in on this device, or prod
 * where the server withholds it), `getOfflineJwtSecret()` returns null and
 * `mintOfflineToken()` will throw a clear error rather than silently mint
 * with a wrong key. The dev fallback exists for offline-only test setups.
 */
const STORAGE_KEY = 'offline_jwt_secret';
// The server's dev fallback (matches the JWT_SECRET dev default in
// server/src/config/index.js). Use only when the user has not yet logged
// in on this device and we still need offline auth to work.
const DEV_FALLBACK = 'dev-only-insecure-secret';

export function getOfflineJwtSecret(): string | null {
  if (typeof localStorage === 'undefined') return null;
  const cached = localStorage.getItem(STORAGE_KEY);
  if (cached) return cached;
  // No cached secret — fall back to dev only in development builds. In
  // production this returns null and `mintOfflineToken` will refuse.
  if (import.meta.env.DEV) return DEV_FALLBACK;
  return null;
}

export function cacheOfflineJwtSecret(secret: string | null): void {
  if (typeof localStorage === 'undefined') return;
  if (secret) localStorage.setItem(STORAGE_KEY, secret);
  else localStorage.removeItem(STORAGE_KEY);
}

const base64url = (input: string) => {
  if (typeof btoa === 'function') {
    return btoa(input).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
  }
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (globalThis as any).Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
};

const hmacSha256 = async (secret: string, data: string): Promise<string> => {
  if (typeof crypto !== 'undefined' && crypto.subtle) {
    const enc = new TextEncoder();
    const key = await crypto.subtle.importKey(
      'raw',
      enc.encode(secret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']
    );
    const sig = await crypto.subtle.sign('HMAC', key, enc.encode(data));
    return Array.from(new Uint8Array(sig))
      .map((b) => b.toString(16).padStart(2, '0'))
      .join('');
  }
  return '';
};

export const mintOfflineToken = async (user: { id: number; email: string; role: string }): Promise<string> => {
  const secret = getOfflineJwtSecret();
  if (!secret) {
    throw new Error(
      'Offline JWT secret not available. Log in once while online to cache it. ' +
        'In production, set ALLOW_CLIENT_OFFLINE_MINT=true on the server to enable offline auth.'
    );
  }
  const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
  const now = Math.floor(Date.now() / 1000);
  const payload = base64url(
    JSON.stringify({
      id: user.id,
      email: user.email,
      role: user.role,
      iat: now,
      exp: now + 7 * 24 * 60 * 60,
      offline: true,
    })
  );
  const signature = await hmacSha256(secret, `${header}.${payload}`);
  return `${header}.${payload}.${signature}`;
};

export const hashPasswordClient = (plain: string): string => bcrypt.hashSync(plain, 10);

export const comparePasswordClient = (plain: string, hash: string): boolean => bcrypt.compareSync(plain, hash);

// Back-compat re-export (still imported in a few places). Prefer getOfflineJwtSecret().
export const OFFLINE_JWT_SECRET = getOfflineJwtSecret();
