// One-time bootstrap: headless OIDC login of the OCIS service user against the
// ocis provider (public client `ocis-web`) to obtain a refresh token, then
// persist it into Mongo (ocis_credentials). The running platform-api then mints
// access tokens with the refresh_token grant (see OcisClient). Re-run this if
// the refresh token is ever revoked/expired.
//
// Run inside the platform-api container (it has the mkcert CA + Mongo access):
//   docker compose exec platform-api node /app/scripts/bootstrap-ocis.mjs
// Requires env: AUTHENTIK_API_URL, OCIS_SVC_USERNAME, OCIS_SVC_PASSWORD, MONGODB_URI.
import crypto from 'node:crypto'
import mongoose from 'mongoose'

const API = process.env.AUTHENTIK_API_URL // https://auth.dezky.local/api/v3
const ROOT = API.replace('/api/v3', '')
const USER = process.env.OCIS_SVC_USERNAME
const PASS = process.env.OCIS_SVC_PASSWORD
const CLIENT_ID = 'ocis-web'
const REDIRECT = 'https://files.dezky.local/oidc-callback'
const SCOPE = 'openid profile email offline_access'

const b64url = (b) => b.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const verifier = b64url(crypto.randomBytes(32))
const challenge = b64url(crypto.createHash('sha256').update(verifier).digest())
const state = b64url(crypto.randomBytes(8))

// --- tiny cookie jar ---
const jar = new Map()
function store(res) {
  for (const c of res.headers.getSetCookie?.() ?? []) {
    const [pair] = c.split(';')
    const i = pair.indexOf('=')
    jar.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim())
  }
}
const cookieHeader = () => [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ')
const csrf = () => jar.get('authentik_csrf')

async function main() {
  const authzQuery = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT,
    scope: SCOPE,
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  }).toString()

  // 1) Hit authorize → 302 into the authentication flow; capture session cookies
  //    AND the flow's `next` query (so the executor resumes THIS authorize plan,
  //    not the brand's default landing app).
  let res = await fetch(`${ROOT}/application/o/authorize/?${authzQuery}`, { redirect: 'manual' })
  store(res)
  const loc1 = res.headers.get('location') || ''
  console.log('authorize ->', res.status, loc1.slice(0, 80))
  // The executor takes the flow URL's whole querystring wrapped in a single
  // `query` param — that's how `next` (the authorize continuation) is carried.
  const flowQuery = loc1.includes('?') ? loc1.slice(loc1.indexOf('?') + 1) : `next=${encodeURIComponent('/application/o/authorize/?' + authzQuery)}`

  // 2) Run the authentication flow via the executor API (identification → password).
  const exec = `${ROOT}/api/v3/flows/executor/default-authentication-flow/?query=${encodeURIComponent(flowQuery)}`
  const hdr = () => ({
    'Content-Type': 'application/json',
    Accept: 'application/json',
    Cookie: cookieHeader(),
    'X-Authentik-CSRF': csrf() ?? '',
    Referer: `${ROOT}/`,
  })

  // Unified flow driver: Authentik mixes JSON stages (POST answers) with
  // HTTP redirects (xak-flow-redirect / 302). Loop until we land on the
  // redirect_uri with a ?code=.
  const abs = (u) => (u.startsWith('http') ? u : ROOT + u)
  let url = exec
  let method = 'GET'
  let body = null
  let code
  for (let i = 0; i < 14 && !code; i++) {
    res = await fetch(url, { method, headers: hdr(), body, redirect: 'manual' })
    store(res)
    if (res.status >= 300 && res.status < 400) {
      const loc = res.headers.get('location') || ''
      console.log('  redirect ->', loc.slice(0, 80))
      if (loc.startsWith(REDIRECT)) { code = new URL(loc).searchParams.get('code'); break }
      url = abs(loc); method = 'GET'; body = null
      continue
    }
    const stage = await res.json().catch(() => ({}))
    console.log('  stage:', stage.component, stage.response_errors ? JSON.stringify(stage.response_errors).slice(0, 120) : '')
    switch (stage.component) {
      case 'ak-stage-identification': {
        const payload = { uid_field: USER }
        if (stage.password_fields) payload.password = PASS
        method = 'POST'; body = JSON.stringify(payload); url = exec; break
      }
      case 'ak-stage-password':
        method = 'POST'; body = JSON.stringify({ password: PASS }); url = exec; break
      case 'xak-flow-redirect': {
        const to = stage.to || ''
        if (to.startsWith(REDIRECT)) { code = new URL(to).searchParams.get('code'); break }
        url = abs(to); method = 'GET'; body = null; break
      }
      default:
        throw new Error('unexpected stage: ' + JSON.stringify(stage).slice(0, 300))
    }
  }
  if (!code) throw new Error('no authorization code captured')
  console.log('CODE captured')

  // 4) Exchange code → tokens.
  const tokenBody = new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: REDIRECT,
    client_id: CLIENT_ID,
    code_verifier: verifier,
  })
  res = await fetch(`${ROOT}/application/o/token/`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
    body: tokenBody,
  })
  const tok = await res.json()
  console.log('token ->', res.status, tok.error || `refresh_token:${tok.refresh_token ? 'yes' : 'no'} scope:${tok.scope}`)
  if (!tok.refresh_token) throw new Error('no refresh_token: ' + JSON.stringify(tok).slice(0, 200))

  // Sanity: decode access token issuer + identity.
  const claims = JSON.parse(Buffer.from(tok.access_token.split('.')[1], 'base64').toString())
  console.log('iss:', claims.iss, '| preferred_username:', claims.preferred_username)

  // 5) Persist refresh token.
  await mongoose.connect(process.env.MONGODB_URI)
  await mongoose.connection.collection('ocis_credentials').updateOne(
    { key: 'ocis-svc' },
    { $set: { key: 'ocis-svc', refreshToken: tok.refresh_token, updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } },
    { upsert: true },
  )
  await mongoose.disconnect()
  console.log('PERSISTED refresh token to ocis_credentials')
}

main().catch((e) => {
  console.log('BOOTSTRAP ERROR:', String(e).slice(0, 400))
  process.exit(1)
})
