import db from '@adonisjs/lucid/services/db'
import Organization from '#models/organization'
import { DateTime } from 'luxon'
import * as clock from '#services/clock'

const ACTIVE_INVOICE_STATUSES = "('pending','in_relance','awaiting_user_confirmation')"

export type DashboardKpis = {
  rubisCount: number
  rubisThisMonth: number
  // 1 rubis = 10 minutes libérées (cf. CLAUDE.md → glossaire)
  hoursLiberatedThisMonth: number
  encaisseCents: number
  encaisseDeltaCents: number
  dsoDays: number
  dsoDeltaDays: number
  factureToRelance: number
  factureInRelance: number
  factureNewToday: number
  miseEnDemeurePending: number
  monthlyGoalProgress: number
  // Rang relatif à la cohorte (placeholder V1, calculé en V2 avec assez de data)
  percentile?: number
}

function startOfMonth(d: DateTime): Date {
  return d.startOf('month').toJSDate()
}

function startOfDay(d: DateTime): Date {
  return d.startOf('day').toJSDate()
}

/**
 * Calcule les KPIs dashboard pour une organisation.
 *
 * V1 — implémentation simple sans cache. Quelques metrics avancés
 * (DSO, percentile) sont à 0 ou null tant qu'on a pas assez d'historique.
 * Le contrat reste stable côté SPA.
 */
export async function computeKpis(organizationId: string): Promise<DashboardKpis> {
  const now = await clock.now(organizationId)
  const monthStart = startOfMonth(now)
  const todayStart = startOfDay(now)
  const prevMonthStart = startOfMonth(now.minus({ months: 1 }))

  const org = await Organization.findOrFail(organizationId)

  // Counts par statut + factures récentes
  const counts = (await db
    .from('invoices')
    .where('organization_id', organizationId)
    .select(
      db.raw(`count(*) filter (where status = 'pending')::int as to_relance`),
      db.raw(`count(*) filter (where status = 'in_relance')::int as in_relance`),
      db.raw(`count(*) filter (where created_at >= ?)::int as new_today`, [todayStart])
    )
    .first()) as { to_relance: number; in_relance: number; new_today: number } | undefined

  // Sommes d'encaissement (paid_at) ce mois et le précédent
  const paidStats = (await db
    .from('invoices')
    .where('organization_id', organizationId)
    .where('status', 'paid')
    .select(
      db.raw(
        `coalesce(sum(amount_ttc_cents) filter (where paid_at >= ?), 0)::int as this_month`,
        [monthStart]
      ),
      db.raw(
        `coalesce(sum(amount_ttc_cents) filter (where paid_at >= ? and paid_at < ?), 0)::int as prev_month`,
        [prevMonthStart, monthStart]
      ),
      db.raw(
        `coalesce(sum(rubis_earned) filter (where paid_at >= ?), 0)::int as rubis_this_month`,
        [monthStart]
      ),
      db.raw(
        `coalesce(avg(extract(epoch from (paid_at - issue_date)) / 86400) filter (where paid_at >= ?), 0)::int as dso_this_month`,
        [monthStart]
      ),
      db.raw(
        `coalesce(avg(extract(epoch from (paid_at - issue_date)) / 86400) filter (where paid_at >= ? and paid_at < ?), 0)::int as dso_prev_month`,
        [prevMonthStart, monthStart]
      )
    )
    .first()) as
    | {
        this_month: number
        prev_month: number
        rubis_this_month: number
        dso_this_month: number
        dso_prev_month: number
      }
    | undefined

  const encaisseCents = paidStats?.this_month ?? 0
  const encaisseDeltaCents = encaisseCents - (paidStats?.prev_month ?? 0)
  const rubisThisMonth = paidStats?.rubis_this_month ?? 0
  const dsoDays = paidStats?.dso_this_month ?? 0
  const dsoDeltaDays = dsoDays - (paidStats?.dso_prev_month ?? 0)

  return {
    rubisCount: org.rubisCount,
    rubisThisMonth,
    hoursLiberatedThisMonth: rubisThisMonth * 10,
    encaisseCents,
    encaisseDeltaCents,
    dsoDays,
    dsoDeltaDays,
    factureToRelance: counts?.to_relance ?? 0,
    factureInRelance: counts?.in_relance ?? 0,
    factureNewToday: counts?.new_today ?? 0,
    // Mise en demeure pending — sera calculé quand RelanceTask est branché
    // (count des steps requires_manual_validation programmées). Pour V1 : 0.
    miseEnDemeurePending: 0,
    // Goal progress (V1 placeholder) : ratio rubis_count / 250 (objectif
    // mensuel arbitraire). À paramétrer plus tard.
    monthlyGoalProgress: Math.min(100, Math.round((rubisThisMonth / 25) * 100)),
    percentile: undefined,
  }
}

/**
 * Top des clients en retard (top 5 par défaut).
 * Compte les factures actives dont due_date est dépassée, agrégées par client.
 */
export async function topLatePayers(
  organizationId: string,
  limit = 5
): Promise<Array<{ clientId: string; name: string; lateInvoicesCount: number }>> {
  const today = startOfDay(await clock.now(organizationId))

  const rows = await db
    .from('invoices')
    .innerJoin('clients', 'clients.id', 'invoices.client_id')
    .where('invoices.organization_id', organizationId)
    .whereRaw(`invoices.status::text in ${ACTIVE_INVOICE_STATUSES}`)
    .where('invoices.due_date', '<', today)
    .groupBy('clients.id', 'clients.name')
    .select('clients.id as client_id', 'clients.name as name')
    .select(db.raw('count(*)::int as late_invoices_count'))
    .orderBy('late_invoices_count', 'desc')
    .limit(limit)

  return rows.map((r) => ({
    clientId: r.client_id,
    name: r.name,
    lateInvoicesCount: r.late_invoices_count,
  }))
}

// ===========================================================================
// Time series — pour les graphes du dashboard et de /insights
// ===========================================================================

export type RangeMonths = 3 | 6 | 12

/**
 * Granularité d'agrégation des séries temporelles.
 *  - `month` : 1 bucket = 1 mois (vue 6m / 12m, lecture macro)
 *  - `week`  : 1 bucket = 1 semaine ISO (lundi → dimanche), pour la vue
 *              3 mois où l'agrégation mensuelle masque trop la dynamique
 *              (3 points seulement, on perd la lecture).
 */
export type Granularity = 'month' | 'week'

export type PaidSeriesPoint = {
  /**
   * Premier jour du bucket en ISO date "YYYY-MM-DD".
   *  - `month`: premier du mois (toujours -01)
   *  - `week` : lundi de la semaine
   */
  bucket: string
  /** Total encaissé sur le bucket (centimes). */
  encaisseCents: number
  /** Nombre de factures payées sur le bucket. */
  paidCount: number
  /** DSO moyen sur le bucket (jours, 0 si aucun paiement). */
  dsoDays: number
}

export type PipelineSlice = {
  status: 'pending' | 'awaiting_user_confirmation' | 'in_relance' | 'litigation' | 'paid'
  count: number
  amountCents: number
}

export type DashboardTimeseries = {
  range: RangeMonths
  granularity: Granularity
  /** Garde le nom pour ne pas casser le contrat — mais les buckets peuvent
   *  être hebdo (cf. `granularity`). */
  paidByMonth: PaidSeriesPoint[]
  pipelineByStatus: PipelineSlice[]
}

/** Choix de granularité par défaut pour un range donné. */
function pickGranularity(range: RangeMonths): Granularity {
  return range === 3 ? 'week' : 'month'
}

/**
 * Calcule les séries temporelles pour le dashboard / insights.
 *
 * `paidByMonth` : N derniers mois (range), 1 ligne par mois même si vide
 * (sinon les charts affichent des "trous").
 *
 * `pipelineByStatus` : breakdown du portefeuille (count + montant) — pour
 * donut/stacked-bar. Cancelled exclus pour réduire le bruit.
 */
export async function computeTimeseries(
  organizationId: string,
  range: RangeMonths = 6
): Promise<DashboardTimeseries> {
  const granularity = pickGranularity(range)
  const paidByMonth = await fetchPaidSeries({ organizationId, range, granularity })

  const pipelineRows = (await db
    .from('invoices')
    .where('organization_id', organizationId)
    .select('status')
    .select(db.raw('count(*)::int as count'))
    .select(db.raw('coalesce(sum(amount_ttc_cents), 0)::int as amount_cents'))
    .groupBy('status')) as Array<{
    status: PipelineSlice['status'] | 'cancelled'
    count: number
    amount_cents: number
  }>

  const pipelineOrder: PipelineSlice['status'][] = [
    'pending',
    'awaiting_user_confirmation',
    'in_relance',
    'litigation',
    'paid',
  ]
  const pipelineMap = new Map(pipelineRows.map((r) => [r.status, r]))
  const pipelineByStatus: PipelineSlice[] = pipelineOrder.map((status) => {
    const r = pipelineMap.get(status)
    return { status, count: r?.count ?? 0, amountCents: r?.amount_cents ?? 0 }
  })

  return { range, granularity, paidByMonth, pipelineByStatus }
}

/** Variante par client — on filtre paidByMonth sur un client_id. */
export async function computeClientTimeseries(
  organizationId: string,
  clientId: string,
  range: RangeMonths = 6
): Promise<{ range: RangeMonths; granularity: Granularity; paidByMonth: PaidSeriesPoint[] }> {
  const granularity = pickGranularity(range)
  const paidByMonth = await fetchPaidSeries({
    organizationId,
    clientId,
    range,
    granularity,
  })
  return { range, granularity, paidByMonth }
}

/**
 * Helper interne — DRY entre computeTimeseries et computeClientTimeseries.
 *
 * Renvoie N buckets ordonnés (mensuels ou hebdomadaires selon `granularity`)
 * couvrant les derniers `range` mois. Chaque bucket porte encaisse/count/dso.
 *
 * - `month` : `range` buckets mensuels (1/mois), label = 1er du mois
 * - `week`  : ~`range * 4.5` buckets hebdo (1/semaine), label = lundi de la
 *             semaine. Postgres `date_trunc('week')` aligne sur lundi (ISO),
 *             Luxon `startOf('week')` aussi → cohérence garantie.
 */
async function fetchPaidSeries(params: {
  organizationId: string
  clientId?: string
  range: RangeMonths
  granularity: Granularity
}): Promise<PaidSeriesPoint[]> {
  const now = await clock.now(params.organizationId)

  // Garde-fou : `truncUnit` est interpolé dans du SQL brut, on whitelist.
  const truncUnit: 'month' | 'week' =
    params.granularity === 'week' ? 'week' : 'month'

  let firstBucket: DateTime
  let bucketCount: number
  let stepUnit: 'months' | 'weeks'

  if (truncUnit === 'week') {
    // ~range * 4.33 semaines couvrent la fenêtre, on prend ceil pour ne pas
    // tronquer le premier mois.
    bucketCount = Math.ceil(params.range * 4.34)
    firstBucket = now.startOf('week').minus({ weeks: bucketCount - 1 })
    stepUnit = 'weeks'
  } else {
    bucketCount = params.range
    firstBucket = now.minus({ months: bucketCount - 1 }).startOf('month')
    stepUnit = 'months'
  }

  const buckets = new Map<string, PaidSeriesPoint>()
  for (let i = 0; i < bucketCount; i++) {
    const inc = stepUnit === 'weeks' ? { weeks: i } : { months: i }
    const b = firstBucket.plus(inc).toFormat('yyyy-LL-dd')
    buckets.set(b, { bucket: b, encaisseCents: 0, paidCount: 0, dsoDays: 0 })
  }

  const query = db
    .from('invoices')
    .where('organization_id', params.organizationId)
    .where('status', 'paid')
    .where('paid_at', '>=', firstBucket.toJSDate())

  if (params.clientId) query.where('client_id', params.clientId)

  const rows = (await query
    .select(
      db.raw(`to_char(date_trunc('${truncUnit}', paid_at), 'YYYY-MM-DD') as bucket`),
      db.raw(`coalesce(sum(amount_ttc_cents), 0)::int as encaisse_cents`),
      db.raw(`count(*)::int as paid_count`),
      db.raw(
        `coalesce(avg(extract(epoch from (paid_at - issue_date)) / 86400)::int, 0) as dso_days`
      )
    )
    .groupByRaw(`date_trunc('${truncUnit}', paid_at)`)
    .orderByRaw(`date_trunc('${truncUnit}', paid_at)`)) as Array<{
    bucket: string
    encaisse_cents: number
    paid_count: number
    dso_days: number
  }>

  for (const r of rows) {
    if (!buckets.has(r.bucket)) continue
    buckets.set(r.bucket, {
      bucket: r.bucket,
      encaisseCents: r.encaisse_cents,
      paidCount: r.paid_count,
      dsoDays: r.dso_days,
    })
  }

  return Array.from(buckets.values())
}
