// ── The OpenAPI document core actually serves ──────────────────────────────
//
// `swagger-output.json` is core's own routes and only core's own routes: it is
// generated by `npm run swagger` on a developer's machine and committed, so it
// must come out the same regardless of which modules that developer happened to
// have checked out. A module's routes cannot be in it, and not merely because
// nobody put them there — a module arrives on a volume long after the image was
// built, and core never has its sources to analyse.
//
// So the document served at `/api/docs.json` is assembled at REQUEST time: core's
// committed spec, plus the `swagger-fragment.json` of every started module
// (docs/website/MODULE_API.md §2.8 and §6.1a). This file is that assembly.
//
// **Core always wins a key collision.** `mergeFragment` enforces it and reports
// what it dropped. A module cannot redefine a core path, tag or schema by shipping
// one with the same name — which is why §6.1a tells modules to namespace the
// schemas they define (`UoShardStatus`) while referencing core's shared ones
// (`Error`) by core's name: the first would collide and lose, the second resolves
// here, in the merged document, which is the only place both exist.
//
// **Cached, keyed on the loader's state version.** Building the document reads a
// file per module and deep-copies a 5,000-line spec; `/api/docs` is a page an
// operator opens occasionally and a crawler may hit repeatedly. The cache is
// invalidated by any module state CHANGE — which is what "started modules only"
// depends on, and the only input here that can move without a restart.

const fs = require('fs')

const modules = require('../src/modules/loader')
const createLogger = require('../src/utils/logger')
const { mergeFragment } = require('./mergeSpec')

const log = createLogger('swagger')

let cached = null
let cachedVersion = -1

/**
 * Core's spec with every started module's fragment merged over it.
 *
 * Never throws: `/api/docs.json` answering with core's routes alone is a worse
 * document than the full one, but it is a document. A fragment that is missing,
 * unreadable or not JSON costs that module its paths and nothing else — the same
 * bargain §4.4 makes everywhere else, where one module's failure is never the
 * site's.
 *
 * @param {object} coreSpec  the committed swagger-output.json — never mutated
 * @returns {object}
 */
function docsSpec(coreSpec) {
  // Before app.js has called modules.load(), asking is a mis-ordered boot rather
  // than a core with nothing installed (§7.6) — but this is a request handler, and
  // 500ing the docs page over it would be the wrong trade. Core's own spec is the
  // honest answer to "what is documented" at that point anyway.
  if (!modules.isLoaded()) return coreSpec

  const version = modules.version()
  if (cached && cachedVersion === version) return cached

  // A structural copy, because mergeFragment writes into what it is given and
  // `coreSpec` is a require()d JSON module: mutating it would make the merge
  // cumulative across rebuilds and permanent for the life of the process.
  const spec = JSON.parse(JSON.stringify(coreSpec))
  spec.paths = spec.paths || {}
  spec.tags = spec.tags || []
  spec.components = spec.components || {}
  spec.components.schemas = spec.components.schemas || {}

  for (const { id, file } of modules.specFragments()) {
    let fragment
    try {
      fragment = JSON.parse(fs.readFileSync(file, 'utf8'))
    } catch (err) {
      log.warn('module OpenAPI fragment could not be read — its routes will be undocumented', {
        module: id,
        file,
        error: err.message,
      })
      continue
    }
    const before = Object.keys(spec.paths).length
    mergeFragment(spec, fragment, `module ${id}`)
    log.debug('merged module OpenAPI fragment', {
      module: id,
      paths: Object.keys(spec.paths).length - before,
    })
  }

  cached = spec
  cachedVersion = version
  return spec
}

/** Test seam: forget the cached document. */
function reset() {
  cached = null
  cachedVersion = -1
}

module.exports = { docsSpec, reset }
