import Client from '#models/client'
import Invoice from '#models/invoice'
import ClientTransformer from '#transformers/client_transformer'
import InvoiceTransformer from '#transformers/invoice_transformer'
import { createClientValidator, updateClientValidator } from '#validators/client'
import { bulkComputeClientStats } from '#services/client_stats'
import { computeClientTimeseries, type RangeMonths } from '#services/dashboard'
import type { HttpContext } from '@adonisjs/core/http'
import { Exception } from '@adonisjs/core/exceptions'
import vine from '@vinejs/vine'

const timeseriesValidator = vine.create({
  range: vine.number().in([3, 6, 12]).optional(),
})

// Priorité d'affichage : ce qui est actionnable en haut.
const INVOICE_STATUS_PRIORITY: Record<string, number> = {
  awaiting_user_confirmation: 0,
  in_relance: 1,
  pending: 2,
  litigation: 3,
  paid: 4,
  cancelled: 5,
}

/**
 * Petite cohérence d'identification orgnisation : si l'utilisateur
 * n'en a pas, on est dans un état illégal V1 — on bloque ferme.
 */
function requireOrgId(auth: HttpContext['auth']): string {
  const user = auth.getUserOrFail()
  if (!user.organizationId) {
    throw new Exception('Aucune organisation rattachée', { status: 404, code: 'not_found' })
  }
  return user.organizationId
}

/**
 * Sérialisation directe (instanciation manuelle du transformer pour
 * éviter le wrapper Item — utile quand on doit fusionner des stats
 * computed par-dessus chaque client dans une liste).
 */
function serializeClient(c: Client) {
  return new ClientTransformer(c).toObject()
}

export default class ClientsController {
  /**
   * GET /clients?withStats=1&q=
   *
   * Sans `withStats`, retour à plat (utilisé par le combobox de saisie).
   * Avec `withStats`, chaque client est enrichi des compteurs de factures
   * et trié par actionnabilité (retards d'abord, puis activité récente).
   */
  async index({ auth, request, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const withStats = request.input('withStats') === '1'
    const q = (request.input('q') ?? '').toString().trim().toLowerCase()

    const query = Client.query().where('organization_id', organizationId)

    if (q.length > 0) {
      query.where((b) => {
        b.whereILike('name', `%${q}%`).orWhereILike('email', `%${q}%`)
      })
    }

    const clients = await query.exec()

    if (!withStats) {
      // Tri alphabétique par défaut pour le combobox.
      clients.sort((a, b) => a.name.localeCompare(b.name, 'fr'))
      return response.json({ data: clients.map(serializeClient) })
    }

    const statsMap = await bulkComputeClientStats(
      organizationId,
      clients.map((c) => c.id)
    )

    const enriched = clients.map((c) => ({
      ...serializeClient(c),
      ...statsMap.get(c.id)!,
    }))

    // Tri actionnable : retards d'abord, puis activité récente.
    enriched.sort((a, b) => {
      if (a.lateInvoiceCount !== b.lateInvoiceCount) {
        return b.lateInvoiceCount - a.lateInvoiceCount
      }
      const aLast = a.lastActivityAt ?? ''
      const bLast = b.lastActivityAt ?? ''
      return bLast.localeCompare(aLast)
    })

    return response.json({ data: enriched })
  }

  /**
   * GET /clients/:id — détail enrichi (stats + invoices à venir).
   */
  async show({ auth, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)

    const client = await Client.query()
      .where('organization_id', organizationId)
      .where('id', params.id)
      .first()

    if (!client) {
      throw new Exception('Client introuvable', { status: 404, code: 'not_found' })
    }

    const statsMap = await bulkComputeClientStats(organizationId, [client.id])
    const stats = statsMap.get(client.id)!

    // Factures du client — actionnables en premier, puis échéance asc.
    const invoices = await Invoice.query()
      .where('organization_id', organizationId)
      .where('client_id', client.id)
      .preload('client')
      .preload('plan')
      .exec()

    invoices.sort((a, b) => {
      const dp =
        (INVOICE_STATUS_PRIORITY[a.status] ?? 99) -
        (INVOICE_STATUS_PRIORITY[b.status] ?? 99)
      if (dp !== 0) return dp
      return a.dueDate.toMillis() - b.dueDate.toMillis()
    })

    return response.json({
      data: {
        ...serializeClient(client),
        ...stats,
        invoices: invoices.map((inv) => new InvoiceTransformer(inv).toObject()),
      },
    })
  }

  /**
   * POST /clients — création manuelle.
   * Détecte les doublons de nom (case-insensitive) et renvoie 409 avec
   * la fiche existante pour permettre au SPA de proposer "voir le client".
   */
  async store({ auth, request, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const payload = await request.validateUsing(createClientValidator)

    // Doublon → 409 (cf. clients.ts MSW pour le contrat exact).
    const existing = await Client.query()
      .where('organization_id', organizationId)
      .whereILike('name', payload.name)
      .first()

    if (existing) {
      return response.status(409).json({
        errors: [
          {
            code: 'duplicate_client',
            message: `Un client nommé « ${existing.name} » existe déjà.`,
            field: 'name',
          },
        ],
        existing: serializeClient(existing),
      })
    }

    const created = await Client.create({
      organizationId,
      name: payload.name,
      email: payload.email,
      contactFirstName: payload.contactFirstName ?? null,
      contactLastName: payload.contactLastName ?? null,
      phone: payload.phone ?? null,
      address: payload.address ?? null,
      siret: payload.siret ?? null,
      notes: payload.notes ?? null,
    })

    return response.status(201).json({ data: serializeClient(created) })
  }

  /**
   * GET /clients/:id/timeseries?range=6 — encaissé mensuel pour ce client.
   * Utilisé par le mini-chart sur la fiche client (santé du compte).
   */
  async timeseries({ auth, request, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)

    const exists = await Client.query()
      .where('organization_id', organizationId)
      .where('id', params.id)
      .first()
    if (!exists) {
      throw new Exception('Client introuvable', { status: 404, code: 'not_found' })
    }

    const { range } = await request.validateUsing(timeseriesValidator, {
      data: {
        range: request.input('range') ? Number(request.input('range')) : undefined,
      },
    })
    const data = await computeClientTimeseries(
      organizationId,
      params.id,
      (range ?? 6) as RangeMonths
    )
    return response.json({ data })
  }

  /**
   * PATCH /clients/:id — édition partielle.
   */
  async update({ auth, request, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const payload = await request.validateUsing(updateClientValidator)

    const client = await Client.query()
      .where('organization_id', organizationId)
      .where('id', params.id)
      .first()

    if (!client) {
      throw new Exception('Client introuvable', { status: 404, code: 'not_found' })
    }

    client.merge(payload)
    await client.save()

    return response.json({ data: serializeClient(client) })
  }
}
