from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import ListView, CreateView, UpdateView, DetailView, View, DeleteView
from django.db.models import Sum, Q, F
from django.utils import timezone
from decimal import Decimal
from .models import Client, Dossier, Proforma, Facture, FactureM2, FactureM3, Paiement, LigneDossier, LigneProforma, FraisStandard, TypeService, Service, Societe, Proforma_model2, LigneProforma_model2, Proforma_model3, LigneProforma_model3
from .forms import DossierForm, PaiementForm, ClientForm, ProformaForm, LigneProformaFormSet, ProformaM2Form, LigneProformaM2FormSet, ProformaM3Form, LigneProformaM3FormSet
from .services import BillingService
from django.db import models, transaction
from django.http import HttpResponse, JsonResponse
from num2words import num2words
from dgi.edef_client import EdefClient, EdefClientError
import time
import json
import logging

logger = logging.getLogger(__name__)

@login_required
@transaction.atomic
def annulation_facture_dgi(request):
    facture_id = request.POST.get("facture_id")
    motif = request.POST.get("motif", "")

    if not facture_id:
        return JsonResponse({"error": "Facture manquante"}, status=400)

    try:
        facture_fv = Facture.objects.select_related(
            "dossier__client"
        ).get(id=facture_id)
    except Facture.DoesNotExist:
        return JsonResponse({"error": "Facture introuvable"}, status=404)
    
    if facture_fv.is_deleted:
        return JsonResponse({"error": "Facture déjà supprimée"}, status=400)

    # 🔒 Vérification DGI
    if not facture_fv.codedefdgi:
         # Si pas DGI, suppression logique simple
        facture_fv.is_deleted = True
        facture_fv.motif_annulation = motif
        facture_fv.save()
        return JsonResponse({
            "success": True, 
            "message": "Facture supprimée logiquement (Non DGI)"
        })

    societe = Societe.objects.first()
    client_api = EdefClient()

    # =====================================================
    # 1️⃣ CONSTRUCTION DES ITEMS (IDENTIQUES À LA FV)
    # =====================================================
    items = []
    total_amount = 0.0

    for ligne in facture_fv.dossier.lignes.all():
        if not ligne.service:
            continue

        tax_group = ligne.service.taxgroup.code if ligne.service.taxgroup else "A"
        item_type = ligne.service.typeart.code if ligne.service.typeart else "SER"

        items.append({
            "code": ligne.service.code,
            "name": ligne.service.designation,
            "type": item_type,
            "quantity": 1,
            "price": float(ligne.montant),
            "taxGroup": tax_group
        })

        total_amount += float(ligne.montant)

    # =====================================================
    # 2️⃣ CLIENT DGI (IDENTIQUE À LA FV, VIA DOSSIER)
    # =====================================================
    # On récupère le client via le dossier car facture_fv.client peut être None
    client_obj = facture_fv.dossier.client
    
    client_dgi = {
        "name": client_obj.name or client_obj.nom or "CLIENT DIVERS",
        "type": "PM",
        "address": getattr(client_obj, "address", None) or client_obj.adresse,
        "contact": getattr(client_obj, "contact", None) or client_obj.telephone
    }
    if client_obj.nif:
        client_dgi["nif"] = client_obj.nif

    # =====================================================
    # 3️⃣ PAYLOAD FA (FACTURE D’AVOIR)
    # =====================================================
    payload = {
        "nif": societe.nif if societe else "A1116228C",
        "rn": f"FA-{facture_fv.id}-{int(time.time())}",
        "mode": societe.mode_dgi or "ttc" if societe else "ttc",
        "isf": societe.isf_dgi if societe else "NON",
        "type": "FA", # Facture d'Avoir

        # 🔐 LIEN AVEC LA FV
        "reference": facture_fv.codedefdgi,
        "referenceType": "RAM", # Référence à Annuler Manuellement
        "referenceDesc": motif or "Annulation de facture",

        "items": items,
        "client": client_dgi,

        "payment": [{
            "name": "ESPECES",
            "amount": total_amount
        }],

        "curCode": "USD",
        "curRate": 1,
        "curDate": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
        "operator": {
            "id": str(request.user.id if request.user.is_authenticated else "1"),
            "name": request.user.username if request.user.is_authenticated else "SYSTEM"
        }
    }

    try:
        # =====================================================
        # 4️⃣ CREATE FA
        # =====================================================
        resp_create = client_api.create_invoice(payload)
        uid = resp_create.get("uid")
        if not uid:
            raise Exception(f"UID FA manquant: {resp_create}")
        
        # Le total est déjà dans resp_create pour FA aussi
        invoice_total = resp_create.get("total", total_amount)
        invoice_vtotal = resp_create.get("vtotal", 0)

        # =====================================================
        # 5️⃣ CONFIRM FA
        # =====================================================
        # Note: Certaines versions de l'API n'exigent pas confirm pour FA, mais d'autres si.
        # On suppose qu'il faut confirmer comme pour FV via get_invoice ou create response
        resp_confirm = client_api.confirm_invoice(uid, {
            "total": invoice_total,
            "vtotal": invoice_vtotal
        })

        # =====================================================
        # 6️⃣ CRÉATION FACTURE FA LOCALE
        # =====================================================
        # Une FA n'a pas nécessairement besoin d'une Proforma, 
        # mais elle doit être liée au dossier pour l'audit.
        # Ici on ne crée pas de Proforma pour la FA pour simplifier.
        
        facture_fa = Facture.objects.create(
            # On réutilise la proforma de la FV car OneToOne, C'EST PROBLÉMATIQUE.
            # Solution: Créer une FA sans proforma (null=True sur le field requis ? Non).
            # Alternative: Créer une proforma dummy pour l'annulation.
            proforma = Proforma.objects.create(
                dossier=facture_fv.dossier, 
                statut='VALIDEE', 
                numero=f"PROF-FA-{int(time.time())}",
                created_by=request.user
            ),
            dossier=facture_fv.dossier,
            facture_originale=facture_fv,
            date_emission=timezone.now(),
            created_by=request.user,
            uid=uid,
            codedefdgi=resp_confirm.get("codeDEFDGI") or resp_confirm.get("mtn"),
            qrCode=resp_confirm.get("qrCode"),
            nim=resp_confirm.get("nim"),
            counters=resp_confirm.get("counters"),
            dateTimedgi=resp_confirm.get("dateTime"),
            est_annulee=False, # C'est la facture d'annulation, elle n'est pas annulée elle-même
            motif_annulation=motif,
            type_facture="FA"
        )

        # =====================================================
        # 7️⃣ MARQUER LA FV COMME ANNULÉE
        # =====================================================
        facture_fv.est_annulee = True
        facture_fv.is_deleted = True # Suppression logique demandée
        facture_fv.motif_annulation = motif
        facture_fv.save()

        return JsonResponse({
            "success": True,
            "facture_fa_id": facture_fa.id,
            "codeDEFDGI_FA": facture_fa.codedefdgi,
            "qrCode": facture_fa.qrCode
        })

    except Exception as e:
        logger.error(f"[ANNULATION DGI] {str(e)}")
        # En cas d'erreur DGI, on ne supprime PAS logiquement la FV pour permettre de réessayer
        return JsonResponse({"error": str(e)}, status=500)

# PDF Generation imports
from django.template.loader import get_template
from xhtml2pdf import pisa
from django.http import HttpResponse
import os
from django.conf import settings
from django.contrib.staticfiles import finders

def link_callback(uri, rel):
    """
    Convert HTML URIs to absolute system paths so xhtml2pdf can access those
    resources
    """
    sUrl = settings.STATIC_URL
    sRoot = settings.STATIC_ROOT
    mUrl = settings.MEDIA_URL
    mRoot = settings.MEDIA_ROOT

    # Ensure URLs start with / for strict matching against URI
    if not sUrl.startswith('/'):
        sUrl = '/' + sUrl
    
    if not mUrl.startswith('/'):
        mUrl = '/' + mUrl

    path = uri
    
    # 1. Handle Media URLs
    if uri.startswith(mUrl):
        path = os.path.join(mRoot, uri.replace(mUrl, "", 1))
        
    # 2. Handle Static URLs
    elif uri.startswith(sUrl):
        # Strip the STATIC_URL prefix to get the relative path
        relative_path = uri.replace(sUrl, "", 1)
        
        # Method A: Try Django Finders (Best for Dev environment)
        # finders.find expects a relative path (e.g. "assets/img/logo.png")
        # It handles searching in APP directories and STATICFILES_DIRS
        found_path = finders.find(relative_path)
        
        if found_path:
            path = found_path
        else:
            # Method B: Fallback to STATIC_ROOT (Best for Prod if collected)
            path = os.path.join(sRoot, relative_path)
            
    # 3. Last Resort: Check manual STATICFILES_DIRS if still not found
    # (Useful if finders failed for some reason or structure mismatch)
    if not os.path.isfile(path):
        if settings.STATICFILES_DIRS and uri.startswith(sUrl):
             for static_dir in settings.STATICFILES_DIRS:
                 relative_path = uri.replace(sUrl, "", 1)
                 potential_path = os.path.join(static_dir, relative_path)
                 if os.path.isfile(potential_path):
                     return potential_path

    # Return the absolute path (or original URI if not resolved, letting xhtml2pdf skip it)
    return path

# QR Code Generation
import qrcode
import base64
from io import BytesIO

# --- Dashboard ---
class DashboardView(LoginRequiredMixin, View):
    def get(self, request):
        recent_proformas = Proforma.objects.all().order_by('-date_emission')[:5]
        recent_factures = Facture.objects.all().order_by('-date_emission')[:5]
        
        # Simple stats
        proforma_count = Proforma.objects.count()
        facture_count = Facture.objects.count()
        client_count = Client.objects.count()
        
        # 1. Chiffre d'affaires annuel (Par mois)
        from django.db.models.functions import TruncMonth
        from django.db.models import Sum

        current_year = timezone.now().year
        
        # On suppose que le montant de la facture est la somme des lignes de son dossier
        # C'est un peu complexe à agréger directement en SQL simple sans champs dénormalisés, 
        # mais on peut faire une approximation avec les paiements pour le Cash Flow.
        
        # Approche Cash Flow (Paiements reçus)
        mensuel_paiements = Paiement.objects.filter(
            date_paiement__year=current_year, 
            devise='USD'
        ).annotate(month=TruncMonth('date_paiement')).values('month').annotate(total=Sum('montant')).order_by('month')
        
        # Format for charts
        months_labels = []
        revenue_data = []
        
        # Pre-fill specific structure if needed, or just loop
        # Mapping simple for Chart.js/Apex
        
        data_map = {}
        for entry in mensuel_paiements:
            m = entry['month'].strftime('%b')
            data_map[m] = float(entry['total'])
            
        all_months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
        revenue_data = [data_map.get(m, 0) for m in all_months]

        # 2. Répartition des dossiers par statut
        dossier_stats = Dossier.objects.values('statut').annotate(count=models.Count('id'))
        status_labels = [d['statut'] for d in dossier_stats]
        status_data = [d['count'] for d in dossier_stats]

        context = {
            'recent_proformas': recent_proformas,
            'recent_factures': recent_factures,
            'proforma_count': proforma_count,
            'facture_count': facture_count,
            'client_count': client_count,
            'revenue_labels': all_months,
            'revenue_data': revenue_data,
            'status_labels': status_labels,
            'status_data': status_data,
        }
        return render(request, 'billing/dashboard.html', context)

@login_required
def update_taux_global(request):
    if request.method == 'POST':
        nouveau_taux = request.POST.get('taux')
        if nouveau_taux:
            societe = Societe.objects.filter(est_actif=True).first()
            if societe:
                societe.taux = nouveau_taux
                societe.save()
                messages.success(request, "Le taux de change a été mis à jour avec succès.")
    return redirect(request.META.get('HTTP_REFERER', 'billing:dashboard'))

# --- Clients ---
class ClientListView(LoginRequiredMixin, ListView):
    model = Client
    template_name = 'billing/client_list.html'
    context_object_name = 'clients'

class ClientCreateView(LoginRequiredMixin, CreateView):
    model = Client
    form_class = ClientForm
    template_name = 'billing/client_form.html'
    success_url = reverse_lazy('billing:client_list')
    
    def form_valid(self, form):
        messages.success(self.request, "Client créé avec succès")
        return super().form_valid(form)

class ClientUpdateView(LoginRequiredMixin, UpdateView):
    model = Client
    form_class = ClientForm
    template_name = 'billing/client_form.html'
    success_url = reverse_lazy('billing:client_list')

class ClientDeleteView(LoginRequiredMixin, DeleteView):
    model = Client
    template_name = 'billing/client_confirm_delete.html'
    success_url = reverse_lazy('billing:client_list')

# --- Dossiers ---
class DossierListView(LoginRequiredMixin, ListView):
    model = Dossier
    template_name = 'billing/dossier_list.html'
    context_object_name = 'dossiers'
    ordering = ['-created_at']

class DossierCreateView(LoginRequiredMixin, CreateView):
    model = Dossier
    form_class = DossierForm
    template_name = 'billing/dossier_form.html'
    success_url = reverse_lazy('billing:dossier_list')

    def form_valid(self, form):
        messages.success(self.request, "Dossier ouvert avec succès")
        return super().form_valid(form)

class DossierUpdateView(LoginRequiredMixin, UpdateView):
    model = Dossier
    form_class = DossierForm
    template_name = 'billing/dossier_form.html'
    success_url = reverse_lazy('billing:dossier_list')

    def form_valid(self, form):
        messages.success(self.request, "Dossier mis à jour avec succès")
        return super().form_valid(form)

class DossierDetailView(LoginRequiredMixin, DetailView):
    model = Dossier
    template_name = 'billing/dossier_detail.html'
    context_object_name = 'dossier'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # Add totals
        totals = BillingService.calculate_dossier_totals(self.object)
        context['totals'] = totals
        
        # Grid Data Preparation
        grouped_services = []
        type_services = TypeService.objects.filter(actif=True)
        # Fetch lines map: service_id -> line_obj
        existing_lines = { line.service_id: line for line in self.object.lignes.all() }

        for ts in type_services:
            services = Service.objects.filter(type_service=ts, actif=True)
            service_list = []
            for svc in services:
                line = existing_lines.get(svc.id)
                service_list.append({
                    'service': svc,
                    'montant': line.montant if line else 0,
                    'devise': line.devise if line else 'USD',
                    'taux': line.taux if line else 0,
                    'montant_conversion': line.montant_conversion if line else 0,
                })
            
            if service_list:
                grouped_services.append({
                    'type': ts,
                    'services': service_list
                })
        
        context['grouped_services'] = grouped_services

        # Infos Société
        societe = Societe.objects.filter(est_actif=True).first()
        context['societe'] = societe

        return context

    def post(self, request, *args, **kwargs):
        dossier = self.get_object()
        
        if 'save_services' in request.POST:
            # We iterate over all active services to find their submitted values
            services = Service.objects.filter(actif=True)
            count_updated = 0
            
            for svc in services:
                # Inputs expected: montant_{svc.id}, devise_{svc.id}
                try:
                    montant_str = request.POST.get(f'montant_{svc.id}', '0')
                    devise = request.POST.get(f'devise_{svc.id}', 'USD')
                    taux_str = request.POST.get(f'taux_{svc.id}', '0')
                    montant_conversion_str = request.POST.get(f'montant_conversion_{svc.id}', '0')
                    
                    montant = float(montant_str) if montant_str else 0.0
                    taux = float(taux_str) if taux_str else 0.0
                    montant_conversion = float(montant_conversion_str) if montant_conversion_str else 0.0
                    
                    if montant > 0:
                        # Update or create
                        LigneDossier.objects.update_or_create(
                            dossier=dossier,
                            service=svc,
                            defaults={
                                'montant': montant,
                                'devise': devise,
                                'taux': taux,
                                'montant_conversion': montant_conversion,
                            }
                        )
                        count_updated += 1
                    else:
                        # If montant is 0, rename delete existing line if any
                        LigneDossier.objects.filter(dossier=dossier, service=svc).delete()
                        
                except ValueError:
                    continue # Skip invalid numbers
            
            messages.success(request, f"{count_updated} services mis à jour.")
            return redirect('billing:dossier_detail', pk=dossier.pk)

        elif 'create_proforma' in request.POST:
            try:
                BillingService.create_proforma(dossier, user=request.user)
                messages.success(request, "Proforma générée avec succès")
            except Exception as e:
                messages.error(request, str(e))
            return redirect('billing:dossier_detail', pk=dossier.pk)
        
        elif 'delete_line' in request.POST:
            line_id = request.POST.get('line_id')
            LigneDossier.objects.filter(id=line_id, dossier=dossier).delete()
            messages.success(request, "Ligne supprimée")
            return redirect('billing:dossier_detail', pk=dossier.pk)
            
        return redirect('billing:dossier_detail', pk=dossier.pk)

# --- Proformas & Factures ---
class ProformaCreateView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_form.html'

    def get(self, request):
        initial_type = request.GET.get('type', 'FV')
        form = ProformaForm(initial_type=initial_type)
        formset = LigneProformaFormSet()
        societe = Societe.objects.filter(est_actif=True).first()
        return render(request, self.template_name, {'form': form, 'formset': formset, 'initial_type': initial_type, 'societe': societe})

    def post(self, request):
        form = ProformaForm(request.POST)
        formset = LigneProformaFormSet(request.POST)
        societe = Societe.objects.filter(est_actif=True).first()
        if form.is_valid() and formset.is_valid():
            proforma = form.save(commit=False)
            proforma.created_by = request.user
            proforma.save()
            formset.instance = proforma
            formset.save()
            messages.success(request, f"Proforma {proforma.numero} créé avec succès.")
            return redirect('billing:proforma_detail', pk=proforma.pk)
        return render(request, self.template_name, {'form': form, 'formset': formset, 'societe': societe})


class ProformaUpdateView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_form.html'

    def get(self, request, pk):
        proforma = get_object_or_404(Proforma, pk=pk)
        form = ProformaForm(instance=proforma)
        formset = LigneProformaFormSet(instance=proforma)
        societe = Societe.objects.filter(est_actif=True).first()
        return render(request, self.template_name, {'form': form, 'formset': formset, 'proforma': proforma, 'societe': societe})

    def post(self, request, pk):
        proforma = get_object_or_404(Proforma, pk=pk)
        if proforma.statut != 'BROUILLON':
            messages.error(request, "Impossible de modifier une proforma déjà validée.")
            return redirect('billing:proforma_detail', pk=proforma.pk)
        form = ProformaForm(request.POST, instance=proforma)
        formset = LigneProformaFormSet(request.POST, instance=proforma)
        societe = Societe.objects.filter(est_actif=True).first()
        if form.is_valid() and formset.is_valid():
            form.save()
            formset.save()
            messages.success(request, f"Proforma {proforma.numero} mis à jour.")
            return redirect('billing:proforma_detail', pk=proforma.pk)
        return render(request, self.template_name, {'form': form, 'formset': formset, 'proforma': proforma, 'societe': societe})


class ProformaListView(LoginRequiredMixin, ListView):
    model = Proforma
    template_name = 'billing/proforma_list.html'
    context_object_name = 'proformas'
    ordering = ['-date_emission']

class ProformaDetailView(LoginRequiredMixin, DetailView):
    model = Proforma
    template_name = 'billing/proforma_detail.html'
    context_object_name = 'proforma'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        proforma = self.get_object()
        societe = Societe.objects.filter(est_actif=True).first()
        context['societe'] = societe

        # Calcul des totaux : Total HT = somme de toutes les lignes, TVA = 16% du HT
        total_ht = Decimal('0')
        for ligne in proforma.lignes.all():
            total_ht += ligne.montant
        tva = total_ht * Decimal('0.16')
        total_ttc = total_ht + tva
        taux = Decimal(societe.taux) if societe and societe.taux else Decimal('2800')
        context['total_ht'] = total_ht
        context['tva'] = tva
        context['total_ttc'] = total_ttc
        context['montant_cdf'] = total_ttc * taux
        context['taux'] = taux
        return context

    def post(self, request, *args, **kwargs):
        proforma = self.get_object()
        
        # Cas 1: Upload pour Validation DGI
        if 'upload_validate' in request.POST and request.FILES.get('invoice_file'):
            try:
                uploaded_file = request.FILES['invoice_file']
                facture = BillingService.convert_proforma_to_invoice_from_upload(proforma, uploaded_file, user=request.user)
                messages.success(request, f"Facture {facture.numero} importée et validée avec succès via DGI !")
                return redirect('billing:facture_detail', pk=facture.pk)
            except Exception as e:
                messages.error(request, f"Erreur lors de l'import : {str(e)}")
                return redirect('billing:proforma_detail', pk=proforma.pk)

        # Cas 2: Validation standard (API DGI)
        if 'validate' in request.POST:
            try:
                facture = BillingService.validate_proforma(proforma, user=request.user)
                messages.success(request, f"Facture {facture.numero} validée et normalisée DGI !")
                return redirect('billing:facture_detail', pk=facture.pk)
            except Exception as e:
                messages.error(request, f"Erreur DGI — la facture n'a pas été créée. Détail : {str(e)}")
                
        return redirect('billing:proforma_detail', pk=proforma.pk)

@login_required
def proforma_pdf_view(request, pk):
    proforma = get_object_or_404(Proforma, pk=pk)
    template_path = 'billing/proforma_pdf.html'
    societe = Societe.objects.filter(est_actif=True).first()

    # Lignes : nouveau flux (LigneProforma) ou fallback ancien (LigneDossier)
    lignes_proforma = list(proforma.lignes.all())
    grouped_services = []
    total_general_usd = Decimal('0')

    if lignes_proforma:
        # Nouveau flux : une seule groupe "Services" sans type_service
        services_list = []
        subtotal_usd = Decimal('0')
        for ligne in lignes_proforma:
            services_list.append({
                'designation': ligne.designation,
                'description': ligne.description,
                'quantity': ligne.quantite,
                'unit_price': ligne.prix_unitaire,
                'tax_code': 'B',
                'montant': ligne.montant,
                'devise': ligne.devise,
            })
            # Toutes les lignes sont sommées dans le Total HT (prix_unitaire est toujours en USD)
            subtotal_usd += ligne.montant
            total_general_usd += ligne.montant
        grouped_services.append({
            'type': type('T', (), {'designation': 'Services', 'id': 0})(),
            'services': services_list,
            'subtotal_usd': subtotal_usd,
        })
    elif proforma.dossier:
        # Fallback ancien flux (LigneDossier)
        dossier = proforma.dossier
        type_services = TypeService.objects.filter(actif=True)
        existing_lines = {line.service_id: line for line in dossier.lignes.all()}
        for ts in type_services:
            services_list = []
            subtotal_usd = Decimal('0')
            for svc in Service.objects.filter(type_service=ts, actif=True):
                line = existing_lines.get(svc.id)
                if line and line.montant > 0:
                    tax_code = svc.taxgroup.code if svc.taxgroup else 'A'
                    services_list.append({
                        'designation': svc.designation,
                        'description': '',
                        'quantity': 1,
                        'unit_price': line.montant,
                        'tax_code': tax_code,
                        'montant': line.montant,
                        'devise': line.devise,
                    })
                    if line.devise == 'USD':
                        subtotal_usd += line.montant
                        total_general_usd += line.montant
            if services_list:
                grouped_services.append({'type': ts, 'services': services_list, 'subtotal_usd': subtotal_usd})

    try:
        solde_en_lettres = num2words(int(total_general_usd), lang='fr').upper() + " DOLLARS AMÉRICAINS"
    except Exception:
        solde_en_lettres = f"{total_general_usd} DOLLARS AMÉRICAINS"

    taux = Decimal(societe.taux) if societe and societe.taux else Decimal('2300')
    tva_usd = total_general_usd * Decimal('0.16')
    total_ttc_usd = total_general_usd + tva_usd
    montant_cdf = total_ttc_usd * taux

    try:
        solde_en_lettres = num2words(int(total_ttc_usd), lang='fr').upper() + " DOLLARS AMÉRICAINS"
    except Exception:
        solde_en_lettres = f"{total_ttc_usd} DOLLARS AMÉRICAINS"

    context = {
        'proforma': proforma,
        'dossier': proforma.dossier,
        'grouped_services': grouped_services,
        'total_general_usd': total_general_usd,
        'tva_usd': tva_usd,
        'total_ttc_usd': total_ttc_usd,
        'montant_cdf': montant_cdf,
        'taux': taux,
        'solde_en_lettres': solde_en_lettres,
        'societe': societe,
        'user': request.user,
    }

    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename="proforma_{proforma.numero}.pdf"'
    template = get_template(template_path)
    html = template.render(context)
    try:
        pisa_status = pisa.CreatePDF(html, dest=response, link_callback=link_callback)
        if pisa_status.err:
            return HttpResponse('We had some errors <pre>' + html + '</pre>')
    except Exception as e:
        import traceback
        return HttpResponse(f'Error generating PDF: {str(e)}<br><pre>{traceback.format_exc()}</pre>')
    return response

class FactureListView(LoginRequiredMixin, ListView):
    model = Facture
    template_name = 'billing/facture_list.html'
    context_object_name = 'factures'
    ordering = ['-date_emission']
    
    def get_queryset(self):
        qs = super().get_queryset()
        # Filter logic if requested via GET params
        filter_status = self.request.GET.get('status') # 'paid', 'unpaid'
        
        # Calculate payment status for each might be heavy in SQL, 
        # normally we would annotate.
        # Ideally, we should add 'statut_paiement' to Facture model or 'montant_paye' cache.
        # Here we will do simple Python filtering if dataset is small, or complex annotation.
        
        # Since we don't have stored total in Facture database, SQL filtering is hard.
        # We will return all and let template/datatable handle it, or filter in Python for 'unpaid' specific view.
        
        if filter_status == 'unpaid':
             # Rough logic: Exclude invoices where connected payments >= estimated total
             # This is hard to do purely in db without denormalized fields.
             pass
             
        return qs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        
        # Add list of definitively unpaid invoices for sidebar/alert
        # Calculating precisely requires iteration over all invoices.
        # If user has many invoices, this is slow. 
        # We'll limit to last 50 or so for the "Recent Unpaid" list if needed.
        
        # Cast to list to ensure attributes attached persist to template
        factures_list = list(context['factures'])
        
        for fac in factures_list:
            # Calcul des totaux depuis LigneProforma ou LigneDossier (fallback)
            if fac.proforma and fac.proforma.lignes.exists():
                tots = BillingService.calculate_proforma_totals(fac.proforma)
                total_lines_usd = tots['USD']['total_ttc']
            elif fac.dossier:
                lignes = fac.dossier.lignes.all()
                total_lines_usd = sum(l.montant for l in lignes if l.service and l.devise == 'USD')
            else:
                total_lines_usd = Decimal('0')
            
            # Let's use total_lines_usd as base (HT)
            # Add approximate tax? User logic was only on last group. 
            # Let's assume total_usd (from calculate_dossier_totals) is the base.
            
            # Recup paid
            # Ensure proper Decimal handling for sum (start=Decimal(0) to avoid int 0 if empty list, though sum() handles mixed usually, but empty defaults to 0 int)
            paid = sum((p.montant for p in fac.paiements.all() if p.devise == 'USD'), Decimal('0.00'))
            
            statut_paiement = 'Non Payé'
            # Use Decimal for comparison tolerance
            if paid >= total_lines_usd - Decimal('0.1'): 
                statut_paiement = 'Payé'
            elif paid > 0:
                statut_paiement = 'Partiel'
                
            fac.statut_paiement_calcule = statut_paiement
            fac.reste_a_payer = max(Decimal('0.00'), total_lines_usd - paid)
            fac.montant_total_calcule = total_lines_usd
            
        context['factures'] = factures_list
        return context

class FactureDetailView(LoginRequiredMixin, DetailView):
    model = Facture
    template_name = 'billing/facture_detail.html'
    context_object_name = 'facture'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # Calcul des totaux depuis LigneProforma (nouveau flux) avec fallback LigneDossier
        if self.object.proforma and self.object.proforma.lignes.exists():
            totals_dict = BillingService.calculate_proforma_totals(self.object.proforma)
        elif self.object.dossier:
            totals_dict = BillingService.calculate_dossier_totals(self.object.dossier)
        else:
            from decimal import Decimal
            totals_dict = {'USD': {'total': Decimal('0'), 'tva': Decimal('0'), 'total_ttc': Decimal('0')}}

        total_usd = totals_dict['USD']['total_ttc']
        total_ht_usd = totals_dict['USD']['total']
        total_tva_usd = totals_dict['USD']['tva']

        # Payments
        paiements = self.object.paiements.all().order_by('-date_paiement')
        total_paiements_usd = sum(p.montant for p in paiements if p.devise == 'USD')

        context['paiement_form'] = PaiementForm()
        context['paiements'] = paiements
        context['total_ht'] = total_ht_usd
        context['total_tva'] = total_tva_usd
        context['total_facture'] = total_usd
        context['total_paiements'] = total_paiements_usd
        context['reste_a_payer'] = total_usd - total_paiements_usd

        return context

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        if 'add_payment' in request.POST:
            form = PaiementForm(request.POST)
            if form.is_valid():
                paiement = form.save(commit=False)
                paiement.facture = self.object
                client_obj = (self.object.proforma.get_client() if self.object.proforma else None) or (self.object.dossier.client if self.object.dossier else None)
                paiement.client = client_obj
                paiement.created_by = request.user
                paiement.save()
                messages.success(request, "Paiement ajouté avec succès")
                return redirect('billing:facture_detail', pk=self.object.pk)
            else:
                messages.error(request, "Erreur dans le formulaire de paiement")
        return self.render_to_response(self.get_context_data())

@login_required
def facture_pdf_view(request, pk):
    facture = get_object_or_404(Facture, pk=pk)
    proforma = facture.proforma
    template_path = 'billing/facture_pdf.html'
    societe = Societe.objects.filter(est_actif=True).first()
    taux = societe.taux if societe else '1'

    grouped_services = []
    total_general_usd = Decimal('0')

    lignes_proforma = list(proforma.lignes.all()) if proforma else []

    if lignes_proforma:
        # Nouveau flux : lignes depuis LigneProforma
        from collections import defaultdict
        groups = defaultdict(list)
        group_subtotals = defaultdict(Decimal)
        group_types = {}

        for ligne in lignes_proforma:
            key = 0
            if 0 not in group_types:
                group_types[0] = type('T', (), {'designation': 'Services', 'id': 0})()

            groups[key].append({
                'designation': ligne.designation,
                'description': ligne.description,
                'quantity': ligne.quantite,
                'unit_price': ligne.prix_unitaire,
                'tax_code': 'B',
                'montant': ligne.montant,
                'devise': ligne.devise,
            })
            if ligne.devise == 'USD':
                group_subtotals[key] += ligne.montant
                total_general_usd += ligne.montant

        for key in groups:
            grouped_services.append({
                'type': group_types[key],
                'services': groups[key],
                'subtotal_usd': group_subtotals[key],
            })
    elif facture.dossier:
        # Fallback ancien flux (LigneDossier)
        dossier = facture.dossier
        type_services = TypeService.objects.filter(actif=True)
        existing_lines = {line.service_id: line for line in dossier.lignes.all()}
        for ts in type_services:
            services_list = []
            subtotal_usd = Decimal('0')
            for svc in Service.objects.filter(type_service=ts, actif=True):
                line = existing_lines.get(svc.id)
                if line and line.montant > 0:
                    tax_code = svc.taxgroup.code if svc.taxgroup else 'A'
                    services_list.append({
                        'designation': svc.designation,
                        'description': '',
                        'quantity': 1,
                        'unit_price': line.montant,
                        'tax_code': tax_code,
                        'montant': line.montant,
                        'devise': line.devise,
                    })
                    if line.devise == 'USD':
                        subtotal_usd += line.montant
                        total_general_usd += line.montant
            if services_list:
                grouped_services.append({'type': ts, 'services': services_list, 'subtotal_usd': subtotal_usd})

    # Totaux via service unifié
    if proforma and proforma.lignes.exists():
        totals_dict = BillingService.calculate_proforma_totals(proforma)
    elif facture.dossier:
        totals_dict = BillingService.calculate_dossier_totals(facture.dossier)
    else:
        totals_dict = {'USD': {'total': Decimal('0'), 'tva': Decimal('0'), 'total_ttc': Decimal('0')}}

    total_ht = totals_dict['USD']['total']
    montant_tva = totals_dict['USD']['tva']
    total_ttc = totals_dict['USD']['total_ttc']

    # Paiements
    paiements = facture.paiements.all()
    total_paiements_usd = sum(p.montant for p in paiements if p.devise == 'USD')
    solde_a_payer = total_ttc - total_paiements_usd

    # Montants contractuels du proforma (champs supprimés du modèle)
    montant_marche = Decimal('0')
    montant_demande = Decimal('0')
    montant_sollicite = Decimal('0')
    montant_deja_percu = total_paiements_usd

    # Conversion en lettres
    try:
        if solde_a_payer % 1 == 0:
            solde_en_lettres = num2words(int(solde_a_payer), lang='fr') + " dollars américains"
        else:
            solde_en_lettres = num2words(solde_a_payer, lang='fr', to='currency', currency='USD')
    except Exception:
        solde_en_lettres = num2words(int(solde_a_payer), lang='fr') + " dollars américains"

    # QR Code DGI
    qr_image_base64 = None
    if facture.qrCode:
        qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=2)
        qr.add_data(facture.qrCode)
        qr.make(fit=True)
        img = qr.make_image(fill_color="black", back_color="white")
        buffer = BytesIO()
        img.save(buffer, format="PNG")
        qr_image_base64 = base64.b64encode(buffer.getvalue()).decode()

    context = {
        'facture': facture,
        'dossier': facture.dossier,
        'proforma': proforma,
        'grouped_services': grouped_services,
        'total_general_usd': total_ht,
        'montant_tva': montant_tva,
        'total_ttc': total_ttc,
        'societe': societe,
        'taux': taux,
        'total_paiements_usd': total_paiements_usd,
        'solde_a_payer': solde_a_payer,
        'solde_en_lettres': solde_en_lettres,
        'montant_cdf': Decimal(str(solde_a_payer)) * Decimal(str(taux)) if taux else Decimal('0'),
        'qr_code_dgi': qr_image_base64,
        'paiements': paiements,
        # Montants contractuels
        'montant_marche': montant_marche,
        'montant_demande': montant_demande,
        'montant_sollicite': montant_sollicite,
        'montant_deja_percu': montant_deja_percu,
        'user': request.user,
    }

    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename="facture_{facture.numero}.pdf"'
    template = get_template(template_path)
    html = template.render(context)
    try:
        pisa_status = pisa.CreatePDF(html, dest=response, link_callback=link_callback)
        if pisa_status.err:
            return HttpResponse('We had some errors <pre>' + html + '</pre>')
    except Exception as e:
        import traceback
        return HttpResponse(f'Error generating PDF: {str(e)}<br><pre>{traceback.format_exc()}</pre>')
    return response


# --- Proforma Model 2 (tableau étapes) ---
class ProformaM2ListView(LoginRequiredMixin, ListView):
    model = Proforma_model2
    template_name = 'billing/proforma_m2_list.html'
    context_object_name = 'proformas'
    ordering = ['-date_emission']


class ProformaM2CreateView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_m2_form.html'

    def get(self, request):
        form = ProformaM2Form()
        formset = LigneProformaM2FormSet()
        return render(request, self.template_name, {'form': form, 'formset': formset})

    def post(self, request):
        form = ProformaM2Form(request.POST)
        formset = LigneProformaM2FormSet(request.POST)
        if form.is_valid() and formset.is_valid():
            proforma = form.save(commit=False)
            proforma.created_by = request.user
            proforma.save()
            formset.instance = proforma
            formset.save()
            messages.success(request, f"Proforma {proforma.numero} créé avec succès.")
            return redirect('billing:proforma_m2_detail', pk=proforma.pk)
        return render(request, self.template_name, {'form': form, 'formset': formset})


class ProformaM2UpdateView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_m2_form.html'

    def get(self, request, pk):
        proforma = get_object_or_404(Proforma_model2, pk=pk)
        form = ProformaM2Form(instance=proforma)
        formset = LigneProformaM2FormSet(instance=proforma)
        return render(request, self.template_name, {'form': form, 'formset': formset, 'proforma': proforma})

    def post(self, request, pk):
        proforma = get_object_or_404(Proforma_model2, pk=pk)
        if proforma.statut != 'BROUILLON':
            messages.error(request, "Impossible de modifier une proforma déjà validée.")
            return redirect('billing:proforma_m2_detail', pk=proforma.pk)
        form = ProformaM2Form(request.POST, instance=proforma)
        formset = LigneProformaM2FormSet(request.POST, instance=proforma)
        if form.is_valid() and formset.is_valid():
            form.save()
            formset.save()
            messages.success(request, f"Proforma {proforma.numero} mis à jour.")
            return redirect('billing:proforma_m2_detail', pk=proforma.pk)
        return render(request, self.template_name, {'form': form, 'formset': formset, 'proforma': proforma})


class ProformaM2DetailView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_m2_detail.html'

    def get(self, request, pk):
        proforma = get_object_or_404(Proforma_model2, pk=pk)
        return render(request, self.template_name, {'proforma': proforma})

    def post(self, request, pk):
        proforma = get_object_or_404(Proforma_model2, pk=pk)
        if 'upload_validate' in request.POST and request.FILES.get('invoice_file'):
            try:
                uploaded_file = request.FILES['invoice_file']
                facture = BillingService.convert_proforma_m2_to_facture_from_upload(proforma, uploaded_file, user=request.user)
                messages.success(request, f"Facture {facture.numero} importée et validée via DGI !")
                return redirect('billing:facture_m2_detail', pk=facture.pk)
            except Exception as e:
                messages.error(request, f"Erreur lors de l'import : {str(e)}")
        return redirect('billing:proforma_m2_detail', pk=proforma.pk)


@login_required
def proforma_m2_pdf_view(request, pk):
    proforma = get_object_or_404(Proforma_model2, pk=pk)
    societe = Societe.objects.filter(est_actif=True).first()
    taux = societe.taux if societe else '1'

    lignes = list(proforma.lignes.all())

    # Totaux par colonne
    total_marche = sum(l.montant_marche or 0 for l in lignes)
    total_demande = sum(l.montant_demande or 0 for l in lignes)

    TVA_RATE = Decimal('0.16')
    tva_marche = total_marche * TVA_RATE
    tva_demande = total_demande * TVA_RATE
    total_ttc_marche = total_marche + tva_marche
    total_ttc_demande = total_demande + tva_demande

    try:
        solde_en_lettres = num2words(int(total_ttc_demande), lang='fr') + " dollars américains"
    except Exception:
        solde_en_lettres = ""

    context = {
        'proforma': proforma,
        'lignes': lignes,
        'societe': societe,
        'taux': taux,
        'total_marche': total_marche,
        'total_demande': total_demande,
        'tva_marche': tva_marche,
        'tva_demande': tva_demande,
        'total_ttc_marche': total_ttc_marche,
        'total_ttc_demande': total_ttc_demande,
        'montant_cdf': Decimal(str(total_ttc_demande)) * Decimal(str(taux)),
        'solde_en_lettres': solde_en_lettres,
    }

    template = get_template('billing/proforma_m2_pdf.html')
    html = template.render(context)
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename="proforma_{proforma.numero}.pdf"'
    try:
        pisa_status = pisa.CreatePDF(html, dest=response, link_callback=link_callback)
        if pisa_status.err:
            return HttpResponse('Erreur PDF: <pre>' + html + '</pre>')
    except Exception as e:
        import traceback
        return HttpResponse(f'Erreur PDF: {str(e)}<pre>{traceback.format_exc()}</pre>')
    return response


# --- Proforma Model 3 (tableau situation) ---
class ProformaM3ListView(LoginRequiredMixin, ListView):
    model = Proforma_model3
    template_name = 'billing/proforma_m3_list.html'
    context_object_name = 'proformas'
    ordering = ['-date_emission']


class ProformaM3CreateView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_m3_form.html'

    def get(self, request):
        form = ProformaM3Form()
        formset = LigneProformaM3FormSet()
        return render(request, self.template_name, {'form': form, 'formset': formset})

    def post(self, request):
        form = ProformaM3Form(request.POST)
        formset = LigneProformaM3FormSet(request.POST)
        if form.is_valid() and formset.is_valid():
            proforma = form.save(commit=False)
            proforma.created_by = request.user
            proforma.save()
            formset.instance = proforma
            formset.save()
            messages.success(request, f"Proforma {proforma.numero} créé avec succès.")
            return redirect('billing:proforma_m3_detail', pk=proforma.pk)
        return render(request, self.template_name, {'form': form, 'formset': formset})


class ProformaM3UpdateView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_m3_form.html'

    def get(self, request, pk):
        proforma = get_object_or_404(Proforma_model3, pk=pk)
        form = ProformaM3Form(instance=proforma)
        formset = LigneProformaM3FormSet(instance=proforma)
        return render(request, self.template_name, {'form': form, 'formset': formset, 'proforma': proforma})

    def post(self, request, pk):
        proforma = get_object_or_404(Proforma_model3, pk=pk)
        if proforma.statut != 'BROUILLON':
            messages.error(request, "Impossible de modifier une proforma déjà validée.")
            return redirect('billing:proforma_m3_detail', pk=proforma.pk)
        form = ProformaM3Form(request.POST, instance=proforma)
        formset = LigneProformaM3FormSet(request.POST, instance=proforma)
        if form.is_valid() and formset.is_valid():
            form.save()
            formset.save()
            messages.success(request, f"Proforma {proforma.numero} mis à jour.")
            return redirect('billing:proforma_m3_detail', pk=proforma.pk)
        return render(request, self.template_name, {'form': form, 'formset': formset, 'proforma': proforma})


class ProformaM3DetailView(LoginRequiredMixin, View):
    template_name = 'billing/proforma_m3_detail.html'

    def get(self, request, pk):
        proforma = get_object_or_404(Proforma_model3, pk=pk)
        return render(request, self.template_name, {'proforma': proforma})

    def post(self, request, pk):
        proforma = get_object_or_404(Proforma_model3, pk=pk)
        if 'upload_validate' in request.POST and request.FILES.get('invoice_file'):
            try:
                uploaded_file = request.FILES['invoice_file']
                facture = BillingService.convert_proforma_m3_to_facture_from_upload(proforma, uploaded_file, user=request.user)
                messages.success(request, f"Facture {facture.numero} importée et validée via DGI !")
                return redirect('billing:facture_m3_detail', pk=facture.pk)
            except Exception as e:
                messages.error(request, f"Erreur lors de l'import : {str(e)}")
        return redirect('billing:proforma_m3_detail', pk=proforma.pk)


@login_required
def proforma_m3_pdf_view(request, pk):
    proforma = get_object_or_404(Proforma_model3, pk=pk)
    societe = Societe.objects.filter(est_actif=True).first()
    taux = societe.taux if societe else '1'

    lignes = list(proforma.lignes.all())

    total_marche = sum(l.montant_marche or 0 for l in lignes)
    total_percu = sum(l.montant_percu or 0 for l in lignes)
    total_restant = sum(l.montant_restant or 0 for l in lignes)
    total_sollicite = sum(l.montant_sollicite or 0 for l in lignes)

    TVA_RATE = Decimal('0.16')
    tva_sollicite = total_sollicite * TVA_RATE
    total_ttc_sollicite = total_sollicite + tva_sollicite

    try:
        solde_en_lettres = num2words(int(total_ttc_sollicite), lang='fr') + " dollars américains"
    except Exception:
        solde_en_lettres = ""

    context = {
        'proforma': proforma,
        'lignes': lignes,
        'societe': societe,
        'taux': taux,
        'total_marche': total_marche,
        'total_percu': total_percu,
        'total_restant': total_restant,
        'total_sollicite': total_sollicite,
        'tva_sollicite': tva_sollicite,
        'total_ttc_sollicite': total_ttc_sollicite,
        'montant_cdf': Decimal(str(total_ttc_sollicite)) * Decimal(str(taux)),
        'solde_en_lettres': solde_en_lettres,
    }

    template = get_template('billing/proforma_m3_pdf.html')
    html = template.render(context)
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename="proforma_{proforma.numero}.pdf"'
    try:
        pisa_status = pisa.CreatePDF(html, dest=response, link_callback=link_callback)
        if pisa_status.err:
            return HttpResponse('Erreur PDF: <pre>' + html + '</pre>')
    except Exception as e:
        import traceback
        return HttpResponse(f'Erreur PDF: {str(e)}<pre>{traceback.format_exc()}</pre>')
    return response


# --- Facture M2 ---
class FactureM2ListView(LoginRequiredMixin, ListView):
    model = FactureM2
    template_name = 'billing/facture_m2_list.html'
    context_object_name = 'factures'
    ordering = ['-date_emission']


class FactureM2DetailView(LoginRequiredMixin, DetailView):
    model = FactureM2
    template_name = 'billing/facture_m2_detail.html'
    context_object_name = 'facture'


@login_required
def facture_m2_pdf_view(request, pk):
    facture = get_object_or_404(FactureM2, pk=pk)
    proforma = facture.proforma
    societe = Societe.objects.filter(est_actif=True).first()
    taux = Decimal(str(societe.taux)) if societe and societe.taux else Decimal('2300')

    lignes = list(proforma.lignes.all())
    total_marche = sum(l.montant_marche or 0 for l in lignes)
    total_demande = sum(l.montant_demande or 0 for l in lignes)

    TVA_RATE = Decimal('0.16')
    tva_demande = Decimal(str(total_demande)) * TVA_RATE
    total_ttc_demande = Decimal(str(total_demande)) + tva_demande

    try:
        solde_en_lettres = num2words(int(total_ttc_demande), lang='fr') + " dollars américains"
    except Exception:
        solde_en_lettres = ""

    qr_image_base64 = None
    if facture.qrCode:
        qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=2)
        qr.add_data(facture.qrCode)
        qr.make(fit=True)
        img = qr.make_image(fill_color="black", back_color="white")
        buffer = BytesIO()
        img.save(buffer, format="PNG")
        qr_image_base64 = base64.b64encode(buffer.getvalue()).decode()

    context = {
        'facture': facture,
        'proforma': proforma,
        'lignes': lignes,
        'societe': societe,
        'taux': taux,
        'total_marche': total_marche,
        'total_demande': total_demande,
        'tva_demande': tva_demande,
        'total_ttc_demande': total_ttc_demande,
        'montant_cdf': total_ttc_demande * taux,
        'solde_en_lettres': solde_en_lettres,
        'qr_code_dgi': qr_image_base64,
    }

    template = get_template('billing/facture_m2_pdf.html')
    html = template.render(context)
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename="facture_{facture.numero}.pdf"'
    try:
        pisa_status = pisa.CreatePDF(html, dest=response, link_callback=link_callback)
        if pisa_status.err:
            return HttpResponse('Erreur PDF: <pre>' + html + '</pre>')
    except Exception as e:
        import traceback
        return HttpResponse(f'Erreur PDF: {str(e)}<pre>{traceback.format_exc()}</pre>')
    return response


# --- Facture M3 ---
class FactureM3ListView(LoginRequiredMixin, ListView):
    model = FactureM3
    template_name = 'billing/facture_m3_list.html'
    context_object_name = 'factures'
    ordering = ['-date_emission']


class FactureM3DetailView(LoginRequiredMixin, DetailView):
    model = FactureM3
    template_name = 'billing/facture_m3_detail.html'
    context_object_name = 'facture'


@login_required
def facture_m3_pdf_view(request, pk):
    facture = get_object_or_404(FactureM3, pk=pk)
    proforma = facture.proforma
    societe = Societe.objects.filter(est_actif=True).first()
    taux = Decimal(str(societe.taux)) if societe and societe.taux else Decimal('2300')

    lignes = list(proforma.lignes.all())
    total_marche = sum(l.montant_marche or 0 for l in lignes)
    total_percu = sum(l.montant_percu or 0 for l in lignes)
    total_restant = sum(l.montant_restant or 0 for l in lignes)
    total_sollicite = sum(l.montant_sollicite or 0 for l in lignes)

    TVA_RATE = Decimal('0.16')
    tva_sollicite = Decimal(str(total_sollicite)) * TVA_RATE
    total_ttc_sollicite = Decimal(str(total_sollicite)) + tva_sollicite

    try:
        solde_en_lettres = num2words(int(total_ttc_sollicite), lang='fr') + " dollars américains"
    except Exception:
        solde_en_lettres = ""

    qr_image_base64 = None
    if facture.qrCode:
        qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=2)
        qr.add_data(facture.qrCode)
        qr.make(fit=True)
        img = qr.make_image(fill_color="black", back_color="white")
        buffer = BytesIO()
        img.save(buffer, format="PNG")
        qr_image_base64 = base64.b64encode(buffer.getvalue()).decode()

    context = {
        'facture': facture,
        'proforma': proforma,
        'lignes': lignes,
        'societe': societe,
        'taux': taux,
        'total_marche': total_marche,
        'total_percu': total_percu,
        'total_restant': total_restant,
        'total_sollicite': total_sollicite,
        'tva_sollicite': tva_sollicite,
        'total_ttc_sollicite': total_ttc_sollicite,
        'montant_cdf': total_ttc_sollicite * taux,
        'solde_en_lettres': solde_en_lettres,
        'qr_code_dgi': qr_image_base64,
    }

    template = get_template('billing/facture_m3_pdf.html')
    html = template.render(context)
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename="facture_{facture.numero}.pdf"'
    try:
        pisa_status = pisa.CreatePDF(html, dest=response, link_callback=link_callback)
        if pisa_status.err:
            return HttpResponse('Erreur PDF: <pre>' + html + '</pre>')
    except Exception as e:
        import traceback
        return HttpResponse(f'Erreur PDF: {str(e)}<pre>{traceback.format_exc()}</pre>')
    return response


class ReportsView(LoginRequiredMixin, View):
    def get(self, request):
        start_date = request.GET.get('start_date')
        end_date = request.GET.get('end_date')
        client_id = request.GET.get('client')
        
        # Default dates if not provided (First day of month to today)
        if not start_date:
            now = timezone.now()
            start_date = now.replace(day=1).strftime('%Y-%m-%d')
        if not end_date:
            end_date = timezone.now().strftime('%Y-%m-%d')

        # Bornes datetime aware (fix MySQL CONVERT_TZ / __date__range bug)
        import datetime as dt_module
        dt_start = timezone.make_aware(dt_module.datetime.strptime(start_date, '%Y-%m-%d'))
        dt_end = timezone.make_aware(dt_module.datetime.strptime(end_date, '%Y-%m-%d').replace(hour=23, minute=59, second=59))

        # Base Filters
        paiements_qs = Paiement.objects.filter(date_paiement__range=[start_date, end_date])
        factures_qs = Facture.objects.filter(date_emission__range=[dt_start, dt_end])
        factures_m2_qs = FactureM2.objects.filter(date_emission__range=[dt_start, dt_end], is_deleted=False)
        factures_m3_qs = FactureM3.objects.filter(date_emission__range=[dt_start, dt_end], is_deleted=False)

        # Apply Client Filter
        if client_id:
            paiements_qs = paiements_qs.filter(client_id=client_id)
            factures_qs = factures_qs.filter(dossier__client_id=client_id)
            factures_m2_qs = factures_m2_qs.filter(proforma__client_id=client_id)
            factures_m3_qs = factures_m3_qs.filter(proforma__client_id=client_id)

        # Aggregates M1
        count_factures = factures_qs.count()
        count_factures_m2 = factures_m2_qs.count()
        count_factures_m3 = factures_m3_qs.count()
        count_factures_total = count_factures + count_factures_m2 + count_factures_m3

        # Total Payments (USD Only as per UI title)
        total_payments_usd = paiements_qs.filter(devise='USD').aggregate(total=Sum('montant'))['total'] or 0

        # M2 totals (HT = sum montant_demande, TTC = HT * 1.16)
        total_m2_ht = LigneProforma_model2.objects.filter(
            proforma__facture__in=factures_m2_qs
        ).aggregate(t=Sum('montant_demande'))['t'] or Decimal('0')
        total_m2_ttc = total_m2_ht * Decimal('1.16')

        # M3 totals (HT = sum montant_sollicite, TTC = HT * 1.16)
        total_m3_ht = LigneProforma_model3.objects.filter(
            proforma__facture__in=factures_m3_qs
        ).aggregate(t=Sum('montant_sollicite'))['t'] or Decimal('0')
        total_m3_ttc = total_m3_ht * Decimal('1.16')

        # M2 list with computed amounts
        factures_m2_list = []
        for fac in factures_m2_qs.select_related('proforma__client'):
            ht = fac.proforma.lignes.aggregate(t=Sum('montant_demande'))['t'] or Decimal('0')
            fac.montant_total_estime = ht * Decimal('1.16')
            factures_m2_list.append(fac)

        # M3 list with computed amounts
        factures_m3_list = []
        for fac in factures_m3_qs.select_related('proforma__client'):
            ht = fac.proforma.lignes.aggregate(t=Sum('montant_sollicite'))['t'] or Decimal('0')
            fac.montant_total_estime = ht * Decimal('1.16')
            factures_m3_list.append(fac)

        # M1: Dettes (Facturé - Payé)
        total_dettes = 0
        impayes_list = []

        for fac in factures_qs:
            if not fac.dossier:
                continue
            tots = BillingService.calculate_dossier_totals(fac.dossier)
            montant_facture = tots['USD']['total_ttc']
            paid_on_invoice = fac.paiements.filter(devise='USD').aggregate(t=Sum('montant'))['t'] or 0
            dette_facture = montant_facture - paid_on_invoice

            fac.montant_total_estime = montant_facture
            fac.montant_paye_estime = paid_on_invoice
            fac.reste_a_payer_estime = dette_facture

            if dette_facture > 0.01:
                total_dettes += dette_facture
                impayes_list.append(fac)

        context = {
            'start_date': start_date,
            'end_date': end_date,
            'clients': Client.objects.all().order_by('nom'),
            'selected_client': client_id,
            # Compteurs par type
            'count_factures': count_factures,
            'count_factures_m2': count_factures_m2,
            'count_factures_m3': count_factures_m3,
            'count_factures_total': count_factures_total,
            # Totaux
            'total_paiements': total_payments_usd,
            'total_dettes': total_dettes,
            'total_m2_ttc': total_m2_ttc,
            'total_m3_ttc': total_m3_ttc,
            # Listes
            'factures_impayees': impayes_list,
            'factures_m2': factures_m2_list,
            'factures_m3': factures_m3_list,
            'paiements': paiements_qs.select_related('client', 'facture').order_by('-date_paiement'),
        }
        return render(request, 'billing/reports.html', context)

# ==========================================
# GESTION DES RAPPORTS DGI (A, X, Z)
# ==========================================

from django.db.models import Count, Avg
from django.db.models.functions import TruncDate
import datetime as dt_module

def make_date_range(start_date_str, end_date_str):
    """
    Retourne un tuple (start_dt, end_dt) timezone-aware pour filtrer sur DateTimeField.
    Contourne le bug MySQL CONVERT_TZ avec USE_TZ=True et __date__range.
    """
    start = timezone.make_aware(dt_module.datetime.strptime(start_date_str, '%Y-%m-%d'))
    end = timezone.make_aware(dt_module.datetime.strptime(end_date_str, '%Y-%m-%d').replace(hour=23, minute=59, second=59))
    return start, end


@login_required
def a_rapport_periode_view(request):
    date_debut, date_fin = get_rapport_context(request)
    lignes = []
    total_ht = 0
    total_qte_vendue = 0

    if date_debut and date_fin:
        dt_start, dt_end = make_date_range(date_debut, date_fin)

        factures_valid = Facture.objects.filter(
            date_emission__range=[dt_start, dt_end],
            est_annulee=False,
            is_deleted=False
        )

        factures_annulees = Facture.objects.filter(
            date_emission__range=[dt_start, dt_end],
            est_annulee=True
        )

        # Nouveau flux : lignes depuis LigneProforma
        raw_lignes = LigneProforma.objects.filter(
            proforma__facture__in=factures_valid
        ).values('designation').annotate(
            qte_vendue=Sum('quantite'),
            total_vente=Sum('montant'),
        ).order_by('designation')

        lignes = [
            {
                'code': '-',
                'designation': l['designation'],
                'tax': '16',
                'qte_vendue': l['qte_vendue'],
                'qte_retournee': 0,
                'total_vente': l['total_vente'],
            }
            for l in raw_lignes
        ]

        # Ancien flux (fallback) : lignes depuis LigneDossier
        raw_dossier = LigneDossier.objects.filter(
            dossier__factures__in=factures_valid,
            service__isnull=False
        ).values(
            'service__code', 'service__designation', 'service__taxgroup__tax',
        ).annotate(
            qte_vendue=Count('id'),
            total_vente=Sum('montant'),
        ).order_by('service__designation')

        lignes_retour = LigneDossier.objects.filter(
            dossier__factures__in=factures_annulees,
            service__isnull=False
        ).values('service__code').annotate(qte_retour=Count('id'))
        map_retours = {l['service__code']: l['qte_retour'] for l in lignes_retour}

        for l in raw_dossier:
            lignes.append({
                'code': l['service__code'] or '-',
                'designation': l['service__designation'],
                'tax': l['service__taxgroup__tax'] or '0',
                'qte_vendue': l['qte_vendue'],
                'qte_retournee': map_retours.get(l['service__code'], 0),
                'total_vente': l['total_vente'],
            })

        total_ht = sum(l['total_vente'] or 0 for l in lignes)
        total_qte_vendue = sum(int(l['qte_vendue'] or 0) for l in lignes)

    return render(request, "billing/rapport/a_rapport_periode.html", {
        "lignes": lignes,
        "total_ht": total_ht,
        "total_qte_vendue": total_qte_vendue,
        "date_debut": date_debut,
        "date_fin": date_fin,
        "societe": Societe.objects.first(),
    })


def get_rapport_context(request):
    """Helper pour récupérer les dates"""
    date_debut = request.GET.get("date_debut")
    date_fin = request.GET.get("date_fin")
    
    if not date_debut:
        date_debut = timezone.now().date().strftime('%Y-%m-%d')
    if not date_fin:
        date_fin = timezone.now().date().strftime('%Y-%m-%d')
        
    return date_debut, date_fin


def get_xz_data_logic(date_debut, date_fin):
    if not (date_debut and date_fin):
        return None

    societe = Societe.objects.first()
    dt_start, dt_end = make_date_range(date_debut, date_fin)

    # Factures Valides M1
    factures = Facture.objects.filter(
        date_emission__range=[dt_start, dt_end],
        est_annulee=False,
        is_deleted=False
    )
    
    # Totaux globaux (Somme des lignes des dossiers facturés)
    # Attention: Si un dossier a plusieurs factures (rare), cela pourrait doubler.
    # Ici on suppose 1 Facture = 1 Dossier facturé.
    total_sales = LigneDossier.objects.filter(
        dossier__factures__in=factures
    ).aggregate(total=Sum('montant'))['total'] or 0
    
    # Analyse Taxes
    lines = LigneDossier.objects.filter(
        dossier__factures__in=factures,
        service__isnull=False
    ).values(
        'service__taxgroup__code',
        'service__taxgroup__tax'
    ).annotate(
        total_ttc_group=Sum('montant')
    )
    
    tax_details = []
    total_ht_global = 0
    total_tax_global = 0
    
    for l in lines:
        code = l['service__taxgroup__code'] or 'A'
        rate = float(l['service__taxgroup__tax'] or 0)
        ttc = float(l['total_ttc_group'] or 0)
        
        # Calcul inverse: HT = TTC / (1 + taux/100)
        ht = ttc / (1 + rate/100)
        tax = ttc - ht
        
        tax_details.append({
            'code': code,
            'rate': rate,
            'ht': ht,
            'tax': tax,
            'ttc': ttc
        })
        
        total_ht_global += ht
        total_tax_global += tax

    # Modes de Paiement (basé sur le modèle Paiement)
    payments = Paiement.objects.filter(
        facture__in=factures
    ).values('devise').annotate(
        total=Sum('montant')
    )
    
    # Factures Annulées M1
    cancelled_factures = Facture.objects.filter(
        date_emission__range=[dt_start, dt_end],
        est_annulee=True
    )
    total_cancelled = LigneDossier.objects.filter(
        dossier__factures__in=cancelled_factures
    ).aggregate(t=Sum('montant'))['t'] or 0

    # Factures M2 (Étapes)
    factures_m2 = FactureM2.objects.filter(
        date_emission__range=[dt_start, dt_end],
        est_annulee=False, is_deleted=False
    )
    total_m2_ht = LigneProforma_model2.objects.filter(
        proforma__facture__in=factures_m2
    ).aggregate(t=Sum('montant_demande'))['t'] or 0
    total_m2_ttc = Decimal(str(total_m2_ht)) * Decimal('1.16')
    cancelled_m2 = FactureM2.objects.filter(
        date_emission__range=[dt_start, dt_end], est_annulee=True
    ).count()

    # Factures M3 (Situation)
    factures_m3 = FactureM3.objects.filter(
        date_emission__range=[dt_start, dt_end],
        est_annulee=False, is_deleted=False
    )
    total_m3_ht = LigneProforma_model3.objects.filter(
        proforma__facture__in=factures_m3
    ).aggregate(t=Sum('montant_sollicite'))['t'] or 0
    total_m3_ttc = Decimal(str(total_m3_ht)) * Decimal('1.16')
    cancelled_m3 = FactureM3.objects.filter(
        date_emission__range=[dt_start, dt_end], est_annulee=True
    ).count()

    return {
        'societe': societe,
        'date_debut': date_debut,
        'date_fin': date_fin,
        # M1
        'total_ttc': total_sales,
        'total_ht': total_ht_global,
        'total_tax': total_tax_global,
        'tax_details': tax_details,
        'payments': payments,
        'count_valid': factures.count(),
        'total_cancelled': total_cancelled,
        'count_cancelled': cancelled_factures.count(),
        # M2
        'count_valid_m2': factures_m2.count(),
        'total_m2_ht': total_m2_ht,
        'total_m2_ttc': total_m2_ttc,
        'count_cancelled_m2': cancelled_m2,
        # M3
        'count_valid_m3': factures_m3.count(),
        'total_m3_ht': total_m3_ht,
        'total_m3_ttc': total_m3_ttc,
        'count_cancelled_m3': cancelled_m3,
        # Totaux combinés
        'count_valid_total': factures.count() + factures_m2.count() + factures_m3.count(),
        'total_ttc_global': float(total_sales) + float(total_m2_ttc) + float(total_m3_ttc),
    }

@login_required
def x_rapport_view(request):
    date_debut, date_fin = get_rapport_context(request)
    data = get_xz_data_logic(date_debut, date_fin)
    return render(request, "billing/rapport/x_rapport.html", {
        "data": data,
        "date_debut": date_debut,
        "date_fin": date_fin,
    })

@login_required
def z_rapport_view(request):
    date_debut, date_fin = get_rapport_context(request)
    data = get_xz_data_logic(date_debut, date_fin)
    return render(request, "billing/rapport/z_rapport.html", {
        "data": data,
        "date_debut": date_debut,
        "date_fin": date_fin,
    })

# --- PDF Views (xhtml2pdf) ---

def generate_rapport_pdf(template_name, context, filename):
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename="{filename}"'
    template = get_template(template_name)
    html = template.render(context)
    pisa_status = pisa.CreatePDF(html, dest=response)
    if pisa_status.err:
        return HttpResponse('Erreur PDF', status=500)
    return response

@login_required
def a_rapport_periode_pdf(request):
    date_debut, date_fin = get_rapport_context(request)
    dt_start, dt_end = make_date_range(date_debut, date_fin)
    # ... Logique identique à a_rapport_periode_view ...
    factures_valid = Facture.objects.filter(date_emission__range=[dt_start, dt_end], est_annulee=False, is_deleted=False)
    factures_annulees = Facture.objects.filter(date_emission__range=[dt_start, dt_end], est_annulee=True)
    
    lignes = LigneDossier.objects.filter(dossier__factures__in=factures_valid, service__isnull=False).values(
            "service__code", "service__designation", "service__taxgroup__tax", "montant"
        ).annotate(qte_vendue=Count('id'), total_vente=Sum('montant')).order_by("service__designation")
    
    lignes_retour = LigneDossier.objects.filter(dossier__factures__in=factures_annulees, service__isnull=False
        ).values("service__code").annotate(qte_retour=Count('id'))
    map_retours = {l['service__code']: l['qte_retour'] for l in lignes_retour}
    for l in lignes: l['qte_retournee'] = map_retours.get(l['service__code'], 0)
    
    # Calculate total
    total_ht = sum(l['total_vente'] for l in lignes)

    context = {
        "lignes": lignes, "date_debut": date_debut, "date_fin": date_fin,
        "societe": Societe.objects.first(), "titre": "A-RAPPORT DGI",
        "total_ht": total_ht
    }
    return generate_rapport_pdf("billing/rapport/pdf/a_rapport_pdf.html", context, "A_RAPPORT.pdf")

@login_required
def x_rapport_pdf(request):
    date_debut, date_fin = get_rapport_context(request)
    data = get_xz_data_logic(date_debut, date_fin)
    data["titre"] = "X-RAPPORT"
    return generate_rapport_pdf("billing/rapport/pdf/xz_rapport_pdf.html", data, "X_RAPPORT.pdf")

@login_required
def z_rapport_pdf(request):
    date_debut, date_fin = get_rapport_context(request)
    data = get_xz_data_logic(date_debut, date_fin)
    data["titre"] = "Z-RAPPORT"
    return generate_rapport_pdf("billing/rapport/pdf/xz_rapport_pdf.html", data, "Z_RAPPORT.pdf")
