/**
 * Factories — créent des entités réalistes (FR, format Rubis) pour
 * peupler une org de démo ou alimenter des tests.
 *
 * Pas de framework lourd type @adonisjs/lucid factories : des fonctions
 * pures, idempotentes, qu'on compose dans une commande Ace ou un test.
 */

import { DateTime } from 'luxon'
import { randomUUID } from 'node:crypto'
import { readdir, readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { existsSync } from 'node:fs'
import type { TransactionClientContract } from '@adonisjs/lucid/types/database'
import drive from '@adonisjs/drive/services/main'

import Client from '#models/client'
import Invoice from '#models/invoice'
import ActivityEvent from '#models/activity_event'
import RelanceTask from '#models/relance_task'
import Plan from '#models/plan'

// ---------------------------------------------------------------------------
// Sources de données déterministes (pas de Faker — moins de deps, plus stable)
// ---------------------------------------------------------------------------

const CLIENT_TEMPLATES: Array<{
  name: string
  firstName: string | null
  lastName: string | null
  emailDomain: string
  phone: string | null
  address: string | null
  siret: string | null
}> = [
  {
    name: 'Boulangerie Martin SARL',
    firstName: 'Marie',
    lastName: 'Martin',
    emailDomain: 'boulangerie-martin.fr',
    phone: '+33 1 23 45 67 89',
    address: '12 rue du Pain, 75011 Paris',
    siret: '82345678900012',
  },
  {
    name: 'Maçonnerie Dupont & Fils',
    firstName: 'Jean',
    lastName: 'Dupont',
    emailDomain: 'maconnerie-dupont.fr',
    phone: '+33 4 78 56 12 34',
    address: '45 chemin des Carrières, 69100 Villeurbanne',
    siret: '53412987600028',
  },
  {
    name: 'Atelier Durand',
    firstName: null,
    lastName: null,
    emailDomain: 'atelier-durand.fr',
    phone: null,
    address: null,
    siret: null,
  },
  {
    name: 'Cabinet Rousseau Conseil',
    firstName: 'Julien',
    lastName: 'Rousseau',
    emailDomain: 'cabinet-rousseau.fr',
    phone: '+33 4 56 78 90 12',
    address: '8 place de la République, 69002 Lyon',
    siret: '53412987600101',
  },
  {
    name: 'Garage Lemoine',
    firstName: 'Pierre',
    lastName: 'Lemoine',
    emailDomain: 'garage-lemoine.fr',
    phone: '+33 2 99 87 65 43',
    address: '23 boulevard de la Liberté, 35000 Rennes',
    siret: '78912345600054',
  },
  {
    name: 'Studio Lefèvre',
    firstName: 'Camille',
    lastName: 'Lefèvre',
    emailDomain: 'studio-lefevre.com',
    phone: null,
    address: null,
    siret: null,
  },
  {
    name: 'Restaurant Le Beauvoir',
    firstName: 'Sophie',
    lastName: 'Beauvoir',
    emailDomain: 'le-beauvoir.fr',
    phone: '+33 1 45 22 33 44',
    address: '15 rue Mouffetard, 75005 Paris',
    siret: '12345678900078',
  },
  {
    name: 'Imprimerie Henri & Fils',
    firstName: 'Henri',
    lastName: 'Petit',
    emailDomain: 'imprimerie-henri.fr',
    phone: '+33 5 61 78 90 23',
    address: '7 avenue Jean Jaurès, 31000 Toulouse',
    siret: '45123789600041',
  },
]

const INVOICE_NOTES_POOL = [
  'Prestation conseil — janvier',
  'Travaux de rénovation second œuvre',
  'Photographe événementiel — mariage',
  'Maintenance trimestrielle',
  'Livraison matières premières',
  'Audit comptable annuel',
  null,
  null,
]

// ---------------------------------------------------------------------------
// Factories
// ---------------------------------------------------------------------------

export type ClientFactoryInput = {
  organizationId: string
  trx?: TransactionClientContract
  /** Index 0..N dans CLIENT_TEMPLATES, sinon valeurs random. */
  index?: number
}

export async function makeClient(input: ClientFactoryInput): Promise<Client> {
  const tpl =
    input.index !== undefined
      ? CLIENT_TEMPLATES[input.index % CLIENT_TEMPLATES.length]!
      : CLIENT_TEMPLATES[Math.floor(Math.random() * CLIENT_TEMPLATES.length)]!

  const inboxName = tpl.firstName
    ? `${tpl.firstName.toLowerCase()}@${tpl.emailDomain}`
    : `compta@${tpl.emailDomain}`

  return Client.create(
    {
      organizationId: input.organizationId,
      name: tpl.name,
      email: inboxName,
      contactFirstName: tpl.firstName,
      contactLastName: tpl.lastName,
      phone: tpl.phone,
      address: tpl.address,
      siret: tpl.siret,
      notes: null,
    },
    { client: input.trx }
  )
}

export type InvoiceStatus = Invoice['status']

export type InvoiceFactoryInput = {
  organizationId: string
  clientId: string
  planId: string | null
  /** Status cible — drive aussi les dates (pending = future, paid = passé). */
  status: InvoiceStatus
  /** Numéro override, sinon F-YYYY-XXXX random. */
  numero?: string
  /** Montant en centimes. Sinon random 250€-8000€. */
  amountTtcCents?: number
  /** Décalage en jours par rapport à aujourd'hui pour issueDate.
   *  Négatif = passé. */
  issueOffsetDays?: number
  /** Délai de paiement en jours (par défaut 30, conforme LME). */
  paymentTermDays?: number
  trx?: TransactionClientContract
}

export async function makeInvoice(input: InvoiceFactoryInput): Promise<Invoice> {
  const issueOffset = input.issueOffsetDays ?? -randomInt(7, 90)
  const paymentTerm = input.paymentTermDays ?? 30
  const issueDate = DateTime.utc().plus({ days: issueOffset }).startOf('day')
  const dueDate = issueDate.plus({ days: paymentTerm })
  const amount = input.amountTtcCents ?? randomInt(25_000, 800_000)
  const numero =
    input.numero ?? `F-${issueDate.year}-${String(randomInt(1, 9999)).padStart(4, '0')}`

  // paidAt : pour les statuts paid, on simule un paiement après l'échéance
  // (parfois en avance, parfois en retard — réaliste).
  let paidAt: DateTime | null = null
  if (input.status === 'paid') {
    const paidOffset = randomInt(-5, 25) // -5 = payé en avance, +25 = en retard
    paidAt = dueDate.plus({ days: paidOffset })
  }

  // rubisEarned : 1 par relance envoyée + 1 si payée
  let rubisEarned = 0
  if (input.status === 'in_relance' || input.status === 'awaiting_user_confirmation') {
    rubisEarned = randomInt(1, 3)
  } else if (input.status === 'paid') {
    rubisEarned = randomInt(0, 4) // certaines payées sans relance
  }

  return Invoice.create(
    {
      organizationId: input.organizationId,
      clientId: input.clientId,
      planId: input.planId,
      numero,
      amountTtcCents: amount,
      issueDate,
      dueDate,
      paidAt,
      status: input.status,
      pdfStorageKey: null,
      rubisEarned,
      notes: pickRandom(INVOICE_NOTES_POOL),
    },
    { client: input.trx }
  )
}

export type ActivityFactoryInput = {
  organizationId: string
  invoice: Invoice
  client: Client
  trx?: TransactionClientContract
}

/**
 * Pour chaque facture, génère les events réalistes :
 *  - invoice_imported (toujours, à issueDate)
 *  - relance_sent N fois si status in_relance/awaiting (entre issueDate et now)
 *  - invoice_paid si status paid
 */
export async function makeActivityForInvoice(
  input: ActivityFactoryInput
): Promise<void> {
  const { invoice, client, trx } = input

  // Import — toujours
  await ActivityEvent.create(
    {
      organizationId: input.organizationId,
      kind: 'invoice_imported',
      at: invoice.issueDate,
      label: `Facture <b>${invoice.numero}</b> importée`,
      meta: { invoiceId: invoice.id, clientId: client.id },
    },
    { client: trx }
  )

  if (invoice.status === 'in_relance' || invoice.status === 'awaiting_user_confirmation') {
    const relanceCount = randomInt(1, 3)
    for (let i = 0; i < relanceCount; i++) {
      const sentAt = invoice.dueDate.plus({ days: 3 + i * 7 })
      if (sentAt > DateTime.utc()) break
      await ActivityEvent.create(
        {
          organizationId: input.organizationId,
          kind: 'relance_sent',
          at: sentAt,
          label: `Relance J+${3 + i * 7} envoyée à <b>${client.name}</b>`,
          meta: {
            invoiceId: invoice.id,
            clientId: client.id,
            planStepOrder: i,
          },
        },
        { client: trx }
      )
    }
  }

  if (invoice.status === 'paid' && invoice.paidAt) {
    await ActivityEvent.create(
      {
        organizationId: input.organizationId,
        kind: 'invoice_paid',
        at: invoice.paidAt,
        label: `<b>${client.name}</b> a réglé ${invoice.numero}`,
        meta: { invoiceId: invoice.id, clientId: client.id },
      },
      { client: trx }
    )
  }
}

// ---------------------------------------------------------------------------
// Recettes — combine les factories pour produire une org démo cohérente
// ---------------------------------------------------------------------------

export type DemoSeedConfig = {
  organizationId: string
  /** Plans déjà provisionnés dans l'org (pour piocher des planId). */
  plans: Plan[]
  trx: TransactionClientContract
  /** Combien de clients (1..N templates dispo). Défaut 8. */
  clientCount?: number
}

export type DemoSeedResult = {
  clients: Client[]
  invoices: Invoice[]
  rubisEarned: number
}

/**
 * Mix de statuts représentatif d'une TPE active :
 *  - 5 paid (DSO calc + encaissé total)
 *  - 4 in_relance (à voir dans le funnel "en relance")
 *  - 2 awaiting_user_confirmation (check-in en attente)
 *  - 3 pending (récentes, pas encore relancées)
 *  - 1 litigation (cas tendu)
 */
const INVOICE_RECIPE: Array<{ status: InvoiceStatus; issueOffsetDays: number; planIdx?: number }> = [
  // Paid — réparties sur 6 mois pour faire vivre le DSO
  { status: 'paid', issueOffsetDays: -180 },
  { status: 'paid', issueOffsetDays: -135 },
  { status: 'paid', issueOffsetDays: -95 },
  { status: 'paid', issueOffsetDays: -65 },
  { status: 'paid', issueOffsetDays: -40 },
  // En relance — échéances passées récentes
  { status: 'in_relance', issueOffsetDays: -55 },
  { status: 'in_relance', issueOffsetDays: -50 },
  { status: 'in_relance', issueOffsetDays: -42 },
  { status: 'in_relance', issueOffsetDays: -38 },
  // Awaiting check-in — échéance toute fraîche
  { status: 'awaiting_user_confirmation', issueOffsetDays: -32 },
  { status: 'awaiting_user_confirmation', issueOffsetDays: -30 },
  // Pending — récentes, pas encore arrivées à échéance
  { status: 'pending', issueOffsetDays: -10 },
  { status: 'pending', issueOffsetDays: -5 },
  { status: 'pending', issueOffsetDays: -2 },
  // Litigation — ancienne, contestée
  { status: 'litigation', issueOffsetDays: -90 },
]

/** CA cible sur 12 mois pour le seed démo (en centimes). */
const TARGET_ANNUAL_REVENUE_CENTS = 40_000_000 // 400 000 €
/** Nombre de factures historiques à générer (paid sur les 12 derniers mois). */
const HISTORICAL_INVOICE_COUNT = 200

export async function seedDemoOrg(config: DemoSeedConfig): Promise<DemoSeedResult> {
  const { organizationId, plans, trx } = config

  // Phase 1 — Actionnables : si on a des PDFs réels dans assets/test-invoices,
  // on les utilise (mix pending/in_relance/paid/litigation, avec preview PDF).
  // Sinon on tombe sur la recipe synthétique.
  const assetsDir = resolveTestInvoicesDir()
  const actionable: DemoSeedResult = assetsDir
    ? await seedFromAssetPdfs({ ...config, assetsDir })
    : await seedSyntheticActionable(config)

  // Phase 2 — Historique : ~200 factures `paid` réparties sur 12 mois pour
  // alimenter les graphes (encaissé mensuel, DSO, etc.) et donner un CA
  // annuel d'environ 400 K€.
  const paidInActionable = actionable.invoices
    .filter((inv) => inv.status === 'paid')
    .reduce((s, inv) => s + inv.amountTtcCents, 0)
  const historicalTarget = Math.max(
    TARGET_ANNUAL_REVENUE_CENTS - paidInActionable,
    20_000_000 // garde-fou : au moins 200 K€ pour avoir des graphes lisibles
  )
  const historical = await seedHistoricalInvoices({
    organizationId,
    clients: actionable.clients,
    plans,
    trx,
    targetRevenueCents: historicalTarget,
    invoiceCount: HISTORICAL_INVOICE_COUNT,
    monthsBack: 12,
  })

  return {
    clients: actionable.clients,
    invoices: [...actionable.invoices, ...historical.invoices],
    rubisEarned: actionable.rubisEarned + historical.rubisEarned,
  }
}

/** Recipe synthétique fallback — utilisée si pas de PDFs dans assets/. */
async function seedSyntheticActionable(
  config: DemoSeedConfig
): Promise<DemoSeedResult> {
  const { organizationId, plans, trx } = config
  const clientCount = Math.min(config.clientCount ?? 8, CLIENT_TEMPLATES.length)
  const clients: Client[] = []
  for (let i = 0; i < clientCount; i++) {
    clients.push(await makeClient({ organizationId, index: i, trx }))
  }

  const invoices: Invoice[] = []
  for (const [i, recipe] of INVOICE_RECIPE.entries()) {
    const client = clients[i % clients.length]!
    const plan = plans[i % plans.length] ?? null
    const invoice = await makeInvoice({
      organizationId,
      clientId: client.id,
      planId: plan?.id ?? null,
      status: recipe.status,
      issueOffsetDays: recipe.issueOffsetDays,
      trx,
    })
    invoices.push(invoice)
    await makeActivityForInvoice({ organizationId, invoice, client, trx })
  }

  const rubisEarned = invoices.reduce((s, inv) => s + inv.rubisEarned, 0)
  return { clients, invoices, rubisEarned }
}

// ---------------------------------------------------------------------------
// Recette "PDFs réels" — utilise assets/test-invoices/*.pdf
// ---------------------------------------------------------------------------

/**
 * Localise le dossier `assets/test-invoices` à la racine du repo. Le command
 * tourne depuis `apps/api/`, donc on remonte de 2 niveaux.
 */
function resolveTestInvoicesDir(): string | null {
  const candidates = [
    join(process.cwd(), '..', '..', 'assets', 'test-invoices'),
    join(process.cwd(), 'assets', 'test-invoices'),
  ]
  for (const c of candidates) {
    if (existsSync(c)) return c
  }
  return null
}

/**
 * Mappe le suffixe descriptif d'un nom de fichier vers (status, dueOffset).
 * Filenames :
 *   - facture-pas-en-retard-echeance-{N}j-XXX.pdf  → due dans +N jours
 *   - facture-echue-aujourdhui-XXX.pdf            → due aujourd'hui
 *   - facture-en-retard-{N}j-XXX.pdf              → due il y a N jours
 *
 * Pour certaines factures très en retard, on simule un règlement tardif
 * (paid) ou une mise en demeure (litigation) — ça donne un mix réaliste.
 */
type AssetSpec = {
  filename: string
  /** Décalage de la dueDate par rapport à aujourd'hui (en jours). */
  dueOffsetDays: number
  status: InvoiceStatus
}

/** Quelques numéros qu'on bascule en paid / litigation pour le mix démo. */
const PAID_OVERRIDES = new Set([
  'facture-en-retard-30j-017.pdf',
  'facture-en-retard-45j-019.pdf',
  'facture-en-retard-60j-022.pdf',
  'facture-en-retard-90j-024.pdf',
  'facture-en-retard-120j-026.pdf',
])
const LITIGATION_OVERRIDES = new Set([
  'facture-en-retard-120j-025.pdf',
  'facture-en-retard-180j-027.pdf',
])

function parseAssetFilename(filename: string): AssetSpec | null {
  // Pattern 1 : pas-en-retard-echeance-{N}j → due in +N
  const futureMatch = filename.match(/pas-en-retard-echeance-(\d+)j/)
  if (futureMatch) {
    return {
      filename,
      dueOffsetDays: Number(futureMatch[1]),
      status: 'pending',
    }
  }
  // Pattern 2 : echue-aujourdhui
  if (/echue-aujourdhui/.test(filename)) {
    return {
      filename,
      dueOffsetDays: 0,
      status: 'awaiting_user_confirmation',
    }
  }
  // Pattern 3 : en-retard-{N}j → due -N
  const lateMatch = filename.match(/en-retard-(\d+)j/)
  if (lateMatch) {
    const days = Number(lateMatch[1])
    let status: InvoiceStatus = 'in_relance'
    if (days <= 3) status = 'awaiting_user_confirmation'
    if (PAID_OVERRIDES.has(filename)) status = 'paid'
    else if (LITIGATION_OVERRIDES.has(filename)) status = 'litigation'
    return {
      filename,
      dueOffsetDays: -days,
      status,
    }
  }
  return null
}

/** Extrait le numéro depuis le filename (ex. `...-007.pdf` → `F2026-0007`). */
function deriveInvoiceNumero(filename: string, fallbackYear: number): string {
  const m = filename.match(/-(\d{3,})\.pdf$/)
  if (!m) return `F-${fallbackYear}-${String(randomInt(1, 9999)).padStart(4, '0')}`
  return `F${fallbackYear}-${m[1]!.padStart(4, '0')}`
}

/**
 * Crée les RelanceTask pour une invoice — uniquement pour les statuts qui ont
 * effectivement passé un check-in et donc déclenché des relances. Les statuts
 * `pending` et `awaiting_user_confirmation` n'en ont volontairement pas : tant
 * que l'user n'a pas répondu à un check-in, AUCUNE relance n'est programmée.
 *
 *   - pending → 0 task (en attente du tout premier check-in)
 *   - awaiting_user_confirmation → 0 task (check-in en cours, en attente)
 *   - in_relance / litigation → sent si passée, scheduled sinon
 *   - paid → sent jusqu'à paidAt, cancelled au-delà
 *
 * Pas d'enqueue BullMQ — sinon les jobs orphelins polluent Redis.
 */
async function seedRelanceTasksForInvoice(
  invoice: Invoice,
  plan: Plan & { steps?: Array<{ id: string; offsetDays: number; order: number }> },
  trx: TransactionClientContract
): Promise<void> {
  if (!plan.steps?.length) return

  // Statuts pré-check-in : aucune task à programmer.
  if (invoice.status === 'pending' || invoice.status === 'awaiting_user_confirmation') {
    return
  }

  const now = DateTime.utc()
  const paidAt = invoice.paidAt
  const sortedSteps = plan.steps.slice().sort((a, b) => a.order - b.order)

  for (const step of sortedSteps) {
    const sendAt = invoice.dueDate.plus({ days: step.offsetDays })

    let status: 'scheduled' | 'sent' | 'cancelled'
    let sentAt: DateTime | null = null

    if (invoice.status === 'paid' && paidAt) {
      if (sendAt <= paidAt) {
        status = 'sent'
        sentAt = sendAt
      } else {
        status = 'cancelled'
      }
    } else {
      // in_relance, litigation
      if (sendAt <= now) {
        status = 'sent'
        sentAt = sendAt
      } else {
        status = 'scheduled'
      }
    }

    await RelanceTask.create(
      {
        organizationId: invoice.organizationId,
        invoiceId: invoice.id,
        planStepId: step.id,
        sendAt,
        status,
        sentAt,
        queueJobId: null,
      },
      { client: trx }
    )
  }
}

type AssetSeedConfig = DemoSeedConfig & { assetsDir: string }

async function seedFromAssetPdfs(config: AssetSeedConfig): Promise<DemoSeedResult> {
  const { organizationId, plans, trx, assetsDir } = config

  const allFiles = await readdir(assetsDir)
  const pdfs = allFiles.filter((f) => f.endsWith('.pdf')).sort()
  const specs = pdfs
    .map(parseAssetFilename)
    .filter((s): s is AssetSpec => s !== null)

  if (specs.length === 0) {
    // Aucun PDF parseable : on n'aurait pas dû arriver ici.
    return { clients: [], invoices: [], rubisEarned: 0 }
  }

  // Charge les plans avec leurs steps — on en a besoin pour seeder les
  // RelanceTasks au bon `sendAt`.
  const plansWithSteps = await Plan.query({ client: trx })
    .whereIn(
      'id',
      plans.map((p) => p.id)
    )
    .preload('steps', (q) => q.orderBy('order', 'asc'))

  // Crée tous les clients du pool — round-robin sur les factures.
  const clientCount = Math.min(config.clientCount ?? 8, CLIENT_TEMPLATES.length)
  const clients: Client[] = []
  for (let i = 0; i < clientCount; i++) {
    clients.push(await makeClient({ organizationId, index: i, trx }))
  }

  const invoices: Invoice[] = []
  const today = DateTime.utc().startOf('day')

  for (const [i, spec] of specs.entries()) {
    const client = clients[i % clients.length]!
    const plan = plansWithSteps[i % plansWithSteps.length] ?? null

    // Upload le PDF vers le drive (MinIO en S3, fs en fallback).
    const filePath = join(assetsDir, spec.filename)
    const buffer = await readFile(filePath)
    const storageKey = `invoice-pdfs/${organizationId}/${randomUUID()}.pdf`
    await drive.use().put(storageKey, buffer)

    // Dates : on cale la dueDate sur l'offset, on assume 30j de termes.
    const dueDate = today.plus({ days: spec.dueOffsetDays })
    const issueDate = dueDate.minus({ days: 30 })

    let paidAt: DateTime | null = null
    if (spec.status === 'paid') {
      // Règlement après l'échéance — entre 5 et 30j de retard côté facture.
      paidAt = dueDate.plus({ days: randomInt(5, 30) })
    }

    let rubisEarned = 0
    if (spec.status === 'in_relance' || spec.status === 'awaiting_user_confirmation') {
      rubisEarned = randomInt(1, 3)
    } else if (spec.status === 'paid') {
      rubisEarned = randomInt(1, 4)
    }

    const invoice = await Invoice.create(
      {
        organizationId,
        clientId: client.id,
        planId: plan?.id ?? null,
        numero: deriveInvoiceNumero(spec.filename, issueDate.year),
        amountTtcCents: randomInt(25_000, 800_000),
        issueDate,
        dueDate,
        paidAt,
        status: spec.status,
        pdfStorageKey: storageKey,
        rubisEarned,
        notes: pickRandom(INVOICE_NOTES_POOL),
      },
      { client: trx }
    )
    invoices.push(invoice)
    await makeActivityForInvoice({ organizationId, invoice, client, trx })
    if (plan) {
      await seedRelanceTasksForInvoice(invoice, plan, trx)
    }
  }

  const rubisEarned = invoices.reduce((s, inv) => s + inv.rubisEarned, 0)
  return { clients, invoices, rubisEarned }
}

// ---------------------------------------------------------------------------
// Recette "historique" — factures paid réparties sur N mois (alimente les
// graphes : encaissé mensuel, DSO, etc.). Pas de PDF, juste des données pour
// que les charts soient parlants.
// ---------------------------------------------------------------------------

type HistoricalSeedConfig = {
  organizationId: string
  clients: Client[]
  plans: Plan[]
  trx: TransactionClientContract
  /** Total cents que la somme des paid doit approcher. */
  targetRevenueCents: number
  /** Nombre de factures à générer. */
  invoiceCount: number
  /** Profondeur en mois (issueDate étalée sur cette fenêtre). */
  monthsBack: number
}

async function seedHistoricalInvoices(
  config: HistoricalSeedConfig
): Promise<DemoSeedResult> {
  const { organizationId, clients, plans, trx, monthsBack } = config
  if (clients.length === 0) return { clients, invoices: [], rubisEarned: 0 }

  // ~5% de cancelled pour la variété (factures annulées en cours de route).
  const cancelledIndexes = new Set<number>()
  const cancelledCount = Math.round(config.invoiceCount * 0.05)
  while (cancelledIndexes.size < cancelledCount) {
    cancelledIndexes.add(randomInt(0, config.invoiceCount - 1))
  }

  // Distribution des montants — log-uniforme entre 0.3 et 2.8, puis rescale
  // pour matcher le CA cible. Ça donne une queue avec quelques grosses
  // factures et beaucoup de petites — réaliste pour une TPE.
  const paidCount = config.invoiceCount - cancelledIndexes.size
  const rawAmounts = Array.from(
    { length: paidCount },
    () => 0.3 + Math.random() * 2.5
  )
  const sumRaw = rawAmounts.reduce((a, b) => a + b, 0)
  const scale = config.targetRevenueCents / sumRaw
  const paidAmounts = rawAmounts.map((r) => Math.max(20_000, Math.round(r * scale)))

  const invoices: Invoice[] = []
  const today = DateTime.utc().startOf('day')
  let paidIdx = 0

  for (let i = 0; i < config.invoiceCount; i++) {
    const isCancelled = cancelledIndexes.has(i)
    const client = clients[i % clients.length]!
    // Plans optionnels — la moitié des historiques n'en avait pas (saisie
    // manuelle), pour varier.
    const plan = i % 2 === 0 ? (plans[i % plans.length] ?? null) : null

    // Date d'émission étalée sur monthsBack mois (avec jitter par jour).
    const monthOffset = randomInt(0, monthsBack - 1)
    const dayJitter = randomInt(0, 27)
    const issueDate = today
      .minus({ months: monthOffset })
      .startOf('month')
      .plus({ days: dayJitter })
    const paymentTerm = 30
    const dueDate = issueDate.plus({ days: paymentTerm })

    let amountTtcCents: number
    let paidAt: DateTime | null = null
    let status: InvoiceStatus
    let rubisEarned = 0

    if (isCancelled) {
      // Cancelled : montant petit-moyen, pas de paidAt, pas de rubis.
      amountTtcCents = randomInt(30_000, 200_000)
      status = 'cancelled'
    } else {
      amountTtcCents = paidAmounts[paidIdx++]!
      // Paiement entre -3j (avance) et +25j (retard) par rapport à dueDate.
      paidAt = dueDate.plus({ days: randomInt(-3, 25) })
      // Mais jamais après aujourd'hui.
      if (paidAt > today) paidAt = today.minus({ days: randomInt(0, 5) })
      status = 'paid'
      // ~70% des paid ont déclenché 0-2 relances avant règlement.
      rubisEarned = Math.random() < 0.7 ? randomInt(0, 2) : 0
    }

    const numero = `F${issueDate.year}-H${String(i + 1).padStart(4, '0')}`

    const invoice = await Invoice.create(
      {
        organizationId,
        clientId: client.id,
        planId: plan?.id ?? null,
        numero,
        amountTtcCents,
        issueDate,
        dueDate,
        paidAt,
        status,
        pdfStorageKey: null,
        rubisEarned,
        notes: pickRandom(INVOICE_NOTES_POOL),
      },
      { client: trx }
    )
    invoices.push(invoice)

    // Activity events minimaux pour la timeline (import + paiement).
    await ActivityEvent.create(
      {
        organizationId,
        kind: 'invoice_imported',
        at: issueDate,
        label: `Facture <b>${numero}</b> importée`,
        meta: { invoiceId: invoice.id, clientId: client.id },
      },
      { client: trx }
    )
    if (status === 'paid' && paidAt) {
      await ActivityEvent.create(
        {
          organizationId,
          kind: 'invoice_paid',
          at: paidAt,
          label: `<b>${client.name}</b> a réglé ${numero}`,
          meta: { invoiceId: invoice.id, clientId: client.id },
        },
        { client: trx }
      )
    }
  }

  const rubisEarned = invoices.reduce((s, inv) => s + inv.rubisEarned, 0)
  return { clients, invoices, rubisEarned }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function randomInt(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min
}

function pickRandom<T>(arr: readonly T[]): T {
  return arr[Math.floor(Math.random() * arr.length)]!
}

// Marque l'export de randomUUID utilisée si on en a besoin ailleurs.
export { randomUUID }
