// Le Sextant — distribution revenue/cost model.
//
// Answers one question: over a lifetime, what does each distributor cost you to
// PORT what you have, KEEP it alive, and ADD new releases — and what's left after
// their cut? RouteNote-free is the baseline everything is measured against.
//
// All facts (fees, cuts) are sourced from armada/manifeste/research-2026-distribution.md.
// All assumptions (streams, sales) are user-tunable. The model never implies more
// precision than the data has: the long tail is a smooth floor, not simulated fan-by-fan.

export type Kind = 'single' | 'album'
export type Bucket = 'existing' | 'backlog' | 'forward'

export interface CatalogInputs {
  existingSingles: number // standalone tracks already on RouteNote/DSP
  existingAlbums: number // albums/EPs already on RouteNote/DSP (one delivery each)
  backlogTracks: number // SoundCloud tracks not yet on any DSP
  backlogTracksPerEp: number // 1 = release all as singles; >1 = bundle into EPs
  cadencePerYear: number // forward releases per year
  cadenceAlbumShare: number // 0..1 — fraction of forward releases that are albums/EPs
  remastersPerYear: number // existing releases re-mastered & re-delivered per year
  horizonYears: number // lifetime horizon
}

export interface RevenueInputs {
  peakStreams: number // streams in a release's first month on a DSP
  floorStreams: number // long-tail monthly floor (the loyal-listener tail)
  halfLifeMonths: number // months for the spike above floor to halve
  perStream: number // payout per stream (USD)
  bcLayer: boolean // run Bandcamp alongside the distributor (independent layer)
  bcSalesPerYear: number // Bandcamp digital sales per year, across the whole catalog
  bcPrice: number // Bandcamp unit price (USD, EUR parity assumed)
}

// Bandcamp's cut: 15% digital (10% after $5k/yr), 0% on the 8 Bandcamp Fridays.
// Blended to ~12% effective → keep ~88%.
export const BC_KEEP = 0.88

// Freedom & control — qualitative, sourced from 2026 distributor help docs.
// You keep 100% of masters with all of them (non-exclusive); these are the axes
// that actually differ.
export type FreedomLevel = 'good' | 'mixed' | 'bad'
export interface FreedomFact {
  level: FreedomLevel
  text: string
}
export interface Freedom {
  remaster: FreedomFact // swap audio on an already-live release
  edit: FreedomFact // change title/art/metadata after release
  pull: FreedomFact // take a release down
  unpaid: FreedomFact // permanence: what happens if you stop paying
}

export interface Option {
  id: string
  name: string
  short: string
  recommended?: boolean
  baseline?: boolean
  // cost facts (USD)
  oneTimeSingle: number
  oneTimeAlbum: number
  annualSingle: number // recurring per-release renewal (RouteNote Premium)
  annualAlbum: number
  flatAnnual: number // platform subscription, unlimited releases (DistroKid)
  flatAnnualWithRemaster?: number // higher tier needed for in-place audio swap (DistroKid Ultimate)
  redeliverCost: number // cost to push a remaster of one existing release (0 = free / in-place)
  // economics
  keepFraction: number // share of streaming revenue kept after their cut
  ownsForever: boolean // music stays up with no further payment
  pullsIfUnpaid: boolean // music removed from DSPs if you stop paying
  freedom: Freedom
  note: string
  facts: string[] // shown verbatim in the option row
}

// Bandcamp the layer, on the same freedom axes — the free-replace outlier.
export const BANDCAMP_FREEDOM: Freedom = {
  remaster: { level: 'good', text: 'Replace the file in place, free, anytime' },
  edit: { level: 'good', text: 'Fully editable, free' },
  pull: { level: 'good', text: 'Free, instant' },
  unpaid: { level: 'good', text: "It's your own store — nothing to lapse" },
}

// Albums earn more than singles but not linearly (not every track gets equal plays).
const ALBUM_STREAM_MULT = 2.5

export const OPTIONS: Option[] = [
  {
    id: 'routenote-free',
    name: 'RouteNote · Free',
    short: 'baseline',
    baseline: true,
    oneTimeSingle: 0,
    oneTimeAlbum: 0,
    annualSingle: 0,
    annualAlbum: 0,
    flatAnnual: 0,
    redeliverCost: 0, // re-upload is free; track-link to keep plays
    keepFraction: 0.85,
    ownsForever: true,
    pullsIfUnpaid: false,
    freedom: {
      remaster: { level: 'mixed', text: 'Re-upload free; track-link to keep plays' },
      edit: { level: 'good', text: 'Title / art / genre editable after approval' },
      pull: { level: 'good', text: 'Free, self-serve, no fee' },
      unpaid: { level: 'good', text: 'Stays up (free tier, 85%)' },
    },
    note: 'Where the catalog lives today. $0, but a 15% cut and slow (3–4wk) moderation.',
    facts: ['$0 forever', '15% rev cut', '3–4wk moderation'],
  },
  {
    id: 'routenote-premium',
    name: 'RouteNote · Premium',
    short: '100% via renewal',
    oneTimeSingle: 0,
    oneTimeAlbum: 0,
    annualSingle: 12.99,
    annualAlbum: 59.99,
    flatAnnual: 0,
    redeliverCost: 12.99, // remaster = re-deliver, re-pay per release
    keepFraction: 1.0,
    ownsForever: false,
    pullsIfUnpaid: true,
    freedom: {
      remaster: { level: 'mixed', text: 'Re-upload, re-pay per release' },
      edit: { level: 'good', text: 'Editable after approval' },
      pull: { level: 'good', text: 'Free, self-serve' },
      unpaid: { level: 'mixed', text: 'Likely reverts to free 85% (unverified)' },
    },
    note: 'Keep 100% — but every release renews yearly. Pricing & lapse behaviour contested (verify).',
    facts: ['$12.99/single·yr', '$59.99/album·yr', 'keep 100%'],
  },
  {
    id: 'cdbaby',
    name: 'CD Baby',
    short: 'recommended',
    recommended: true,
    oneTimeSingle: 9.99,
    oneTimeAlbum: 14.99,
    annualSingle: 0,
    annualAlbum: 0,
    flatAnnual: 0,
    redeliverCost: 9.99, // remaster = a brand-new release + new fee, loses date/plays
    keepFraction: 0.91,
    ownsForever: true,
    pullsIfUnpaid: false,
    freedom: {
      remaster: { level: 'bad', text: 'New release · new fee · loses date & plays' },
      edit: { level: 'bad', text: 'Mostly locked after delivery' },
      pull: { level: 'good', text: 'Free, self-serve (~30 days)' },
      unpaid: { level: 'good', text: 'Stays up forever — no subscription' },
    },
    note: 'One-time per release, stays up forever, keep 91%. No recurring tax — but releases lock after delivery.',
    facts: ['$9.99/single', '$14.99/album', 'keep 91%', 'paid once'],
  },
  {
    id: 'distrokid',
    name: 'DistroKid',
    short: 'flat, unlimited',
    oneTimeSingle: 0,
    oneTimeAlbum: 0,
    annualSingle: 0,
    annualAlbum: 0,
    flatAnnual: 22.99,
    flatAnnualWithRemaster: 89.99, // Audio Swap (in-place) requires the Ultimate plan
    redeliverCost: 0, // in-place swap on Ultimate; tier bump handled in cost()
    keepFraction: 1.0,
    ownsForever: false,
    pullsIfUnpaid: true,
    freedom: {
      remaster: { level: 'mixed', text: 'In-place swap — Ultimate $90/yr, minor tweaks only' },
      edit: { level: 'good', text: 'Editable after upload' },
      pull: { level: 'good', text: 'Free, self-serve' },
      unpaid: { level: 'bad', text: 'Pulled, unless Leave-a-Legacy ($29/$49 once each)' },
    },
    note: 'Flat yearly, unlimited releases, keep 100% — but music is pulled if you stop paying.',
    facts: ['$22.99/yr flat', 'unlimited', 'keep 100%', 'rented shelf'],
  },
]

export interface Release {
  kind: Kind
  bucket: Bucket
  goLiveMonth: number
  aged: boolean // already past its release spike (existing catalog)
}

function bundle(tracks: number, perEp: number): { singles: number; albums: number } {
  if (perEp <= 1) return { singles: Math.max(0, Math.round(tracks)), albums: 0 }
  const albums = Math.floor(tracks / perEp)
  const singles = tracks - albums * perEp // leftover tracks ship as singles
  return { singles, albums }
}

/** Expand catalog inputs into individual dated releases over the horizon. */
export function buildReleases(c: CatalogInputs): Release[] {
  const rs: Release[] = []
  for (let i = 0; i < c.existingSingles; i++)
    rs.push({ kind: 'single', bucket: 'existing', goLiveMonth: 0, aged: true })
  for (let i = 0; i < c.existingAlbums; i++)
    rs.push({ kind: 'album', bucket: 'existing', goLiveMonth: 0, aged: true })

  // Backlog: re-released to DSP, staggered across the first year (they re-spike).
  const back = bundle(c.backlogTracks, c.backlogTracksPerEp)
  const backTotal = back.singles + back.albums
  let bi = 0
  const stagger = (idx: number, n: number) => (n <= 1 ? 0 : Math.floor((idx / n) * 12))
  for (let i = 0; i < back.albums; i++, bi++)
    rs.push({ kind: 'album', bucket: 'backlog', goLiveMonth: stagger(bi, backTotal), aged: false })
  for (let i = 0; i < back.singles; i++, bi++)
    rs.push({ kind: 'single', bucket: 'backlog', goLiveMonth: stagger(bi, backTotal), aged: false })

  // Forward cadence: per year, split single/album, spread within the year.
  for (let y = 0; y < c.horizonYears; y++) {
    const perYear = Math.round(c.cadencePerYear)
    const albums = Math.round(perYear * c.cadenceAlbumShare)
    const singles = perYear - albums
    let j = 0
    const within = (idx: number) => y * 12 + (perYear <= 1 ? 0 : Math.floor((idx / perYear) * 12))
    for (let i = 0; i < albums; i++, j++)
      rs.push({ kind: 'album', bucket: 'forward', goLiveMonth: within(j), aged: false })
    for (let i = 0; i < singles; i++, j++)
      rs.push({ kind: 'single', bucket: 'forward', goLiveMonth: within(j), aged: false })
  }
  return rs
}

export interface CostBreakdown {
  port: number // get current catalog (existing + backlog) onto the platform
  keep: number // maintain current catalog over the horizon
  add: number // release the forward pipeline
  update: number // re-master / re-deliver existing releases
  total: number
}

export function cost(
  opt: Option,
  releases: Release[],
  horizonYears: number,
  remastersPerYear = 0,
): CostBreakdown {
  let port = 0
  let keep = 0
  let add = 0
  for (const r of releases) {
    const one = r.kind === 'album' ? opt.oneTimeAlbum : opt.oneTimeSingle
    const ann = r.kind === 'album' ? opt.annualAlbum : opt.annualSingle
    const liveYears = Math.max(1, horizonYears - Math.floor(r.goLiveMonth / 12))
    if (r.bucket === 'forward') {
      add += one + ann * liveYears
    } else {
      port += one + ann // delivery + year-0 renewal
      keep += ann * (liveYears - 1) // renewals after year 0
    }
  }
  // Re-mastering an already-live release: a re-delivery (and a new fee) on most
  // platforms; in-place on DistroKid but only by paying up to the Ultimate tier.
  const update = remastersPerYear * horizonYears * opt.redeliverCost
  const flat =
    remastersPerYear > 0 && opt.flatAnnualWithRemaster ? opt.flatAnnualWithRemaster : opt.flatAnnual
  // Flat platform subscription is the price of keeping the catalog alive at all.
  if (flat > 0) keep += flat * horizonYears
  return { port, keep, add, update, total: port + keep + add + update }
}

function streamCurve(ageMonths: number, r: RevenueInputs): number {
  return r.floorStreams + (r.peakStreams - r.floorStreams) * Math.pow(0.5, ageMonths / r.halfLifeMonths)
}

/** Total DSP streams over the horizon (distributor-independent). */
export function totalStreams(releases: Release[], rev: RevenueInputs, horizonYears: number): number {
  const months = horizonYears * 12
  let streams = 0
  for (let m = 0; m < months; m++) {
    for (const r of releases) {
      if (m < r.goLiveMonth) continue
      const base = r.aged ? rev.floorStreams : streamCurve(m - r.goLiveMonth, rev)
      streams += base * (r.kind === 'album' ? ALBUM_STREAM_MULT : 1)
    }
  }
  return streams
}

/** Bandcamp is an independent direct-to-fan layer: same revenue whatever the
 *  distributor, so it shifts every option equally and never decides the comparison.
 *  Modelled as flat catalog-wide digital sales per year (a few, realistically). */
export function bandcampRevenue(rev: RevenueInputs, horizonYears: number): number {
  if (!rev.bcLayer) return 0
  return rev.bcSalesPerYear * horizonYears * rev.bcPrice * BC_KEEP
}

export interface OptionResult {
  opt: Option
  cost: CostBreakdown
  streamNet: number // streaming revenue kept after the distributor's cut
  bcRevenue: number // shared Bandcamp layer (identical across options)
  profit: number // streamNet + bcRevenue − distributor cost
  vsBaseline: number // profit delta against RouteNote-free
}

export function simulate(
  cat: CatalogInputs,
  rev: RevenueInputs,
): {
  releases: Release[]
  results: OptionResult[]
  counts: ReturnType<typeof catalogCounts>
  streams: number
  bcRevenue: number
} {
  const releases = buildReleases(cat)
  const streams = totalStreams(releases, rev, cat.horizonYears)
  const streamGross = streams * rev.perStream
  const bcRevenue = bandcampRevenue(rev, cat.horizonYears)
  const raw = OPTIONS.map((opt) => {
    const c = cost(opt, releases, cat.horizonYears, cat.remastersPerYear)
    const streamNet = streamGross * opt.keepFraction
    return { opt, cost: c, streamNet, bcRevenue, profit: streamNet + bcRevenue - c.total }
  })
  const baseline = raw.find((x) => x.opt.baseline)?.profit ?? 0
  const results = raw.map((x) => ({ ...x, vsBaseline: x.profit - baseline }))
  return { releases, results, counts: catalogCounts(releases), streams, bcRevenue }
}

export function catalogCounts(releases: Release[]) {
  const by = (b: Bucket) => releases.filter((r) => r.bucket === b)
  return {
    total: releases.length,
    existing: by('existing').length,
    backlog: by('backlog').length,
    forward: by('forward').length,
    singles: releases.filter((r) => r.kind === 'single').length,
    albums: releases.filter((r) => r.kind === 'album').length,
  }
}

// ── Break-even: lifetime cost as a function of forward cadence ──────────────
export interface BreakEvenPoint {
  cadence: number
  costs: Record<string, number>
}

export function breakEvenSweep(cat: CatalogInputs, maxCadence = 16): BreakEvenPoint[] {
  const pts: BreakEvenPoint[] = []
  for (let cad = 0; cad <= maxCadence; cad++) {
    const releases = buildReleases({ ...cat, cadencePerYear: cad })
    const costs: Record<string, number> = {}
    for (const opt of OPTIONS)
      costs[opt.id] = cost(opt, releases, cat.horizonYears, cat.remastersPerYear).total
    pts.push({ cadence: cad, costs })
  }
  return pts
}

/** First cadence (releases/yr) at which DistroKid becomes cheaper than CD Baby. */
export function distrokidCrossover(cat: CatalogInputs): number | null {
  const cdbaby = OPTIONS.find((o) => o.id === 'cdbaby')!
  const dk = OPTIONS.find((o) => o.id === 'distrokid')!
  for (let cad = 0; cad <= 40; cad++) {
    const releases = buildReleases({ ...cat, cadencePerYear: cad })
    const c = cost(cdbaby, releases, cat.horizonYears).total
    const d = cost(dk, releases, cat.horizonYears).total
    if (d < c) return cad
  }
  return null
}
