// ── Shared OAuth2 / OIDC provider ──────────────────────────────────────────
//
// Implements the reusable authorization-code + PKCE flow so the concrete
// providers (google, discord, generic OIDC) only supply their endpoints, scope,
// and a normalizeProfile(). Uses Node's global fetch (no new dependency).
//
// Flow:
//   getAuthorizationUrl(state, { redirectUri, codeChallenge }) → redirect the browser
//   handleCallback({ code, redirectUri, codeVerifier })
//     → exchangeCode (POST token endpoint) → getUserProfile (GET userinfo)
//     → mapUser → { subject, email, name }

const BaseProvider = require('./base.provider')
const userIdentities = require('../../model/userIdentities/userIdentities.model')
const log = require('../../utils/logger')('sso')

class OAuth2Provider extends BaseProvider {
  constructor(config = {}) {
    super(config)
    this.clientId = config.clientId ?? config.client_id ?? null
    this.clientSecret = config.clientSecret ?? config.client_secret ?? null
  }

  // ── Subclass hooks (endpoints / scope / profile mapping) ──────────────────
  authEndpoint() {
    throw new Error(`authEndpoint() not set for provider '${this.id}'`)
  }
  tokenEndpoint() {
    throw new Error(`tokenEndpoint() not set for provider '${this.id}'`)
  }
  userinfoEndpoint() {
    throw new Error(`userinfoEndpoint() not set for provider '${this.id}'`)
  }
  scopeString() {
    return 'openid email profile'
  }
  // Extra provider-specific authorize-URL params (e.g. Google's prompt).
  authParams() {
    return {}
  }
  // Map a raw profile → { subject, email, name }. Subclasses must implement.
  normalizeProfile(profile) {
    throw new Error(`normalizeProfile() not implemented for provider '${this.id}'`)
  }

  // ── Flow ──────────────────────────────────────────────────────────────────
  getAuthorizationUrl(state, { redirectUri, codeChallenge } = {}) {
    const params = new URLSearchParams({
      client_id: this.clientId || '',
      redirect_uri: redirectUri,
      response_type: 'code',
      scope: this.scopeString(),
      state,
    })
    if (codeChallenge) {
      params.set('code_challenge', codeChallenge)
      params.set('code_challenge_method', 'S256')
    }
    for (const [k, v] of Object.entries(this.authParams())) params.set(k, v)
    return `${this.authEndpoint()}?${params.toString()}`
  }

  async handleCallback({ code, redirectUri, codeVerifier } = {}) {
    const tokenSet = await this.exchangeCode({ code, redirectUri, codeVerifier })
    const profile = await this.getUserProfile(tokenSet.access_token)
    return this.mapUser(profile)
  }

  async exchangeCode({ code, redirectUri, codeVerifier }) {
    const body = new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: redirectUri,
      client_id: this.clientId || '',
      client_secret: this.clientSecret || '',
    })
    if (codeVerifier) body.set('code_verifier', codeVerifier)
    const res = await fetch(this.tokenEndpoint(), {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
      body,
    })
    if (!res.ok) {
      const detail = await res.text().catch(() => '')
      log.warn('token exchange failed', { provider: this.id, status: res.status })
      throw new Error(`token exchange failed (${res.status}): ${detail.slice(0, 200)}`)
    }
    return res.json()
  }

  async getUserProfile(accessToken) {
    const res = await fetch(this.userinfoEndpoint(), {
      headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
    })
    if (!res.ok) {
      log.warn('userinfo fetch failed', { provider: this.id, status: res.status })
      throw new Error(`userinfo failed (${res.status})`)
    }
    return res.json()
  }

  mapUser(profile) {
    const mapped = this.normalizeProfile(profile)
    if (!mapped || !mapped.subject) throw new Error(`provider '${this.id}' returned no subject`)
    return mapped
  }

  // Persist the external → internal user link. Shared by every OAuth provider.
  async linkAccount(user, profile) {
    const p = this.mapUser(profile)
    return userIdentities.link({ userId: user.id, provider: this.id, subject: p.subject, email: p.email })
  }
}

module.exports = OAuth2Provider
