/**
 * Factories — créent des entités réalistes (FR, format Rubis) pour
 * peupler une org de démo.
 *
 * Particularité notable : pour CHAQUE invoice on génère un vrai PDF
 * cohérent (vendeur = org du user, client = client réel, montant et
 * dates = données BDD) via `invoice_pdf_factory.ts`, puis on l'upload
 * sur MinIO. Les fiches facture montrent ainsi un document réel
 * dont le contenu correspond exactement aux meta de la BDD.
 */

import { DateTime } from 'luxon'
import { randomUUID } from 'node:crypto'
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'
import Organization from '#models/organization'
import User from '#models/user'

import {
  generateInvoicePdfBuffer,
  type InvoiceItem,
  type PdfInvoiceInput,
} from '#database/invoice_pdf_factory'

// ---------------------------------------------------------------------------
// 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: '3 impasse des Artisans, 33000 Bordeaux',
    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: '17 rue des Lilas, 44000 Nantes',
    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,
]

/**
 * Pool d'items B2B variés. Chaque item a une fourchette de prix unitaire
 * (HT, en €) — on tire random à l'intérieur, et la quantité s'ajuste
 * pour atteindre le total cible.
 */
const ITEM_POOL: Array<{
  description: string
  unit: string
  /** Prix unitaire HT, fourchette en € (pas en centimes). */
  unitPriceMinEur: number
  unitPriceMaxEur: number
}> = [
  { description: 'Conseil stratégique', unit: 'jour', unitPriceMinEur: 600, unitPriceMaxEur: 1200 },
  { description: 'Développement sur mesure', unit: 'h', unitPriceMinEur: 80, unitPriceMaxEur: 150 },
  { description: 'Audit financier', unit: 'forfait', unitPriceMinEur: 800, unitPriceMaxEur: 2400 },
  { description: 'Maintenance trimestrielle', unit: 'forfait', unitPriceMinEur: 400, unitPriceMaxEur: 900 },
  { description: 'Formation équipe', unit: 'jour', unitPriceMinEur: 800, unitPriceMaxEur: 1500 },
  { description: 'Design graphique', unit: 'h', unitPriceMinEur: 60, unitPriceMaxEur: 120 },
  { description: 'Livraison matières premières', unit: 'palette', unitPriceMinEur: 180, unitPriceMaxEur: 800 },
  { description: 'Prestation photographique', unit: 'jour', unitPriceMinEur: 500, unitPriceMaxEur: 1100 },
  { description: 'Travaux second œuvre', unit: 'jour', unitPriceMinEur: 380, unitPriceMaxEur: 850 },
  { description: 'Pain de campagne 1kg', unit: 'u', unitPriceMinEur: 4, unitPriceMaxEur: 8 },
  { description: 'Quiche lorraine 6 parts', unit: 'u', unitPriceMinEur: 12, unitPriceMaxEur: 18 },
  { description: 'Composition florale événement', unit: 'u', unitPriceMinEur: 40, unitPriceMaxEur: 110 },
  { description: 'Réparation moteur', unit: 'forfait', unitPriceMinEur: 220, unitPriceMaxEur: 1500 },
  { description: 'Impression brochures', unit: '1000ex', unitPriceMinEur: 90, unitPriceMaxEur: 280 },
  { description: 'Couvert (déjeuner d\'affaires)', unit: 'u', unitPriceMinEur: 28, unitPriceMaxEur: 65 },
]

// ---------------------------------------------------------------------------
// Vendeur (info "fixe" du PDF — l'org du user en émetteur de facture)
// ---------------------------------------------------------------------------

/**
 * Construit l'info vendeur exposée dans le PDF à partir de l'org + user.
 * Quelques placeholders raisonnables (adresse, IBAN, téléphone) sont
 * fournis car ces champs n'existent pas encore au niveau Org.
 *
 * Quand on aura `address`, `iban`, `phone` dans la table organizations,
 * on lira directement depuis là.
 */
async function buildSellerInfoForOrg(
  organizationId: string,
  trx?: TransactionClientContract
): Promise<PdfInvoiceInput['seller']> {
  const org = await Organization.findOrFail(organizationId, trx ? { client: trx } : undefined)
  const user = await User.query(trx ? { client: trx } : undefined)
    .where('organization_id', organizationId)
    .first()

  // Simulé pour la démo. La V2 ajoutera ces champs sur Organization.
  return {
    name: org.name || 'Mon entreprise',
    address: '12 rue de la République, 75001 Paris',
    siret: org.siret || '83245678900015',
    tvaNumber: org.siret ? `FR${(org.siret).slice(0, 11)}` : 'FR83245678900',
    email: user?.email || 'contact@rubis-demo.fr',
    phone: '+33 1 84 60 12 34',
    iban: 'FR76 3000 4012 3456 7890 1234 567',
  }
}

function clientInfoForPdf(client: Client): PdfInvoiceInput['client'] {
  return {
    name: client.name,
    contactFirstName: client.contactFirstName,
    contactLastName: client.contactLastName,
    address: client.address,
    siret: client.siret,
  }
}

// ---------------------------------------------------------------------------
// Générateur d'items — crée 1-3 items dont le total HT match la cible
// ---------------------------------------------------------------------------

/**
 * Génère 1 à 3 items dont la somme des `quantity * unitPriceHtCents`
 * approche le `targetHtCents` donné (rounding ±1 cent par item, négligeable).
 */
function makeItemsForTargetHt(targetHtCents: number): InvoiceItem[] {
  const itemCount = targetHtCents > 80_000 ? randomInt(2, 3) : randomInt(1, 2)
  const picked = sampleN(ITEM_POOL, itemCount)

  // Splits aléatoires (ratios qui somment à 1).
  const ratios = picked.map(() => 0.3 + Math.random())
  const ratioSum = ratios.reduce((a, b) => a + b, 0)
  const splits = ratios.map((r) => Math.round((targetHtCents * r) / ratioSum))

  return picked.map((tpl, i) => {
    const itemTotal = Math.max(1_00, splits[i]!)
    // Prix unitaire dans la fourchette du template (en centimes).
    const minUnit = tpl.unitPriceMinEur * 100
    const maxUnit = tpl.unitPriceMaxEur * 100
    const targetUnit = randomInt(minUnit, maxUnit)
    // Quantité ajustée pour atteindre itemTotal en restant entière >= 1.
    const quantity = Math.max(1, Math.round(itemTotal / targetUnit))
    // Re-calibrage du prix unitaire pour que qty*price = itemTotal pile.
    const unitPriceHtCents = Math.max(50, Math.round(itemTotal / quantity))
    return {
      description: tpl.description,
      unit: tpl.unit,
      quantity,
      unitPriceHtCents,
    }
  })
}

/** Total TTC en centimes pour une liste d'items (HT + 20% TVA). */
function computeTotalTtc(items: InvoiceItem[]): number {
  const ht = items.reduce((s, i) => s + i.quantity * i.unitPriceHtCents, 0)
  const tva = Math.round(ht * 0.2)
  return ht + tva
}

// ---------------------------------------------------------------------------
// Upload PDF — génère le buffer puis push sur Drive (MinIO en prod, FS dev)
// ---------------------------------------------------------------------------

async function generateAndUploadInvoicePdf(
  organizationId: string,
  pdfInput: PdfInvoiceInput
): Promise<string> {
  const buffer = await generateInvoicePdfBuffer(pdfInput)
  const storageKey = `invoice-pdfs/${organizationId}/${randomUUID()}.pdf`
  await drive.use().put(storageKey, buffer)
  return storageKey
}

// ---------------------------------------------------------------------------
// Cleanup MinIO — drop tous les PDFs d'une org (appelé au --reset)
// ---------------------------------------------------------------------------

/**
 * Supprime tous les `invoice-pdfs/{orgId}/...` du drive. Utilisé par le
 * command `seed:demo --reset` pour repartir d'une MinIO propre.
 *
 * Boucle sur `paginationToken` pour gérer les listes > page-size par défaut
 * (généralement 1000 — suffisant en une page pour nos 227 PDFs, mais on
 * gère propre au cas où on monte en volume).
 */
export async function wipeOrgInvoicePdfs(organizationId: string): Promise<number> {
  const disk = drive.use()
  const prefix = `invoice-pdfs/${organizationId}/`
  let count = 0
  let paginationToken: string | undefined

  do {
    const result = await disk.listAll(prefix, {
      recursive: true,
      paginationToken,
    })
    for (const item of result.objects) {
      if (!item.isFile) continue
      await disk.delete(item.key)
      count++
    }
    paginationToken = result.paginationToken
  } while (paginationToken)

  return count
}

// ---------------------------------------------------------------------------
// Clients
// ---------------------------------------------------------------------------

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']

// ---------------------------------------------------------------------------
// Activity events
// ---------------------------------------------------------------------------

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

  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 — orchestration globale du seed démo
// ---------------------------------------------------------------------------

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
}

/**
 * Recipe actionnable — 27 factures représentatives du quotidien :
 *
 *   - 6 pending (échéance future)
 *   - 3 awaiting_user_confirmation (échéance toute fraîche, check-in)
 *   - 11 in_relance (déjà échues, relances en cours)
 *   - 5 paid récentes (donnent du DSO et du delta d'encaissement)
 *   - 2 litigation (cas tendu, mise en demeure imminente)
 */
type ActionableSpec = {
  status: InvoiceStatus
  /** Décalage en jours de la dueDate par rapport à aujourd'hui. */
  dueOffsetDays: number
}

const ACTIONABLE_RECIPE: ActionableSpec[] = [
  // Pending — échéance future
  { status: 'pending', dueOffsetDays: 30 },
  { status: 'pending', dueOffsetDays: 30 },
  { status: 'pending', dueOffsetDays: 15 },
  { status: 'pending', dueOffsetDays: 15 },
  { status: 'pending', dueOffsetDays: 5 },
  { status: 'pending', dueOffsetDays: 5 },
  // Awaiting check-in — échue tout pile / tout récente
  { status: 'awaiting_user_confirmation', dueOffsetDays: 0 },
  { status: 'awaiting_user_confirmation', dueOffsetDays: -3 },
  { status: 'awaiting_user_confirmation', dueOffsetDays: -3 },
  // In relance — échue, relances en cours
  { status: 'in_relance', dueOffsetDays: -7 },
  { status: 'in_relance', dueOffsetDays: -7 },
  { status: 'in_relance', dueOffsetDays: -15 },
  { status: 'in_relance', dueOffsetDays: -15 },
  { status: 'in_relance', dueOffsetDays: -15 },
  { status: 'in_relance', dueOffsetDays: -30 },
  { status: 'in_relance', dueOffsetDays: -30 },
  { status: 'in_relance', dueOffsetDays: -45 },
  { status: 'in_relance', dueOffsetDays: -60 },
  { status: 'in_relance', dueOffsetDays: -60 },
  { status: 'in_relance', dueOffsetDays: -90 },
  // Paid récentes (réglées tardivement, dans les ~30 derniers jours)
  { status: 'paid', dueOffsetDays: -30 },
  { status: 'paid', dueOffsetDays: -45 },
  { status: 'paid', dueOffsetDays: -60 },
  { status: 'paid', dueOffsetDays: -90 },
  { status: 'paid', dueOffsetDays: -120 },
  // Litigation
  { status: 'litigation', dueOffsetDays: -120 },
  { status: 'litigation', dueOffsetDays: -180 },
]

/** 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

  // Charge les plans avec leurs steps (utile pour seeder les RelanceTask).
  const plansWithSteps = await Plan.query({ client: trx })
    .whereIn(
      'id',
      plans.map((p) => p.id)
    )
    .preload('steps', (q) => q.orderBy('order', 'asc'))

  // Crée les clients du pool.
  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 }))
  }

  // Vendeur — l'org du user. Construit une fois, réutilisé pour les 227 PDFs.
  const seller = await buildSellerInfoForOrg(organizationId, trx)

  // Phase 1 — Actionnables (27 factures avec PDF coherent)
  const actionable = await seedActionableInvoices({
    organizationId,
    clients,
    plans: plansWithSteps,
    trx,
    seller,
  })

  // Phase 2 — Historique (200 factures paid sur 12 mois pour les graphes)
  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,
    plans: plansWithSteps,
    trx,
    seller,
    targetRevenueCents: historicalTarget,
    invoiceCount: HISTORICAL_INVOICE_COUNT,
    monthsBack: 12,
    numeroOffset: actionable.invoices.length,
  })

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

// ---------------------------------------------------------------------------
// Phase 1 — actionnables (recipe 27 factures)
// ---------------------------------------------------------------------------

type ActionableSeedConfig = {
  organizationId: string
  clients: Client[]
  plans: Array<Plan & { steps?: Array<{ id: string; offsetDays: number; order: number }> }>
  trx: TransactionClientContract
  seller: PdfInvoiceInput['seller']
}

async function seedActionableInvoices(
  config: ActionableSeedConfig
): Promise<DemoSeedResult> {
  const { organizationId, clients, plans, trx, seller } = config
  const invoices: Invoice[] = []
  const today = DateTime.utc().startOf('day')
  const yearPrefix = today.year

  for (const [i, spec] of ACTIONABLE_RECIPE.entries()) {
    const client = clients[i % clients.length]!
    const plan = plans[i % plans.length] ?? null
    const numero = `F${yearPrefix}-${String(i + 1).padStart(4, '0')}`

    // Items + total TTC réel = somme exacte des items × 1.20.
    // Cible HT random entre 200€ et 6000€ (large fourchette, lisible).
    const targetHtCents = randomInt(20_000, 600_000)
    const items = makeItemsForTargetHt(targetHtCents)
    const amountTtcCents = computeTotalTtc(items)

    // Dates issues du recipe (terme 30 jours).
    const dueDate = today.plus({ days: spec.dueOffsetDays })
    const issueDate = dueDate.minus({ days: 30 })

    let paidAt: DateTime | null = null
    if (spec.status === 'paid') {
      // Réglée 5-30j après dueDate (mais pas dans le futur)
      const proposed = dueDate.plus({ days: randomInt(5, 30) })
      paidAt = proposed > today ? today.minus({ days: randomInt(1, 5) }) : proposed
    }

    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)
    }

    // Génère le PDF + upload sur MinIO
    const pdfStorageKey = await generateAndUploadInvoicePdf(organizationId, {
      seller,
      client: clientInfoForPdf(client),
      invoice: {
        numero,
        issueDate: issueDate.toJSDate(),
        dueDate: dueDate.toJSDate(),
        items,
      },
    })

    const invoice = await Invoice.create(
      {
        organizationId,
        clientId: client.id,
        planId: plan?.id ?? null,
        numero,
        amountTtcCents,
        issueDate,
        dueDate,
        paidAt,
        status: spec.status,
        pdfStorageKey,
        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 }
}

// ---------------------------------------------------------------------------
// Phase 2 — historique (200 paid sur 12 mois pour alimenter les graphes)
// ---------------------------------------------------------------------------

type HistoricalSeedConfig = {
  organizationId: string
  clients: Client[]
  plans: Array<Plan & { steps?: Array<{ id: string; offsetDays: number; order: number }> }>
  trx: TransactionClientContract
  seller: PdfInvoiceInput['seller']
  /** Cible CA cumulé visé (centimes). Soft : approximé via les items. */
  targetRevenueCents: number
  /** Nombre de factures à générer. */
  invoiceCount: number
  /** Profondeur en mois (issueDate étalée sur cette fenêtre). */
  monthsBack: number
  /** Décalage de numérotation pour ne pas écraser les actionnables (F2026-XXXX). */
  numeroOffset: number
}

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

  // ~5% de cancelled pour la variété.
  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 de "tailles cibles" log-uniformes, rescalées pour
  // approximer le CA total cible.
  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 targetAmounts = 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]!
    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 dueDate = issueDate.plus({ days: 30 })

    // Items en partant d'une cible TTC ; on dérive ensuite la cible HT.
    const targetTtcCents = isCancelled
      ? randomInt(30_000, 200_000)
      : targetAmounts[paidIdx++]!
    const targetHtCents = Math.round(targetTtcCents / 1.2)
    const items = makeItemsForTargetHt(targetHtCents)
    const amountTtcCents = computeTotalTtc(items)

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

    if (isCancelled) {
      status = 'cancelled'
    } else {
      status = 'paid'
      const proposed = dueDate.plus({ days: randomInt(-3, 25) })
      paidAt = proposed > today ? today.minus({ days: randomInt(0, 5) }) : proposed
      rubisEarned = Math.random() < 0.7 ? randomInt(0, 2) : 0
    }

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

    const pdfStorageKey = await generateAndUploadInvoicePdf(organizationId, {
      seller,
      client: clientInfoForPdf(client),
      invoice: {
        numero,
        issueDate: issueDate.toJSDate(),
        dueDate: dueDate.toJSDate(),
        items,
      },
    })

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

    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 }
}

// ---------------------------------------------------------------------------
// RelanceTasks — câblage des tâches de relance pour une invoice (no-enqueue)
// ---------------------------------------------------------------------------

/**
 * 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

  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 }
    )
  }
}

// ---------------------------------------------------------------------------
// 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)]!
}

/** Fisher-Yates partiel — sample N éléments distincts d'un array. */
function sampleN<T>(arr: readonly T[], n: number): T[] {
  const copy = arr.slice()
  const out: T[] = []
  const k = Math.min(n, copy.length)
  for (let i = 0; i < k; i++) {
    const j = randomInt(i, copy.length - 1)
    ;[copy[i], copy[j]] = [copy[j]!, copy[i]!]
    out.push(copy[i]!)
  }
  return out
}

export { randomUUID }
