import ImportBatch from '#models/import_batch'
import Invoice from '#models/invoice'
import Plan from '#models/plan'
import ImportBatchTransformer, { serializeDraft } from '#transformers/import_batch_transformer'
import InvoiceTransformer from '#transformers/invoice_transformer'
import { uploadValidator, validateDraftValidator } from '#validators/import_batch'
import { resolveClient } from '#services/resolve_client'
import {
  createImportBatch,
  createImportBatchFromFilenames,
  type ImportSource,
} from '#services/import_batch'
import { recordActivity } from '#services/activity_recorder'
import { scheduleCheckinForInvoice } from '#services/checkin_scheduler'
import { canCreateInvoices } from '#services/billing'
import logger from '@adonisjs/core/services/logger'
import drive from '@adonisjs/drive/services/main'
import { createReadStream } from 'node:fs'
import { randomUUID } from 'node:crypto'
import type { HttpContext } from '@adonisjs/core/http'
import { Exception } from '@adonisjs/core/exceptions'
import db from '@adonisjs/lucid/services/db'
import { DateTime } from 'luxon'

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 serializeBatch(b: ImportBatch) {
  return new ImportBatchTransformer(b).toObject()
}

async function loadBatchOrFail(organizationId: string, id: string): Promise<ImportBatch> {
  const batch = await ImportBatch.query()
    .where('organization_id', organizationId)
    .where('id', id)
    .preload('drafts', (q) => q.orderBy('created_at', 'asc'))
    .first()
  if (!batch) {
    throw new Exception('Batch introuvable', { status: 404, code: 'not_found' })
  }
  return batch
}

export default class ImportBatchesController {
  /**
   * POST /invoices/upload — démarre un batch OCR.
   *
   * Deux modes selon Content-Type :
   *  - **multipart/form-data** : champ `files[]` avec les vrais PDFs.
   *    Stockage MinIO + OCR (mock OU mistral selon OCR_PROVIDER).
   *  - **application/json** : `{ filenames: string[] }` (V1 démo).
   *    Aucun fichier stocké → ne marche QU'AVEC OCR_PROVIDER=mock.
   */
  async upload(ctx: HttpContext) {
    const { auth, request, response } = ctx
    const organizationId = requireOrgId(auth)

    const isMultipart = (request.header('content-type') ?? '').startsWith('multipart/')

    if (isMultipart) {
      const files = request.files('files', {
        size: '10mb',
        extnames: ['pdf', 'png', 'jpg', 'jpeg'],
      })
      if (files.length === 0) {
        return response.status(422).json({
          errors: [
            { code: 'validation_failed', field: 'files', message: 'Au moins un fichier requis' },
          ],
        })
      }

      // Upload vers Drive (MinIO) AVANT l'OCR — l'OCR Mistral télécharge
      // depuis Drive donc il faut que le fichier soit déjà posé.
      // Clé : import-drafts/<orgId>/<draftId>.<ext> — pas de batchId
      // dans la clé car le batch est créé après.
      const sources: ImportSource[] = []
      for (const f of files) {
        if (!f.isValid || !f.tmpPath || !f.extname) {
          return response.status(422).json({
            errors: [
              {
                code: 'validation_failed',
                field: 'files',
                message: f.errors?.[0]?.message ?? 'Fichier invalide',
              },
            ],
          })
        }
        const draftKey = randomUUID()
        const storageKey = `import-drafts/${organizationId}/${draftKey}.${f.extname}`
        await drive.use().putStream(storageKey, createReadStream(f.tmpPath))
        sources.push({
          filename: f.clientName ?? `${draftKey}.${f.extname}`,
          storageKey,
        })
      }

      const batch = await createImportBatch(organizationId, sources)
      return response.status(201).json({ data: serializeBatch(batch) })
    }

    // Mode JSON — compat V1 démo.
    const { filenames } = await request.validateUsing(uploadValidator)
    const batch = await createImportBatchFromFilenames(organizationId, filenames)
    return response.status(201).json({ data: serializeBatch(batch) })
  }

  /**
   * GET /invoices/import-batch/:id — état courant d'un batch.
   * Le SPA poll cet endpoint pendant la review (drafts pending → validated/skipped).
   */
  async show({ auth, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const batch = await loadBatchOrFail(organizationId, params.id)
    return response.json({ data: serializeBatch(batch) })
  }

  /**
   * GET /invoices/import-batch/:id/drafts/:draftId/pdf
   *
   * Stream le PDF stocké dans MinIO. Auth via Bearer (le SPA fetch via
   * api.ts puis crée un objectURL — on ne peut pas mettre Authorization
   * sur un <iframe src>). On vérifie l'ownership de l'org en chargeant
   * le batch (loadBatchOrFail) puis on cherche le draft dedans.
   */
  async draftPdf({ auth, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const batch = await loadBatchOrFail(organizationId, params.id)
    const draft = batch.drafts.find((d) => d.id === params.draftId)
    if (!draft) {
      throw new Exception('Draft introuvable', { status: 404, code: 'not_found' })
    }
    if (!draft.pdfStorageKey) {
      throw new Exception('Aucun PDF stocké pour ce draft', {
        status: 404,
        code: 'pdf_not_available',
      })
    }
    const ext = (draft.filename.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(draft.pdfStorageKey))
    response.header('Content-Type', contentType)
    response.header('Cache-Control', 'private, max-age=300')
    // Inline = visualisable dans un <iframe>/<embed> sans télécharger.
    response.header(
      'Content-Disposition',
      `inline; filename="${encodeURIComponent(draft.filename)}"`
    )
    return response.send(buffer)
  }

  /**
   * POST /invoices/import-batch/:id/drafts/:draftId/validate
   *
   * L'utilisateur valide un draft → on crée l'Invoice avec les champs
   * éventuellement édités. Même logique de résolution client que POST
   * /invoices (clientId → match nom → création + email requis).
   */
  async validateDraft({ auth, params, request, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const fields = await request.validateUsing(validateDraftValidator)

    const batch = await loadBatchOrFail(organizationId, params.id)
    const draft = batch.drafts.find((d) => d.id === params.draftId)
    if (!draft) {
      throw new Exception('Brouillon introuvable', { status: 404, code: 'not_found' })
    }
    if (draft.status !== 'pending') {
      throw new Exception('Brouillon déjà traité', {
        status: 409,
        code: 'draft_already_processed',
      })
    }

    // Enforce le plan : si Free post-grace + déjà 5 actives → 402.
    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 valider cette facture.`,
        { status: 402, code: 'plan_limit_reached' }
      )
    }

    const invoice = await db.transaction(async (trx) => {
      const result = await resolveClient(
        organizationId,
        {
          clientId: fields.clientId,
          clientName: fields.clientName,
          clientEmail: fields.clientEmail,
        },
        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

      // Plan : si 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
      }

      const created = await 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 import OCR (cf. CLAUDE.md → glossaire)
          pdfStorageKey: draft.pdfStorageKey,
          notes: null,
          paidAt: null,
        },
        { client: trx }
      )

      draft.useTransaction(trx)
      draft.status = 'validated'
      draft.edited = {
        clientId: client.id,
        clientName: client.name,
        clientEmail: client.email,
        numero: fields.numero,
        amountTtcCents: fields.amountTtcCents,
        issueDate: fields.issueDate,
        dueDate: fields.dueDate,
        planId,
      }
      draft.invoiceId = created.id
      await draft.save()

      await recordActivity({
        organizationId,
        kind: 'invoice_imported',
        label: `Facture <b>${created.numero}</b> importée et validée`,
        meta: { invoiceId: created.id, clientId: client.id },
        trx,
      })

      return created
    })

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

    try {
      await scheduleCheckinForInvoice(invoice)
    } catch (err) {
      logger.warn({ err, invoiceId: invoice.id }, 'failed to schedule checkin')
    }

    return response.status(201).json({ data: new InvoiceTransformer(invoice).toObject() })
  }

  /**
   * POST /invoices/import-batch/:id/drafts/:draftId/skip
   * Marque un brouillon comme skippé (l'utilisateur ne veut pas le valider).
   */
  async skipDraft({ auth, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const batch = await loadBatchOrFail(organizationId, params.id)
    const draft = batch.drafts.find((d) => d.id === params.draftId)
    if (!draft) {
      throw new Exception('Brouillon introuvable', { status: 404, code: 'not_found' })
    }
    if (draft.status === 'validated') {
      throw new Exception('Brouillon déjà validé', {
        status: 409,
        code: 'draft_already_processed',
      })
    }

    if (draft.status !== 'skipped') {
      draft.status = 'skipped'
      await draft.save()
    }

    return response.json({ data: serializeDraft(draft) })
  }

  /**
   * DELETE /invoices/import-batch/:id — annule le batch entier.
   * CASCADE supprime les drafts. Les invoices validées (si y'en a déjà)
   * restent intactes, le FK draft.invoice_id est SET NULL côté ImportDraft.
   */
  async destroy({ auth, params, response }: HttpContext) {
    const organizationId = requireOrgId(auth)
    const batch = await loadBatchOrFail(organizationId, params.id)
    await batch.delete()
    return response.status(204).send('')
  }
}
