import logging
from datetime import datetime
from collections import defaultdict
from models.donaciones import Donacion
from models.alimento_preparado import AlimentoPreparado
from models.salones import Salon
from models.areas import Area
from models.recepcion import Recepcion
from models.distribucion import Distribucion
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import joinedload
from controllers import BaseController

# Configuración de logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class DistribucionesController(BaseController):
    def __init__(self, session=None):
        super().__init__(model=Distribucion, session=session)
        logger.info("DistribucionesController inicializado.")

    def crear_distribucion(self, datos, user_context=None):
        """
        Crea una nueva distribución en la base de datos.
        :param datos: Diccionario con los datos de la distribución.
        :return: (Exito, Mensaje)
        """
        if not isinstance(datos, dict):
            return False, "Los datos proporcionados no son válidos."

        datos_normalizados = self._normalizar_datos(datos)
        fecha = self.validar_y_convertir_fecha(datos_normalizados.get('fecha'))
        if not fecha:
            return False, "Formato de fecha incorrecto. Debe ser YYYY-MM-DD."
        datos_normalizados['fecha'] = fecha

        def operacion(db):
            errores = self.validar_datos(datos_normalizados, db)
            if errores:
                raise ValueError("\n".join(errores))

            distribucion = Distribucion(**datos_normalizados)
            db.add(distribucion)
            db.flush()
            self.registrar_evento_sync(db, 'distribuciones', distribucion, 'upsert')

        return self.ejecutar_transaccion(operacion, "Distribución creada exitosamente.", user_context=user_context)

    def listar_distribuciones(self, fecha: str = None):
        """
        Lista las distribuciones desde la base de datos, opcionalmente filtradas por fecha.
        :return: Lista de objetos Distribucion.
        """
        db = self.get_db_session()
        try:
            query = self.query_activa(db).options(
                joinedload(Distribucion.donacion),
                joinedload(Distribucion.alimento_preparado),
                joinedload(Distribucion.salon),
                joinedload(Distribucion.area),
                joinedload(Distribucion.recepcion)
            )

            if fecha:
                fecha_dt = self.validar_y_convertir_fecha(fecha)
                if fecha_dt:
                    query = query.filter(Distribucion.fecha == fecha_dt)

            distribuciones = query.order_by(Distribucion.fecha.desc(), Distribucion.id.desc()).all()
            logger.info(f"{len(distribuciones)} distribuciones obtenidas.")
            return distribuciones
        except SQLAlchemyError as e:
            logger.error(f"Error al listar distribuciones: {e}")
            return []
        finally:
            if not self.session:
                db.close()

    def listar_distribuciones_concentradas(self, fecha):
        """
        Lista distribuciones de una fecha y arma una vista concentrada:
        - Si un origen se repite, se agrupa por origen + unidad.
        - Si no se repite, se devuelve como registro simple.
        """
        if not fecha:
            logger.warning("Se requiere una fecha para listar distribuciones concentradas.")
            return []

        fecha_filtro = fecha
        if isinstance(fecha, str):
            try:
                fecha_filtro = datetime.strptime(fecha, '%Y-%m-%d').date()
            except ValueError:
                logger.warning(f"Fecha inválida recibida para distribución concentrada: {fecha}")
                return []

        db = self.get_db_session()
        try:
            distribuciones = db.query(Distribucion).options(
                joinedload(Distribucion.donacion),
                joinedload(Distribucion.alimento_preparado),
                joinedload(Distribucion.salon),
                joinedload(Distribucion.area),
                joinedload(Distribucion.recepcion)
            ).filter(
                Distribucion.fecha == fecha_filtro,
                Distribucion.is_deleted.is_(False),
            ).all()

            grupos = {}
            for dist in distribuciones:
                origen_tipo = 'sin_origen'
                origen_id = None
                origen_nombre = 'Sin origen'

                if dist.donacion:
                    origen_tipo = 'donacion'
                    origen_id = dist.donacion.id
                    origen_nombre = dist.donacion.descripcion
                elif dist.alimento_preparado:
                    origen_tipo = 'preparado'
                    origen_id = dist.alimento_preparado.id
                    origen_nombre = dist.alimento_preparado.descripcion
                elif dist.donacion_id:
                    origen_tipo = 'donacion'
                    origen_id = dist.donacion_id
                    origen_nombre = f"Donación ID {dist.donacion_id}"
                elif dist.alimento_preparado_id:
                    origen_tipo = 'preparado'
                    origen_id = dist.alimento_preparado_id
                    origen_nombre = f"Preparado ID {dist.alimento_preparado_id}"

                if dist.salon:
                    destino_nombre = f"Salón: {dist.salon.salon}"
                elif dist.area:
                    destino_nombre = f"Área: {dist.area.area}"
                elif dist.recepcion:
                    destino_nombre = f"Recepción: {dist.recepcion.nombre}"
                elif dist.salon_id:
                    destino_nombre = f"Salón ID {dist.salon_id}"
                elif dist.area_id:
                    destino_nombre = f"Área ID {dist.area_id}"
                elif dist.recepcion_id:
                    destino_nombre = f"Recepción ID {dist.recepcion_id}"
                else:
                    destino_nombre = 'Sin destino'

                unidad = str(dist.unidad or '').strip()

                if origen_tipo == 'sin_origen':
                    clave = ('simple', dist.id)
                else:
                    clave = (origen_tipo, origen_id, unidad)

                if clave not in grupos:
                    grupos[clave] = {
                        'origen_tipo': origen_tipo,
                        'origen_id': origen_id,
                        'origen_nombre': origen_nombre,
                        'unidad': unidad,
                        'fecha': dist.fecha,
                        'items': [],
                        'destinos': defaultdict(float),
                    }

                grupos[clave]['items'].append(dist)
                grupos[clave]['destinos'][destino_nombre] += float(dist.cantidad or 0)

            salida = []
            for grupo in grupos.values():
                items = grupo['items']
                if len(items) > 1 and grupo['origen_tipo'] != 'sin_origen':
                    salida.append({
                        'modo': 'agrupado',
                        'origen_tipo': grupo['origen_tipo'],
                        'origen_id': grupo['origen_id'],
                        'origen_nombre': grupo['origen_nombre'],
                        'unidad': grupo['unidad'],
                        'fecha': grupo['fecha'],
                        'total_cantidad': sum(float(x.cantidad or 0) for x in items),
                        'cantidad_registros': len(items),
                        'destinos': dict(sorted(grupo['destinos'].items(), key=lambda x: x[0])),
                        'distribuciones_ids': [x.id for x in items],
                    })
                else:
                    dist = items[0]
                    salida.append({
                        'modo': 'simple',
                        'distribucion': dist,
                    })

            salida.sort(
                key=lambda x: (
                    0 if x.get('modo') == 'agrupado' else 1,
                    str(x.get('origen_nombre') if x.get('modo') == 'agrupado' else getattr(x.get('distribucion'), 'id', '')),
                )
            )
            logger.info(f"{len(salida)} filas concentradas generadas para {fecha_filtro}.")
            return salida
        except SQLAlchemyError as e:
            logger.error(f"Error al listar distribuciones concentradas: {e}")
            return []
        finally:
            if not self.session:
                db.close()

    def actualizar_distribucion(self, id, datos, user_context=None):
        """
        Actualiza una distribución existente.
        :param id: ID de la distribución a actualizar.
        :param datos: Diccionario con los datos actualizados.
        :return: (Exito, Mensaje)
        """
        if not id or not isinstance(id, int):
            return False, "El ID de la distribución es obligatorio y debe ser un número entero."

        datos_normalizados = self._normalizar_datos(datos)
        fecha = self.validar_y_convertir_fecha(datos_normalizados.get('fecha'))
        if fecha:
            datos_normalizados['fecha'] = fecha

        def operacion(db):
            distribucion = db.query(Distribucion).filter(Distribucion.id == id, Distribucion.is_deleted.is_(False)).first()
            if not distribucion:
                raise ValueError("Distribución no encontrada.")

            datos_validar = {
                "donacion_id": datos_normalizados.get("donacion_id", distribucion.donacion_id),
                "alimento_preparado_id": datos_normalizados.get("alimento_preparado_id", distribucion.alimento_preparado_id),
                "salon_id": datos_normalizados.get("salon_id", distribucion.salon_id),
                "area_id": datos_normalizados.get("area_id", distribucion.area_id),
                "recepcion_id": datos_normalizados.get("recepcion_id", distribucion.recepcion_id),
                "cantidad": datos_normalizados.get("cantidad", distribucion.cantidad),
                "unidad": datos_normalizados.get("unidad", distribucion.unidad),
                "fecha": datos_normalizados.get("fecha", distribucion.fecha),
            }

            errores = self.validar_datos(datos_validar, db)
            if errores:
                raise ValueError("\n".join(errores))

            for key, value in datos_normalizados.items():
                setattr(distribucion, key, value)
            
            self.registrar_evento_sync(db, 'distribuciones', distribucion, 'upsert')
            logger.info(f"Distribución actualizada: ID {id}")

        return self.ejecutar_transaccion(operacion, "Distribución actualizada exitosamente.", user_context=user_context)

    def eliminar_distribucion(self, id, user_context=None):
        """
        Elimina una distribución de la base de datos.
        :param id: ID de la distribución a eliminar.
        :return: (Exito, Mensaje)
        """
        if not id or not isinstance(id, int):
            return False, "El ID de la distribución es obligatorio y debe ser un número entero."

        def operacion(db):
            distribucion = db.query(Distribucion).filter(Distribucion.id == id, Distribucion.is_deleted.is_(False)).first()
            if not distribucion:
                raise ValueError("Distribución no encontrada.")
            
            self.marcar_eliminado(distribucion, db)
            self.registrar_evento_sync(db, 'distribuciones', distribucion, 'delete')
            logger.info(f"Distribución eliminada: ID {id}")

        return self.ejecutar_transaccion(operacion, "Distribución eliminada exitosamente.", user_context=user_context)

    def obtener_distribucion(self, id):
        """
        Obtiene una distribución por su ID.
        :param id: ID de la distribución.
        :return: Objeto Distribucion o None.
        """
        db = self.get_db_session()
        try:
            return db.query(Distribucion).options(
                joinedload(Distribucion.donacion),
                joinedload(Distribucion.alimento_preparado),
                joinedload(Distribucion.salon),
                joinedload(Distribucion.area),
                joinedload(Distribucion.recepcion)
            ).filter(Distribucion.id == id, Distribucion.is_deleted.is_(False)).first()
        except SQLAlchemyError as e:
            logger.error(f"Error al obtener distribución: {e}")
            return None
        finally:
            if not self.session:
                db.close()

    def buscar_distribucion(self, id=None):
        """
        Busca una distribución por ID.
        :param id: ID de la distribución a buscar.
        :return: (Exito, Objeto, Mensaje)
        """
        if not id:
            return False, None, "Debe proporcionar un ID para buscar la distribución."
        if not isinstance(id, int):
            return False, None, "El ID debe ser un número entero."

        distribucion = self.buscar_por_id_o_nombre(id=id, nombre_campo="id")
        if distribucion:
            return True, distribucion, "Distribución encontrada exitosamente."
        else:
            return False, None, f"No existe una distribución con ID {id}."

    def _normalizar_datos(self, datos):
        """
        Normaliza los datos de entrada para facilitar validaciones.
        """
        datos_normalizados = dict(datos)

        for campo in ("donacion_id", "alimento_preparado_id", "salon_id", "area_id", "recepcion_id", "unidad"):
            if campo in datos_normalizados and isinstance(datos_normalizados[campo], str):
                datos_normalizados[campo] = datos_normalizados[campo].strip()

        if datos_normalizados.get("donacion_id") in ("", "None", "none"):
            datos_normalizados["donacion_id"] = None
        if datos_normalizados.get("alimento_preparado_id") in ("", "None", "none"):
            datos_normalizados["alimento_preparado_id"] = None
        if datos_normalizados.get("salon_id") == "":
            datos_normalizados["salon_id"] = None
        if datos_normalizados.get("area_id") == "":
            datos_normalizados["area_id"] = None
        if datos_normalizados.get("recepcion_id") == "":
            datos_normalizados["recepcion_id"] = None

        return datos_normalizados

    def validar_datos(self, datos, db):
        """
        Valida los datos para crear o actualizar una distribución.
        :return: Lista de errores encontrados.
        """
        errores = []
        donacion_id = datos.get("donacion_id")
        alimento_preparado_id = datos.get("alimento_preparado_id")
        salon_id = datos.get("salon_id")
        area_id = datos.get("area_id")
        recepcion_id = datos.get("recepcion_id")
        unidad = datos.get("unidad")

        if bool(donacion_id) == bool(alimento_preparado_id):
            errores.append("Debe seleccionar exactamente un origen: donación o alimento preparado.")
        else:
            if donacion_id:
                if not isinstance(donacion_id, int):
                    errores.append("El campo 'donacion_id' debe ser un número entero.")
                elif db.query(Donacion).filter(Donacion.id == donacion_id).first() is None:
                    errores.append(f"No existe una donación con ID {donacion_id}.")
            if alimento_preparado_id:
                if not isinstance(alimento_preparado_id, int):
                    errores.append("El campo 'alimento_preparado_id' debe ser un número entero.")
                elif db.query(AlimentoPreparado).filter(AlimentoPreparado.id == alimento_preparado_id).first() is None:
                    errores.append(f"No existe un alimento preparado con ID {alimento_preparado_id}.")

        if salon_id and not isinstance(salon_id, int):
            errores.append("El campo 'salon_id' debe ser un número entero.")
        if area_id and not isinstance(area_id, int):
            errores.append("El campo 'area_id' debe ser un número entero.")

        destinos_seleccionados = [bool(salon_id), bool(area_id), bool(recepcion_id)]
        if sum(destinos_seleccionados) != 1:
            errores.append("Debe seleccionar exactamente un destino: salón, área o recepción.")
        elif salon_id and db.query(Salon).filter(Salon.id == salon_id).first() is None:
            errores.append(f"No existe un salón con ID {salon_id}.")
        elif area_id and db.query(Area).filter(Area.id == area_id).first() is None:
            errores.append(f"No existe un área con ID {area_id}.")
        elif recepcion_id and db.query(Recepcion).filter(Recepcion.id == recepcion_id).first() is None:
            errores.append(f"No existe una recepción con ID {recepcion_id}.")

        cantidad = datos.get("cantidad")
        if cantidad is None:
            errores.append("El campo 'cantidad' es obligatorio.")
        elif not isinstance(cantidad, (int, float)):
            errores.append("El campo 'cantidad' debe ser un número.")
        elif cantidad <= 0:
            errores.append("El campo 'cantidad' debe ser un número positivo.")
        elif cantidad > 1000:
            errores.append("El campo 'cantidad' no puede ser mayor a 1000.")

        if not isinstance(unidad, str) or not unidad.strip():
            errores.append("El campo 'unidad' es obligatorio y debe ser texto.")
        elif len(unidad.strip()) > 50:
            errores.append("El campo 'unidad' no puede superar 50 caracteres.")

        fecha = datos.get("fecha")
        if isinstance(fecha, str):
            try:
                datetime.strptime(fecha, '%Y-%m-%d')
            except ValueError:
                errores.append("El campo 'fecha' debe tener el formato 'YYYY-MM-DD'.")
        elif fecha is None or not hasattr(fecha, "year"):
            errores.append("El campo 'fecha' es obligatorio y debe tener formato 'YYYY-MM-DD'.")

        return errores