from decimal import Decimal
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, viewsets
from rest_framework.decorators import action
import pandas as pd
from django.db import transaction
from .models import Cuenta, Asiento, DetalleAsiento
RenglonAsiento = DetalleAsiento
from .serializers import CuentaSerializer, AsientoSerializer
from rest_framework.permissions import IsAuthenticated
from .permissions import ContabilidadPermission, SoloContadorOAdminPermission

class CuentaViewSet(viewsets.ModelViewSet):
    """
    ViewSet completo para realizar operaciones CRUD sobre el modelo Cuenta.
    Permite puntos (.) en el código del plan de cuentas.
    """
    queryset = Cuenta.objects.all().order_by('codigo')
    serializer_class = CuentaSerializer
    lookup_value_regex = '[^/]+'  # Permitir puntos (.) en el código de cuenta
    permission_classes = [IsAuthenticated, ContabilidadPermission]

    def perform_create(self, serializer):
        cuenta = serializer.save()
        from .models import AuditoriaLog
        AuditoriaLog.objects.create(usuario=self.request.user, accion=f"Creó Cuenta {cuenta.codigo}")

    def retrieve(self, request, *args, **kwargs):
        cuenta = self.get_object()
        tiene_movimientos = RenglonAsiento.objects.filter(cuenta_id=cuenta.id).exists()
        return Response({
            'id': cuenta.id,
            'codigo': cuenta.codigo,
            'nombre': cuenta.nombre,
            'activa': cuenta.activa,
            'tiene_movimientos': tiene_movimientos
        })

    def update(self, request, *args, **kwargs):
        cuenta = self.get_object()
        old_pk = cuenta.codigo
        tiene_movimientos = RenglonAsiento.objects.filter(cuenta_id=cuenta.id).exists()
        nuevo_codigo = request.data.get('codigo')
        
        if tiene_movimientos:
            if nuevo_codigo and nuevo_codigo != old_pk:
                return Response({
                    "error": "Esta cuenta posee movimientos contables. No puede ser editada ni eliminada. Para corregir, cree una nueva cuenta, realice un asiento de reclasificación e inactive esta cuenta."
                }, status=status.HTTP_400_BAD_REQUEST)
            
            data = request.data.copy()
            data['codigo'] = old_pk
            serializer = self.get_serializer(cuenta, data=data, partial=kwargs.get('partial', False))
            serializer.is_valid(raise_exception=True)
            self.perform_update(serializer)
            return Response(serializer.data)
        else:
            if nuevo_codigo and nuevo_codigo != old_pk:
                if Cuenta.objects.filter(codigo=nuevo_codigo).exists():
                    return Response({"error": f"La cuenta con el código {nuevo_codigo} ya existe."}, status=status.HTTP_400_BAD_REQUEST)
                Cuenta.objects.filter(codigo=old_pk).update(codigo=nuevo_codigo)
                cuenta = Cuenta.objects.get(codigo=nuevo_codigo)
            
            serializer = self.get_serializer(cuenta, data=request.data, partial=kwargs.get('partial', False))
            serializer.is_valid(raise_exception=True)
            self.perform_update(serializer)
            return Response(serializer.data)

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

    def perform_update(self, serializer):
        cuenta = serializer.save()
        from .models import AuditoriaLog
        AuditoriaLog.objects.create(usuario=self.request.user, accion=f"Modificó Cuenta {cuenta.codigo}")

    def destroy(self, request, *args, **kwargs):
        cuenta = self.get_object()
        tiene_movimientos = RenglonAsiento.objects.filter(cuenta_id=cuenta.id).exists()
        if tiene_movimientos:
            return Response({
                "error": "Esta cuenta posee movimientos contables. No puede ser editada ni eliminada. Para corregir, cree una nueva cuenta, realice un asiento de reclasificación e inactive esta cuenta."
            }, status=status.HTTP_400_BAD_REQUEST)
        return super().destroy(request, *args, **kwargs)

    def perform_destroy(self, instance):
        from .models import AuditoriaLog
        AuditoriaLog.objects.create(usuario=self.request.user, accion=f"Eliminó Cuenta {instance.codigo}")
        instance.delete()

    @action(detail=False, methods=['post'], url_path='eliminar-masivo')
    def eliminar_masivo(self, request):
        from rest_framework.decorators import action
        from .models import DetalleAsiento, AuditoriaLog
        ids = request.data.get('ids', [])
        if not ids:
            return Response({"error": "Debe suministrar una lista de ids"}, status=status.HTTP_400_BAD_REQUEST)
            
        exitos = 0
        rechazados = []
        
        for c_id in ids:
            try:
                cuenta = Cuenta.objects.get(codigo=c_id)
                if DetalleAsiento.objects.filter(cuenta=cuenta).exists():
                    rechazados.append({"codigo": c_id, "nombre": cuenta.nombre, "motivo": "Tiene movimientos contables asociados"})
                else:
                    nombre = cuenta.nombre
                    cuenta.delete()
                    exitos += 1
                    AuditoriaLog.objects.create(usuario=request.user, accion=f"Eliminó Cuenta {c_id} via borrado masivo")
            except Cuenta.DoesNotExist:
                rechazados.append({"codigo": c_id, "motivo": "La cuenta no existe"})
                
        return Response({
            "exitos": exitos,
            "rechazados": rechazados,
            "detail": f"Eliminadas con éxito: {exitos}. Rechazadas: {len(rechazados)}."
        }, status=status.HTTP_200_OK)

    @action(detail=False, methods=['post'], url_path='vaciar-catalogo')
    def vaciar_catalogo(self, request):
        from .models import DetalleAsiento, AuditoriaLog
        if hasattr(request.user, 'perfil') and request.user.perfil.rol != 'ADMINISTRADOR':
            return Response({"error": "No autorizado. Solo los administradores pueden vaciar el catálogo."}, status=403)
            
        todas_cuentas = Cuenta.objects.all()
        exitos = 0
        rechazados = 0
        
        for cuenta in todas_cuentas:
            if DetalleAsiento.objects.filter(cuenta=cuenta).exists():
                rechazados += 1
            else:
                try:
                    cuenta.delete()
                    exitos += 1
                except Exception:
                    rechazados += 1
                    
        if exitos > 0:
            AuditoriaLog.objects.create(
                usuario=request.user,
                accion=f"El Administrador vació el catálogo contable (eliminó {exitos} cuentas sin movimientos)"
            )
            
        return Response({
            "exitos": exitos,
            "rechazados": rechazados,
            "detail": f"Catálogo purgado. Cuentas eliminadas con éxito: {exitos}. Cuentas conservadas con movimientos: {rechazados}."
        }, status=status.HTTP_200_OK)

def check_periodo_cerrado(fecha):
    if not fecha:
        return
    from datetime import date, datetime
    if not isinstance(fecha, (date, datetime)):
        try:
            parts = str(fecha).split('-')
            fecha = date(int(parts[0]), int(parts[1]), int(parts[2]))
        except (ValueError, IndexError):
            return
            
    from .models import PeriodoContable
    closed_periods = PeriodoContable.objects.filter(estado='CERRADO')
    for p in closed_periods:
        if p.fecha_inicio and p.fecha_fin:
            if p.fecha_inicio <= fecha <= p.fecha_fin:
                from rest_framework.exceptions import PermissionDenied
                raise PermissionDenied("La fecha pertenece a un período contable cerrado y no admite modificaciones")
        else:
            if p.ano == fecha.year:
                from rest_framework.exceptions import PermissionDenied
                raise PermissionDenied("Este período contable está cerrado y no admite modificaciones")

class AsientoViewSet(viewsets.ModelViewSet):
    """
    ViewSet completo para operaciones CRUD sobre el modelo Asiento.
    """
    queryset = Asiento.objects.all().order_by('-id')
    serializer_class = AsientoSerializer
    permission_classes = [IsAuthenticated, ContabilidadPermission]

    def perform_create(self, serializer):
        fecha = serializer.validated_data.get('fecha')
        check_periodo_cerrado(fecha)
        asiento = serializer.save(creado_por=self.request.user)
        from .models import AuditoriaLog
        AuditoriaLog.objects.create(usuario=self.request.user, accion=f"Creó Asiento #{asiento.id}")

    def perform_update(self, serializer):
        if serializer.instance and serializer.instance.fecha:
            check_periodo_cerrado(serializer.instance.fecha)
        fecha = serializer.validated_data.get('fecha')
        if fecha:
            check_periodo_cerrado(fecha)
        asiento = serializer.save()
        from .models import AuditoriaLog
        AuditoriaLog.objects.create(usuario=self.request.user, accion=f"Modificó Asiento #{asiento.id}")

    def perform_destroy(self, instance):
        check_periodo_cerrado(instance.fecha)
        from .models import AuditoriaLog
        AuditoriaLog.objects.create(usuario=self.request.user, accion=f"Eliminó Asiento #{instance.id}")
        instance.delete()

    def destroy(self, request, *args, **kwargs):
        asiento = self.get_object()
        check_periodo_cerrado(asiento.fecha)
        
        from django.utils import timezone
        hoy = timezone.now().date()
        
        rol = request.user.perfil.rol if hasattr(request.user, 'perfil') else 'AUXILIAR'
        if rol == 'AUXILIAR' and asiento.fecha < hoy:
            return Response({
                "error": "Los auxiliares no pueden eliminar asientos históricos."
            }, status=status.HTTP_403_FORBIDDEN)
            
        return super().destroy(request, *args, **kwargs)

    def update(self, request, *args, **kwargs):
        asiento = self.get_object()
        check_periodo_cerrado(asiento.fecha)
        nueva_fecha = request.data.get('fecha')
        if nueva_fecha:
            check_periodo_cerrado(nueva_fecha)
            
        from django.utils import timezone
        import json
        hoy = timezone.now().date()
        
        rol = request.user.perfil.rol if hasattr(request.user, 'perfil') else 'AUXILIAR'
        if rol == 'AUXILIAR' and asiento.fecha < hoy:
            from .models import SolicitudCambioAsiento
            SolicitudCambioAsiento.objects.create(
                usuario=request.user,
                asiento=asiento,
                datos_nuevos=json.dumps(request.data),
                atendida=False,
                aprobada=False
            )
            return Response({
                "detail": "Los asientos antiguos no pueden modificarse directamente. Se ha enviado una solicitud de aprobación al Contador."
            }, status=status.HTTP_202_ACCEPTED)
            
        return super().update(request, *args, **kwargs)

    def partial_update(self, request, *args, **kwargs):
        asiento = self.get_object()
        check_periodo_cerrado(asiento.fecha)
        nueva_fecha = request.data.get('fecha')
        if nueva_fecha:
            check_periodo_cerrado(nueva_fecha)
            
        from django.utils import timezone
        import json
        hoy = timezone.now().date()
        
        rol = request.user.perfil.rol if hasattr(request.user, 'perfil') else 'AUXILIAR'
        if rol == 'AUXILIAR' and asiento.fecha < hoy:
            from .models import SolicitudCambioAsiento
            SolicitudCambioAsiento.objects.create(
                usuario=request.user,
                asiento=asiento,
                datos_nuevos=json.dumps(request.data),
                atendida=False,
                aprobada=False
            )
            return Response({
                "detail": "Los asientos antiguos no pueden modificarse directamente. Se ha enviado una solicitud de aprobación al Contador."
            }, status=status.HTTP_202_ACCEPTED)
            
        return super().partial_update(request, *args, **kwargs)


class ImportarCatalogoView(APIView):
    """
    Procesa un archivo Excel, limpia las cuentas y las inserta masivamente en MariaDB.
    """
    def post(self, request, *args, **kwargs):
        file_obj = request.FILES.get('file')
        if not file_obj:
            return Response(
                {"error": "No se proporcionó ningún archivo en la petición."},
                status=status.HTTP_400_BAD_REQUEST
            )
            
        try:
            # Leer el archivo con Pandas sin asumir cabecera inicial
            df = pd.read_excel(file_obj, header=None)
            
            # Buscar dinámicamente la fila donde están los encabezados (por ejemplo, buscando 'CODIGO')
            header_idx = 5  # Fila 6 (0-indexed 5) por defecto
            for r_idx in range(min(20, len(df))):
                row_vals = [str(x).upper().strip() for x in df.iloc[r_idx].dropna()]
                if any('COD' in val or 'CÓD' in val for val in row_vals):
                    header_idx = r_idx
                    break
            
            # Configurar las columnas y saltar filas superiores
            df.columns = [str(x).upper().strip() if not pd.isna(x) else "" for x in df.iloc[header_idx]]
            df = df.iloc[header_idx + 1:].reset_index(drop=True)
            
            # Buscar columnas críticas
            code_col = None
            name_col = None
            mov_col = None
            
            for col in df.columns:
                if 'COD' in col:
                    code_col = col
                elif 'CUENTA' in col or 'NOM' in col or 'DES' in col:
                    name_col = col
                elif 'MOV' in col:
                    mov_col = col
                    
            if not code_col or not name_col:
                return Response(
                    {"error": "No se encontraron las columnas de Código o Cuenta en la cabecera del archivo."},
                    status=status.HTTP_400_BAD_REQUEST
                )
                
            accounts_dict = {}
            
            # Fase 1: Limpieza, filtros estrictos de nulos/totales y recolección
            for idx, row in df.iterrows():
                if pd.isna(row[code_col]) or pd.isna(row[name_col]):
                    continue
                    
                code_str = str(row[code_col]).strip()
                name_str = str(row[name_col]).strip()
                
                # Limpieza estricta de espacios en blanco del código
                code = "".join(code_str.split())
                name = name_str
                
                # Filtrado de nulos y NAN strings
                if not code or not name or code.upper() == 'NAN' or name.upper() == 'NAN':
                    continue
                    
                # Si empieza por TOTAL, se salta
                if name.upper().startswith('TOTAL'):
                    continue
                    
                explicit_mov = None
                if mov_col and not pd.isna(row[mov_col]):
                    val = str(row[mov_col]).strip().upper()
                    if val in ['SI', 'NO']:
                        explicit_mov = val
                    elif val in ['S', 'N']:
                        explicit_mov = 'SI' if val == 'S' else 'NO'
                        
                accounts_dict[code] = {
                    'codigo': code,
                    'nombre': name,
                    'explicit_mov': explicit_mov,
                }
                
            if not accounts_dict:
                return Response(
                    {"error": "El archivo cargado no contiene cuentas que pasaran los filtros estrictos."},
                    status=status.HTTP_400_BAD_REQUEST
                )
                
            all_codes = list(accounts_dict.keys())
            
            # Fase 2: Jerarquía, naturaleza de saldo y guardado masivo atómico
            with transaction.atomic():
                for code, acc_data in accounts_dict.items():
                    # 1. Determinar naturaleza del saldo según primer dígito del código
                    first_char = code[0] if code else '1'
                    if first_char in ['1', '5', '6']:
                        tipo_saldo = 'DEBE'
                    elif first_char in ['2', '3', '4']:
                        tipo_saldo = 'HABER'
                    else:
                        tipo_saldo = 'DEBE'
                        
                    # 2. Detectar movimiento de forma jerárquica
                    has_descendants = False
                    prefix = code + "."
                    for other_code in all_codes:
                        if other_code.startswith(prefix):
                            has_descendants = True
                            break
                            
                    if has_descendants:
                        movimiento = 'NO'
                    else:
                        movimiento = acc_data['explicit_mov'] if acc_data['explicit_mov'] else 'SI'
                        
                    # 3. Guardar en la base de datos (actualizar si ya existe)
                    Cuenta.objects.update_or_create(
                        codigo=code,
                        defaults={
                            'nombre': acc_data['nombre'],
                            'movimiento': movimiento,
                            'tipo_saldo': tipo_saldo
                        }
                    )
                    
            return Response(
                {"status": "success", "cuentas_indexadas": len(accounts_dict)},
                status=status.HTTP_200_OK
            )
            
        except Exception as e:
            return Response(
                {"error": f"Error al procesar el catálogo: {str(e)}"},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )

class ImportarExtractoBancoView(APIView):
    """
    Procesa un archivo Excel de extracto bancario (formato crudo o conciliación de Banesco),
    limpia los datos y retorna un arreglo JSON con los movimientos sin guardarlos en la BD.
    """
    def post(self, request, *args, **kwargs):
        file_obj = request.FILES.get('file')
        if not file_obj:
            return Response(
                {"error": "No se proporcionó ningún archivo en la petición."},
                status=status.HTTP_400_BAD_REQUEST
            )
            
        try:
            # Leer excel crudo (intentar ubicar headers dinámicamente)
            df = pd.read_excel(file_obj, header=None)
            
            header_idx = -1
            for r_idx in range(min(30, len(df))):
                row_vals = [str(x).upper().strip() for x in df.iloc[r_idx].dropna()]
                # Buscamos combinaciones típicas: FECHA, REFERENCIA y (MONTO o DEBE/HABER)
                if any('FECHA' in val for val in row_vals) and any('REFER' in val for val in row_vals):
                    header_idx = r_idx
                    break
                    
            if header_idx == -1:
                return Response(
                    {"error": "No se pudieron detectar las columnas del extracto bancario (FECHA, REFERENCIA, etc)."},
                    status=status.HTTP_400_BAD_REQUEST
                )
                
            df.columns = [str(x).upper().strip() if not pd.isna(x) else "" for x in df.iloc[header_idx]]
            df = df.iloc[header_idx + 1:].reset_index(drop=True)
            
            # Buscar índices de columnas
            fecha_col = next((c for c in df.columns if 'FECHA' in c), None)
            ref_col = next((c for c in df.columns if 'REFER' in c), None)
            desc_col = next((c for c in df.columns if 'DESC' in c or 'CONCEPTO' in c), None)
            
            monto_col = next((c for c in df.columns if 'MONTO' in c), None)
            debe_col = next((c for c in df.columns if 'DEBE' in c or 'DEBITO' in c or 'CARGO' in c), None)
            haber_col = next((c for c in df.columns if 'HABER' in c or 'CREDITO' in c or 'ABONO' in c), None)
            
            if not fecha_col or not ref_col:
                return Response(
                    {"error": "El archivo debe contener columnas de FECHA y REFERENCIA."},
                    status=status.HTTP_400_BAD_REQUEST
                )
                
            # Parseo de Fechas Vectorizado para ambos formatos (String DD/MM/YYYY y Numérico Serial de Excel)
            fecha_str = df[fecha_col].astype(str).str.strip()
            df['Fecha_Limpia'] = pd.to_datetime(fecha_str, format='%d/%m/%Y', errors='coerce')
            
            # Soporte para fechas que Excel pasa como número (días desde 1899-12-30)
            numeric_dates = pd.to_numeric(df[fecha_col], errors='coerce')
            excel_dates = pd.to_datetime(numeric_dates, unit='D', origin='1899-12-30', errors='coerce')
            
            # Fusionar ambos intentos
            df['Fecha_Limpia'] = df['Fecha_Limpia'].fillna(excel_dates)
            # Fallback genérico final
            df['Fecha_Limpia'] = df['Fecha_Limpia'].fillna(pd.to_datetime(fecha_str, errors='coerce'))

            movimientos = []
            
            for _, row in df.iterrows():
                if pd.isna(row[fecha_col]) or pd.isna(row[ref_col]):
                    continue
                    
                # Extraer datos bases y generar formato ISO para Django
                fecha = row['Fecha_Limpia'].strftime('%Y-%m-%d') if pd.notnull(row['Fecha_Limpia']) else '2025-12-01'
                    
                referencia = str(row[ref_col]).split('.')[0].strip() # Evitar decimales en referencias numéricas
                descripcion = str(row[desc_col]).strip() if desc_col and not pd.isna(row[desc_col]) else "MOVIMIENTO BANCARIO"
                
                # Omitir filas de subtotales/saldos si existieran por error (ej. "SALDO INICIAL")
                if "SALDO" in descripcion.upper() or "TOTAL" in descripcion.upper():
                    continue

                monto = 0.0
                tipo = ""
                
                # Determinar si es formato crudo o conciliación
                if monto_col and not pd.isna(row[monto_col]):
                    # Formato crudo (Monto único)
                    val = float(str(row[monto_col]).replace(',', '.'))
                    monto = abs(val)
                    tipo = "CREDITO" if val > 0 else "DEBITO"
                else:
                    # Formato conciliación (Debe/Haber separados)
                    debe_val = float(str(row[debe_col]).replace(',', '.')) if debe_col and not pd.isna(row[debe_col]) else 0.0
                    haber_val = float(str(row[haber_col]).replace(',', '.')) if haber_col and not pd.isna(row[haber_col]) else 0.0
                    
                    if debe_val > 0:
                        monto = debe_val
                        tipo = "DEBITO"
                    elif haber_val > 0:
                        monto = haber_val
                        tipo = "CREDITO"
                    else:
                        continue # Saldo cero
                
                if monto > 0:
                    movimientos.append({
                        "fecha": fecha,
                        "referencia": referencia,
                        "descripcion": descripcion,
                        "monto": round(monto, 2),
                        "tipo": tipo
                    })
                    
            from .models import AuditoriaLog
            AuditoriaLog.objects.create(usuario=request.user, accion="Cargó Excel Banesco (Extracto/Conciliación)")
            
            return Response(movimientos, status=status.HTTP_200_OK)
            
        except Exception as e:
            return Response(
                {"error": f"Error al procesar el extracto bancario: {str(e)}"},
                status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )

class LibroDiarioPeriodosView(APIView):
    """
    Retorna la lista de periodos (año y mes) que tienen transacciones contables.
    """
    permission_classes = [IsAuthenticated, ContabilidadPermission]
    
    def get(self, request, *args, **kwargs):
        dates = Asiento.objects.dates('fecha', 'month', order='DESC')
        
        meses_nombres = {
            1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
            5: "Mayo", 6: "Junio", 7: "Julio", 8: "Agosto",
            9: "Septiembre", 10: "Octubre", 11: "Noviembre", 12: "Diciembre"
        }
        
        periodos = []
        for d in dates:
            periodos.append({
                "ano": d.year,
                "mes": d.month,
                "nombre": f"{meses_nombres[d.month]} {d.year}"
            })
            
        return Response(periodos, status=status.HTTP_200_OK)

class LibroDiarioView(APIView):
    """
    Retorna los asientos contables filtrados de acuerdo al modo (mes, año o rango de fechas).
    """
    permission_classes = [IsAuthenticated, ContabilidadPermission]
    
    def get(self, request, *args, **kwargs):
        from datetime import datetime
        from django.db.models import Q
        
        modo = request.query_params.get('modo', 'mes')
        filtros = Q()
        
        if modo == 'mes':
            mes = request.query_params.get('mes')
            ano = request.query_params.get('ano')
            
            if not mes or not ano:
                return Response({"error": "Parámetros 'mes' y 'ano' son requeridos para el modo 'mes'."}, status=status.HTTP_400_BAD_REQUEST)
                
            try:
                mes = int(mes)
                ano = int(ano)
            except ValueError:
                return Response({"error": "Los parámetros deben ser números enteros."}, status=status.HTTP_400_BAD_REQUEST)
            
            filtros &= Q(fecha__year=ano, fecha__month=mes)
            
        elif modo == 'ano':
            ano = request.query_params.get('ano')
            if not ano:
                return Response({"error": "El parámetro 'ano' es requerido para el modo 'ano'."}, status=status.HTTP_400_BAD_REQUEST)
            try:
                ano = int(ano)
            except ValueError:
                return Response({"error": "El parámetro 'ano' debe ser un número entero."}, status=status.HTTP_400_BAD_REQUEST)
            
            filtros &= Q(fecha__year=ano)
            
        elif modo == 'rango':
            desde = request.query_params.get('desde')
            hasta = request.query_params.get('hasta')
            if not desde or not hasta:
                return Response({"error": "Los parámetros 'desde' y 'hasta' son requeridos para el modo 'rango'."}, status=status.HTTP_400_BAD_REQUEST)
            
            filtros &= Q(fecha__range=[desde, hasta])
            
        else:
            return Response({"error": "Modo de filtrado no válido."}, status=status.HTTP_400_BAD_REQUEST)
            
        tipo = request.query_params.get('tipo', 'general')
        asientos = Asiento.objects.filter(filtros).order_by('fecha', 'id')
        serializer = AsientoSerializer(asientos, many=True, context={'tipo': tipo})
        return Response(serializer.data, status=status.HTTP_200_OK)

class LibroMayorPeriodosView(APIView):
    """
    Retorna la lista de periodos (año y mes) que tienen transacciones contables para el Libro Mayor.
    """
    permission_classes = [IsAuthenticated, ContabilidadPermission]
    
    def get(self, request, *args, **kwargs):
        dates = Asiento.objects.dates('fecha', 'month', order='DESC')
        meses_nombres = {
            1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
            5: "Mayo", 6: "Junio", 7: "Julio", 8: "Agosto",
            9: "Septiembre", 10: "Octubre", 11: "Noviembre", 12: "Diciembre"
        }
        periodos = []
        for d in dates:
            periodos.append({
                "ano": d.year,
                "mes": d.month,
                "nombre": f"{meses_nombres[d.month]} {d.year}"
            })
        return Response(periodos, status=status.HTTP_200_OK)

class LibroMayorView(APIView):
    """
    Retorna el Libro Mayor consolidado y jerárquico para un mes, año o rango de fechas específico, y nivel específico.
    """
    permission_classes = [IsAuthenticated, ContabilidadPermission]
    
    def get(self, request, *args, **kwargs):
        from decimal import Decimal
        from datetime import date, datetime
        import calendar
        from django.db.models import Sum

        modo = request.query_params.get('modo', 'mes')
        nivel_str = request.query_params.get('nivel', '5') # default a 5 si no se envía

        try:
            nivel = int(nivel_str)
        except ValueError:
            return Response({"error": "El parámetro 'nivel' debe ser un número entero."}, status=status.HTTP_400_BAD_REQUEST)

        # Determinar el rango de fechas [fecha_inicio, fecha_fin]
        if modo == 'mes':
            mes_str = request.query_params.get('mes')
            ano_str = request.query_params.get('ano')
            if not mes_str or not ano_str:
                return Response({"error": "Parámetros 'mes' y 'ano' son requeridos para el modo 'mes'."}, status=status.HTTP_400_BAD_REQUEST)
            try:
                mes = int(mes_str)
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "Los parámetros 'mes' y 'ano' deben ser números enteros."}, status=status.HTTP_400_BAD_REQUEST)
            
            fecha_inicio = date(ano, mes, 1)
            ultimo_dia = calendar.monthrange(ano, mes)[1]
            fecha_fin = date(ano, mes, ultimo_dia)

        elif modo == 'ano':
            ano_str = request.query_params.get('ano')
            if not ano_str:
                return Response({"error": "El parámetro 'ano' es requerido para el modo 'ano'."}, status=status.HTTP_400_BAD_REQUEST)
            try:
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "El parámetro 'ano' debe ser un número entero."}, status=status.HTTP_400_BAD_REQUEST)
            
            fecha_inicio = date(ano, 1, 1)
            fecha_fin = date(ano, 12, 31)

        elif modo == 'rango':
            desde_str = request.query_params.get('desde')
            hasta_str = request.query_params.get('hasta')
            if not desde_str or not hasta_str:
                return Response({"error": "Los parámetros 'desde' y 'hasta' son requeridos para el modo 'rango'."}, status=status.HTTP_400_BAD_REQUEST)
            
            try:
                fecha_inicio = datetime.strptime(desde_str, '%Y-%m-%d').date()
                fecha_fin = datetime.strptime(hasta_str, '%Y-%m-%d').date()
            except ValueError:
                return Response({"error": "Las fechas deben tener el formato YYYY-MM-DD."}, status=status.HTTP_400_BAD_REQUEST)

        else:
            return Response({"error": "Modo de filtrado no válido."}, status=status.HTTP_400_BAD_REQUEST)

        # 1. Traer todas las cuentas
        cuentas_db = Cuenta.objects.all().order_by('codigo')
        
        cuentas_data = {}
        for c in cuentas_db:
            cuentas_data[c.codigo] = {
                "codigo": c.codigo,
                "nombre": c.nombre,
                "movimiento": c.movimiento,
                "tipo_saldo": c.tipo_saldo,
                "nivel_profundidad": len(c.codigo.split('.')),
                "saldo_inicial": Decimal('0.00'),
                "debe": Decimal('0.00'),
                "haber": Decimal('0.00'),
                "saldo_final": Decimal('0.00'),
                "movimientos_mes": []
            }

        # 2. Calcular Saldos Iniciales (acumulado antes de fecha_inicio)
        detalles_inicial = DetalleAsiento.objects.filter(asiento__fecha__lt=fecha_inicio).values('cuenta_id').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )
        for det in detalles_inicial:
            c_codigo = det['cuenta_id']
            if c_codigo in cuentas_data:
                cuentas_data[c_codigo]["saldo_inicial_debe"] = det['sum_debe'] or Decimal('0.00')
                cuentas_data[c_codigo]["saldo_inicial_haber"] = det['sum_haber'] or Decimal('0.00')

        # 3. Calcular Movimientos del Rango Seleccionado
        detalles_rango = DetalleAsiento.objects.filter(asiento__fecha__range=[fecha_inicio, fecha_fin]).select_related('asiento')
        for d in detalles_rango:
            c_codigo = d.cuenta_id
            if c_codigo in cuentas_data:
                cuentas_data[c_codigo]["debe"] += d.debe
                cuentas_data[c_codigo]["haber"] += d.haber
                cuentas_data[c_codigo]["movimientos_mes"].append({
                    "fecha": d.asiento.fecha,
                    "asiento_id": d.asiento.id,
                    "glosa": d.asiento.glosa,
                    "descripcion": d.descripcion,
                    "debe": d.debe,
                    "haber": d.haber
                })

        # 4. Consolidación Jerárquica (Bubble up)
        codigos_ordenados_inverso = sorted(cuentas_data.keys(), key=lambda x: cuentas_data[x]['nivel_profundidad'], reverse=True)
        for codigo in codigos_ordenados_inverso:
            datos = cuentas_data[codigo]
            
            si_debe = datos.get("saldo_inicial_debe", Decimal('0.00'))
            si_haber = datos.get("saldo_inicial_haber", Decimal('0.00'))
            
            if datos["tipo_saldo"] == "DEBE":
                datos["saldo_inicial"] = si_debe - si_haber
            else:
                datos["saldo_inicial"] = si_haber - si_debe
                
            partes = codigo.split('.')
            if len(partes) > 1:
                padre_codigo = '.'.join(partes[:-1])
                if padre_codigo in cuentas_data:
                    padre_datos = cuentas_data[padre_codigo]
                    padre_datos.setdefault("saldo_inicial_debe", Decimal('0.00'))
                    padre_datos.setdefault("saldo_inicial_haber", Decimal('0.00'))
                    
                    padre_datos["saldo_inicial_debe"] += si_debe
                    padre_datos["saldo_inicial_haber"] += si_haber
                    padre_datos["debe"] += datos["debe"]
                    padre_datos["haber"] += datos["haber"]

        # 5. Calcular Saldo Final
        for codigo, datos in cuentas_data.items():
            if datos["tipo_saldo"] == "DEBE":
                datos["saldo_final"] = datos["saldo_inicial"] + datos["debe"] - datos["haber"]
            else:
                datos["saldo_final"] = datos["saldo_inicial"] + datos["haber"] - datos["debe"]

        # 6. Filtrar por Nivel / Tipo
        resultado = []
        tipo = request.query_params.get('tipo', 'general')
        
        for codigo in sorted(cuentas_data.keys()):
            datos = cuentas_data[codigo]
            # Si es detallado, solo mostramos cuentas auxiliares de movimiento
            if tipo == 'detallado' and datos["movimiento"] != 'SI':
                continue
                
            if datos["nivel_profundidad"] <= nivel or tipo == 'detallado':
                if datos["saldo_inicial"] != 0 or datos["debe"] != 0 or datos["haber"] != 0 or datos["saldo_final"] != 0:
                    movs_ordenados = sorted(datos["movimientos_mes"], key=lambda x: (x['fecha'], x['asiento_id']))
                    
                    # Calcular saldo acumulado de forma progresiva
                    saldo_acumulado = datos["saldo_inicial"]
                    movs_con_saldo = []
                    for m in movs_ordenados:
                        m_debe = m["debe"]
                        m_haber = m["haber"]
                        if datos["tipo_saldo"] == "DEBE":
                            saldo_acumulado += (m_debe - m_haber)
                        else:
                            saldo_acumulado += (m_haber - m_debe)
                        
                        movs_con_saldo.append({
                            "fecha": m["fecha"].strftime('%Y-%m-%d') if hasattr(m["fecha"], 'strftime') else str(m["fecha"]),
                            "asiento_id": m["asiento_id"],
                            "glosa": m["glosa"],
                            "descripcion": m["descripcion"],
                            "debe": float(m_debe),
                            "haber": float(m_haber),
                            "saldo_acumulado": float(saldo_acumulado)
                        })

                    resultado.append({
                        "codigo": datos["codigo"],
                        "nombre": datos["nombre"],
                        "movimiento": datos["movimiento"],
                        "tipo_saldo": datos["tipo_saldo"],
                        "nivel_profundidad": datos["nivel_profundidad"],
                        "saldo_inicial": float(datos["saldo_inicial"]),
                        "debe": float(datos["debe"]),
                        "haber": float(datos["haber"]),
                        "saldo_final": float(datos["saldo_final"]),
                        "movimientos_mes": movs_con_saldo
                    })
                    
        return Response(resultado, status=status.HTTP_200_OK)

class DashboardMetricasView(APIView):
    """
    Retorna métricas consolidadas para el dashboard financiero.
    """
    permission_classes = [IsAuthenticated, SoloContadorOAdminPermission]

    def get(self, request, *args, **kwargs):
        from datetime import date
        from django.db.models import Sum, Q

        hoy = date.today()
        ano = request.query_params.get('ano', str(hoy.year))
        mes = request.query_params.get('mes', str(hoy.month))
        
        ano = int(ano)
        mes = int(mes)

        # 1. Saldo en Banco (Cuentas que empiezan por 1.1.1.02 u otras equivalentes al banco)
        # Buscar el saldo neto (Debe - Haber) de todas las cuentas auxiliares de Banco
        banco_movs = DetalleAsiento.objects.filter(cuenta__codigo__startswith='1.1.1.02')
        banco_aggs = banco_movs.aggregate(t_debe=Sum('debe'), t_haber=Sum('haber'))
        saldo_banco = (banco_aggs['t_debe'] or Decimal('0.00')) - (banco_aggs['t_haber'] or Decimal('0.00'))

        # 2. Total Ingresos del Mes (Cuentas 4.X, sum of Haber)
        ingresos_movs = DetalleAsiento.objects.filter(
            cuenta__codigo__startswith='4.',
            asiento__fecha__year=ano,
            asiento__fecha__month=mes
        )
        ingresos_aggs = ingresos_movs.aggregate(t_haber=Sum('haber'))
        total_ingresos = ingresos_aggs['t_haber'] or Decimal('0.00')

        # 3. Total Gastos del Mes (Cuentas 5.X, sum of Debe)
        gastos_movs = DetalleAsiento.objects.filter(
            cuenta__codigo__startswith='5.',
            asiento__fecha__year=ano,
            asiento__fecha__month=mes
        )
        gastos_aggs = gastos_movs.aggregate(t_debe=Sum('debe'))
        total_gastos = gastos_aggs['t_debe'] or Decimal('0.00')

        # 4. Volumen de Operaciones
        total_asientos = Asiento.objects.count()

        return Response({
            "saldo_banco": saldo_banco,
            "total_ingresos": total_ingresos,
            "total_gastos": total_gastos,
            "volumen_operaciones": total_asientos,
            "periodo": {"ano": ano, "mes": mes}
        }, status=status.HTTP_200_OK)

class DatosEmpresaView(APIView):
    permission_classes = [IsAuthenticated]
    def get(self, request, *args, **kwargs):
        from .models import DatosEmpresa
        from .serializers import DatosEmpresaSerializer
        obj = DatosEmpresa.get_solo()
        return Response(DatosEmpresaSerializer(obj).data)
        
    def post(self, request, *args, **kwargs):
        from .models import DatosEmpresa
        from .serializers import DatosEmpresaSerializer
        if hasattr(request.user, 'perfil') and request.user.perfil.rol == 'AUXILIAR':
            return Response({"error": "No autorizado"}, status=403)
            
        obj = DatosEmpresa.get_solo()
        serializer = DatosEmpresaSerializer(obj, data=request.data, partial=True)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data)

class AuditoriaLogView(APIView):
    permission_classes = [IsAuthenticated]
    def get(self, request, *args, **kwargs):
        from .models import AuditoriaLog
        from .serializers import AuditoriaLogSerializer
        if hasattr(request.user, 'perfil') and request.user.perfil.rol != 'ADMINISTRADOR':
            return Response({"error": "No autorizado"}, status=403)
            
        logs = AuditoriaLog.objects.all()[:100]
        return Response(AuditoriaLogSerializer(logs, many=True).data)

def get_mysql_executable(cmd_name):
    import shutil, os
    exe = shutil.which(cmd_name)
    if exe: return exe
    # Buscar en rutas comunes de Windows
    for version in ['12.2', '11.4', '11.3', '11.2', '11.1', '10.11', '10.6']:
        path = f"C:\\Program Files\\MariaDB {version}\\bin\\{cmd_name}.exe"
        if os.path.exists(path): return path
    if os.path.exists(f"C:\\xampp\\mysql\\bin\\{cmd_name}.exe"):
        return f"C:\\xampp\\mysql\\bin\\{cmd_name}.exe"
    return cmd_name

class BackupCrearView(APIView):
    permission_classes = [IsAuthenticated]
    def post(self, request, *args, **kwargs):
        is_admin = hasattr(request.user, 'perfil') and request.user.perfil.rol == 'ADMINISTRADOR'
        is_contador = hasattr(request.user, 'perfil') and request.user.perfil.rol == 'CONTADOR'
        
        solicitud_id = request.data.get('solicitud_id')
        tipo_respaldo = request.data.get('tipo_respaldo', 'TOTAL')
        
        if is_contador:
            if not solicitud_id:
                if tipo_respaldo == 'CONTABLE':
                    pass
                else:
                    return Response({"error": "El rol CONTADOR no tiene permisos para crear directamente respaldos de tipo diferente a CONTABLE."}, status=403)
            else:
                from .models import SolicitudBackup
                try:
                    sol = SolicitudBackup.objects.get(id=solicitud_id, usuario=request.user, tipo='RESPALDAR', aprobada=True)
                    tipo_respaldo = sol.tipo_respaldo
                except SolicitudBackup.DoesNotExist:
                    return Response({"error": "Solicitud no autorizada o inexistente"}, status=403)
        elif is_admin:
            # El Administrador no puede generar de tipo CONTABLE directamente sin una solicitud delegada/creada
            if tipo_respaldo == 'CONTABLE':
                return Response({"error": "El Administrador no puede generar directamente un respaldo puramente CONTABLE sin solicitud de un Contador."}, status=403)
        else:
            return Response({"error": "No autorizado"}, status=403)
            
        import os, tempfile, subprocess
        from django.conf import settings
        from django.http import FileResponse
        
        db = settings.DATABASES['default']
        try:
            fd, temp_path = tempfile.mkstemp(suffix='.sql')
            os.close(fd)
            
            exe = get_mysql_executable('mysqldump')
            if exe.startswith('"') and exe.endswith('"'):
                exe = exe[1:-1]
            
            args = [exe, f"-u{db['USER']}"]
            if db.get('PASSWORD'):
                args.append(f"-p{db['PASSWORD']}")
            args.append(db['NAME'])
            
            # Filtrar tablas según el tipo de respaldo
            if tipo_respaldo == 'CONTABLE':
                args.extend(['contabilidad_cuenta', 'contabilidad_asiento', 'contabilidad_detalleasiento'])
            elif tipo_respaldo == 'SEGURIDAD':
                args.extend([
                    'auth_user', 'auth_group', 'auth_permission', 
                    'auth_user_groups', 'auth_user_user_permissions', 
                    'auth_group_permissions', 'usuarios_perfilusuario', 
                    'usuarios_solicitudpassword', 'contabilidad_auditorialog', 
                    'contabilidad_solicitudbackup', 'contabilidad_solicitudcuenta'
                ])
            
            with open(temp_path, 'w', encoding='utf-8', errors='ignore') as stdout_file:
                subprocess.run(args, stdout=stdout_file, check=True)
            
            from .models import AuditoriaLog
            AuditoriaLog.objects.create(usuario=request.user, accion=f"Creó copia de seguridad de Base de Datos ({tipo_respaldo})")
            
            response = FileResponse(open(temp_path, 'rb'), as_attachment=True, filename=f'backup_{tipo_respaldo.lower()}.sql')
            return response
        except Exception as e:
            return Response({"error": str(e)}, status=500)

            from .models import AuditoriaLog
            AuditoriaLog.objects.create(usuario=request.user, accion="Restauró copia de seguridad de Base de Datos")
            
            return Response({"status": "Base de datos restaurada correctamente"})
        except Exception as e:
            return Response({"error": str(e)}, status=500)

class EstadoResultadosView(APIView):
    permission_classes = [IsAuthenticated, SoloContadorOAdminPermission]

    def get(self, request, *args, **kwargs):
        from decimal import Decimal
        from datetime import date
        from django.db.models import Sum, Q

        modo = request.query_params.get('modo', 'mes')
        
        # Filtros de fecha
        filtros = Q()
        
        if modo == 'mes':
            mes_str = request.query_params.get('mes')
            ano_str = request.query_params.get('ano')
            if not mes_str or not ano_str:
                return Response({"error": "Parámetros 'mes' y 'ano' son requeridos para el modo 'mes'."}, status=status.HTTP_400_BAD_REQUEST)
            try:
                mes = int(mes_str)
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "Los parámetros deben ser números enteros."}, status=status.HTTP_400_BAD_REQUEST)
            filtros &= Q(asiento__fecha__year=ano, asiento__fecha__month=mes)
        elif modo == 'ano':
            ano_str = request.query_params.get('ano')
            if not ano_str:
                return Response({"error": "Parámetro 'ano' es requerido para el modo 'ano'."}, status=status.HTTP_400_BAD_REQUEST)
            try:
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "El parámetro 'ano' debe ser un número entero."}, status=status.HTTP_400_BAD_REQUEST)
            filtros &= Q(asiento__fecha__year=ano)
            mes = 1 # valor por defecto para retornar en la respuesta
        elif modo == 'rango':
            desde = request.query_params.get('desde')
            hasta = request.query_params.get('hasta')
            if not desde or not hasta:
                return Response({"error": "Parámetros 'desde' y 'hasta' son requeridos para el modo 'rango'."}, status=status.HTTP_400_BAD_REQUEST)
            filtros &= Q(asiento__fecha__range=[desde, hasta])
            try:
                from datetime import datetime
                dt = datetime.strptime(desde, '%Y-%m-%d')
                mes = dt.month
                ano = dt.year
            except:
                mes = 1
                ano = 2026
        else:
            return Response({"error": "Modo no válido."}, status=status.HTTP_400_BAD_REQUEST)

        # Traer todas las cuentas de Ingresos (4), Costos (5 o 6), Gastos (5 o 6)
        cuentas_db = Cuenta.objects.filter(codigo__regex=r'^[456]').order_by('codigo')

        cuentas_data = {}
        for c in cuentas_db:
            cuentas_data[c.codigo] = {
                "codigo": c.codigo,
                "nombre": c.nombre,
                "movimiento": c.movimiento,
                "tipo_saldo": c.tipo_saldo,
                "nivel_profundidad": len(c.codigo.split('.')),
                "debe": Decimal('0.00'),
                "haber": Decimal('0.00'),
                "saldo": Decimal('0.00')
            }

        # Calcular movimientos del periodo con los filtros
        detalles_mes = DetalleAsiento.objects.filter(
            filtros,
            cuenta__codigo__regex=r'^[456]'
        ).values('cuenta_id').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )

        for det in detalles_mes:
            c_codigo = det['cuenta_id']
            if c_codigo in cuentas_data:
                cuentas_data[c_codigo]["debe"] = det['sum_debe'] or Decimal('0.00')
                cuentas_data[c_codigo]["haber"] = det['sum_haber'] or Decimal('0.00')

        # Bubble up jerárquico
        codigos_ordenados_inverso = sorted(cuentas_data.keys(), key=lambda x: cuentas_data[x]['nivel_profundidad'], reverse=True)
        for codigo in codigos_ordenados_inverso:
            datos = cuentas_data[codigo]
            partes = codigo.split('.')
            if len(partes) > 1:
                padre_codigo = '.'.join(partes[:-1])
                if padre_codigo in cuentas_data:
                    padre_datos = cuentas_data[padre_codigo]
                    padre_datos["debe"] += datos["debe"]
                    padre_datos["haber"] += datos["haber"]

        # Calcular saldo según naturaleza
        for codigo, datos in cuentas_data.items():
            if codigo.startswith('4'):
                # Ingresos: Haber - Debe
                datos["saldo"] = datos["haber"] - datos["debe"]
            else:
                # Gastos/Costos (5 y 6): Debe - Haber
                datos["saldo"] = datos["debe"] - datos["haber"]

        # Agrupar según los nombres del catálogo
        cuenta_5 = Cuenta.objects.filter(codigo='5').first()
        is_5_costos = True
        if cuenta_5 and 'GASTO' in cuenta_5.nombre.upper():
            is_5_costos = False

        ingresos_list = []
        costos_list = []
        gastos_list = []

        total_ingresos = Decimal('0.00')
        total_costos = Decimal('0.00')
        total_gastos = Decimal('0.00')

        for codigo in sorted(cuentas_data.keys()):
            datos = cuentas_data[codigo]
            # No incluir si no tiene saldo ni movimientos
            if datos["saldo"] == 0 and datos["debe"] == 0 and datos["haber"] == 0:
                continue

            item = {
                "codigo": datos["codigo"],
                "nombre": datos["nombre"],
                "movimiento": datos["movimiento"],
                "nivel_profundidad": datos["nivel_profundidad"],
                "debe": round(datos["debe"], 2),
                "haber": round(datos["haber"], 2),
                "saldo": round(datos["saldo"], 2)
            }

            if codigo.startswith('4'):
                ingresos_list.append(item)
                if datos["nivel_profundidad"] == 1:
                    total_ingresos += datos["saldo"]
            elif codigo.startswith('5'):
                if is_5_costos:
                    costos_list.append(item)
                    if datos["nivel_profundidad"] == 1:
                        total_costos += datos["saldo"]
                else:
                    gastos_list.append(item)
                    if datos["nivel_profundidad"] == 1:
                        total_gastos += datos["saldo"]
            elif codigo.startswith('6'):
                if is_5_costos:
                    gastos_list.append(item)
                    if datos["nivel_profundidad"] == 1:
                        total_gastos += datos["saldo"]
                else:
                    costos_list.append(item)
                    if datos["nivel_profundidad"] == 1:
                        total_costos += datos["saldo"]

        utilidad_neta = total_ingresos - total_costos - total_gastos

        return Response({
            "periodo": {"mes": mes, "ano": ano, "modo": modo},
            "ingresos": ingresos_list,
            "costos": costos_list,
            "gastos": gastos_list,
            "total_ingresos": round(total_ingresos, 2),
            "total_costos": round(total_costos, 2),
            "total_gastos": round(total_gastos, 2),
            "utilidad_neta": round(utilidad_neta, 2)
        }, status=status.HTTP_200_OK)

class EstadoSituacionView(APIView):
    permission_classes = [IsAuthenticated, SoloContadorOAdminPermission]

    def get(self, request, *args, **kwargs):
        from decimal import Decimal
        from datetime import date
        import calendar
        from django.db.models import Sum, Q

        modo = request.query_params.get('modo', 'mes')
        
        # Filtro de fecha de corte para acumulados
        filtros = Q()
        
        if modo == 'mes':
            mes_str = request.query_params.get('mes')
            ano_str = request.query_params.get('ano')
            if not mes_str or not ano_str:
                return Response({"error": "Parámetros 'mes' y 'ano' son requeridos para el modo 'mes'."}, status=status.HTTP_400_BAD_REQUEST)
            try:
                mes = int(mes_str)
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "Los parámetros deben ser números enteros."}, status=status.HTTP_400_BAD_REQUEST)
            
            ultimo_dia = calendar.monthrange(ano, mes)[1]
            fecha_corte = date(ano, mes, ultimo_dia)
            filtros &= Q(asiento__fecha__lte=fecha_corte)
        elif modo == 'ano':
            ano_str = request.query_params.get('ano')
            if not ano_str:
                return Response({"error": "Parámetro 'ano' es requerido para el modo 'ano'."}, status=status.HTTP_400_BAD_REQUEST)
            try:
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "El parámetro 'ano' debe ser un número entero."}, status=status.HTTP_400_BAD_REQUEST)
            
            fecha_corte = date(ano, 12, 31)
            filtros &= Q(asiento__fecha__lte=fecha_corte)
            mes = 12
        elif modo == 'rango':
            hasta = request.query_params.get('hasta')
            if not hasta:
                return Response({"error": "Parámetro 'hasta' es requerido para el modo 'rango'."}, status=status.HTTP_400_BAD_REQUEST)
            filtros &= Q(asiento__fecha__lte=hasta)
            try:
                from datetime import datetime
                dt = datetime.strptime(hasta, '%Y-%m-%d')
                mes = dt.month
                ano = dt.year
            except:
                mes = 12
                ano = 2026
        else:
            return Response({"error": "Modo no válido."}, status=status.HTTP_400_BAD_REQUEST)

        # Cuentas de balance (1, 2, 3)
        cuentas_db = Cuenta.objects.filter(codigo__regex=r'^[123]').order_by('codigo')

        cuentas_data = {}
        for c in cuentas_db:
            cuentas_data[c.codigo] = {
                "codigo": c.codigo,
                "nombre": c.nombre,
                "movimiento": c.movimiento,
                "tipo_saldo": c.tipo_saldo,
                "nivel_profundidad": len(c.codigo.split('.')),
                "debe": Decimal('0.00'),
                "haber": Decimal('0.00'),
                "saldo": Decimal('0.00')
            }

        # Calcular saldos acumulados hasta fecha_corte
        detalles_acum = DetalleAsiento.objects.filter(
            filtros,
            cuenta__codigo__regex=r'^[123]'
        ).values('cuenta_id').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )

        for det in detalles_acum:
            c_codigo = det['cuenta_id']
            if c_codigo in cuentas_data:
                cuentas_data[c_codigo]["debe"] = det['sum_debe'] or Decimal('0.00')
                cuentas_data[c_codigo]["haber"] = det['sum_haber'] or Decimal('0.00')

        # Bubble up jerárquico estricto (asegurar que la cuenta padre pertenezca al catálogo cargado)
        codigos_ordenados_inverso = sorted(cuentas_data.keys(), key=lambda x: cuentas_data[x]['nivel_profundidad'], reverse=True)
        for codigo in codigos_ordenados_inverso:
            datos = cuentas_data[codigo]
            partes = codigo.split('.')
            if len(partes) > 1:
                padre_codigo = '.'.join(partes[:-1])
                if padre_codigo in cuentas_data:
                    # Regla contable: solo sumar si pertenecen a la misma clase raíz
                    if padre_codigo[0] == codigo[0]:
                        padre_datos = cuentas_data[padre_codigo]
                        padre_datos["debe"] += datos["debe"]
                        padre_datos["haber"] += datos["haber"]

        # Calcular saldo según naturaleza
        for codigo, datos in cuentas_data.items():
            if codigo.startswith('1'):
                # Activos: Debe - Haber
                datos["saldo"] = datos["debe"] - datos["haber"]
            else:
                # Pasivos y Patrimonio (2 y 3): Haber - Debe
                datos["saldo"] = datos["haber"] - datos["debe"]

        # Agrupar cuentas en Activos (Corrientes/No Corrientes), Pasivos (Corrientes/No Corrientes) y Patrimonio
        activos_corrientes = []
        activos_no_corrientes = []
        pasivos_corrientes = []
        pasivos_no_corrientes = []
        patrimonio = []

        total_activos_corrientes = Decimal('0.00')
        total_activos_no_corrientes = Decimal('0.00')
        total_pasivos_corrientes = Decimal('0.00')
        total_pasivos_no_corrientes = Decimal('0.00')
        total_patrimonio = Decimal('0.00')

        for codigo in sorted(cuentas_data.keys()):
            datos = cuentas_data[codigo]
            if datos["saldo"] == 0 and datos["debe"] == 0 and datos["haber"] == 0:
                continue

            item = {
                "codigo": datos["codigo"],
                "nombre": datos["nombre"],
                "movimiento": datos["movimiento"],
                "nivel_profundidad": datos["nivel_profundidad"],
                "debe": round(datos["debe"], 2),
                "haber": round(datos["haber"], 2),
                "saldo": round(datos["saldo"], 2)
            }

            if codigo.startswith('1'):
                # Clasificar corriente (1.1) vs no corriente (1.2 u otros)
                if codigo == '1.1' or codigo.startswith('1.1.'):
                    activos_corrientes.append(item)
                    if datos["nivel_profundidad"] == 2:
                        total_activos_corrientes += datos["saldo"]
                else:
                    activos_no_corrientes.append(item)
                    if datos["nivel_profundidad"] == 2:
                        total_activos_no_corrientes += datos["saldo"]
            elif codigo.startswith('2'):
                # Clasificar corriente (2.1) vs no corriente (2.2 u otros)
                if codigo == '2.1' or codigo.startswith('2.1.'):
                    pasivos_corrientes.append(item)
                    if datos["nivel_profundidad"] == 2:
                        total_pasivos_corrientes += datos["saldo"]
                else:
                    pasivos_no_corrientes.append(item)
                    if datos["nivel_profundidad"] == 2:
                        total_pasivos_no_corrientes += datos["saldo"]
            elif codigo.startswith('3'):
                patrimonio.append(item)
                if datos["nivel_profundidad"] == 1:
                    total_patrimonio += datos["saldo"]

        # Calcular la utilidad real del ejercicio sobre el mismo período seleccionado
        # Ingresos (Clase 4) y Gastos/Costos (Clases 5 y 6)
        detalles_utilidad = DetalleAsiento.objects.filter(
            filtros,
            cuenta__codigo__regex=r'^[456]'
        ).values('cuenta__codigo', 'cuenta__tipo_saldo').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )

        total_ingresos_periodo = Decimal('0.00')
        total_gastos_periodo = Decimal('0.00')

        for det in detalles_utilidad:
            codigo = det['cuenta__codigo']
            debe = det['sum_debe'] or Decimal('0.00')
            haber = det['sum_haber'] or Decimal('0.00')
            if codigo.startswith('4'):
                # Ingresos: Haber - Debe
                total_ingresos_periodo += (haber - debe)
            else:
                # Gastos y Costos (5 y 6): Debe - Haber
                total_gastos_periodo += (debe - haber)

        utilidad_real = total_ingresos_periodo - total_gastos_periodo

        # Inyectar utilidad en el patrimonio
        utilidad_item = {
            "codigo": "3.99.99",
            "nombre": "Utilidad del Ejercicio (vía Estado de Resultados)",
            "movimiento": "SI",
            "nivel_profundidad": 2,
            "debe": 0.0,
            "haber": round(float(utilidad_real), 2),
            "saldo": round(float(utilidad_real), 2)
        }
        patrimonio.append(utilidad_item)
        total_patrimonio += utilidad_real

        # --- ESTADO DE EVOLUCIÓN DEL PATRIMONIO ---
        # 1. Determinar fechas de inicio y fin del periodo
        if modo == 'mes':
            fecha_inicio = date(ano, mes, 1)
            fecha_fin = fecha_corte
        elif modo == 'ano':
            fecha_inicio = date(ano, 1, 1)
            fecha_fin = fecha_corte
        elif modo == 'rango':
            desde_str = request.query_params.get('desde')
            hasta_str = request.query_params.get('hasta')
            from datetime import datetime
            fecha_inicio = datetime.strptime(desde_str, '%Y-%m-%d').date()
            fecha_fin = datetime.strptime(hasta_str, '%Y-%m-%d').date()

        # Obtener cuentas reales de Clase 3
        cuentas_pat = Cuenta.objects.filter(codigo__startswith='3').exclude(codigo='3.99.99').order_by('codigo')
        
        # Consultar saldos iniciales (antes de fecha_inicio) y variaciones (entre fecha_inicio y fecha_fin)
        saldos_iniciales_db = DetalleAsiento.objects.filter(
            asiento__fecha__lt=fecha_inicio,
            cuenta__codigo__startswith='3'
        ).values('cuenta_id').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )
        
        variaciones_db = DetalleAsiento.objects.filter(
            asiento__fecha__range=[fecha_inicio, fecha_fin],
            cuenta__codigo__startswith='3'
        ).values('cuenta_id').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )

        saldos_ini_map = {item['cuenta_id']: (item['sum_haber'] or Decimal('0.00')) - (item['sum_debe'] or Decimal('0.00')) for item in saldos_iniciales_db}
        var_map = {item['cuenta_id']: (item['sum_haber'] or Decimal('0.00')) - (item['sum_debe'] or Decimal('0.00')) for item in variaciones_db}

        estado_patrimonio_list = []
        total_inicial_pat = Decimal('0.00')
        total_variaciones_pat = Decimal('0.00')
        total_final_pat = Decimal('0.00')

        # Bubble up para saldos iniciales y variaciones del patrimonio
        pat_data = {}
        for c in Cuenta.objects.filter(codigo__startswith='3').exclude(codigo='3.99.99').order_by('codigo'):
            pat_data[c.codigo] = {
                "nombre": c.nombre,
                "nivel_profundidad": len(c.codigo.split('.')),
                "saldo_inicial": saldos_ini_map.get(c.codigo, Decimal('0.00')),
                "variaciones": var_map.get(c.codigo, Decimal('0.00'))
            }

        codigos_pat_inverso = sorted(pat_data.keys(), key=lambda x: pat_data[x]['nivel_profundidad'], reverse=True)
        for codigo in codigos_pat_inverso:
            datos = pat_data[codigo]
            partes = codigo.split('.')
            if len(partes) > 1:
                padre_codigo = '.'.join(partes[:-1])
                if padre_codigo in pat_data:
                    pat_data[padre_codigo]["saldo_inicial"] += datos["saldo_inicial"]
                    pat_data[padre_codigo]["variaciones"] += datos["variaciones"]

        for codigo in sorted(pat_data.keys()):
            datos = pat_data[codigo]
            saldo_ini = datos["saldo_inicial"]
            var_val = datos["variaciones"]
            saldo_fin = saldo_ini + var_val
            
            # Solo incluir si tiene movimientos o saldos
            if saldo_ini == 0 and var_val == 0:
                continue

            estado_patrimonio_list.append({
                "codigo": codigo,
                "nombre": datos["nombre"],
                "nivel_profundidad": datos["nivel_profundidad"],
                "saldo_inicial": round(float(saldo_ini), 2),
                "variaciones": round(float(var_val), 2),
                "utilidad_periodo": 0.00,
                "saldo_final": round(float(saldo_fin), 2)
            })

            if datos["nivel_profundidad"] == 1:
                total_inicial_pat += saldo_ini
                total_variaciones_pat += var_val
                total_final_pat += saldo_fin

        # 2. Calcular Utilidad acumulada antes del periodo (saldo inicial de 3.99.99)
        detalles_utilidad_inicial = DetalleAsiento.objects.filter(
            asiento__fecha__lt=fecha_inicio,
            cuenta__codigo__regex=r'^[456]'
        ).values('cuenta__codigo').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )
        utilidad_inicial_acum = Decimal('0.00')
        for det in detalles_utilidad_inicial:
            c_cod = det['cuenta__codigo']
            debe = det['sum_debe'] or Decimal('0.00')
            haber = det['sum_haber'] or Decimal('0.00')
            if c_cod.startswith('4'):
                utilidad_inicial_acum += (haber - debe)
            else:
                utilidad_inicial_acum += (debe - haber)

        saldo_final_utilidad = utilidad_inicial_acum + utilidad_real

        # Inyectar fila de Utilidad del Ejercicio
        estado_patrimonio_list.append({
            "codigo": "3.99.99",
            "nombre": "Utilidad / Pérdida del Ejercicio",
            "nivel_profundidad": 2,
            "saldo_inicial": round(float(utilidad_inicial_acum), 2),
            "variaciones": 0.00,
            "utilidad_periodo": round(float(utilidad_real), 2),
            "saldo_final": round(float(saldo_final_utilidad), 2)
        })

        total_inicial_pat += utilidad_inicial_acum
        total_final_pat += saldo_final_utilidad

        return Response({
            "periodo": {"mes": mes, "ano": ano, "modo": modo},
            "activos_corrientes": activos_corrientes,
            "activos_no_corrientes": activos_no_corrientes,
            "total_activos_corrientes": round(total_activos_corrientes, 2),
            "total_activos_no_corrientes": round(total_activos_no_corrientes, 2),
            "total_activos": round(total_activos_corrientes + total_activos_no_corrientes, 2),
            
            "pasivos_corrientes": pasivos_corrientes,
            "pasivos_no_corrientes": pasivos_no_corrientes,
            "total_pasivos_corrientes": round(total_pasivos_corrientes, 2),
            "total_pasivos_no_corrientes": round(total_pasivos_no_corrientes, 2),
            "total_pasivos": round(total_pasivos_corrientes + total_pasivos_no_corrientes, 2),
            
            "patrimonio": patrimonio,
            "total_patrimonio": round(total_patrimonio, 2),
            "total_pasivo_patrimonio": round(total_pasivos_corrientes + total_pasivos_no_corrientes + total_patrimonio, 2),
            "utilidad_neta": round(utilidad_real, 2),
            "estado_patrimonio": estado_patrimonio_list,
            "total_inicial_pat": round(total_inicial_pat, 2),
            "total_variaciones_pat": round(total_variaciones_pat, 2),
            "total_utilidad_pat": round(utilidad_real, 2),
            "total_final_pat": round(total_final_pat, 2)
        }, status=status.HTTP_200_OK)

class BackupImportarView(APIView):
    permission_classes = [IsAuthenticated]
    def post(self, request, *args, **kwargs):
        is_admin = hasattr(request.user, 'perfil') and request.user.perfil.rol == 'ADMINISTRADOR'
        is_contador = hasattr(request.user, 'perfil') and request.user.perfil.rol == 'CONTADOR'
        
        solicitud_id = request.data.get('solicitud_id')
        
        if is_contador:
            if not solicitud_id:
                return Response({"error": "El rol CONTADOR requiere un ID de solicitud para esta operación."}, status=403)
            from .models import SolicitudBackup
            try:
                sol = SolicitudBackup.objects.get(id=solicitud_id, usuario=request.user, tipo='RESTAURAR', aprobada=True)
            except SolicitudBackup.DoesNotExist:
                return Response({"error": "Solicitud no autorizada o inexistente"}, status=403)
        elif not is_admin:
            return Response({"error": "No autorizado"}, status=403)
            
        file_obj = request.FILES.get('file')
        if not file_obj:
            return Response({"error": "Falta archivo sql"}, status=400)
            
        import os, tempfile, subprocess
        from django.conf import settings
        
        db = settings.DATABASES['default']
        try:
            fd, temp_path = tempfile.mkstemp(suffix='.sql')
            with os.fdopen(fd, 'wb') as f:
                for chunk in file_obj.chunks():
                    f.write(chunk)
                    
            exe = get_mysql_executable('mysql')
            if exe.startswith('"') and exe.endswith('"'):
                exe = exe[1:-1]
            
            args = [exe, f"-u{db['USER']}"]
            if db.get('PASSWORD'):
                args.append(f"-p{db['PASSWORD']}")
            args.append(db['NAME'])
            
            with open(temp_path, 'r', encoding='utf-8', errors='ignore') as stdin_file:
                subprocess.run(args, stdin=stdin_file, check=True)
            os.remove(temp_path)
            
            from .models import AuditoriaLog
            AuditoriaLog.objects.create(usuario=request.user, accion="Restauró copia de seguridad de Base de Datos")
            
            return Response({"status": "Base de datos restaurada correctamente"})
        except Exception as e:
            return Response({"error": str(e)}, status=500)

import json

class SolicitudCuentaViewSet(viewsets.ViewSet):
    permission_classes = [IsAuthenticated]

    def create(self, request):
        from .models import SolicitudCuenta
        accion = request.data.get('accion')
        datos_json = request.data.get('datos_json')
        if isinstance(datos_json, dict):
            datos_json = json.dumps(datos_json)
            
        if not accion or not datos_json:
            return Response({"error": "Faltan parámetros 'accion' o 'datos_json'."}, status=400)
            
        solicitud = SolicitudCuenta.objects.create(
            usuario=request.user,
            accion=accion,
            datos_json=datos_json
        )
        
        from .models import AuditoriaLog
        AuditoriaLog.objects.create(
            usuario=request.user,
            accion=f"Envió solicitud de cambio en catálogo: {accion} cuenta"
        )
        
        return Response({"status": "Solicitud creada con éxito", "id": solicitud.id})

    def list(self, request):
        from .models import SolicitudCuenta
        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol not in ['ADMINISTRADOR', 'CONTADOR']:
            return Response({"error": "No autorizado"}, status=403)
            
        solicitudes = SolicitudCuenta.objects.filter(atendida=False).select_related('usuario')
        data = [{
            "id": s.id,
            "usuario": s.usuario.username,
            "accion": s.accion,
            "datos_json": json.loads(s.datos_json),
            "fecha_solicitud": s.fecha_solicitud.isoformat()
        } for s in solicitudes]
        return Response(data)

    @action(detail=True, methods=['post'])
    def aprobar(self, request, pk=None):
        from .models import SolicitudCuenta, Cuenta, AuditoriaLog
        from django.db import transaction
        
        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol not in ['ADMINISTRADOR', 'CONTADOR']:
            return Response({"error": "No autorizado"}, status=403)
            
        try:
            solicitud = SolicitudCuenta.objects.get(id=pk, atendida=False)
        except SolicitudCuenta.DoesNotExist:
            return Response({"error": "La solicitud no existe o ya fue atendida."}, status=404)
            
        datos = json.loads(solicitud.datos_json)
        codigo = datos.get('codigo')
        nombre = datos.get('nombre')
        movimiento = datos.get('movimiento', 'SI')
        tipo_saldo = datos.get('tipo_saldo', 'DEBE')
        
        try:
            with transaction.atomic():
                if solicitud.accion == 'CREAR':
                    Cuenta.objects.create(
                        codigo=codigo,
                        nombre=nombre,
                        movimiento=movimiento,
                        tipo_saldo=tipo_saldo
                    )
                    msg_log = f"Aprobó solicitud de creación de cuenta {codigo}"
                elif solicitud.accion == 'EDITAR':
                    cuenta = Cuenta.objects.get(codigo=codigo)
                    cuenta.nombre = nombre
                    cuenta.movimiento = movimiento
                    cuenta.tipo_saldo = tipo_saldo
                    cuenta.save()
                    msg_log = f"Aprobó solicitud de modificación de cuenta {codigo}"
                elif solicitud.accion == 'ELIMINAR':
                    cuenta = Cuenta.objects.get(codigo=codigo)
                    from .models import DetalleAsiento
                    if DetalleAsiento.objects.filter(cuenta=cuenta).exists():
                        return Response({"error": "No se puede aprobar la eliminación porque la cuenta tiene movimientos asociados."}, status=400)
                    cuenta.delete()
                    msg_log = f"Aprobó solicitud de eliminación de cuenta {codigo}"
                
                solicitud.atendida = True
                solicitud.aprobada = True
                solicitud.save()
                
                AuditoriaLog.objects.create(
                    usuario=request.user,
                    accion=msg_log
                )
            return Response({"status": "Solicitud aprobada y ejecutada correctamente"})
        except Exception as e:
            return Response({"error": f"Error al procesar la solicitud: {str(e)}"}, status=500)

    @action(detail=True, methods=['post'])
    def rechazar(self, request, pk=None):
        from .models import SolicitudCuenta, AuditoriaLog
        
        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol not in ['ADMINISTRADOR', 'CONTADOR']:
            return Response({"error": "No autorizado"}, status=403)
            
        try:
            solicitud = SolicitudCuenta.objects.get(id=pk, atendida=False)
        except SolicitudCuenta.DoesNotExist:
            return Response({"error": "La solicitud no existe o ya fue atendida."}, status=404)
            
        solicitud.atendida = True
        solicitud.aprobada = False
        solicitud.save()
        
        AuditoriaLog.objects.create(
            usuario=request.user,
            accion=f"Rechazó solicitud de cambio ({solicitud.accion}) para cuenta"
        )
        return Response({"status": "Solicitud rechazada correctamente"})

class SolicitudBackupViewSet(viewsets.ViewSet):
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self):
        from .models import SolicitudBackup
        if hasattr(self.request.user, 'perfil') and self.request.user.perfil.rol == 'ADMINISTRADOR':
            return SolicitudBackup.objects.all()
        return SolicitudBackup.objects.filter(usuario=self.request.user)

    def list(self, request):
        queryset = self.get_queryset()
        data = [{
            "id": s.id,
            "usuario": s.usuario.username,
            "tipo": s.tipo,
            "tipo_respaldo": s.tipo_respaldo,
            "justificacion": s.justificacion,
            "archivo_nombre": s.archivo_nombre,
            "fecha_solicitud": s.fecha_solicitud.isoformat(),
            "atendida": s.atendida,
            "aprobada": s.aprobada
        } for s in queryset]
        return Response(data)

    def create(self, request):
        from .models import SolicitudBackup
        tipo = request.data.get('tipo')
        tipo_respaldo = request.data.get('tipo_respaldo', 'TOTAL')
        justificacion = request.data.get('justificacion')
        archivo_nombre = request.data.get('archivo_nombre')
        archivo_contenido = request.data.get('archivo_contenido')
        
        if not tipo or not justificacion:
            return Response({"error": "Faltan datos obligatorios"}, status=400)
            
        rol = request.user.perfil.rol if hasattr(request.user, 'perfil') else 'AUXILIAR'
        if rol == 'CONTADOR':
            # Rol CONTADOR: Solo tiene permiso de solicitar/ejecutar el tipo CONTABLE
            tipo_respaldo = 'CONTABLE'
        elif rol == 'ADMINISTRADOR':
            # El Administrador no puede generar de tipo CONTABLE directamente
            if tipo_respaldo == 'CONTABLE':
                return Response({"error": "El Administrador no puede generar directamente un respaldo puramente CONTABLE sin solicitud de un Contador."}, status=403)
            
        solicitud = SolicitudBackup.objects.create(
            usuario=request.user,
            tipo=tipo,
            tipo_respaldo=tipo_respaldo,
            justificacion=justificacion,
            archivo_nombre=archivo_nombre,
            archivo_contenido=archivo_contenido
        )
        return Response({"status": "Solicitud creada con éxito.", "id": solicitud.id})

    @action(detail=True, methods=['post'], url_path='aprobar')
    def aprobar(self, request, pk=None):
        if hasattr(request.user, 'perfil') and request.user.perfil.rol != 'ADMINISTRADOR':
            return Response({"error": "No autorizado"}, status=403)
            
        from .models import SolicitudBackup, AuditoriaLog
        try:
            solicitud = SolicitudBackup.objects.get(id=pk, atendida=False)
        except SolicitudBackup.DoesNotExist:
            return Response({"error": "La solicitud no existe o ya fue atendida."}, status=404)
            
        import os, tempfile, subprocess
        from django.conf import settings
        db = settings.DATABASES['default']
        
        try:
            if solicitud.tipo == 'RESPALDAR':
                fd, temp_path = tempfile.mkstemp(suffix='.sql')
                os.close(fd)
                from .views import get_mysql_executable
                exe = get_mysql_executable('mysqldump')
                if exe.startswith('"') and exe.endswith('"'):
                    exe = exe[1:-1]
                
                args = [exe, f"-u{db['USER']}"]
                if db.get('PASSWORD'):
                    args.append(f"-p{db['PASSWORD']}")
                args.append(db['NAME'])
                
                # Filtrar tablas según el tipo de respaldo solicitado
                if solicitud.tipo_respaldo == 'CONTABLE':
                    args.extend(['contabilidad_cuenta', 'contabilidad_asiento', 'contabilidad_detalleasiento'])
                elif solicitud.tipo_respaldo == 'SEGURIDAD':
                    args.extend([
                        'auth_user', 'auth_group', 'auth_permission', 
                        'auth_user_groups', 'auth_user_user_permissions', 
                        'auth_group_permissions', 'usuarios_perfilusuario', 
                        'usuarios_solicitudpassword', 'contabilidad_auditorialog', 
                        'contabilidad_solicitudbackup', 'contabilidad_solicitudcuenta'
                    ])
                
                with open(temp_path, 'w', encoding='utf-8', errors='ignore') as stdout_file:
                    subprocess.run(args, stdout=stdout_file, check=True)
                
                with open(temp_path, 'r', encoding='utf-8', errors='ignore') as f:
                    solicitud.archivo_contenido = f.read()
                solicitud.archivo_nombre = f'backup_{solicitud.tipo_respaldo.lower()}.sql'
                os.remove(temp_path)
                
                AuditoriaLog.objects.create(
                    usuario=request.user,
                    accion=f"Aprobó solicitud de respaldo ({solicitud.tipo_respaldo}) del usuario {solicitud.usuario.username}"
                )
                
            elif solicitud.tipo == 'RESTAURAR':
                if not solicitud.archivo_contenido:
                    return Response({"error": "No hay archivo de respaldo cargado en la solicitud"}, status=400)

                # 1. GENERAR EL RESPALDO AUTOMÁTICO DE SEGURIDAD (PRE-MIGRACIÓN)
                # Si es restauración de Contador (CONTABLE), el preventivo automático también debe ser de tipo CONTABLE.
                preventivo_tipo = solicitud.tipo_respaldo
                
                fd_prev, temp_path_prev = tempfile.mkstemp(suffix='.sql')
                os.close(fd_prev)
                from .views import get_mysql_executable
                exe_dump = get_mysql_executable('mysqldump')
                if exe_dump.startswith('"') and exe_dump.endswith('"'):
                    exe_dump = exe_dump[1:-1]
                
                dump_args = [exe_dump, f"-u{db['USER']}"]
                if db.get('PASSWORD'):
                    dump_args.append(f"-p{db['PASSWORD']}")
                dump_args.append(db['NAME'])
                
                if preventivo_tipo == 'CONTABLE':
                    dump_args.extend(['contabilidad_cuenta', 'contabilidad_asiento', 'contabilidad_detalleasiento'])
                elif preventivo_tipo == 'SEGURIDAD':
                    dump_args.extend([
                        'auth_user', 'auth_group', 'auth_permission', 
                        'auth_user_groups', 'auth_user_user_permissions', 
                        'auth_group_permissions', 'usuarios_perfilusuario', 
                        'usuarios_solicitudpassword', 'contabilidad_auditorialog', 
                        'contabilidad_solicitudbackup', 'contabilidad_solicitudcuenta'
                    ])
                
                try:
                    with open(temp_path_prev, 'w', encoding='utf-8', errors='ignore') as stdout_file:
                        subprocess.run(dump_args, stdout=stdout_file, check=True)
                    
                    with open(temp_path_prev, 'r', encoding='utf-8', errors='ignore') as f_prev:
                        preventivo_contenido = f_prev.read()
                    os.remove(temp_path_prev)
                    
                    # REGISTRAMOS EL RESPALDO PREVENTIVO DIRECTAMENTE COMO COMPLETO/APROBADO
                    SolicitudBackup.objects.create(
                        usuario=solicitud.usuario, 
                        tipo='RESPALDAR',
                        tipo_respaldo=preventivo_tipo,
                        justificacion=f"Respaldo automático de seguridad ({preventivo_tipo}) generado por el sistema antes de ejecutar la restauración ID #{solicitud.id}",
                        archivo_nombre=f'respaldo_preventivo_automatico_{preventivo_tipo.lower()}.sql',
                        archivo_contenido=preventivo_contenido,
                        aprobada=True,
                        atendida=True
                    )
                    AuditoriaLog.objects.create(
                        usuario=request.user,
                        accion=f"Generó respaldo preventivo automático ({preventivo_tipo}) antes de restaurar solicitud ID #{solicitud.id}"
                    )
                except Exception as dump_err:
                    return Response({'error': f'Error al crear el respaldo de seguridad preventivo: {str(dump_err)}'}, status=500)

                # 2. EJECUTAR LA RESTAURACIÓN REAL PEDIDA POR EL CONTADOR
                fd, temp_path = tempfile.mkstemp(suffix='.sql')
                with os.fdopen(fd, 'w', encoding='utf-8') as f:
                    f.write(solicitud.archivo_contenido)
                
                exe_mysql = get_mysql_executable('mysql')
                if exe_mysql.startswith('"') and exe_mysql.endswith('"'):
                    exe_mysql = exe_mysql[1:-1]
                
                args = [exe_mysql, f"-u{db['USER']}"]
                if db.get('PASSWORD'):
                    args.append(f"-p{db['PASSWORD']}")
                args.append(db['NAME'])
                
                with open(temp_path, 'r', encoding='utf-8', errors='ignore') as stdin_file:
                    subprocess.run(args, stdin=stdin_file, check=True)
                os.remove(temp_path)
                
                AuditoriaLog.objects.create(
                    usuario=request.user,
                    accion=f"Aprobó solicitud de restauración ({solicitud.tipo_respaldo}) del usuario {solicitud.usuario.username}"
                )
                
            # 3. ACTUALIZAR LA SOLICITUD DE RESTAURACIÓN ORIGINAL A ATENDIDA
            solicitud.aprobada = True
            solicitud.atendida = True
            solicitud.save()
            return Response({"status": f"Solicitud de {solicitud.tipo} ejecutada y aprobada con éxito."})
        except Exception as e:
            return Response({"error": str(e)}, status=500)


    @action(detail=True, methods=['post'])
    def status_checking(self, request, pk=None):
        return Response({"detail": "Checking"})

    @action(detail=True, methods=['get'], url_path='descargar')
    def descargar(self, request, pk=None):
        from .models import SolicitudBackup
        from django.http import Http404, FileResponse
        import tempfile, os

        try:
            solicitud = SolicitudBackup.objects.get(pk=pk)
        except SolicitudBackup.DoesNotExist:
            raise Http404("La solicitud de respaldo no existe.")

        if not solicitud.aprobada or not solicitud.archivo_contenido:
            raise Http404("El archivo de respaldo no se encuentra aprobado o disponible en el servidor.")

        # Guardar en archivo temporal para simular el archivo físico en el servidor
        fd, temp_path = tempfile.mkstemp(suffix='.sql')
        with os.fdopen(fd, 'w', encoding='utf-8') as f:
            f.write(solicitud.archivo_contenido)

        nombre_descarga = solicitud.archivo_nombre or "respaldo.sql"
        archivo_binario = open(temp_path, 'rb')
        response = FileResponse(archivo_binario, content_type='application/sql')
        response['Content-Disposition'] = f'attachment; filename="{nombre_descarga}"'
        return response

    @action(detail=True, methods=['post'])
    def rechazar(self, request, pk=None):
        if hasattr(request.user, 'perfil') and request.user.perfil.rol != 'ADMINISTRADOR':
            return Response({"error": "No autorizado"}, status=403)
            
        from .models import SolicitudBackup, AuditoriaLog
        try:
            solicitud = SolicitudBackup.objects.get(id=pk, atendida=False)
        except SolicitudBackup.DoesNotExist:
            return Response({"error": "La solicitud no existe o ya fue atendida."}, status=404)
            
        solicitud.aprobada = False
        solicitud.atendida = True
        solicitud.save()
        
        AuditoriaLog.objects.create(
            usuario=request.user,
            accion=f"Rechazó solicitud de {solicitud.tipo} de base de datos del usuario {solicitud.usuario.username}"
        )
        return Response({"status": "Solicitud rechazada con éxito."})


class SolicitudCambioAsientoViewSet(viewsets.ViewSet):
    permission_classes = [IsAuthenticated]

    def list(self, request):
        from .models import SolicitudCambioAsiento
        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol not in ['ADMINISTRADOR', 'CONTADOR']:
            return Response({"error": "No autorizado"}, status=403)
            
        solicitudes = SolicitudCambioAsiento.objects.filter(atendida=False).select_related('usuario', 'asiento')
        data = []
        for s in solicitudes:
            import json
            try:
                datos = json.loads(s.datos_nuevos)
            except Exception:
                datos = {}
            data.append({
                "id": s.id,
                "usuario": s.usuario.username,
                "asiento_id": s.asiento.id,
                "glosa_original": s.asiento.glosa,
                "fecha_original": s.asiento.fecha.isoformat(),
                "datos_nuevos": datos,
                "fecha_solicitud": s.fecha_solicitud.isoformat()
            })
        return Response(data)

    @action(detail=True, methods=['post'])
    def aprobar(self, request, pk=None):
        from .models import SolicitudCambioAsiento, Asiento, DetalleAsiento, Cuenta, AuditoriaLog
        from django.db import transaction
        import json
        
        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol not in ['ADMINISTRADOR', 'CONTADOR']:
            return Response({"error": "No autorizado"}, status=403)
            
        try:
            solicitud = SolicitudCambioAsiento.objects.get(id=pk, atendida=False)
        except SolicitudCambioAsiento.DoesNotExist:
            return Response({"error": "La solicitud no existe o ya fue atendida."}, status=404)
            
        check_periodo_cerrado(solicitud.asiento.fecha)
            
        try:
            datos = json.loads(solicitud.datos_nuevos)
        except Exception:
            return Response({"error": "Formato de datos no válido en la solicitud."}, status=400)
            
        nueva_fecha = datos.get('fecha')
        if nueva_fecha:
            check_periodo_cerrado(nueva_fecha)
            
        try:
            with transaction.atomic():
                asiento = solicitud.asiento
                
                # Actualizar cabecera del asiento
                asiento.fecha = datos.get('fecha', asiento.fecha)
                asiento.glosa = datos.get('glosa', asiento.glosa)
                asiento.save()
                
                # Eliminar detalles viejos e insertar los nuevos de la solicitud
                asiento.detalles.all().delete()
                
                detalles = datos.get('detalles', [])
                for d in detalles:
                    cuenta_id = d.get('cuenta')
                    # Soporte por si viene como objeto o string directo
                    if isinstance(cuenta_id, dict):
                        cuenta_id = cuenta_id.get('codigo')
                    
                    cuenta = Cuenta.objects.get(codigo=cuenta_id)
                    DetalleAsiento.objects.create(
                        asiento=asiento,
                        cuenta=cuenta,
                        descripcion=d.get('descripcion', ''),
                        debe=d.get('debe', 0.0),
                        haber=d.get('haber', 0.0)
                    )
                
                solicitud.atendida = True
                solicitud.aprobada = True
                solicitud.save()
                
                AuditoriaLog.objects.create(
                    usuario=request.user,
                    accion=f"Aprobó modificación de asiento histórico #{asiento.id} solicitada por {solicitud.usuario.username}"
                )
                
            return Response({"status": "Solicitud aprobada y asiento contable actualizado correctamente."})
        except Exception as e:
            return Response({"error": f"Error al procesar el cambio: {str(e)}"}, status=500)

    @action(detail=True, methods=['post'])
    def rechazar(self, request, pk=None):
        from .models import SolicitudCambioAsiento, AuditoriaLog
        
        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol not in ['ADMINISTRADOR', 'CONTADOR']:
            return Response({"error": "No autorizado"}, status=403)
            
        try:
            solicitud = SolicitudCambioAsiento.objects.get(id=pk, atendida=False)
        except SolicitudCambioAsiento.DoesNotExist:
            return Response({"error": "La solicitud no existe o ya fue atendida."}, status=404)
            
        solicitud.atendida = True
        solicitud.aprobada = False
        solicitud.save()
        
        AuditoriaLog.objects.create(
            usuario=request.user,
            accion=f"Rechazó solicitud de modificación de asiento histórico #{solicitud.asiento.id} del usuario {solicitud.usuario.username}"
        )
        return Response({"status": "Solicitud rechazada con éxito."})


class DatosGraficosView(APIView):
    permission_classes = [IsAuthenticated, SoloContadorOAdminPermission]

    def get(self, request, *args, **kwargs):
        from django.db.models.functions import ExtractMonth
        from django.db.models import Sum
        from decimal import Decimal
        
        ano_str = request.query_params.get('ano', '2026')
        try:
            ano = int(ano_str)
        except ValueError:
            ano = 2026

        # Calcular ingresos por mes (Clase 4)
        ingresos_por_mes = DetalleAsiento.objects.filter(
            asiento__fecha__year=ano,
            cuenta__codigo__startswith='4'
        ).annotate(
            mes_num=ExtractMonth('asiento__fecha')
        ).values('mes_num').annotate(
            total=Sum('haber') - Sum('debe')
        )

        # Calcular gastos por mes (Clase 6)
        gastos_por_mes = DetalleAsiento.objects.filter(
            asiento__fecha__year=ano,
            cuenta__codigo__startswith='6'
        ).annotate(
            mes_num=ExtractMonth('asiento__fecha')
        ).values('mes_num').annotate(
            total=Sum('debe') - Sum('haber')
        )

        tendencia = {m: {"mes": name, "ingresos": 0.0, "gastos": 0.0} for m, name in enumerate(
            ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"], 1
        )}

        for item in ingresos_por_mes:
            m = item['mes_num']
            if m in tendencia:
                val = item['total'] or Decimal('0.00')
                tendencia[m]['ingresos'] = float(val)

        for item in gastos_por_mes:
            m = item['mes_num']
            if m in tendencia:
                val = item['total'] or Decimal('0.00')
                tendencia[m]['gastos'] = float(val)

        tendencia_mensual = list(tendencia.values())

        # Calcular distribución de gastos (Clase 6) agrupados por la cuenta de nivel 3 (sedes)
        renglones_gastos = DetalleAsiento.objects.filter(
            asiento__fecha__year=ano,
            cuenta__codigo__startswith='6'
        ).select_related('cuenta')

        # Cargar los nombres de las cuentas del catálogo de la clase 6 en memoria para mapear rápidamente
        nombres_clase6 = {c.codigo: c.nombre for c in Cuenta.objects.filter(codigo__startswith='6')}

        datos_agrupados = {}
        for renglon in renglones_gastos:
            cuenta_actual = renglon.cuenta
            codigo_actual = cuenta_actual.codigo
            partes = codigo_actual.split('.')
            
            nombre_grafico = cuenta_actual.nombre
            if len(partes) > 3:
                codigo_madre = '.'.join(partes[:3])
                if codigo_madre in nombres_clase6:
                    nombre_grafico = nombres_clase6[codigo_madre]
            
            monto = float(renglon.debe)
            if monto > 0:
                datos_agrupados[nombre_grafico] = datos_agrupados.get(nombre_grafico, 0) + monto

        distribucion_gastos = [
            {"name": nombre, "value": round(total, 2)}
            for nombre, total in datos_agrupados.items()
        ]

        # Calcular distribución de ingresos (Clase 4) agrupados por la cuenta de nivel 3
        renglones_ingresos = DetalleAsiento.objects.filter(
            asiento__fecha__year=ano,
            cuenta__codigo__startswith='4'
        ).select_related('cuenta')

        nombres_clase4 = {c.codigo: c.nombre for c in Cuenta.objects.filter(codigo__startswith='4')}

        datos_agrupados_ingresos = {}
        for renglon in renglones_ingresos:
            cuenta_actual = renglon.cuenta
            codigo_actual = cuenta_actual.codigo
            partes = codigo_actual.split('.')
            
            nombre_grafico = cuenta_actual.nombre
            if len(partes) > 3:
                codigo_madre = '.'.join(partes[:3])
                if codigo_madre in nombres_clase4:
                    nombre_grafico = nombres_clase4[codigo_madre]
            
            monto = float(renglon.haber)
            if monto > 0:
                datos_agrupados_ingresos[nombre_grafico] = datos_agrupados_ingresos.get(nombre_grafico, 0) + monto

        distribucion_ingresos = [
            {"name": nombre, "value": round(total, 2)}
            for nombre, total in datos_agrupados_ingresos.items()
        ]

        return Response({
            "tendencia_mensual": tendencia_mensual,
            "distribucion_gastos": distribucion_gastos,
            "distribucion_ingresos": distribucion_ingresos
        })


class LibroDiarioExportarExcelView(APIView):
    permission_classes = [IsAuthenticated, ContabilidadPermission]

    def get(self, request, *args, **kwargs):
        from openpyxl import Workbook
        from openpyxl.styles import Font, Alignment, PatternFill
        from django.http import HttpResponse
        from django.db.models import Q
        from datetime import datetime

        # 1. Obtener filtros igual que LibroDiarioView
        modo = request.query_params.get('modo', 'mes')
        tipo = request.query_params.get('tipo', 'general')
        filtros = Q()
        rango_str = ""

        if modo == 'mes':
            mes = request.query_params.get('mes')
            ano = request.query_params.get('ano')
            if not mes or not ano:
                return Response({"error": "Parámetros 'mes' y 'ano' son requeridos."}, status=400)
            filtros &= Q(fecha__year=ano, fecha__month=mes)
            meses_nombres = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
            rango_str = f"Mes: {meses_nombres[int(mes)-1]} {ano}"
        elif modo == 'ano':
            ano = request.query_params.get('ano')
            if not ano:
                return Response({"error": "Parámetro 'ano' es requerido."}, status=400)
            filtros &= Q(fecha__year=ano)
            rango_str = f"Año: {ano}"
        elif modo == 'rango':
            desde = request.query_params.get('desde')
            hasta = request.query_params.get('hasta')
            if not desde or not hasta:
                return Response({"error": "Parámetros 'desde' y 'hasta' son requeridos."}, status=400)
            filtros &= Q(fecha__range=[desde, hasta])
            rango_str = f"Rango: Desde {desde} hasta {hasta}"
        else:
            return Response({"error": "Modo no válido."}, status=400)

        # 2. Consultar asientos
        asientos = Asiento.objects.filter(filtros).order_by('fecha', 'id')

        # 3. Generar Excel
        wb = Workbook()
        ws = wb.active
        ws.title = "Libro Diario"

        # Membrete
        from .models import DatosEmpresa
        empresa = DatosEmpresa.get_solo()
        ws['A1'] = f"{empresa.nombre_empresa.upper()} - RIF: {empresa.rif}"
        ws['A1'].font = Font(bold=True, size=12)
        ws['A2'] = "LIBRO DIARIO GENERAL"
        ws['A2'].font = Font(bold=True, size=14)
        ws['A3'] = rango_str
        ws['A3'].font = Font(italic=True, size=10)

        # Cabecera de Tabla
        headers = ["Fecha", "Código Cuenta", "Nombre Cuenta / Detalle", "Debe", "Haber"]
        ws.append([]) # Fila 4 vacía
        ws.append(headers) # Fila 5

        # Estilo de Cabecera
        header_fill = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid")
        header_font = Font(bold=True)
        for col_num in range(1, 6):
            cell = ws.cell(row=5, column=col_num)
            cell.font = header_font
            cell.fill = header_fill
            if col_num in [4, 5]:
                cell.alignment = Alignment(horizontal="right")

        # Filas de Datos
        row_idx = 6
        total_debe = 0.0
        total_haber = 0.0
        
        for asiento in asientos:
            # Encabezado del Asiento
            ws.cell(row=row_idx, column=1, value=asiento.fecha.strftime('%Y-%m-%d')).font = Font(bold=True)
            cell_asiento = ws.cell(row=row_idx, column=3, value=f"- ASIENTO N° {asiento.id} -")
            cell_asiento.font = Font(bold=True)
            cell_asiento.alignment = Alignment(horizontal="center")
            row_idx += 1

            from types import SimpleNamespace
            from django.db.models import Sum

            if tipo == 'resumido':
                grouped_detalles = asiento.detalles.values(
                    'cuenta__codigo', 'cuenta__nombre'
                ).annotate(
                    total_debe=Sum('debe'),
                    total_haber=Sum('haber')
                ).order_by('cuenta__codigo')
                detalles_list = [
                    SimpleNamespace(
                        cuenta=SimpleNamespace(codigo=g['cuenta__codigo'], nombre=g['cuenta__nombre']),
                        debe=g['total_debe'] or 0.0,
                        haber=g['total_haber'] or 0.0
                    )
                    for g in grouped_detalles
                ]
            else:
                detalles_list = asiento.detalles.all()

            for d in detalles_list:
                ws.cell(row=row_idx, column=2, value=d.cuenta.codigo)
                ws.cell(row=row_idx, column=3, value=f"   {d.cuenta.nombre}" if d.haber > 0 else d.cuenta.nombre)
                
                debe_val = float(d.debe) if d.debe else 0.0
                haber_val = float(d.haber) if d.haber else 0.0
                total_debe += debe_val
                total_haber += haber_val

                if debe_val > 0:
                    c_debe = ws.cell(row=row_idx, column=4, value=debe_val)
                    c_debe.number_format = '#,##0.00'
                if haber_val > 0:
                    c_haber = ws.cell(row=row_idx, column=5, value=haber_val)
                    c_haber.number_format = '#,##0.00'
                row_idx += 1

            # Glosa
            ws.cell(row=row_idx, column=3, value=f"Glosa: {asiento.glosa}").font = Font(italic=True)
            row_idx += 1
            # Fila vacía entre asientos
            row_idx += 1

        # Totales Finales
        ws.cell(row=row_idx, column=3, value="TOTAL GENERAL").font = Font(bold=True)
        c_tot_debe = ws.cell(row=row_idx, column=4, value=total_debe)
        c_tot_debe.font = Font(bold=True)
        c_tot_debe.number_format = '#,##0.00'
        c_tot_haber = ws.cell(row=row_idx, column=5, value=total_haber)
        c_tot_haber.font = Font(bold=True)
        c_tot_haber.number_format = '#,##0.00'

        # Auto-ajustar columnas
        for col in ws.columns:
            max_len = 0
            col_letter = col[0].column_letter
            for cell in col:
                val_str = str(cell.value or '')
                if cell.row in [1, 2, 3]:
                    continue
                if len(val_str) > max_len:
                    max_len = len(val_str)
            ws.column_dimensions[col_letter].width = max(max_len + 3, 12)

        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = f'attachment; filename="libro_diario_{modo}.xlsx"'
        wb.save(response)
        return response


class LibroMayorExportarExcelView(APIView):
    permission_classes = [IsAuthenticated, ContabilidadPermission]

    def get(self, request, *args, **kwargs):
        from openpyxl import Workbook
        from openpyxl.styles import Font, Alignment, PatternFill
        from django.http import HttpResponse
        from decimal import Decimal
        from datetime import date, datetime
        import calendar
        from django.db.models import Sum

        # 1. Recuperar query params igual que LibroMayorView
        modo = request.query_params.get('modo', 'mes')
        nivel_str = request.query_params.get('nivel', '5')
        rango_str = ""

        try:
            nivel = int(nivel_str)
        except ValueError:
            return Response({"error": "El nivel debe ser un número entero."}, status=400)

        if modo == 'mes':
            mes_str = request.query_params.get('mes')
            ano_str = request.query_params.get('ano')
            if not mes_str or not ano_str:
                return Response({"error": "Parámetros 'mes' y 'ano' son requeridos."}, status=400)
            try:
                mes = int(mes_str)
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "Los parámetros deben ser enteros."}, status=400)
            fecha_inicio = date(ano, mes, 1)
            ultimo_dia = calendar.monthrange(ano, mes)[1]
            fecha_fin = date(ano, mes, ultimo_dia)
            meses_nombres = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
            rango_str = f"Mes: {meses_nombres[mes-1]} {ano}"
        elif modo == 'ano':
            ano_str = request.query_params.get('ano')
            if not ano_str:
                return Response({"error": "El parámetro 'ano' es requerido."}, status=400)
            try:
                ano = int(ano_str)
            except ValueError:
                return Response({"error": "El parámetro 'ano' debe ser entero."}, status=400)
            fecha_inicio = date(ano, 1, 1)
            fecha_fin = date(ano, 12, 31)
            rango_str = f"Año: {ano}"
        elif modo == 'rango':
            desde_str = request.query_params.get('desde')
            hasta_str = request.query_params.get('hasta')
            if not desde_str or not hasta_str:
                return Response({"error": "Parámetros 'desde' y 'hasta' son requeridos."}, status=400)
            try:
                fecha_inicio = datetime.strptime(desde_str, '%Y-%m-%d').date()
                fecha_fin = datetime.strptime(hasta_str, '%Y-%m-%d').date()
            except ValueError:
                return Response({"error": "Formato de fecha inválido."}, status=400)
            rango_str = f"Rango: Desde {desde_str} hasta {hasta_str}"
        else:
            return Response({"error": "Modo no válido."}, status=400)

        # 2. Consultar y consolidar cuentas igual que LibroMayorView
        cuentas_db = Cuenta.objects.all().order_by('codigo')
        cuentas_data = {}
        for c in cuentas_db:
            cuentas_data[c.codigo] = {
                "codigo": c.codigo,
                "nombre": c.nombre,
                "movimiento": c.movimiento,
                "tipo_saldo": c.tipo_saldo,
                "nivel_profundidad": len(c.codigo.split('.')),
                "saldo_inicial": Decimal('0.00'),
                "debe": Decimal('0.00'),
                "haber": Decimal('0.00'),
                "saldo_final": Decimal('0.00')
            }

        # Saldos iniciales
        detalles_inicial = DetalleAsiento.objects.filter(asiento__fecha__lt=fecha_inicio).values('cuenta_id').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )
        for det in detalles_inicial:
            c_codigo = det['cuenta_id']
            if c_codigo in cuentas_data:
                cuentas_data[c_codigo]["saldo_inicial_debe"] = det['sum_debe'] or Decimal('0.00')
                cuentas_data[c_codigo]["saldo_inicial_haber"] = det['sum_haber'] or Decimal('0.00')

        # Movimientos período
        detalles_rango = DetalleAsiento.objects.filter(asiento__fecha__range=[fecha_inicio, fecha_fin])
        for d in detalles_rango:
            c_codigo = d.cuenta_id
            if c_codigo in cuentas_data:
                cuentas_data[c_codigo]["debe"] += d.debe
                cuentas_data[c_codigo]["haber"] += d.haber

        # Bubble up
        codigos_ordenados_inverso = sorted(cuentas_data.keys(), key=lambda x: cuentas_data[x]['nivel_profundidad'], reverse=True)
        for codigo in codigos_ordenados_inverso:
            datos = cuentas_data[codigo]
            si_debe = datos.get("saldo_inicial_debe", Decimal('0.00'))
            si_haber = datos.get("saldo_inicial_haber", Decimal('0.00'))
            
            if datos["tipo_saldo"] == "DEBE":
                datos["saldo_inicial"] = si_debe - si_haber
            else:
                datos["saldo_inicial"] = si_haber - si_debe
                
            partes = codigo.split('.')
            if len(partes) > 1:
                padre_codigo = '.'.join(partes[:-1])
                if padre_codigo in cuentas_data:
                    padre_datos = cuentas_data[padre_codigo]
                    padre_datos.setdefault("saldo_inicial_debe", Decimal('0.00'))
                    padre_datos.setdefault("saldo_inicial_haber", Decimal('0.00'))
                    padre_datos["saldo_inicial_debe"] += si_debe
                    padre_datos["saldo_inicial_haber"] += si_haber
                    padre_datos["debe"] += datos["debe"]
                    padre_datos["haber"] += datos["haber"]

        # Saldo final
        for codigo, datos in cuentas_data.items():
            if datos["tipo_saldo"] == "DEBE":
                datos["saldo_final"] = datos["saldo_inicial"] + datos["debe"] - datos["haber"]
            else:
                datos["saldo_final"] = datos["saldo_inicial"] + datos["haber"] - datos["debe"]

        # Generar Excel
        wb = Workbook()
        ws = wb.active
        ws.title = "Libro Mayor"

        tipo = request.query_params.get('tipo', 'general')

        # Membrete
        from .models import DatosEmpresa
        empresa = DatosEmpresa.get_solo()
        ws['A1'] = f"{empresa.nombre_empresa.upper()} - RIF: {empresa.rif}"
        ws['A1'].font = Font(bold=True, size=12)
        ws['A2'] = "LIBRO MAYOR DETALLADO" if tipo == 'detallado' else "LIBRO MAYOR ANALÍTICO"
        ws['A2'].font = Font(bold=True, size=14)
        ws['A3'] = f"{rango_str} | Nivel de Detalle: {nivel}" if tipo == 'general' else rango_str
        ws['A3'].font = Font(italic=True, size=10)

        if tipo == 'general':
            headers = ["Código", "Nombre Cuenta", "Saldo Inicial", "Debe", "Haber", "Saldo Final"]
            ws.append([]) # Fila 4 vacía
            ws.append(headers) # Fila 5

            header_fill = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid")
            header_font = Font(bold=True)
            for col_num in range(1, 7):
                cell = ws.cell(row=5, column=col_num)
                cell.font = header_font
                cell.fill = header_fill
                if col_num in [3, 4, 5, 6]:
                    cell.alignment = Alignment(horizontal="right")

            # Filas de datos
            row_idx = 6
            for codigo in sorted(cuentas_data.keys()):
                datos = cuentas_data[codigo]
                if datos["nivel_profundidad"] <= nivel:
                    if datos["saldo_inicial"] != 0 or datos["debe"] != 0 or datos["haber"] != 0 or datos["saldo_final"] != 0:
                        ws.cell(row=row_idx, column=1, value=datos["codigo"])
                        
                        # Identación visual según profundidad
                        indent = "    " * (datos["nivel_profundidad"] - 1)
                        ws.cell(row=row_idx, column=2, value=f"{indent}{datos['nombre']}")
                        
                        c_si = ws.cell(row=row_idx, column=3, value=float(datos["saldo_inicial"]))
                        c_si.number_format = '#,##0.00'
                        c_d = ws.cell(row=row_idx, column=4, value=float(datos["debe"]))
                        c_d.number_format = '#,##0.00'
                        c_h = ws.cell(row=row_idx, column=5, value=float(datos["haber"]))
                        c_h.number_format = '#,##0.00'
                        c_sf = ws.cell(row=row_idx, column=6, value=float(datos["saldo_final"]))
                        c_sf.number_format = '#,##0.00'

                        if datos["nivel_profundidad"] == 1:
                            ws.cell(row=row_idx, column=1).font = Font(bold=True)
                            ws.cell(row=row_idx, column=2).font = Font(bold=True)
                            c_si.font = Font(bold=True)
                            c_d.font = Font(bold=True)
                            c_h.font = Font(bold=True)
                            c_sf.font = Font(bold=True)

                        row_idx += 1
        else: # detallado
            # Consultamos todos los detalles con sus asientos para la lógica de transacciones
            # Ya que en la sección general no consultamos los objetos DetalleAsiento de forma individual en cuentas_data,
            # pero para el detallado necesitamos agruparlos igual que LibroMayorView.
            detalles_rango = DetalleAsiento.objects.filter(asiento__fecha__range=[fecha_inicio, fecha_fin]).select_related('asiento')
            
            # Limpiar movimientos por mes anteriores para cargarlos limpios
            for codigo in cuentas_data:
                cuentas_data[codigo]["movimientos_mes"] = []
                
            for d in detalles_rango:
                c_codigo = d.cuenta_id
                if c_codigo in cuentas_data:
                    cuentas_data[c_codigo]["movimientos_mes"].append({
                        "fecha": d.asiento.fecha,
                        "asiento_id": d.asiento.id,
                        "glosa": d.asiento.glosa,
                        "descripcion": d.descripcion,
                        "debe": d.debe,
                        "haber": d.haber
                    })
                    
            row_idx = 5
            for codigo in sorted(cuentas_data.keys()):
                datos = cuentas_data[codigo]
                if datos["movimiento"] != 'SI':
                    continue
                if datos["saldo_inicial"] != 0 or datos["debe"] != 0 or datos["haber"] != 0 or datos["saldo_final"] != 0:
                    ws.cell(row=row_idx, column=1, value="") # empty spacer
                    row_idx += 1
                    
                    # Header row for the account
                    ws.merge_cells(start_row=row_idx, start_column=1, end_row=row_idx, end_column=7)
                    cell_acc = ws.cell(row=row_idx, column=1, value=f"{datos['codigo']} - {datos['nombre']} ({datos['tipo_saldo']})")
                    cell_acc.font = Font(bold=True, size=11)
                    cell_acc.fill = PatternFill(start_color="E2E8F0", end_color="E2E8F0", fill_type="solid")
                    row_idx += 1
                    
                    # Columns Headers for detailed movements
                    headers = ["Fecha", "Asiento ID", "Glosa", "Descripción", "Debe", "Haber", "Saldo Acumulado"]
                    for col_num, h_text in enumerate(headers, 1):
                        cell = ws.cell(row=row_idx, column=col_num, value=h_text)
                        cell.font = Font(bold=True, size=9)
                        cell.fill = PatternFill(start_color="F8FAFC", end_color="F8FAFC", fill_type="solid")
                        if col_num in [5, 6, 7]:
                            cell.alignment = Alignment(horizontal="right")
                    row_idx += 1
                    
                    # Saldo Inicial Row
                    ws.cell(row=row_idx, column=3, value="SALDO INICIAL ANTES DEL PERÍODO").font = Font(italic=True)
                    c_si = ws.cell(row=row_idx, column=7, value=float(datos["saldo_inicial"]))
                    c_si.number_format = '#,##0.00'
                    c_si.font = Font(bold=True, italic=True)
                    row_idx += 1
                    
                    # Transactions
                    movs_ordenados = sorted(datos["movimientos_mes"], key=lambda x: (x['fecha'], x['asiento_id']))
                    saldo_acumulado = datos["saldo_inicial"]
                    for m in movs_ordenados:
                        m_debe = m["debe"]
                        m_haber = m["haber"]
                        if datos["tipo_saldo"] == "DEBE":
                            saldo_acumulado += (m_debe - m_haber)
                        else:
                            saldo_acumulado += (m_haber - m_debe)
                            
                        ws.cell(row=row_idx, column=1, value=m["fecha"].strftime('%Y-%m-%d') if hasattr(m["fecha"], 'strftime') else str(m["fecha"]))
                        ws.cell(row=row_idx, column=2, value=f"#{m['asiento_id']}")
                        ws.cell(row=row_idx, column=3, value=m["glosa"])
                        ws.cell(row=row_idx, column=4, value=m["descripcion"] or "-")
                        
                        if m_debe > 0:
                            ws.cell(row=row_idx, column=5, value=float(m_debe)).number_format = '#,##0.00'
                        if m_haber > 0:
                            ws.cell(row=row_idx, column=6, value=float(m_haber)).number_format = '#,##0.00'
                        ws.cell(row=row_idx, column=7, value=float(saldo_acumulado)).number_format = '#,##0.00'
                        row_idx += 1
                        
                    # Totals
                    ws.cell(row=row_idx, column=3, value="CIERRE DE CUENTA").font = Font(bold=True)
                    ws.cell(row=row_idx, column=5, value=float(datos["debe"])).number_format = '#,##0.00'
                    ws.cell(row=row_idx, column=5).font = Font(bold=True)
                    ws.cell(row=row_idx, column=6, value=float(datos["haber"])).number_format = '#,##0.00'
                    ws.cell(row=row_idx, column=6).font = Font(bold=True)
                    ws.cell(row=row_idx, column=7, value=float(datos["saldo_final"])).number_format = '#,##0.00'
                    ws.cell(row=row_idx, column=7).font = Font(bold=True)
                    row_idx += 1

        # Auto-ajustar columnas
        for col in ws.columns:
            max_len = 0
            col_letter = col[0].column_letter
            for cell in col:
                val_str = str(cell.value or '')
                if cell.row in [1, 2, 3]:
                    continue
                if len(val_str) > max_len:
                    max_len = len(val_str)
            ws.column_dimensions[col_letter].width = max(max_len + 3, 12)

        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = f'attachment; filename="libro_mayor_{tipo}_{modo}.xlsx"'
        wb.save(response)
        return response


class PlanCuentasExportarExcelView(APIView):
    permission_classes = [IsAuthenticated, ContabilidadPermission]

    def get(self, request, *args, **kwargs):
        from openpyxl import Workbook
        from openpyxl.styles import Font, Alignment, PatternFill
        from django.http import HttpResponse

        cuentas = Cuenta.objects.all().order_by('codigo')

        wb = Workbook()
        ws = wb.active
        ws.title = "Plan de Cuentas"

        # Membrete
        from .models import DatosEmpresa
        empresa = DatosEmpresa.get_solo()
        ws['A1'] = f"{empresa.nombre_empresa.upper()} - RIF: {empresa.rif}"
        ws['A1'].font = Font(bold=True, size=12)
        ws['A2'] = "PLAN DE CUENTAS OFICIAL"
        ws['A2'].font = Font(bold=True, size=14)

        headers = ["Código", "Descripción / Nombre", "Recibe Movimiento", "Tipo de Saldo", "Estado"]
        ws.append([]) # Fila 3 vacía
        ws.append(headers) # Fila 4

        header_fill = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid")
        header_font = Font(bold=True)
        for col_num in range(1, 6):
            cell = ws.cell(row=4, column=col_num)
            cell.font = header_font
            cell.fill = header_fill
            if col_num in [3, 4, 5]:
                cell.alignment = Alignment(horizontal="center")

        row_idx = 5
        for c in cuentas:
            depth = len(c.codigo.split('.'))
            indent = "    " * (depth - 1)
            
            ws.cell(row=row_idx, column=1, value=c.codigo)
            ws.cell(row=row_idx, column=2, value=f"{indent}{c.nombre}")
            
            c_mov = ws.cell(row=row_idx, column=3, value=c.movimiento)
            c_mov.alignment = Alignment(horizontal="center")
            
            c_saldo = ws.cell(row=row_idx, column=4, value=c.tipo_saldo)
            c_saldo.alignment = Alignment(horizontal="center")
            
            estado_str = "Activa" if c.activa else "Inactiva"
            c_est = ws.cell(row=row_idx, column=5, value=estado_str)
            c_est.alignment = Alignment(horizontal="center")

            if c.movimiento == 'NO':
                ws.cell(row=row_idx, column=1).font = Font(bold=True)
                ws.cell(row=row_idx, column=2).font = Font(bold=True)
                c_mov.font = Font(bold=True)
                c_saldo.font = Font(bold=True)
                c_est.font = Font(bold=True)

            row_idx += 1

        # Auto-ajustar columnas
        for col in ws.columns:
            max_len = 0
            col_letter = col[0].column_letter
            for cell in col:
                val_str = str(cell.value or '')
                if cell.row in [1, 2]:
                    continue
                if len(val_str) > max_len:
                    max_len = len(val_str)
            ws.column_dimensions[col_letter].width = max(max_len + 3, 12)

        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = 'attachment; filename="plan_de_cuentas.xlsx"'
        wb.save(response)
        return response


class EstadoResultadosExportarExcelView(APIView):
    permission_classes = [IsAuthenticated, SoloContadorOAdminPermission]

    def get(self, request, *args, **kwargs):
        from openpyxl import Workbook
        from openpyxl.styles import Font, Alignment, PatternFill
        from django.http import HttpResponse

        # 1. Obtener datos llamando al view principal
        er_view = EstadoResultadosView()
        er_response = er_view.get(request)
        if er_response.status_code != 200:
            return er_response

        data = er_response.data
        periodo = data.get('periodo', {})
        modo = periodo.get('modo', 'mes')
        ano = periodo.get('ano', 2026)
        mes = periodo.get('mes', 1)

        meses_nombres = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
        if modo == 'mes':
            rango_str = f"Período: {meses_nombres[int(mes)-1]} {ano}"
        elif modo == 'ano':
            rango_str = f"Período: Año {ano}"
        elif modo == 'rango':
            desde = request.query_params.get('desde', '')
            hasta = request.query_params.get('hasta', '')
            rango_str = f"Período: Desde {desde} hasta {hasta}"
        else:
            rango_str = ""

        wb = Workbook()
        ws = wb.active
        ws.title = "Estado de Resultados"

        # Membrete institucional
        ws['A1'] = "FONJUPAD"
        ws['A1'].font = Font(bold=True, size=12)
        ws['A2'] = "ESTADO DE RESULTADOS"
        ws['A2'].font = Font(bold=True, size=14)
        ws['A3'] = rango_str
        ws['A3'].font = Font(italic=True, size=10)

        headers = ["Código", "Cuenta / Descripción", "Debe", "Haber", "Saldo Acumulado"]
        ws.append([]) # Fila 4 vacía
        ws.append(headers) # Fila 5

        header_fill = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid")
        header_font = Font(bold=True)
        for col_num in range(1, 6):
            cell = ws.cell(row=5, column=col_num)
            cell.font = header_font
            cell.fill = header_fill
            if col_num in [3, 4, 5]:
                cell.alignment = Alignment(horizontal="right")

        row_idx = 6

        def append_section(title, accounts, section_total):
            nonlocal row_idx
            # Título de Sección
            ws.cell(row=row_idx, column=1, value="").font = Font(bold=True)
            ws.cell(row=row_idx, column=2, value=title).font = Font(bold=True)
            ws.cell(row=row_idx, column=5, value=float(section_total)).font = Font(bold=True)
            ws.cell(row=row_idx, column=5).number_format = '#,##0.00'
            ws.cell(row=row_idx, column=5).alignment = Alignment(horizontal="right")
            row_idx += 1

            for acc in accounts:
                depth = acc.get('nivel_profundidad', 1)
                indent = "    " * (depth - 1)
                
                ws.cell(row=row_idx, column=1, value=acc['codigo'])
                ws.cell(row=row_idx, column=2, value=f"{indent}{acc['nombre']}")
                
                c_debe = ws.cell(row=row_idx, column=3, value=float(acc['debe']))
                c_debe.number_format = '#,##0.00'
                c_debe.alignment = Alignment(horizontal="right")
                
                c_haber = ws.cell(row=row_idx, column=4, value=float(acc['haber']))
                c_haber.number_format = '#,##0.00'
                c_haber.alignment = Alignment(horizontal="right")
                
                c_saldo = ws.cell(row=row_idx, column=5, value=float(acc['saldo']))
                c_saldo.number_format = '#,##0.00'
                c_saldo.alignment = Alignment(horizontal="right")

                if depth == 1:
                    ws.cell(row=row_idx, column=1).font = Font(bold=True)
                    ws.cell(row=row_idx, column=2).font = Font(bold=True)
                    c_debe.font = Font(bold=True)
                    c_haber.font = Font(bold=True)
                    c_saldo.font = Font(bold=True)
                
                row_idx += 1
            row_idx += 1 # Espacio vacío

        # Ingresos
        append_section("INGRESOS OPERACIONALES", data.get('ingresos', []), data.get('total_ingresos', 0))

        # Costos
        if float(data.get('total_costos', 0)) > 0:
            append_section("COSTOS", data.get('costos', []), data.get('total_costos', 0))
            # Utilidad Bruta
            ws.cell(row=row_idx, column=2, value="UTILIDAD BRUTA").font = Font(bold=True)
            utilidad_bruta = float(data.get('total_ingresos', 0)) - float(data.get('total_costos', 0))
            ws.cell(row=row_idx, column=5, value=utilidad_bruta).font = Font(bold=True)
            ws.cell(row=row_idx, column=5).number_format = '#,##0.00'
            ws.cell(row=row_idx, column=5).alignment = Alignment(horizontal="right")
            row_idx += 2

        # Gastos
        append_section("GASTOS OPERACIONALES", data.get('gastos', []), data.get('total_gastos', 0))

        # Utilidad Neta
        ws.cell(row=row_idx, column=2, value="UTILIDAD / PÉRDIDA NETA DEL EJERCICIO").font = Font(bold=True)
        ws.cell(row=row_idx, column=5, value=float(data.get('utilidad_neta', 0))).font = Font(bold=True)
        ws.cell(row=row_idx, column=5).number_format = '#,##0.00'
        ws.cell(row=row_idx, column=5).alignment = Alignment(horizontal="right")

        # Auto-ajustar columnas
        for col in ws.columns:
            max_len = 0
            col_letter = col[0].column_letter
            for cell in col:
                val_str = str(cell.value or '')
                if cell.row in [1, 2, 3]:
                    continue
                if len(val_str) > max_len:
                    max_len = len(val_str)
            ws.column_dimensions[col_letter].width = max(max_len + 3, 12)

        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = f'attachment; filename="estado_resultados_{modo}.xlsx"'
        wb.save(response)
        return response


class EstadoSituacionExportarExcelView(APIView):
    permission_classes = [IsAuthenticated, SoloContadorOAdminPermission]

    def get(self, request, *args, **kwargs):
        from openpyxl import Workbook
        from openpyxl.styles import Font, Alignment, PatternFill
        from django.http import HttpResponse

        # 1. Obtener datos llamando al view principal
        es_view = EstadoSituacionView()
        es_response = es_view.get(request)
        if es_response.status_code != 200:
            return es_response

        data = es_response.data
        periodo = data.get('periodo', {})
        modo = periodo.get('modo', 'mes')
        ano = periodo.get('ano', 2026)
        mes = periodo.get('mes', 1)

        meses_nombres = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
        if modo == 'mes':
            rango_str = f"Al: {meses_nombres[int(mes)-1]} {ano}"
        elif modo == 'ano':
            rango_str = f"Al: Año {ano}"
        elif modo == 'rango':
            hasta = request.query_params.get('hasta', '')
            rango_str = f"Al: {hasta}"
        else:
            rango_str = ""

        wb = Workbook()
        ws = wb.active
        ws.title = "Balance General"

        # Membrete institucional
        ws['A1'] = "FONJUPAD"
        ws['A1'].font = Font(bold=True, size=12)
        ws['A2'] = "ESTADO DE SITUACIÓN FINANCIERA"
        ws['A2'].font = Font(bold=True, size=14)
        ws['A3'] = rango_str
        ws['A3'].font = Font(italic=True, size=10)

        headers = ["Código", "Cuenta / Descripción", "Debe", "Haber", "Saldo"]
        ws.append([]) # Fila 4 vacía
        ws.append(headers) # Fila 5

        header_fill = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid")
        header_font = Font(bold=True)
        for col_num in range(1, 6):
            cell = ws.cell(row=5, column=col_num)
            cell.font = header_font
            cell.fill = header_fill
            if col_num in [3, 4, 5]:
                cell.alignment = Alignment(horizontal="right")

        row_idx = 6

        def append_section(title, accounts, section_total):
            nonlocal row_idx
            ws.cell(row=row_idx, column=2, value=title).font = Font(bold=True)
            ws.cell(row=row_idx, column=5, value=float(section_total)).font = Font(bold=True)
            ws.cell(row=row_idx, column=5).number_format = '#,##0.00'
            ws.cell(row=row_idx, column=5).alignment = Alignment(horizontal="right")
            row_idx += 1

            for acc in accounts:
                depth = acc.get('nivel_profundidad', 1)
                indent = "    " * (depth - 1)
                
                ws.cell(row=row_idx, column=1, value=acc['codigo'])
                ws.cell(row=row_idx, column=2, value=f"{indent}{acc['nombre']}")
                
                c_debe = ws.cell(row=row_idx, column=3, value=float(acc['debe']))
                c_debe.number_format = '#,##0.00'
                c_debe.alignment = Alignment(horizontal="right")
                
                c_haber = ws.cell(row=row_idx, column=4, value=float(acc['haber']))
                c_haber.number_format = '#,##0.00'
                c_haber.alignment = Alignment(horizontal="right")
                
                c_saldo = ws.cell(row=row_idx, column=5, value=float(acc['saldo']))
                c_saldo.number_format = '#,##0.00'
                c_saldo.alignment = Alignment(horizontal="right")

                if depth <= 2:
                    ws.cell(row=row_idx, column=1).font = Font(bold=True)
                    ws.cell(row=row_idx, column=2).font = Font(bold=True)
                    c_debe.font = Font(bold=True)
                    c_haber.font = Font(bold=True)
                    c_saldo.font = Font(bold=True)
                
                row_idx += 1
            row_idx += 1

        # 1. Activos
        ws.cell(row=row_idx, column=2, value="1. ACTIVOS").font = Font(bold=True, size=11)
        row_idx += 1
        append_section("ACTIVOS CORRIENTES", data.get('activos_corrientes', []), data.get('total_activos_corrientes', 0))
        append_section("ACTIVOS NO CORRIENTES", data.get('activos_no_corrientes', []), data.get('total_activos_no_corrientes', 0))

        # Total Activos
        ws.cell(row=row_idx, column=2, value="TOTAL ACTIVOS").font = Font(bold=True)
        ws.cell(row=row_idx, column=5, value=float(data.get('total_activos', 0))).font = Font(bold=True)
        ws.cell(row=row_idx, column=5).number_format = '#,##0.00'
        ws.cell(row=row_idx, column=5).alignment = Alignment(horizontal="right")
        row_idx += 2

        # 2. Pasivos y Patrimonio
        ws.cell(row=row_idx, column=2, value="2. PASIVOS Y PATRIMONIO").font = Font(bold=True, size=11)
        row_idx += 1
        append_section("PASIVOS CORRIENTES", data.get('pasivos_corrientes', []), data.get('total_pasivos_corrientes', 0))
        append_section("PASIVOS NO CORRIENTES", data.get('pasivos_no_corrientes', []), data.get('total_pasivos_no_corrientes', 0))
        append_section("3. PATRIMONIO", data.get('patrimonio', []), data.get('total_patrimonio', 0))

        # Total Pasivos y Patrimonio
        ws.cell(row=row_idx, column=2, value="TOTAL PASIVO Y PATRIMONIO").font = Font(bold=True)
        ws.cell(row=row_idx, column=5, value=float(data.get('total_pasivo_patrimonio', 0))).font = Font(bold=True)
        ws.cell(row=row_idx, column=5).number_format = '#,##0.00'
        ws.cell(row=row_idx, column=5).alignment = Alignment(horizontal="right")

        # Auto-ajustar columnas
        for col in ws.columns:
            max_len = 0
            col_letter = col[0].column_letter
            for cell in col:
                val_str = str(cell.value or '')
                if cell.row in [1, 2, 3]:
                    continue
                if len(val_str) > max_len:
                    max_len = len(val_str)
            ws.column_dimensions[col_letter].width = max(max_len + 3, 12)

        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = f'attachment; filename="estado_situacion_{modo}.xlsx"'
        wb.save(response)
        return response


class EstadoEvolucionPatrimonioExportarExcelView(APIView):
    permission_classes = [IsAuthenticated, SoloContadorOAdminPermission]

    def get(self, request, *args, **kwargs):
        from openpyxl import Workbook
        from openpyxl.styles import Font, Alignment, PatternFill
        from django.http import HttpResponse

        # 1. Obtener datos llamando al view principal (EstadoSituacionView) que contiene el cálculo del patrimonio
        es_view = EstadoSituacionView()
        es_response = es_view.get(request)
        if es_response.status_code != 200:
            return es_response

        data = es_response.data
        periodo = data.get('periodo', {})
        modo = periodo.get('modo', 'mes')
        ano = periodo.get('ano', 2026)
        mes = periodo.get('mes', 1)

        meses_nombres = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]
        if modo == 'mes':
            rango_str = f"Período: {meses_nombres[int(mes)-1]} {ano}"
        elif modo == 'ano':
            rango_str = f"Período: Año {ano}"
        elif modo == 'rango':
            desde = request.query_params.get('desde', '')
            hasta = request.query_params.get('hasta', '')
            rango_str = f"Período: Desde {desde} hasta {hasta}"
        else:
            rango_str = ""

        wb = Workbook()
        ws = wb.active
        ws.title = "Evolución Patrimonio"

        # Membrete institucional
        ws['A1'] = "FONJUPAD"
        ws['A1'].font = Font(bold=True, size=12)
        ws['A2'] = "ESTADO DE EVOLUCIÓN DEL PATRIMONIO"
        ws['A2'].font = Font(bold=True, size=14)
        ws['A3'] = rango_str
        ws['A3'].font = Font(italic=True, size=10)

        headers = ["Código", "Concepto / Cuenta", "Saldo Inicial", "Aumentos/Disminuciones", "Utilidad del Período", "Saldo Final"]
        ws.append([]) # Fila 4 vacía
        ws.append(headers) # Fila 5

        header_fill = PatternFill(start_color="F1F5F9", end_color="F1F5F9", fill_type="solid")
        header_font = Font(bold=True)
        for col_num in range(1, 7):
            cell = ws.cell(row=5, column=col_num)
            cell.font = header_font
            cell.fill = header_fill
            if col_num in [3, 4, 5, 6]:
                cell.alignment = Alignment(horizontal="right")

        row_idx = 6
        accounts = data.get('estado_patrimonio', [])

        for acc in accounts:
            depth = acc.get('nivel_profundidad', 1)
            indent = "    " * (depth - 1)
            
            ws.cell(row=row_idx, column=1, value=acc['codigo'])
            ws.cell(row=row_idx, column=2, value=f"{indent}{acc['nombre']}")
            
            c_ini = ws.cell(row=row_idx, column=3, value=float(acc['saldo_inicial']))
            c_ini.number_format = '#,##0.00'
            c_ini.alignment = Alignment(horizontal="right")
            
            c_var = ws.cell(row=row_idx, column=4, value=float(acc['variaciones']))
            c_var.number_format = '#,##0.00'
            c_var.alignment = Alignment(horizontal="right")
            
            c_util = ws.cell(row=row_idx, column=5, value=float(acc['utilidad_periodo']))
            c_util.number_format = '#,##0.00'
            c_util.alignment = Alignment(horizontal="right")
            
            c_fin = ws.cell(row=row_idx, column=6, value=float(acc['saldo_final']))
            c_fin.number_format = '#,##0.00'
            c_fin.alignment = Alignment(horizontal="right")

            if depth == 1 or acc['codigo'] == '3.99.99':
                ws.cell(row=row_idx, column=1).font = Font(bold=True)
                ws.cell(row=row_idx, column=2).font = Font(bold=True)
                c_ini.font = Font(bold=True)
                c_var.font = Font(bold=True)
                c_util.font = Font(bold=True)
                c_fin.font = Font(bold=True)
            
            row_idx += 1

        # Agregar fila de Totales
        ws.cell(row=row_idx, column=2, value="TOTAL PATRIMONIO NETO").font = Font(bold=True)
        
        tot_ini = ws.cell(row=row_idx, column=3, value=float(data.get('total_inicial_pat', 0)))
        tot_ini.font = Font(bold=True)
        tot_ini.number_format = '#,##0.00'
        tot_ini.alignment = Alignment(horizontal="right")
        
        tot_var = ws.cell(row=row_idx, column=4, value=float(data.get('total_variaciones_pat', 0)))
        tot_var.font = Font(bold=True)
        tot_var.number_format = '#,##0.00'
        tot_var.alignment = Alignment(horizontal="right")
        
        tot_util = ws.cell(row=row_idx, column=5, value=float(data.get('total_utilidad_pat', 0)))
        tot_util.font = Font(bold=True)
        tot_util.number_format = '#,##0.00'
        tot_util.alignment = Alignment(horizontal="right")
        
        tot_fin = ws.cell(row=row_idx, column=6, value=float(data.get('total_final_pat', 0)))
        tot_fin.font = Font(bold=True)
        tot_fin.number_format = '#,##0.00'
        tot_fin.alignment = Alignment(horizontal="right")

        # Auto-ajustar columnas
        for col in ws.columns:
            max_len = 0
            col_letter = col[0].column_letter
            for cell in col:
                val_str = str(cell.value or '')
                if cell.row in [1, 2, 3]:
                    continue
                if len(val_str) > max_len:
                    max_len = len(val_str)
            ws.column_dimensions[col_letter].width = max(max_len + 3, 12)

        response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
        response['Content-Disposition'] = f'attachment; filename="estado_evolucion_patrimonio_{modo}.xlsx"'
        wb.save(response)
        return response


class CierreContablePrevisualizarView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        from decimal import Decimal
        from django.db.models import Sum
        from datetime import datetime, date, timedelta
        from .models import Asiento, PeriodoContable, DetalleAsiento

        inicio_str = request.query_params.get('inicio')
        fin_str = request.query_params.get('fin')

        if not inicio_str or not fin_str:
            hoy = date.today()
            fecha_inicio = date(hoy.year, 1, 1)
            fecha_fin = date(hoy.year, 12, 31)
        else:
            try:
                fecha_inicio = datetime.strptime(inicio_str, '%Y-%m-%d').date()
                fecha_fin = datetime.strptime(fin_str, '%Y-%m-%d').date()
            except ValueError:
                return Response({"error": "Las fechas deben estar en formato AAAA-MM-DD."}, status=status.HTTP_400_BAD_REQUEST)

        # a) El rango de fechas seleccionado para un cierre no puede ser mayor a 365 días (un año)
        if (fecha_fin - fecha_inicio).days > 365:
            return Response({"error": "El rango de fechas seleccionado para un cierre no puede ser mayor a 365 días (un año)."}, status=status.HTTP_400_BAD_REQUEST)

        # b) La fecha_inicio del nuevo cierre debe coincidir exactamente con el día siguiente al último cierre registrado
        ultimo_cierre = PeriodoContable.objects.filter(estado='CERRADO').order_by('-fecha_fin').first()
        if ultimo_cierre and ultimo_cierre.fecha_fin:
            expected_start = ultimo_cierre.fecha_fin + timedelta(days=1)
            if fecha_inicio != expected_start:
                return Response({
                    "error": f"La fecha de inicio debe ser exactamente {expected_start.strftime('%d/%m/%Y')} (el día siguiente al último cierre registrado)."
                }, status=status.HTTP_400_BAD_REQUEST)

        # c) Bloqueo en Cascada: existen asientos en fechas anteriores que no han sido cerrados?
        closed_periods = PeriodoContable.objects.filter(estado='CERRADO')
        asientos_pasados = Asiento.objects.filter(fecha__lt=fecha_inicio, estado='APROBADO')
        for period in closed_periods:
            if period.fecha_inicio and period.fecha_fin:
                asientos_pasados = asientos_pasados.exclude(fecha__range=(period.fecha_inicio, period.fecha_fin))
            else:
                asientos_pasados = asientos_pasados.exclude(fecha__year=period.ano)

        if asientos_pasados.exists():
            fechas_huerfanas = list(asientos_pasados.values_list('fecha', flat=True).distinct()[:5])
            fechas_str = ", ".join([f.strftime('%d/%m/%Y') for f in fechas_huerfanas])
            return Response({
                "error": f"Existen asientos aprobados en fechas pasadas que no han sido incluidos en ningún cierre contable anterior. Realice los cierres en orden cronológico."
            }, status=status.HTTP_400_BAD_REQUEST)

        tiene_borradores = Asiento.objects.filter(fecha__range=(fecha_inicio, fecha_fin), estado='BORRADOR').exists()

        # check if this range overlaps with any closed period
        periodo_cerrado = False
        for p in closed_periods:
            if p.fecha_inicio and p.fecha_fin:
                if not (fecha_fin < p.fecha_inicio or fecha_inicio > p.fecha_fin):
                    periodo_cerrado = True
                    break
            else:
                if fecha_inicio.year <= p.ano <= fecha_fin.year:
                    periodo_cerrado = True
                    break

        # Sumar ingresos y gastos (sólo de asientos aprobados)
        detalles = DetalleAsiento.objects.filter(
            asiento__fecha__range=(fecha_inicio, fecha_fin),
            asiento__estado='APROBADO',
            cuenta__movimiento='SI',
            cuenta__codigo__regex=r'^[456]'
        ).values('cuenta_id', 'cuenta__tipo_saldo').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )

        total_ingresos = Decimal('0.00')
        total_gastos = Decimal('0.00')

        for d in detalles:
            code = d['cuenta_id']
            debe = d['sum_debe'] or Decimal('0.00')
            haber = d['sum_haber'] or Decimal('0.00')

            if code.startswith('4'):
                total_ingresos += (haber - debe)
            elif code.startswith('5') or code.startswith('6'):
                total_gastos += (debe - haber)

        utilidad_neta = total_ingresos - total_gastos

        return Response({
            "fecha_inicio": fecha_inicio.strftime('%Y-%m-%d'),
            "fecha_fin": fecha_fin.strftime('%Y-%m-%d'),
            "total_ingresos": float(total_ingresos),
            "total_gastos": float(total_gastos),
            "utilidad_neta": float(utilidad_neta),
            "tiene_borradores": tiene_borradores,
            "periodo_cerrado": periodo_cerrado
        }, status=status.HTTP_200_OK)


class CierreContableEjecutarView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request, *args, **kwargs):
        from decimal import Decimal
        from datetime import date, datetime, timedelta
        from django.db import transaction
        from django.db.models import Sum
        from django.utils import timezone
        from .models import Cuenta, Asiento, DetalleAsiento, PeriodoContable, AuditoriaLog

        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol not in ['ADMINISTRADOR', 'CONTADOR']:
            return Response({"error": "No autorizado para ejecutar el cierre contable."}, status=status.HTTP_403_FORBIDDEN)

        inicio_str = request.data.get('fecha_inicio')
        fin_str = request.data.get('fecha_fin')

        if not inicio_str or not fin_str:
            return Response({"error": "Los campos 'fecha_inicio' y 'fecha_fin' son requeridos."}, status=status.HTTP_400_BAD_REQUEST)

        try:
            fecha_inicio = datetime.strptime(inicio_str, '%Y-%m-%d').date()
            fecha_fin = datetime.strptime(fin_str, '%Y-%m-%d').date()
        except ValueError:
            return Response({"error": "Las fechas deben estar en formato AAAA-MM-DD."}, status=status.HTTP_400_BAD_REQUEST)

        # a) Rango <= 365 dias
        if (fecha_fin - fecha_inicio).days > 365:
            return Response({"error": "El rango de fechas seleccionado para un cierre no puede ser mayor a 365 días (un año)."}, status=status.HTTP_400_BAD_REQUEST)

        # b) fecha_inicio debe coincidir con ultimo_cierre.fecha_fin + 1 dia
        ultimo_cierre = PeriodoContable.objects.filter(estado='CERRADO').order_by('-fecha_fin').first()
        if ultimo_cierre and ultimo_cierre.fecha_fin:
            expected_start = ultimo_cierre.fecha_fin + timedelta(days=1)
            if fecha_inicio != expected_start:
                return Response({
                    "error": f"La fecha de inicio debe ser exactamente {expected_start.strftime('%d/%m/%Y')} (el día siguiente al último cierre registrado)."
                }, status=status.HTTP_400_BAD_REQUEST)

        # c) Bloqueo en Cascada: existen asientos en fechas anteriores sin cerrar?
        closed_periods = PeriodoContable.objects.filter(estado='CERRADO')
        asientos_pasados = Asiento.objects.filter(fecha__lt=fecha_inicio, estado='APROBADO')
        for period in closed_periods:
            if period.fecha_inicio and period.fecha_fin:
                asientos_pasados = asientos_pasados.exclude(fecha__range=(period.fecha_inicio, period.fecha_fin))
            else:
                asientos_pasados = asientos_pasados.exclude(fecha__year=period.ano)

        if asientos_pasados.exists():
            fechas_huerfanas = list(asientos_pasados.values_list('fecha', flat=True).distinct()[:5])
            fechas_str = ", ".join([f.strftime('%d/%m/%Y') for f in fechas_huerfanas])
            return Response({
                "error": f"Existen asientos aprobados en fechas pasadas ({fechas_str}) que no han sido incluidos en ningún cierre contable anterior. Realice los cierres en orden cronológico."
            }, status=status.HTTP_400_BAD_REQUEST)

        # Validaciones de seguridad
        if Asiento.objects.filter(fecha__range=(fecha_inicio, fecha_fin), estado='BORRADOR').exists():
            return Response({"error": "No se puede realizar el cierre con asientos pendientes por aprobar en este período."}, status=status.HTTP_400_BAD_REQUEST)

        # check if this range overlaps with any closed period
        for p in closed_periods:
            if p.fecha_inicio and p.fecha_fin:
                if not (fecha_fin < p.fecha_inicio or fecha_inicio > p.fecha_fin):
                    return Response({"error": "Este período contable (o parte de él) ya está cerrado."}, status=status.HTTP_400_BAD_REQUEST)
            else:
                if fecha_inicio.year <= p.ano <= fecha_fin.year:
                    return Response({"error": "Este período contable (o parte de él) ya está cerrado."}, status=status.HTTP_400_BAD_REQUEST)

        try:
            with transaction.atomic():
                # Obtener todas las cuentas de movimiento (SI) de ingresos, costos y gastos
                cuentas_mov = Cuenta.objects.filter(movimiento='SI', codigo__regex=r'^[456]')
                
                saldos_db = DetalleAsiento.objects.filter(
                    asiento__fecha__range=(fecha_inicio, fecha_fin),
                    asiento__estado='APROBADO',
                    cuenta__in=cuentas_mov
                ).values('cuenta_id').annotate(
                    sum_debe=Sum('debe'),
                    sum_haber=Sum('haber')
                )
                
                saldos_map = {x['cuenta_id']: (x['sum_debe'] or Decimal('0.00'), x['sum_haber'] or Decimal('0.00')) for x in saldos_db}

                # Crear el Asiento de Cierre
                asiento_cierre = Asiento.objects.create(
                    fecha=fecha_fin,
                    glosa=f"CIERRE CONTABLE - PERIODO {fecha_inicio.strftime('%d/%m/%Y')} AL {fecha_fin.strftime('%d/%m/%Y')}",
                    creado_por=request.user,
                    estado='APROBADO'
                )

                detalles_a_crear = []
                debe_total = Decimal('0.00')
                haber_total = Decimal('0.00')

                for cuenta in cuentas_mov:
                    debe_mes, haber_mes = saldos_map.get(cuenta.codigo, (Decimal('0.00'), Decimal('0.00')))
                    
                    if cuenta.codigo.startswith('4'):
                        # Ingresos: saldo Haber - Debe. Para cerrar, cargamos al Debe.
                        balance = haber_mes - debe_mes
                        if balance != 0:
                            if balance > 0:
                                debe_val = balance
                                haber_val = Decimal('0.00')
                            else:
                                debe_val = Decimal('0.00')
                                haber_val = abs(balance)
                            detalles_a_crear.append(DetalleAsiento(
                                asiento=asiento_cierre,
                                cuenta=cuenta,
                                descripcion=f"Cierre de cuenta de ingresos {cuenta.codigo}",
                                debe=debe_val,
                                haber=haber_val
                            ))
                            debe_total += debe_val
                            haber_total += haber_val
                            
                    elif cuenta.codigo.startswith('5') or cuenta.codigo.startswith('6'):
                        # Gastos/Costos: saldo Debe - Haber. Para cerrar, abonamos al Haber.
                        balance = debe_mes - haber_mes
                        if balance != 0:
                            if balance > 0:
                                debe_val = Decimal('0.00')
                                haber_val = balance
                            else:
                                debe_val = abs(balance)
                                haber_val = Decimal('0.00')
                            detalles_a_crear.append(DetalleAsiento(
                                asiento=asiento_cierre,
                                cuenta=cuenta,
                                descripcion=f"Cierre de cuenta de egresos {cuenta.codigo}",
                                debe=debe_val,
                                haber=haber_val
                            ))
                            debe_total += debe_val
                            haber_total += haber_val

                # Registrar diferencia en patrimonio
                diferencia = debe_total - haber_total
                if diferencia != 0:
                    cuenta_patrimonio = Cuenta.objects.filter(codigo='3.0005').first()
                    if not cuenta_patrimonio:
                        cuenta_patrimonio = Cuenta.objects.filter(codigo__startswith='3', movimiento='SI').first()
                    if not cuenta_patrimonio:
                        cuenta_patrimonio = Cuenta.objects.create(
                            codigo='3.0005',
                            nombre='RESULTADOS ACUMULADOS',
                            movimiento='SI',
                            tipo_saldo='HABER',
                            activa=True
                        )
                    
                    if diferencia > 0:
                        # Necesitamos Haber para balancear
                        DetalleAsiento.objects.create(
                            asiento=asiento_cierre,
                            cuenta=cuenta_patrimonio,
                            descripcion=f"Utilidad neta del ejercicio - Cierre {fecha_fin.strftime('%d/%m/%Y')}",
                            debe=Decimal('0.00'),
                            haber=diferencia
                        )
                    else:
                        # Necesitamos Debe para balancear
                        DetalleAsiento.objects.create(
                            asiento=asiento_cierre,
                            cuenta=cuenta_patrimonio,
                            descripcion=f"Pérdida neta del ejercicio - Cierre {fecha_fin.strftime('%d/%m/%Y')}",
                            debe=abs(diferencia),
                            haber=Decimal('0.00')
                        )

                # Guardar los detalles
                if detalles_a_crear:
                    DetalleAsiento.objects.bulk_create(detalles_a_crear)

                # Bloquear período
                PeriodoContable.objects.update_or_create(
                    ano=int(fecha_fin.strftime('%Y%m%d')),
                    defaults={
                        'fecha_inicio': fecha_inicio,
                        'fecha_fin': fecha_fin,
                        'estado': 'CERRADO',
                        'fecha_cierre': timezone.now()
                    }
                )

                AuditoriaLog.objects.create(
                    usuario=request.user,
                    accion=f"Ejecutó el Cierre Contable para el período {fecha_inicio.strftime('%d/%m/%Y')} al {fecha_fin.strftime('%d/%m/%Y')}"
                )

            return Response({
                "status": "success",
                "detail": f"Cierre contable del período {fecha_inicio.strftime('%d/%m/%Y')} al {fecha_fin.strftime('%d/%m/%Y')} ejecutado correctamente.",
                "asiento_cierre_id": asiento_cierre.id
            }, status=status.HTTP_200_OK)
            
        except Exception as e:
            return Response({"error": f"Error al ejecutar el cierre: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


class CierreContablePrecierreView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        from decimal import Decimal
        from django.db.models import Sum
        from datetime import datetime, date
        from .models import Asiento, DetalleAsiento

        inicio_str = request.query_params.get('inicio')
        fin_str = request.query_params.get('fin')

        if not inicio_str or not fin_str:
            hoy = date.today()
            fecha_inicio = date(hoy.year, 1, 1)
            fecha_fin = date(hoy.year, 12, 31)
        else:
            try:
                fecha_inicio = datetime.strptime(inicio_str, '%Y-%m-%d').date()
                fecha_fin = datetime.strptime(fin_str, '%Y-%m-%d').date()
            except ValueError:
                return Response({"error": "Las fechas deben estar en formato AAAA-MM-DD."}, status=status.HTTP_400_BAD_REQUEST)

        if (fecha_fin - fecha_inicio).days > 365:
            return Response({"error": "El rango de fechas seleccionado no puede ser mayor a 365 días."}, status=status.HTTP_400_BAD_REQUEST)

        tiene_borradores = Asiento.objects.filter(fecha__range=(fecha_inicio, fecha_fin), estado='BORRADOR').exists()

        # Sumar ingresos y gastos (sólo de asientos aprobados)
        detalles = DetalleAsiento.objects.filter(
            asiento__fecha__range=(fecha_inicio, fecha_fin),
            asiento__estado='APROBADO',
            cuenta__movimiento='SI',
            cuenta__codigo__regex=r'^[456]'
        ).values('cuenta_id', 'cuenta__tipo_saldo').annotate(
            sum_debe=Sum('debe'),
            sum_haber=Sum('haber')
        )

        total_ingresos = Decimal('0.00')
        total_gastos = Decimal('0.00')

        for d in detalles:
            code = d['cuenta_id']
            debe = d['sum_debe'] or Decimal('0.00')
            haber = d['sum_haber'] or Decimal('0.00')

            if code.startswith('4'):
                total_ingresos += (haber - debe)
            elif code.startswith('5') or code.startswith('6'):
                total_gastos += (debe - haber)

        utilidad_neta = total_ingresos - total_gastos

        return Response({
            "is_simulacion": True,
            "mensaje": "Simulación Activa - Entorno Abierto",
            "fecha_inicio": fecha_inicio.strftime('%Y-%m-%d'),
            "fecha_fin": fecha_fin.strftime('%Y-%m-%d'),
            "total_ingresos": float(total_ingresos),
            "total_gastos": float(total_gastos),
            "utilidad_neta": float(utilidad_neta),
            "tiene_borradores": tiene_borradores
        }, status=status.HTTP_200_OK)


class CierreContableRevertirView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request, *args, **kwargs):
        from django.db import transaction
        from .models import PeriodoContable, Asiento, AuditoriaLog

        # Seguridad estricta: solo ADMINISTRADOR
        perfil = getattr(request.user, 'perfil', None)
        rol = perfil.rol if perfil else 'AUXILIAR'
        if rol != 'ADMINISTRADOR':
            return Response({"error": "Solo el administrador del sistema puede revertir cierres contables."}, status=status.HTTP_403_FORBIDDEN)

        try:
            with transaction.atomic():
                # a) Buscar el último período contable cerrado en la base de datos
                ultimo_cierre = PeriodoContable.objects.filter(estado='CERRADO').order_by('-fecha_fin').first()
                # b) Si no hay períodos cerrados, devuelve un error 400
                if not ultimo_cierre:
                    return Response({"error": "No existen períodos contables cerrados que revertir."}, status=status.HTTP_400_BAD_REQUEST)

                # c) Reabrir el período (eliminando el registro)
                fecha_inicio = ultimo_cierre.fecha_inicio
                fecha_fin = ultimo_cierre.fecha_fin
                ultimo_cierre.delete()

                # d) Buscar y eliminar automáticamente el Asiento de Cierre
                glosa_cierre_1 = f"CIERRE CONTABLE - PERIODO {fecha_inicio.strftime('%d/%m/%Y')} AL {fecha_fin.strftime('%d/%m/%Y')}"
                
                asientos_cierre = Asiento.objects.filter(glosa=glosa_cierre_1, fecha=fecha_fin)
                if not asientos_cierre.exists() and fecha_inicio and fecha_fin:
                    # intente con la glosa antigua anual
                    asientos_cierre = Asiento.objects.filter(glosa=f"CIERRE CONTABLE ANUAL - AÑO {fecha_fin.year}")
                
                count_deleted = 0
                for a in asientos_cierre:
                    a.delete()
                    count_deleted += 1

                AuditoriaLog.objects.create(
                    usuario=request.user,
                    accion=f"Revirtió el cierre contable del período {fecha_inicio.strftime('%d/%m/%Y')} al {fecha_fin.strftime('%d/%m/%Y')}."
                )

                return Response({
                    "status": "success",
                    "detail": f"El cierre contable del período {fecha_inicio.strftime('%d/%m/%Y')} al {fecha_fin.strftime('%d/%m/%Y')} ha sido revertido. Se eliminaron {count_deleted} asientos de cierre y el período está abierto nuevamente."
                }, status=status.HTTP_200_OK)

        except Exception as e:
            return Response({"error": f"Error al revertir el cierre: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


class CierreContableHistorialView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        from decimal import Decimal
        from django.db.models import Sum
        from .models import PeriodoContable, Asiento, DetalleAsiento
        
        periodos = PeriodoContable.objects.filter(estado='CERRADO').order_by('-fecha_fin')
        historial = []
        
        for p in periodos:
            asiento_cierre = Asiento.objects.filter(
                fecha=p.fecha_fin,
                glosa__icontains="CIERRE"
            ).first()
            
            usuario = asiento_cierre.creado_por.username if (asiento_cierre and asiento_cierre.creado_por) else "N/A"
            
            # Calcular resultado neto del período
            detalles = DetalleAsiento.objects.filter(
                asiento__fecha__range=(p.fecha_inicio, p.fecha_fin),
                asiento__estado='APROBADO',
                cuenta__movimiento='SI',
                cuenta__codigo__regex=r'^[456]'
            ).exclude(asiento__glosa__icontains="CIERRE").values('cuenta_id').annotate(
                sum_debe=Sum('debe'),
                sum_haber=Sum('haber')
            )
            
            total_ingresos = Decimal('0.00')
            total_gastos = Decimal('0.00')
            for d in detalles:
                code = d['cuenta_id']
                debe = d['sum_debe'] or Decimal('0.00')
                haber = d['sum_haber'] or Decimal('0.00')
                if code.startswith('4'):
                    total_ingresos += (haber - debe)
                elif code.startswith('5') or code.startswith('6'):
                    total_gastos += (debe - haber)
                    
            utilidad_neta = total_ingresos - total_gastos
            
            historial.append({
                "id": p.ano,
                "fecha_inicio": p.fecha_inicio.strftime('%Y-%m-%d') if p.fecha_inicio else "",
                "fecha_fin": p.fecha_fin.strftime('%Y-%m-%d') if p.fecha_fin else "",
                "fecha_cierre": p.fecha_cierre.strftime('%Y-%m-%d %H:%M:%S') if p.fecha_cierre else "",
                "usuario": usuario,
                "utilidad_neta": float(utilidad_neta)
            })
            
        return Response(historial, status=status.HTTP_200_OK)





