#!/usr/bin/env node
// Mint a fresh Pixelfed access token for every pixelfed rig slot from its
// durable refresh token, and write it back to the rig JSON in place.
//
// THE single implementation of the refresh-token grant. Invoked two ways:
//   - jest globalSetup (__tests__/integration/_setup/globalSetup.ts) spawns it
//     once at the start of every `npm run test:integration` run.
//   - scripts/dev/run-e2e.sh runs it before base64-encoding the rig for Maestro
//     (the Maestro tier has no jest globalSetup, so the stored access token
//     would otherwise be stale by the time a flow launches).
//
// Why the access token is a *derived cache*, not a credential: Laravel Passport
// revokes the old access token on every refresh, so any stored access token
// goes stale the moment a refresh happens. Minting one here means the first
// consumer seeds a valid token instead of paying a 401-self-heal round-trip.
// The durable refresh token (the credential we actually persist) is untouched.
//
// Mastodon slots have non-expiring tokens and no refresh token — skipped. A
// dead refresh token is a warning (re-bootstrap that slot), not a hard failure:
// the consumer decides what to do next (jest skips the suite; the app's in-app
// 401 self-heal can't recover a dead refresh token, so the flow surfaces it).
//
// Output is slot-keyed, never host-keyed, so the rig hostnames never reach the
// operator's terminal scrollback or a CI log.

import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))

/** Same resolution order as __tests__/_helpers/loadTestRig.ts `resolveRigPath`:
 *  explicit arg → HALFLIGHT_TEST_RIG env → repo-root default. */
function resolveRigPath() {
  const fromArg = process.argv[2]
  if (fromArg && fromArg.length > 0) return fromArg
  const env = process.env.HALFLIGHT_TEST_RIG
  if (env && env.length > 0) return env
  // scripts/dev/refresh-rig-tokens.mjs → repo root is two levels up.
  return resolve(__dirname, '..', '..', 'secrets', 'test-rigs', 'halflight.test.json')
}

/** One refresh-token grant. Returns the new access token, or undefined on any
 *  failure (with a loud, actionable, host-free warning). */
async function refreshOnce(slot, entry) {
  const body = new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: entry.refreshToken,
    client_id: entry.oauthClientId,
    client_secret: entry.oauthClientSecret,
    scope: entry.scopes ?? '',
  }).toString()

  try {
    const resp = await fetch(`https://${entry.instance}/oauth/token`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Accept: 'application/json',
      },
      body,
    })
    if (!resp.ok) {
      console.warn(
        `[rig-refresh] ${slot}: refresh-token grant returned HTTP ${resp.status}. ` +
          'The refresh token is dead — re-run scripts/dev/bootstrap-test-rig.sh for this slot.'
      )
      return undefined
    }
    const json = await resp.json()
    if (!json.access_token) {
      console.warn(`[rig-refresh] ${slot}: refresh response had no access_token.`)
      return undefined
    }
    return json.access_token
  } catch (e) {
    console.warn(`[rig-refresh] ${slot}: refresh error — ${e.message}`)
    return undefined
  }
}

async function main() {
  const path = resolveRigPath()
  let parsed
  try {
    parsed = JSON.parse(readFileSync(path, 'utf8'))
  } catch {
    // Missing/malformed rig: nothing to mint. The per-consumer loader surfaces
    // the skip/failure; this script stays silent-but-clean.
    console.log('[rig-refresh] no usable rig file — nothing to mint.')
    return
  }

  const rigs = parsed.rigs ?? {}
  const targets = Object.entries(rigs).filter(
    ([, e]) => e && e.type === 'pixelfed' && !!e.refreshToken
  )
  if (targets.length === 0) {
    console.log('[rig-refresh] no pixelfed slot with a refresh token — nothing to mint.')
    return
  }

  let dirty = false
  for (const [slot, entry] of targets) {
    const access = await refreshOnce(slot, entry)
    if (!access) continue
    entry.accessToken = access
    entry.tokenObtainedAt = new Date().toISOString()
    dirty = true
    console.log(`[rig-refresh] ${slot}: minted fresh access token.`)
  }

  if (dirty) writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8')
}

await main()
