#!/usr/bin/env node
/*
 * Stub OAuth2 / OIDC IdP — DEV ONLY.
 *
 * Dev environments have no real OAuth provider configured, so `GET /auth/providers`
 * returns `[]` and the native mobile SSO flow can never be exercised. This tiny,
 * dependency-free IdP stands in for Google/Discord/a custom OIDC so the full bridge
 * (app → website `/auth/mobile/sso/start` → IdP → callback → `/exchange`) can be
 * driven end-to-end against the local site. It mirrors the throwaway-stub precedent
 * in `servuo-plugins/tools/stub_sidecar.ps1`.
 *
 * It implements the three endpoints `oauth2.provider.js` calls:
 *   GET  /authorize  → account picker, then 302 to redirect_uri?code&state
 *   POST /token      → { access_token, token_type, expires_in }
 *   GET  /userinfo   → { sub, email, name }  (Bearer <access_token>)
 *
 * Pair it with `seed-sso-provider.js`, which registers a matching `auth_providers`
 * row and pre-links each principal's `sub` to a dev account (SSO is link-only).
 *
 * Run:  node website/scripts/dev/stub-idp.js
 * Env:  STUB_IDP_PORT (default 9099), STUB_IDP_HOST (default 127.0.0.1)
 *
 * NEVER deploy this. It performs no credential checks and signs in anyone.
 */

'use strict'

const http = require('http')
const crypto = require('crypto')
const { URL, URLSearchParams } = require('url')

const PORT = Number(process.env.STUB_IDP_PORT || 9099)
const HOST = process.env.STUB_IDP_HOST || '127.0.0.1'

// Test principals the picker offers. Each `sub` must be pre-linked to a real dev
// account by seed-sso-provider.js, or the link-only login will reject it. Keep
// this list in sync with that script's PRINCIPALS.
const PRINCIPALS = [
  { sub: 'stub-admin', email: 'admin@dev.local', name: 'Dev Admin' },
  { sub: 'stub-colby', email: 'colby@dev.local', name: 'Dev Colby' },
]

// Short-lived in-memory maps: auth code → principal, access token → principal.
// Codes are single-use; both are cleared on process exit (dev only).
const codes = new Map()
const tokens = new Map()

function log(...args) {
  // eslint-disable-next-line no-console
  console.log(`[stub-idp ${new Date().toISOString()}]`, ...args)
}

function pickerPage(query) {
  const rows = PRINCIPALS.map((p) => {
    const q = new URLSearchParams(query)
    q.set('login_as', p.sub)
    return `<li><a href="/authorize?${q.toString()}">${p.name} &lt;${p.email}&gt; <code>${p.sub}</code></a></li>`
  }).join('\n')
  return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Stub IdP</title>
<style>body{font:16px system-ui;margin:3rem auto;max-width:34rem}a{display:block;padding:.6rem;border:1px solid #ccc;border-radius:8px;margin:.4rem 0;text-decoration:none;color:#123}code{color:#888}</style>
<h1>Stub IdP — choose a test account</h1>
<p>DEV ONLY. Signs you in as the selected pre-linked identity.</p>
<ul style="list-style:none;padding:0">${rows}</ul>`
}

function sendJson(res, status, obj) {
  const body = JSON.stringify(obj)
  res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) })
  res.end(body)
}

function handleAuthorize(req, res, url) {
  const params = url.searchParams
  const redirectUri = params.get('redirect_uri')
  const state = params.get('state') || ''
  const loginAs = params.get('login_as')

  if (!redirectUri) {
    res.writeHead(400, { 'Content-Type': 'text/plain' })
    return res.end('missing redirect_uri')
  }

  // No account chosen yet → show the picker (preserving the OAuth query params).
  if (!loginAs) {
    const html = pickerPage(params)
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
    return res.end(html)
  }

  const principal = PRINCIPALS.find((p) => p.sub === loginAs)
  if (!principal) {
    res.writeHead(400, { 'Content-Type': 'text/plain' })
    return res.end(`unknown principal '${loginAs}'`)
  }

  // Mint a single-use authorization code bound to the principal and redirect back.
  const code = crypto.randomBytes(24).toString('hex')
  codes.set(code, principal)
  const back = new URL(redirectUri)
  back.searchParams.set('code', code)
  if (state) back.searchParams.set('state', state)
  log('authorize → issuing code for', principal.sub, '→', back.toString())
  res.writeHead(302, { Location: back.toString() })
  res.end()
}

function readBody(req) {
  return new Promise((resolve) => {
    let data = ''
    req.on('data', (c) => {
      data += c
    })
    req.on('end', () => resolve(data))
  })
}

async function handleToken(req, res) {
  const raw = await readBody(req)
  const body = new URLSearchParams(raw)
  const code = body.get('code')
  const principal = code && codes.get(code)
  if (!principal) {
    log('token → invalid/expired code', code)
    return sendJson(res, 400, { error: 'invalid_grant' })
  }
  codes.delete(code) // single-use
  const accessToken = crypto.randomBytes(24).toString('hex')
  tokens.set(accessToken, principal)
  log('token → access token for', principal.sub)
  return sendJson(res, 200, {
    access_token: accessToken,
    token_type: 'Bearer',
    expires_in: 3600,
    scope: body.get('scope') || 'openid email profile',
  })
}

function handleUserinfo(req, res) {
  const auth = req.headers.authorization || ''
  const token = auth.startsWith('Bearer ') ? auth.slice(7) : null
  const principal = token && tokens.get(token)
  if (!principal) {
    log('userinfo → missing/invalid bearer')
    return sendJson(res, 401, { error: 'invalid_token' })
  }
  log('userinfo → returning profile for', principal.sub)
  return sendJson(res, 200, { sub: principal.sub, email: principal.email, name: principal.name })
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`)
  log(req.method, url.pathname)
  try {
    if (req.method === 'GET' && url.pathname === '/authorize') return handleAuthorize(req, res, url)
    if (req.method === 'POST' && url.pathname === '/token') return await handleToken(req, res)
    if (req.method === 'GET' && url.pathname === '/userinfo') return handleUserinfo(req, res)
    if (req.method === 'GET' && url.pathname === '/') {
      res.writeHead(200, { 'Content-Type': 'text/plain' })
      return res.end('stub-idp OK. Endpoints: GET /authorize, POST /token, GET /userinfo')
    }
    res.writeHead(404, { 'Content-Type': 'text/plain' })
    res.end('not found')
  } catch (err) {
    log('error', err)
    sendJson(res, 500, { error: 'server_error' })
  }
})

server.listen(PORT, HOST, () => {
  log(`listening on http://${HOST}:${PORT}`)
  log('authorize:', `http://${HOST}:${PORT}/authorize`)
  log('token:    ', `http://${HOST}:${PORT}/token`)
  log('userinfo: ', `http://${HOST}:${PORT}/userinfo`)
  log('principals:', PRINCIPALS.map((p) => p.sub).join(', '))
})
