// ── Merging an OpenAPI fragment into a spec ────────────────────────────────
//
// The merge half of docs/website/MODULE_API.md §6.1's settled decision: routes
// that reach the app through something swagger-autogen cannot statically follow
// contribute a FRAGMENT, and core merges it.
//
// Two callers, one function, deliberately:
//   • build time — swagger/slotSpecs.js, for core's own extension-slot routers
//     (§1.9). Those are core routes that a static parse of app.js cannot see,
//     because the slot's router is created by registries.declareSlot() and filled
//     later. They belong in the committed `swagger-output.json`.
//   • request time (Phase 2 PR 6+) — an installed module's `swagger-fragment.json`,
//     merged over the committed spec for `/api/docs.json`.
//
// **Core always wins a key collision** (§6.1a). A fragment cannot redefine a path,
// a tag or a schema core already declares; the collision is reported and the
// fragment's version dropped. Merging is shallow-per-section — `paths`, `tags`
// and `components.schemas` — because those are the only three sections a fragment
// is allowed to carry, and a deeper merge would let a fragment reach into
// `info`, `servers` or the security schemes.

/**
 * Merge `fragment` into `spec`, in place, with core winning every collision.
 *
 * @param {object} spec      the base spec — mutated
 * @param {object} fragment  `{ paths?, tags?, components?: { schemas? } }`
 * @param {string} source    who the fragment came from, for the collision message
 * @returns {string[]} the collisions that were dropped (empty when clean)
 */
function mergeFragment(spec, fragment, source) {
  const dropped = []

  for (const [path, item] of Object.entries(fragment.paths || {})) {
    if (spec.paths[path]) {
      // Not a merge of the two path items: a fragment adding a METHOD to a core
      // path is the same overreach as replacing it, and the extension-slot
      // contract already says core owns the resource (§2.4).
      dropped.push(`path ${path}`)
      continue
    }
    spec.paths[path] = item
  }

  const tagNames = new Set((spec.tags || []).map((t) => t.name))
  for (const tag of fragment.tags || []) {
    if (tagNames.has(tag.name)) continue // same tag, not a collision worth reporting
    spec.tags.push(tag)
    tagNames.add(tag.name)
  }

  const schemas = (fragment.components && fragment.components.schemas) || {}
  for (const [name, schema] of Object.entries(schemas)) {
    if (spec.components.schemas[name]) {
      dropped.push(`schema ${name}`)
      continue
    }
    spec.components.schemas[name] = schema
  }

  if (dropped.length > 0) {
    process.stderr.write(
      `swagger: dropped ${dropped.length} colliding key(s) from ${source} — core wins: ${dropped.join(', ')}\n`,
    )
  }
  return dropped
}

/**
 * Re-root a fragment's paths under the prefix its router is actually mounted at.
 *
 * A fragment generated by pointing swagger-autogen at a router file alone has
 * paths relative to that router (`/shard/accounts`), because nothing in the file
 * says where it hangs. The prefix comes from the LIVE express stack rather than a
 * table, so it cannot drift the way a hand-written mount list would.
 *
 * Express path params (`:id`) become OpenAPI's (`{id}`) — swagger-autogen already
 * does that for the paths it generates, so the prefix has to match.
 */
function prefixPaths(fragment, prefix) {
  const oas = prefix.replace(/:([A-Za-z0-9_]+)/g, '{$1}').replace(/\/+$/, '')
  const outer = [...oas.matchAll(/\{([A-Za-z0-9_]+)\}/g)].map((m) => m[1])
  const paths = {}
  for (const [p, item] of Object.entries(fragment.paths || {})) {
    paths[`${oas}${p}`] = orderParams(item, outer)
  }
  return { ...fragment, paths }
}

/**
 * Put the prefix's own path parameters first, in prefix order.
 *
 * swagger-autogen orders parameters by where they appear in the path it saw, and
 * the fragment's path is only the tail — so `/{id}/shard/link/{account}` comes
 * out as (account, id) rather than (id, account). Re-rooting the path has to
 * re-root the parameter order with it, or every slot route churns the committed
 * spec by a reorder that means nothing.
 */
function orderParams(item, outer) {
  for (const operation of Object.values(item)) {
    const params = operation && operation.parameters
    if (!Array.isArray(params)) continue
    const rank = (p) => {
      const i = outer.indexOf(p && p.name)
      return i === -1 ? outer.length : i
    }
    // Stable: only the prefix params move, and only ahead of the rest.
    operation.parameters = params
      .map((p, i) => ({ p, i }))
      .sort((a, b) => rank(a.p) - rank(b.p) || a.i - b.i)
      .map(({ p }) => p)
  }
  return item
}

module.exports = { mergeFragment, prefixPaths }
