/**
 * Auth-result cache shape + transitions.
 *
 * The webServer caches the most recent auth event in `lastAuthResult` so
 * a frontend that missed the WS broadcast (page reload, dropped
 * connection, race) can recover via polling `/api/auth/status`. The
 * cache shape and three state transitions live here so they're
 * testable without standing up Express.
 *
 * Cache shape:
 *   { user, hasApiKey, jwt, timestamp }
 *
 *   - user       — { id, email, ... } or null
 *   - hasApiKey  — bool
 *   - jwt        — raw bearer token from OAuth, or null. Mandatory for
 *                  marketplace publish/install/rate calls (the marketplace
 *                  service verifies JWTs directly, not lx_* keys).
 *   - timestamp  — ISO of when this cache row was written; used for
 *                  staleness checks via `?since=` polling.
 *
 * Bug history this guards against:
 *   - 2026-05: marketplace publish 401 "Authentication required" because
 *     OAuth callback cached only {user, hasApiKey, timestamp} without
 *     the JWT, then a page reload triggered the polling fallback which
 *     returned the user but no JWT. The FE thought "I'm signed in" but
 *     localStorage.loxia-auth-token stayed empty → no Bearer header on
 *     marketplace calls → 401.
 */

/**
 * Build a cache row for a successful OAuth callback. The JWT is the
 * critical field — without it, the polling fallback can't restore
 * marketplace auth on a refresh.
 *
 * @param {Object} input
 * @param {Object} input.user      - User account object
 * @param {string|null} input.jwt  - Raw JWT from the OAuth callback
 * @param {boolean} input.hasApiKey
 * @param {string} [input.timestamp] - Override (for tests); defaults to now
 * @returns {Object} cache row
 */
export function recordOAuthResult({ user, jwt, hasApiKey, timestamp } = {}) {
  return {
    user: user || null,
    hasApiKey: !!hasApiKey,
    jwt: typeof jwt === 'string' && jwt.length > 0 ? jwt : null,
    timestamp: timestamp || new Date().toISOString(),
  };
}

/**
 * Build a cache row when a user is resolved from an API key alone (no
 * OAuth event, so no JWT minted in this flow). PRESERVES any JWT from
 * a previous OAuth login — overwriting `lastAuthResult` with no `jwt`
 * field would silently wipe marketplace auth for users who hit
 * /auth/resolve after /auth/callback.
 *
 * @param {Object} input
 * @param {Object} input.user
 * @param {Object|null} input.prevCache - previous lastAuthResult
 * @param {string} [input.timestamp]
 * @returns {Object} cache row
 */
export function recordResolvedUser({ user, prevCache, timestamp } = {}) {
  const carriedJwt = prevCache && typeof prevCache.jwt === 'string' && prevCache.jwt.length > 0
    ? prevCache.jwt
    : null;
  return {
    user: user || null,
    hasApiKey: true,                        // resolve path requires an API key
    jwt: carriedJwt,
    timestamp: timestamp || new Date().toISOString(),
  };
}

/**
 * Project a cache row into the JSON the `/api/auth/status` endpoint
 * returns. Implements two pieces of business logic:
 *
 *   1. Staleness gate via `?since=`: if the caller is polling for an
 *      auth event AFTER a known timestamp, but the cache row predates
 *      that timestamp, the row is considered "not the auth we're
 *      waiting for" and the user is reported as null. Prevents a
 *      previously-cached login from masquerading as the in-flight one.
 *
 *   2. JWT only flows out when a fresh user is reported. When `user`
 *      is null (no cached row, or stale), `jwt` is also null — the
 *      pair is always coherent so the FE never sees user=null with
 *      jwt=<something>.
 *
 * @param {Object|null} cache - lastAuthResult value
 * @param {Object} [options]
 * @param {string} [options.since] - ISO timestamp from the caller
 * @param {boolean} [options.hasApiKey] - hasApiKey value at request time
 *   (cache.hasApiKey is the value AT CACHE TIME — the live answer comes
 *   from the apiKeyManager). The route passes the live one in.
 * @returns {{ success: true, authenticated: boolean, user: Object|null, jwt: string|null, timestamp: string|null }}
 */
export function projectAuthStatus(cache, options = {}) {
  const since = options.since;
  const liveHasApiKey = options.hasApiKey === undefined ? !!cache?.hasApiKey : !!options.hasApiKey;

  let user = cache?.user || null;
  if (since && cache?.timestamp) {
    if (new Date(cache.timestamp).getTime() <= new Date(since).getTime()) {
      // Cache predates the login attempt the caller is polling for.
      user = null;
    }
  }

  // Pair invariant: jwt is null whenever user is null.
  const jwt = user ? (cache?.jwt || null) : null;

  return {
    success: true,
    authenticated: liveHasApiKey,
    user,
    jwt,
    timestamp: cache?.timestamp || null,
  };
}
