/**
 * AuthProvider — pluggable authentication provider interface.
 *
 * Every login method (TOTP, email magic link, OIDC, SAML, passkeys, …) is an
 * implementation of this interface. The auth route layer does not know which
 * provider is in use — it just walks `getMethods()` and dispatches to the
 * named provider's `initiate` / `verify` handlers.
 *
 * Why an interface (not a router-per-provider)?
 *   - Adding a new provider = one new file under src/auth/providers/, wired
 *     into the registry. No edits to routes/auth/* or src/app.js.
 *   - Provider-agnostic security middleware (rate limits, audit log) sits
 *     ABOVE the provider boundary in the auth router, so every provider
 *     inherits it for free.
 *   - The frontend only needs to render `GET /api/v1/auth/methods` and
 *     POST to `/api/v1/auth/login/:method/initiate` and
 *     `/api/v1/auth/login/:method/verify`. No frontend edits per provider.
 *
 * Contract — all methods MUST be implemented by every provider:
 *
 *   async getConfig()
 *     Return the public-safe configuration for this provider (no secrets).
 *     Used by /api/v1/auth/methods and /api/v1/auth/login-page rendering.
 *
 *   async listMethods()
 *     Return the array of UI-visible login methods for this provider.
 *     Example: [{ id: 'totp-code', label: 'Enter TOTP code', description: … }].
 *     For email magic link: [{ id: 'magic-link', label: 'Email me a sign-in link' }].
 *
 *   async isSetUp()
 *     Whether this provider has the state it needs to authenticate. e.g.
 *     TOTP requires a stored secret. Email requires nothing (always ready).
 *
 *   async isEnabled()
 *     Whether this provider is currently active. Operators may disable TOTP
 *     without deleting the secret, etc.
 *
 *   async initiate(methodId, req, res)
 *     Begin the auth flow for `methodId`. For challenge-response providers
 *     (TOTP) this is a no-op and just returns { challenge: 'code' }. For
 *     out-of-band providers (email magic link) this generates the token,
 *     sends the email, and returns { sent: true, to: '<masked>' }.
 *     MUST set the HTTP status + body via `res`. MUST be idempotent enough
 *     to handle double-submit (don't actually send 2 emails).
 *
 *   async verify(methodId, req, res)
 *     Complete the auth flow. For TOTP: validate the 6-digit code in the
 *     request body, create the session cookie, respond 200. For email:
 *     validate the token from the request body OR query string, mark it
 *     used, create the session, respond 200. MUST respond on `res`.
 *
 *   async disable(req, res)
 *     Turn the provider off (e.g. TOTP removes the secret). MUST respond on `res`.
 *     Provider-specific auth requirements (re-verify current code) live here.
 *
 *   async recoveryInfo()
 *     Return { status, hint, … } for lockout-recovery UIs. TOTP needs to
 *     distinguish healthy / unreadable / corrupt; email has no analog.
 *
 *   async setConfig(updates)
 *     Apply non-secret config updates (e.g. TOTP sessionDuration).
 *
 * Errors:
 *   Providers throw the standard errors from src/utilities/errors:
 *     ValidationError        (400) — bad input shape
 *     AuthenticationError    (401) — invalid credentials / expired token
 *     AuthorizationError     (403) — disabled / not set up
 *     NotFoundError          (404)
 *     ConflictError          (409)
 *   The route layer wraps them via boundAsyncHandler.
 *
 * The auth router's CSRF / rate-limit / audit-log middleware runs for every
 * provider uniformly — providers do NOT re-implement those.
 */
class AuthProvider {
  /**
   * @param {Object} deps  Shared infrastructure every provider needs:
   *   - credentialManager: src/managers/credential-manager (encrypted KV)
   *   - session:           session API from src/utilities/middleware
   *   - saveProviderConfig(): async () => void  — flush in-memory config to disk
   *   - config:            mutable per-provider config object (provider owns shape)
   *   - log:               logger
   *   - renewCSRFToken:    fn(res, isSecure) => newCsrfToken
   */
  constructor(deps) {
    if (new.target === AuthProvider) {
      throw new Error('AuthProvider is abstract — implement a subclass');
    }
    this.deps = deps;
    this.name = 'unnamed';
  }

  // ── Abstract methods (subclasses MUST override) ──────────────────────────
  async getConfig() { throw new Error('not implemented'); }
  async listMethods() { throw new Error('not implemented'); }
  async isSetUp() { throw new Error('not implemented'); }
  async isEnabled() { throw new Error('not implemented'); }
  async initiate(/* methodId, req, res */) { throw new Error('not implemented'); }
  async verify(/* methodId, req, res */) { throw new Error('not implemented'); }
  async disable(/* req, res */) { throw new Error('not implemented'); }
  async recoveryInfo() { throw new Error('not implemented'); }
  async setConfig(/* updates */) { throw new Error('not implemented'); }

  // ── Optional helpers ─────────────────────────────────────────────────────

  /**
   * Mask an email address for UI display: "sa****@example.com".
   * Returns null if the input doesn't look like an email — callers should
   * pass the result straight to the UI without further validation.
   */
  static maskEmail(email) {
    if (typeof email !== 'string') return null;
    const at = email.indexOf('@');
    if (at <= 0 || at === email.length - 1) return null;
    const local = email.slice(0, at);
    const domain = email.slice(at);
    if (local.length <= 2) return local[0] + '****' + domain;
    return local.slice(0, 2) + '****' + domain;
  }

  /**
   * Constant-time string comparison. Providers must use this for any token
   * comparison to avoid timing oracles. Returns false on type mismatch.
   */
  static timingSafeEqual(a, b) {
    if (typeof a !== 'string' || typeof b !== 'string') return false;
    const ab = Buffer.from(a);
    const bb = Buffer.from(b);
    if (ab.length !== bb.length) return false;
    return require('crypto').timingSafeEqual(ab, bb);
  }
}

module.exports = AuthProvider;