import Invoice from '#models/invoice'
import Plan from '#models/plan'
import RelanceTask from '#models/relance_task'
import InvoiceTransformer from '#transformers/invoice_transformer'
import { createInvoiceValidator, listInvoicesValidator } from '#validators/invoice'
import type { HttpContext } from '@adonisjs/core/http'
import { Exception } from '@adonisjs/core/exceptions'
import db from '@adonisjs/lucid/services/db'
import { DateTime } from 'luxon'
import { resolveClient } from '#services/resolve_client'
import { recordActivity } from '#services/activity_recorder'
import { cancelFutureRelances } from '#services/relance_scheduler'
import { scheduleCheckinForInvoice, cancelCheckinForInvoice } from '#services/checkin_scheduler'
import { canCreateInvoices } from '#services/billing'
import logger from '@adonisjs/core/services/logger'
import * as clock from '#services/clock'
import drive from '@adonisjs/drive/services/main'

const PAGE_SIZE = 50

// Priorité d'affichage côté liste : ce qui est actionnable d'abord.
const STATUS_PRIORITY: Record<string, number> = {
  awaiting_user_confirmation: 0,
  in_relance: 1,
  pending: 2,
  litigation: 3,
  paid: 4,
  cancelled: 5,
}

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
}

function serializeInvoice(i: Invoice) {
  return new InvoiceTransformer(i).toObject()
}

/**
 * Construit la timeline d'une facture en composant les étapes du plan
 * avec l'état courant (V1 simplifié — les RelanceTask viendront plus tard).
 *
 *  - étapes dont sendDay <= aujourd'hui : 'past' (envoyées)
 *  - étape actuelle (la prochaine future) : 'current'
 *  - étapes futures : 'future'
 */
function buildTimeline(
  invoice: Invoice,
  relanceTasks: RelanceTask[] = [],
  // `now` injecté par le caller — orgs en mode démo lisent depuis virtualNow.
  now: DateTime = DateTime.utc()
): Array<{
  id: string
  state: 'past' | 'current' | 'future'
  when: string
  what: string
}> {
  const events: Array<{
    id: string
    state: 'past' | 'current' | 'future'
    when: string
    what: string
  }> = [
    {
      id: `${invoice.id}__issued`,
      state: 'past',
      when: `${formatShortDate(invoice.issueDate)} · facture émise`,
      what: 'Importée',
    },
  ]

  if (invoice.plan?.steps?.length && invoice.status !== 'paid' && invoice.status !== 'cancelled') {
    const dueMs = invoice.dueDate.toMillis()
    const nowMs = now.toMillis()
    const taskByStepId = new Map(relanceTasks.map((task) => [task.planStepId, task]))
    let currentSet = false

    for (const step of invoice.plan.steps.slice().sort((a, b) => a.order - b.order)) {
      const sendMs = dueMs + step.offsetDays * 24 * 60 * 60 * 1000
      const task = taskByStepId.get(step.id)
      const stepDate = task?.sentAt ?? task?.sendAt ?? DateTime.fromMillis(sendMs)
      const labelStep = `J${step.offsetDays >= 0 ? '+' : ''}${step.offsetDays} — Étape ${step.order + 1}`

      let state: 'past' | 'current' | 'future'
      if (task?.status === 'sent') state = 'past'
      else if (task?.status === 'scheduled' && task.sendAt.toMillis() < nowMs) state = 'current'
      else if (!task && invoice.status === 'pending' && !currentSet) {
        state = 'current'
        currentSet = true
      } else if (!currentSet) {
        state = 'current'
        currentSet = true
      } else state = 'future'

      const subject = step.subject.replace('{{numero}}', invoice.numero)
      // Wording uniforme et rassurant : aucune relance ne part sans que l'user
      // confirme l'impayé. On évite "programmé" tout court qui sonne comme
      // "ça va partir tout seul".
      const what = task
        ? task.status === 'sent'
          ? `Envoyée après votre confirmation · "${subject}"`
          : task.status === 'cancelled'
            ? `Annulée — facture encaissée · "${subject}"`
            : `Confirmation avant envoi · "${subject}"`
        : `Confirmation avant envoi · "${subject}"`

      events.push({
        id: `${invoice.id}__step_${step.order}`,
        state,
        when: `${formatShortDate(stepDate)} · ${labelStep}`,
        what,
      })
    }
  }

  if (invoice.status === 'paid' && invoice.paidAt) {
    events.push({
      id: `${invoice.id}__paid`,
      state: 'past',
      when: `${formatShortDate(invoice.paidAt)} · facture encaissée`,
      what: 'Marquée encaissée — relances stoppées',
    })
  }

  return events
}

function formatShortDate(d: DateTime): string {
  return d.toFormat('dd/LL/yyyy')
}

export default class InvoicesController {
  /**
   * GET /invoices?status=&q=&clientId=&page=
   */
  async index({ auth, request, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const filters = await request.validateUsing(listInvoicesValidator)

    const query = Invoice.query()
      .where('organization_id', organizationId)
      .preload('client')
      .preload('plan')

    if (filters.status && filters.status !== 'all') {
      query.where('status', filters.status)
    }
    if (filters.clientId) {
      query.where('client_id', filters.clientId)
    }
    if (filters.q) {
      const q = filters.q.toLowerCase()
      query.where((b) => {
        b.whereILike('numero', `%${q}%`).orWhereExists((sub) => {
          sub
            .from('clients')
            .whereColumn('clients.id', 'invoices.client_id')
            .whereILike('clients.name', `%${q}%`)
        })
      })
    }

    const invoices = await query.exec()

    // Tri : actionnable d'abord (status priority), puis échéance croissante.
    invoices.sort((a, b) => {
      const dp = (STATUS_PRIORITY[a.status] ?? 99) - (STATUS_PRIORITY[b.status] ?? 99)
      if (dp !== 0) return dp
      return a.dueDate.toMillis() - b.dueDate.toMillis()
    })

    // Pagination simple en V1 (cf. backend.md §6 — cursor-based plus tard).
    const page = filters.page ?? 1
    const total = invoices.length
    const sliced = invoices.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)

    return response.json({
      data: sliced.map(serializeInvoice),
      meta: { total, page },
    })
  }

  /**
   * GET /invoices/counts — compteurs par statut pour les chips dashboard.
   */
  async counts({ auth, response }: HttpContext) {
    const organizationId = requireOrgId(auth)

    const rows = await db
      .from('invoices')
      .where('organization_id', organizationId)
      .select('status')
      .count('* as count')
      .groupBy('status')

    const counts = {
      all: 0,
      pending: 0,
      in_relance: 0,
      awaiting_user_confirmation: 0,
      paid: 0,
      litigation: 0,
      cancelled: 0,
    }
    for (const r of rows) {
      const c = Number(r.count)
      counts.all += c
      const s = r.status as keyof typeof counts
      if (s in counts) counts[s] = c
    }

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

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

    const invoice = await Invoice.query()
      .where('organization_id', organizationId)
      .where('id', params.id)
      .preload('client')
      .preload('plan', (q) => q.preload('steps'))
      .first()

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

    const data = serializeInvoice(invoice)
    const relanceTasks = await RelanceTask.query()
      .where('invoice_id', invoice.id)
      .whereNot('status', 'cancelled')
    return response.json({
      data: {
        ...data,
        client: invoice.client && {
          id: invoice.client.id,
          name: invoice.client.name,
          email: invoice.client.email,
          phone: invoice.client.phone,
          address: invoice.client.address,
          siret: invoice.client.siret,
        },
        plan: invoice.plan && {
          id: invoice.plan.id,
          slug: invoice.plan.slug,
          name: invoice.plan.name,
          steps: (invoice.plan.steps ?? [])
            .slice()
            .sort((a, b) => a.order - b.order)
            .map((s) => ({
              id: s.id,
              order: s.order,
              offsetDays: s.offsetDays,
              tone: s.tone,
              subject: s.subject,
              body: s.body,
              requiresManualValidation: s.requiresManualValidation,
            })),
        },
        timeline: buildTimeline(invoice, relanceTasks, await clock.now(invoice.organizationId)),
      },
    })
  }

  /**
   * POST /invoices — saisie manuelle.
   */
  async store({ auth, request, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const fields = await request.validateUsing(createInvoiceValidator)

    // Plan limit Free : bloque la création si l'org a déjà 5 actives
    // après la période de grâce.
    const enforcement = await canCreateInvoices(organizationId, 1)
    if (!enforcement.allowed) {
      throw new Exception(
        `Limite atteinte : ${enforcement.limit} factures actives sur le plan Free. Passez Pro pour créer cette facture.`,
        { status: 402, code: 'plan_limit_reached' }
      )
    }

    const invoice = await db.transaction(async (trx) => {
      const result = await resolveClient(organizationId, fields, trx)
      if ('errorCode' in result) {
        throw new Exception(
          'Email du client requis — Rubis en a besoin pour envoyer les relances.',
          { status: 422, code: result.errorCode }
        )
      }
      const client = result.client

      // Vérification plan (s'il est fourni, doit appartenir à l'org).
      let planId: string | null = null
      if (fields.planId) {
        const plan = await Plan.query({ client: trx })
          .where('organization_id', organizationId)
          .where('id', fields.planId)
          .first()
        if (plan) planId = plan.id
      }

      return Invoice.create(
        {
          organizationId,
          clientId: client.id,
          planId,
          numero: fields.numero,
          amountTtcCents: fields.amountTtcCents,
          issueDate: DateTime.fromISO(fields.issueDate),
          dueDate: DateTime.fromISO(fields.dueDate),
          status: 'pending',
          rubisEarned: 1, // bonus saisie initiale (cf. CLAUDE.md → glossaire)
          pdfStorageKey: null,
          notes: null,
          paidAt: null,
        },
        { client: trx }
      )
    })

    await invoice.load('client')
    await invoice.load('plan')

    // Programme uniquement le check-in (envoyé à dueDate). Les relances
    // client ne partent qu'après confirmation "toujours en attente".
    try {
      await scheduleCheckinForInvoice(invoice)
    } catch (err) {
      logger.warn({ err, invoiceId: invoice.id }, 'failed to schedule checkin')
    }

    return response.status(201).json({ data: serializeInvoice(invoice) })
  }

  /**
   * GET /invoices/:id/pdf — stream le PDF/image originel de la facture.
   *
   * Source : `pdfStorageKey` propagé depuis le draft d'import lors de la
   * validation. 404 si la facture n'a pas de fichier (saisie manuelle).
   * Auth : Bearer (vérifié sur l'org). Le SPA fetch via api.fetchBlob
   * puis affiche dans un <iframe>/<img> via objectURL.
   */
  async pdf({ auth, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const invoice = await Invoice.query()
      .where('organization_id', organizationId)
      .where('id', params.id)
      .first()

    if (!invoice) {
      throw new Exception('Facture introuvable', { status: 404, code: 'not_found' })
    }
    if (!invoice.pdfStorageKey) {
      throw new Exception('Aucun PDF stocké pour cette facture', {
        status: 404,
        code: 'pdf_not_available',
      })
    }

    const ext = (invoice.pdfStorageKey.split('.').pop() ?? '').toLowerCase()
    const contentType =
      ext === 'pdf'
        ? 'application/pdf'
        : ext === 'png'
          ? 'image/png'
          : ext === 'jpg' || ext === 'jpeg'
            ? 'image/jpeg'
            : 'application/octet-stream'

    const buffer = Buffer.from(await drive.use().getArrayBuffer(invoice.pdfStorageKey))
    response.header('Content-Type', contentType)
    response.header('Cache-Control', 'private, max-age=300')
    response.header(
      'Content-Disposition',
      `inline; filename="${invoice.numero}.${ext || 'pdf'}"`
    )
    return response.send(buffer)
  }

  /**
   * POST /invoices/:id/mark-paid
   * Marque encaissée + bonus +1 rubis (à la fois sur invoice.rubisEarned
   * et sur organization.rubisCount). Annule toutes les relances futures.
   */
  async markPaid({ auth, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)

    const invoice = await Invoice.query()
      .where('organization_id', organizationId)
      .where('id', params.id)
      .preload('client')
      .preload('plan')
      .first()

    if (!invoice) {
      throw new Exception('Facture introuvable', { status: 404, code: 'not_found' })
    }
    if (invoice.status === 'paid') {
      // Idempotent : déjà payée, on renvoie l'état courant sans bumper.
      return response.json({ data: serializeInvoice(invoice) })
    }

    await db.transaction(async (trx) => {
      invoice.useTransaction(trx)
      invoice.status = 'paid'
      invoice.paidAt = await clock.now(invoice.organizationId)
      invoice.rubisEarned = invoice.rubisEarned + 1
      await invoice.save()

      // Bump du compteur agrégé sur l'organisation
      await trx.from('organizations').where('id', organizationId).increment('rubis_count', 1)

      // Journal d'activité (cf. dashboard activity feed).
      await recordActivity({
        organizationId,
        kind: 'invoice_paid',
        label: `Facture <b>${invoice.numero}</b> marquée encaissée`,
        meta: { invoiceId: invoice.id, clientId: invoice.clientId },
        trx,
      })

      // Annule toutes les relances + le check-in programmés pour cette
      // facture (idempotent, BullMQ.remove peut échouer silencieusement
      // si le job a déjà été consommé).
      await cancelFutureRelances(invoice.id, trx)
      await cancelCheckinForInvoice(invoice.id, trx)
    })

    return response.json({ data: serializeInvoice(invoice) })
  }
}
