// ── Announcement pipeline: pure logic ──────────────────────────────────────
//
// No DB, no network — just the LEG-AGNOSTIC decisions the worker and model make,
// kept here so they are unit-testable in isolation (server/test/announceJobs.test.js):
//   • scheduleAfter — exponential backoff schedule + the attempt cap
//   • rollupStatus  — derive the parent job status from its legs
//   • legError      — squeeze a client result into one error line
//   • baseUrl / articleUrl — the public link an announcement carries
//
// What used to be here and is not any more: `buildTownCrierText`,
// `classifyTownCrier` and `classifyDiscord`. A leg's own text-building and result
// classification belong to the leg, and a leg is a registration now
// (MODULE_SYSTEM.md §1.8) — they live in utils/shardAnnounce.js and
// utils/discordAnnounce.js. This file is what every leg shares.

// Backoff between retries, indexed by attempts-so-far. Six attempts spread over
// ~a couple of hours; after the last one a leg is marked failed and surfaced in
// the post's admin panel. Shared by every leg.
const BACKOFF_MS = [30_000, 120_000, 600_000, 1_800_000, 3_600_000, 7_200_000]
const MAX_ATTEMPTS = BACKOFF_MS.length

// The site's public base, used to build the link an announcement carries.
function baseUrl() {
  return (process.env.APP_BASE_URL || 'http://localhost:5173').replace(/\/+$/, '')
}

// The public link that goes in the announcement. News has no per-post route
// (App.jsx only has the /site/news list), so we link the list — matches the
// pre-pipeline Discord announce behavior.
function articleUrl(base) {
  return `${String(base || '').replace(/\/+$/, '')}/site/news`
}

// Every leg's client returns { ok, status, data, error }. Squeeze a failure into
// the one line stored in announce_job_legs.last_error and shown in the panel.
function legError(result) {
  if (!result) return 'no response'
  if (result.status) {
    return result.data && result.data.message
      ? `${result.status}: ${result.data.message}`
      : result.error || `status ${result.status}`
  }
  return result.error || 'request failed'
}

// Given the number of attempts already made (>= 1), how long to wait before the
// next one — or null when the cap is reached and the leg should be failed.
function scheduleAfter(attempts) {
  if (attempts >= MAX_ATTEMPTS) return null
  return BACKOFF_MS[Math.min(attempts - 1, BACKOFF_MS.length - 1)]
}

// Parent job status derived from its leg statuses:
//   done    — every leg delivered
//   failed  — every leg gave up
//   partial — at least one leg reached a terminal state without all of them
//             agreeing (some still pending/retrying, or a mix of done and failed)
//   pending — no leg is terminal yet
//
// Takes the list of leg statuses rather than two named arguments, because the leg
// set is registered rather than fixed (MODULE_SYSTEM.md §1.8). No legs at all
// rolls up `done`: with nothing registered there is nothing left to deliver, and
// leaving such jobs `pending` would pile up rows the worker never touches.
function rollupStatus(statuses) {
  const list = Array.isArray(statuses) ? statuses : []
  const terminal = (s) => s === 'done' || s === 'failed'
  if (list.every((s) => s === 'done')) return 'done'
  if (list.every((s) => s === 'failed')) return 'failed'
  if (list.some(terminal)) return 'partial'
  return 'pending'
}

module.exports = {
  MAX_ATTEMPTS,
  BACKOFF_MS,
  baseUrl,
  articleUrl,
  legError,
  scheduleAfter,
  rollupStatus,
}
