// ── OpenAPI for core's extension-slot routers ──────────────────────────────
//
// The second half of `npm run swagger`. It exists because of a failure mode that
// announces itself as a success.
//
// `swagger/swagger.js` is STATIC analysis: swagger-autogen parses `src/app.js` as
// text and follows the literal `app.use(...)` mount chain. An extension slot
// (MODULE_SYSTEM.md §1.9) breaks that chain on purpose — the slot's router is
// created by `registries.declareSlot()` and filled later, so there is no literal
// require for the parser to follow. When PR 4 moved the six `/admin/users/:id/shard/*`
// routes behind the `admin.users.detail` slot, regenerating the spec printed
// `Swagger-autogen: Success` and deleted 407 lines. Nothing failed. The spike hit
// the identical thing (MODULE_API.md §7.4) and it is why the fragment merge is
// the settled answer (§6.1).
//
// So: generate a fragment per filled slot by pointing swagger-autogen at that
// router's own file, re-root its paths at the prefix the router is ACTUALLY
// mounted at in the live app, and merge. Two things are deliberately derived
// rather than written down, because a written-down copy is a copy that drifts:
//
//   • WHICH slots — from `registries.filledSlots()`, not a list here.
//   • WHERE each hangs — by finding the slot's own router object in the live
//     express stack and accumulating the mount prefixes above it, using
//     `scripts/routeManifest.js`'s `mountPath` so the manifest and the spec can
//     never disagree about what a mount decodes to.
//
// This is core's own slot fill only. A MODULE ships a prebuilt
// `swagger-fragment.json` in its bundle and core merges it at request time
// (§6.1a) — core never has a module's sources to analyse.

const fs = require('fs')
const os = require('os')
const path = require('path')

const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' })

const { mergeFragment, prefixPaths } = require('./mergeSpec')
const { mountPath } = require('../scripts/routeManifest')

const SERVER_ROOT = path.join(__dirname, '..')

/**
 * Find `target` in an express stack and return the path prefix it is mounted at.
 *
 * Depth-first, accumulating each enclosing mount. Returns null when the router is
 * not on the stack at all — which for a filled slot means core declared it and
 * never mounted it, a bug worth failing the build over rather than papering over
 * with an unprefixed path.
 */
function findMountPrefix(stack, target, prefix = '') {
  for (const layer of stack || []) {
    if (!layer.handle || !Array.isArray(layer.handle.stack)) continue
    const here = prefix + mountPath(layer)
    if (layer.handle === target) return here
    const found = findMountPrefix(layer.handle.stack, target, here)
    if (found !== null) return found
  }
  return null
}

/**
 * Generate one fragment by running swagger-autogen over a single router file.
 *
 * Its paths come out relative to that router (`/shard/accounts`) because nothing
 * in the file says where it hangs; `prefixPaths` supplies the rest.
 */
async function fragmentFor(specFile) {
  const out = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'rg-swagger-')), 'fragment.json')
  await swaggerAutogen(out, [path.relative(SERVER_ROOT, specFile).split(path.sep).join('/')], {
    info: { title: 'slot fragment', version: '0' },
  })
  const fragment = JSON.parse(fs.readFileSync(out, 'utf8'))
  fs.rmSync(path.dirname(out), { recursive: true, force: true })
  return fragment
}

/**
 * Merge every filled core slot's routes into the generated spec file, in place.
 *
 * @param {string} outputFile  the swagger-output.json swagger.js just wrote
 * @returns {Promise<number>} how many paths were added
 */
async function mergeSlotSpecs(outputFile) {
  /* eslint-disable global-require */
  const app = require('../src/app') // builds the app: declares and fills the slots
  const registries = require('../src/modules/registries')
  /* eslint-enable global-require */

  const filled = registries.filledSlots().filter((s) => s.specFile)
  if (filled.length === 0) return 0

  const spec = JSON.parse(fs.readFileSync(outputFile, 'utf8'))
  let added = 0

  for (const slot of filled) {
    const prefix = findMountPrefix(app._router.stack, slot.router)
    if (prefix === null) {
      throw new Error(
        `swagger: extension slot "${slot.slot}" is filled but its router is not mounted on the app — ` +
          'declareSlot() returned a router nobody use()d.',
      )
    }
    const fragment = prefixPaths(await fragmentFor(slot.specFile), prefix)
    const paths = Object.keys(fragment.paths || {}).length
    if (paths === 0) {
      throw new Error(
        `swagger: extension slot "${slot.slot}" generated an EMPTY fragment from ${slot.specFile}. ` +
          'That is the silent-drop failure this step exists to catch, not a slot with no routes.',
      )
    }
    mergeFragment(spec, fragment, `slot ${slot.slot}`)
    added += paths
    process.stdout.write(`merged ${paths} path(s) from slot ${slot.slot} at ${prefix}\n`)
  }

  fs.writeFileSync(outputFile, `${JSON.stringify(spec, null, 2)}\n`)
  return added
}

module.exports = { mergeSlotSpecs, findMountPrefix }
