from datetime import time, timedelta

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
from django.utils import timezone

IMAGEN_PRODUCTO_EXTENSIONES = ['jpg', 'jpeg', 'png', 'webp']
IMAGEN_PRODUCTO_MAX_BYTES = 2 * 1024 * 1024  # 2 MB


def validar_tamano_imagen(archivo):
    if archivo.size > IMAGEN_PRODUCTO_MAX_BYTES:
        raise ValidationError('La imagen no debe superar los 2 MB.')


DIA_CHOICES = [
    (0, 'Lunes'), (1, 'Martes'), (2, 'Miércoles'), (3, 'Jueves'),
    (4, 'Viernes'), (5, 'Sábado'), (6, 'Domingo'),
]
# Por defecto se trabaja de miércoles a domingo; miércoles, jueves y domingo
# cierran a la 1am, viernes y sábado hasta las 3am.
DIAS_DEFAULT = {
    0: {'activo': False, 'hora_fin': time(1, 0)},   # lunes
    1: {'activo': False, 'hora_fin': time(1, 0)},   # martes
    2: {'activo': True, 'hora_fin': time(1, 0)},    # miércoles
    3: {'activo': True, 'hora_fin': time(1, 0)},    # jueves
    4: {'activo': True, 'hora_fin': time(3, 0)},    # viernes
    5: {'activo': True, 'hora_fin': time(3, 0)},    # sábado
    6: {'activo': True, 'hora_fin': time(1, 0)},    # domingo
}


class HorarioLaboral(models.Model):
    """Configuración del horario en el que los meseros pueden usar la app para
    tomar pedidos. Es una configuración única (singleton); el detalle de horas
    por día vive en los DiaLaboral relacionados (ver inline en /admin/)."""
    activo = models.BooleanField(
        default=True,
        help_text='Si está desactivado, los meseros pueden usar la app cualquier día y a cualquier hora.'
    )

    class Meta:
        verbose_name = 'Horario laboral'
        verbose_name_plural = 'Horario laboral'

    def __str__(self):
        return 'Restricción de horario activa' if self.activo else 'Restricción de horario desactivada'

    @classmethod
    def get_solo(cls):
        obj = cls.objects.first()
        if not obj:
            obj = cls.objects.create()
        obj.asegurar_dias()
        return obj

    def asegurar_dias(self):
        """Crea las filas de días faltantes (0-6) con los valores por defecto del negocio."""
        existentes = set(self.dias.values_list('dia', flat=True))
        faltantes = [
            DiaLaboral(horario=self, dia=d, **DIAS_DEFAULT[d])
            for d in range(7) if d not in existentes
        ]
        if faltantes:
            DiaLaboral.objects.bulk_create(faltantes)

    def _dias_mapa(self):
        self.asegurar_dias()
        return {d.dia: d for d in self.dias.all()}

    @property
    def dias_activos_display(self):
        dias = [d for d in self._dias_mapa().values() if d.activo]
        if not dias:
            return 'Ningún día habilitado'
        grupos = {}
        for d in sorted(dias, key=lambda x: x.dia):
            grupos.setdefault((d.hora_inicio, d.hora_fin), []).append(d.get_dia_display())
        partes = [
            f"{', '.join(nombres)}: {hi.strftime('%H:%M')}–{hf.strftime('%H:%M')}"
            for (hi, hf), nombres in grupos.items()
        ]
        return ' · '.join(partes)

    def fecha_operativa(self, momento=None):
        """La 'fecha de hoy' para efectos de negocio: si el turno de ayer cruza
        medianoche y seguimos dentro de su madrugada, esa madrugada sigue
        perteneciendo al día que abrió el turno, no al día calendario del reloj."""
        momento = momento or timezone.localtime()
        dias = self._dias_mapa()
        ayer = dias[(momento.weekday() - 1) % 7]
        if ayer.activo and ayer.hora_fin < ayer.hora_inicio and momento.time() <= ayer.hora_fin:
            return momento.date() - timedelta(days=1)
        return momento.date()

    def dentro_de_horario(self, momento=None):
        if not self.activo:
            return True
        momento = momento or timezone.localtime()
        hora = momento.time()
        dias = self._dias_mapa()
        hoy = dias[momento.weekday()]
        if hoy.activo:
            if hoy.hora_inicio <= hoy.hora_fin:
                if hoy.hora_inicio <= hora <= hoy.hora_fin:
                    return True
            elif hora >= hoy.hora_inicio:
                return True
        ayer = dias[(momento.weekday() - 1) % 7]
        if ayer.activo and ayer.hora_fin < ayer.hora_inicio and hora <= ayer.hora_fin:
            return True
        return False


class DiaLaboral(models.Model):
    """Horario de un día de la semana en el que los meseros pueden tomar pedidos.
    Hay exactamente una fila por día (0=lunes ... 6=domingo) por cada HorarioLaboral
    (que es único), editable en línea desde la página de 'Horario laboral' en /admin/.
    El horario de cierre puede variar por día (ej. miércoles/jueves/domingo hasta
    la 1am, viernes/sábado hasta las 3am)."""
    horario = models.ForeignKey(HorarioLaboral, on_delete=models.CASCADE, related_name='dias')
    dia = models.PositiveSmallIntegerField(choices=DIA_CHOICES)
    activo = models.BooleanField(default=False, help_text='Si los meseros pueden tomar pedidos este día.')
    hora_inicio = models.TimeField(default=time(17, 30))
    hora_fin = models.TimeField(
        default=time(1, 0),
        help_text='Si es menor que la hora de inicio, se entiende que cruza la medianoche.'
    )

    class Meta:
        ordering = ['dia']
        unique_together = ['horario', 'dia']
        verbose_name = 'Día laboral'
        verbose_name_plural = 'Días laborales'

    def __str__(self):
        estado = f"{self.hora_inicio.strftime('%H:%M')}–{self.hora_fin.strftime('%H:%M')}" if self.activo else 'cerrado'
        return f"{self.get_dia_display()} ({estado})"


class Sede(models.Model):
    nombre = models.CharField(max_length=100)

    class Meta:
        ordering = ['nombre']

    def __str__(self):
        return self.nombre


METODOS_PAGO = [
    ('Efectivo', 'Efectivo'),
    ('Nequi', 'Nequi'),
    ('Bold', 'Bold'),
    ('Promocion', 'Promoción'),
    ('Sin Pagar', 'Sin Pagar'),
]

# Métodos donde el dinero queda físicamente en manos del mesero hasta que lo entrega al cajero.
METODOS_QUE_RETIENE_MESERO = ('Efectivo', 'Nequi', 'Bold')


class Categoria(models.Model):
    nombre = models.CharField(max_length=100)

    class Meta:
        ordering = ['nombre']
        verbose_name_plural = 'Categorías'

    def __str__(self):
        return self.nombre


class Producto(models.Model):
    categoria = models.ForeignKey(
        Categoria, on_delete=models.SET_NULL,
        null=True, blank=True, related_name='productos'
    )
    nombre = models.CharField(max_length=200)
    precio = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True)
    imagen = models.ImageField(
        upload_to='productos/',
        null=True, blank=True,
        validators=[
            FileExtensionValidator(allowed_extensions=IMAGEN_PRODUCTO_EXTENSIONES),
            validar_tamano_imagen,
        ],
        help_text='JPG, PNG o WEBP. Máximo 2 MB. Se mostrará recortada en cuadrado.'
    )
    activo = models.BooleanField(default=True)

    class Meta:
        ordering = ['nombre']

    def __str__(self):
        return self.nombre


VARIACION_TIPOS = [
    ('FIJO', 'Precio fijo'),      # reemplaza el precio del producto (12oz, Explosiva)
    ('EXTRA', 'Adicional'),       # se suma al precio del producto (Michelada +$2.000)
    ('COMBO', 'Combo'),           # N unidades reales, se cobran M, sobre el precio del producto (Cubetazo)
]


class VariacionCatalogo(models.Model):
    nombre = models.CharField(max_length=100, unique=True)
    grupo = models.CharField(max_length=50, blank=True, default='')
    tipo = models.CharField(max_length=10, choices=VARIACION_TIPOS, default='FIJO')
    precio = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True)
    unidades_por_combo = models.PositiveIntegerField(default=1)
    unidades_cobradas = models.PositiveIntegerField(default=1)
    activo = models.BooleanField(default=True)

    class Meta:
        ordering = ['grupo', 'nombre']
        verbose_name_plural = 'Variaciones (catálogo)'

    def __str__(self):
        return self.nombre


class ProductoVariacion(models.Model):
    producto = models.ForeignKey(Producto, on_delete=models.CASCADE, related_name='variaciones')
    variacion = models.ForeignKey(VariacionCatalogo, on_delete=models.CASCADE, related_name='productos_asignados')
    precio = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True)
    activo = models.BooleanField(default=True)

    class Meta:
        ordering = ['variacion__grupo', 'variacion__nombre']
        unique_together = ['producto', 'variacion']
        verbose_name_plural = 'Variaciones asignadas'

    def __str__(self):
        return f"{self.producto} — {self.variacion}"

    @property
    def precio_efectivo(self):
        return self.precio if self.precio is not None else self.variacion.precio


class Jornada(models.Model):
    fecha = models.DateField(default=timezone.now)
    sede = models.ForeignKey(
        Sede, on_delete=models.SET_NULL, null=True, blank=True, related_name='jornadas'
    )
    base = models.DecimalField(max_digits=12, decimal_places=0, default=0)
    cerrada = models.BooleanField(default=False)
    efectivo_contado = models.DecimalField(max_digits=12, decimal_places=0, null=True, blank=True)
    cierre_nota = models.CharField(max_length=255, blank=True, default='')
    cerrada_en = models.DateTimeField(null=True, blank=True)
    cerrada_por = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name='jornadas_cerradas'
    )

    class Meta:
        ordering = ['-fecha']

    def __str__(self):
        sede = f" — {self.sede}" if self.sede else ""
        return f"Jornada {self.fecha}{sede}"

    @classmethod
    def activa(cls):
        """La jornada abierta más reciente: la que ve un mesero sin tener que elegirla."""
        return cls.objects.filter(cerrada=False).order_by('-fecha', '-id').first()

    def total_por_metodo(self, metodo):
        return self.ventas.filter(metodo_pago=metodo).aggregate(
            t=models.Sum('valor')
        )['t'] or 0

    @property
    def total_efectivo(self):
        return self.total_por_metodo('Efectivo')

    @property
    def total_nequi(self):
        return self.total_por_metodo('Nequi')

    @property
    def total_bold(self):
        return self.total_por_metodo('Bold')

    @property
    def total_promocion(self):
        return self.total_por_metodo('Promocion')

    @property
    def total_sin_pagar(self):
        return self.total_por_metodo('Sin Pagar')

    @property
    def total_ventas(self):
        return self.ventas.exclude(metodo_pago__in=['Promocion', 'Sin Pagar']).aggregate(
            t=models.Sum('valor')
        )['t'] or 0

    @property
    def total_gastos(self):
        return self.gastos.aggregate(t=models.Sum('valor'))['t'] or 0

    @property
    def caja(self):
        """Efectivo que debería haber en caja: base + efectivo vendido - gastos pagados con esa caja."""
        return self.base + self.total_efectivo - self.total_gastos

    @property
    def diferencia_caja(self):
        """Diferencia entre lo contado físicamente y lo que debería haber. None si aún no se ha contado."""
        if self.efectivo_contado is None:
            return None
        return self.efectivo_contado - self.caja

    @property
    def diferencia_caja_abs(self):
        diferencia = self.diferencia_caja
        return None if diferencia is None else abs(diferencia)


class Mesa(models.Model):
    sede = models.ForeignKey(Sede, on_delete=models.CASCADE, related_name='mesas')
    nombre = models.CharField(max_length=50)
    activa = models.BooleanField(default=True)

    class Meta:
        ordering = ['nombre']
        unique_together = ['sede', 'nombre']

    def __str__(self):
        return f"{self.nombre} — {self.sede}"


ESTADOS_PEDIDO = [
    ('TOMADO', 'Tomado'),
    ('PAGADO', 'Pagado al mesero'),
    ('LIQUIDADO', 'Confirmado por cajero'),
]


class Pedido(models.Model):
    jornada = models.ForeignKey(Jornada, on_delete=models.CASCADE, related_name='pedidos')
    mesa = models.ForeignKey(Mesa, on_delete=models.PROTECT, related_name='pedidos')
    mesero = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, related_name='pedidos_tomados'
    )
    estado = models.CharField(max_length=10, choices=ESTADOS_PEDIDO, default='TOMADO')
    metodo_pago = models.CharField(max_length=20, choices=METODOS_PAGO, blank=True, default='')
    creado = models.DateTimeField(auto_now_add=True)
    pagado_en = models.DateTimeField(null=True, blank=True)
    liquidado_en = models.DateTimeField(null=True, blank=True)
    liquidado_por = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name='pedidos_liquidados'
    )

    class Meta:
        ordering = ['-creado']

    def __str__(self):
        return f"{self.mesa} — {self.get_estado_display()}"

    @property
    def total(self):
        return self.lineas.aggregate(t=models.Sum('valor'))['t'] or 0

    def cobrar(self, metodo_pago):
        necesita_liquidacion = metodo_pago in METODOS_QUE_RETIENE_MESERO
        self.metodo_pago = metodo_pago
        self.pagado_en = timezone.now()
        self.estado = 'PAGADO' if necesita_liquidacion else 'LIQUIDADO'
        if not necesita_liquidacion:
            self.liquidado_en = timezone.now()
        self.save()
        self.lineas.update(metodo_pago=metodo_pago, liquidado=not necesita_liquidacion)

    def confirmar(self, user):
        self.estado = 'LIQUIDADO'
        self.liquidado_en = timezone.now()
        self.liquidado_por = user
        self.save()
        self.lineas.update(liquidado=True)


class Venta(models.Model):
    jornada = models.ForeignKey(Jornada, on_delete=models.CASCADE, related_name='ventas')
    pedido = models.ForeignKey(
        Pedido, on_delete=models.SET_NULL, null=True, blank=True, related_name='lineas'
    )
    mesa = models.CharField(max_length=100, blank=True, default='')
    cantidad = models.PositiveIntegerField(default=1)
    producto = models.ForeignKey(Producto, on_delete=models.PROTECT, related_name='ventas')
    variacion = models.ForeignKey(
        VariacionCatalogo, on_delete=models.SET_NULL,
        null=True, blank=True, related_name='ventas'
    )
    valor = models.DecimalField(max_digits=12, decimal_places=0)
    metodo_pago = models.CharField(max_length=20, choices=METODOS_PAGO, default='Efectivo')
    liquidado = models.BooleanField(
        default=True,
        help_text='False cuando un mesero cobró en mesa y el cajero aún no recibe/confirma ese dinero.'
    )
    nota = models.CharField(max_length=255, blank=True, default='')
    vendedor = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
        null=True, blank=True, related_name='ventas_realizadas'
    )
    creado = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-creado']

    def __str__(self):
        return f"{self.cantidad}x {self.producto} — ${self.valor:,.0f}"

    @staticmethod
    def precio_calculado(producto, variacion, cantidad):
        cantidad = int(cantidad)
        if variacion:
            if variacion.tipo == 'COMBO':
                # cantidad = número de combos (ej. cubetazos); unidades_cobradas ya
                # descuenta la promo (ej. 5 de 6) sobre el precio del producto elegido.
                return cantidad * variacion.unidades_cobradas * (producto.precio or 0)
            pv = ProductoVariacion.objects.filter(
                producto=producto, variacion=variacion, activo=True
            ).first()
            precio_var = (pv.precio_efectivo if pv else variacion.precio) or 0
            if variacion.tipo == 'EXTRA':
                return cantidad * (precio_var + (producto.precio or 0))
            return cantidad * precio_var  # FIJO
        elif producto.precio:
            return cantidad * producto.precio
        return 0

    def save(self, *args, **kwargs):
        if not self.pk and not self.valor:
            self.valor = self.precio_calculado(self.producto, self.variacion, self.cantidad)
        super().save(*args, **kwargs)


class InventarioItem(models.Model):
    jornada = models.ForeignKey(Jornada, on_delete=models.CASCADE, related_name='inventario')
    nombre = models.CharField(max_length=100)
    inicial = models.IntegerField(default=0)
    compras = models.IntegerField(default=0)
    faltan = models.IntegerField(default=0)
    sobran = models.IntegerField(default=0)

    class Meta:
        ordering = ['nombre']
        unique_together = ['jornada', 'nombre']
        verbose_name_plural = 'Items de inventario'

    def __str__(self):
        return f"{self.nombre} — {self.jornada.fecha}"

    @property
    def ventas_contadas(self):
        return Venta.objects.filter(
            jornada=self.jornada,
            variacion__nombre=self.nombre
        ).aggregate(t=models.Sum('cantidad'))['t'] or 0

    @property
    def final(self):
        return self.inicial + self.compras - self.ventas_contadas - self.faltan + self.sobran


GASTO_CATEGORIAS = [
    ('Compra', 'Compra / Insumos'),
    ('Devolucion', 'Devolución a cliente'),
    ('Personal', 'Pago de personal'),
    ('Dj', 'Pago DJ'),
    ('Otro', 'Otro'),
]


class Gasto(models.Model):
    jornada = models.ForeignKey(Jornada, on_delete=models.CASCADE, related_name='gastos')
    categoria = models.CharField(max_length=20, choices=GASTO_CATEGORIAS, default='Otro')
    descripcion = models.CharField(max_length=255, blank=True, default='')
    valor = models.DecimalField(max_digits=12, decimal_places=0)
    creado = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-creado']

    def __str__(self):
        return f"{self.get_categoria_display()} — ${self.valor:,.0f}"
