"""
# utils.py
# Módulo de utilidades para manejo de errores en DAOs síncronos
# Contiene decoradores para manejo de errores, carga de relaciones y opciones de carga optimizadas
"""
import functools
from typing import (
    Any,
    List,
    Optional
)

from sqlalchemy import exc
from sqlalchemy.orm import (
    Session,
    class_mapper,
    selectinload,
    joinedload,
    RelationshipProperty
)

from pydantic import BaseModel
from tai_alphi import Alphi

# Logger
logger = Alphi.get_logger_by_name("tai-chatbot")

def error_handler(func):
    """
    Decorador para manejo de errores consistente en métodos DAO.
    Siempre debe usarse sin paréntesis: @error_handler
    """

    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        operation_verbs = {
            "create": "crear",
            "create_many": "crear",
            "find": "buscar",
            "find_many": "buscar",
            "update": "actualizar",
            "update_many": "actualizar",
            "delete": "eliminar",
            "delete_many": "eliminar",
            "count": "contar",
            "exists": "verificar existencia de"
        }

        operation_type = func.__name__
        class_name: str = self.__class__.__name__
        model_name = class_name.removesuffix("AsyncDAO") if class_name.endswith("AsyncDAO") else "Modelo"

        try:
            return func(self, *args, **kwargs)

        except exc.NoResultFound:
            logger.warning(f"[public] ⚠️ Registro no encontrado en {operation_type}: {model_name}")
            raise

        except exc.IntegrityError as e:
            error_msg = str(e.orig) if hasattr(e, 'orig') else str(e)
            logger.error(f"[public] ❌ Error de integridad en {operation_type}: {error_msg}")

            if "UNIQUE constraint failed" in error_msg or "duplicate key" in error_msg.lower():
                logger.error(f"[public] ❌ Ya existe un {model_name} con esos datos")
                raise 
            elif "FOREIGN KEY constraint failed" in error_msg or "foreign key" in error_msg.lower():
                if operation_type == "delete":
                    logger.error(f"[public] ❌ No se puede eliminar el {model_name} porque está siendo referenciado por otros registros")
                    raise 
                else:
                    logger.error(f"[public] ❌ Error de referencia en los datos del {model_name}")
                    raise
            else:
                logger.error(f"[public] ❌ Error de integridad al {operation_verbs.get(operation_type, 'procesar')} {model_name}")
                raise 

        except exc.SQLAlchemyError as e:
            logger.error(f"[public] ❌ SQLAlchemy error en {operation_type}: {str(e)}")
            logger.error(f"[public] ❌ Error en la base de datos al {operation_verbs.get(operation_type, 'procesar')} {model_name}")
            raise

        except Exception as e:
            error_msg = str(e)
            logger.error(f"[public] ❌ Error inesperado en {operation_type}: {error_msg}")
            logger.error(f"[public] ❌ Error inesperado al {operation_verbs.get(operation_type, 'procesar')} {model_name}")
            raise 

    return wrapper

def should_include_relation(relation_name: str, includes: List[str]) -> bool:
    """
    Determina si una relación debe ser incluida basándose en la lista de includes.
    
    Args:
        relation_name: Nombre de la relación a verificar
        includes: Lista de relaciones a incluir
        
    Returns:
        bool: True si la relación debe incluirse
    """
    return any(
        include == relation_name or include.startswith(f"{relation_name}.")
        for include in includes
    )

def get_nested_includes(relation_name: str, includes: List[str]) -> List[str]:
    """
    Extrae las relaciones anidadas para una relación específica.
    
    Args:
        relation_name: Nombre de la relación padre
        includes: Lista completa de includes
        
    Returns:
        List[str]: Lista de includes anidados para la relación
        
    Example:
        includes = ['author', 'author.posts', 'author.posts.comments']
        get_nested_includes('author', includes) -> ['posts', 'posts.comments']
    """
    nested = []
    prefix = f"{relation_name}."
    
    for include in includes:
        if include.startswith(prefix):
            # Remover el prefijo y añadir a nested
            nested_path = include[len(prefix):]
            nested.append(nested_path)
    
    return nested

def get_loading_options(model_class, includes: Optional[List[str]] = None) -> List[Any]:
    """
    Genera las opciones de carga optimizadas para SQLAlchemy basándose en los includes.
    
    Args:
        model_class: Clase del modelo SQLAlchemy base
        includes: Lista de relaciones a incluir
        
    Returns:
        List[Any]: Lista de opciones de carga (joinedload/selectinload)
    """
    if not includes:
        return []
    
    options = []
    processed_relations = set()
    
    for include_path in includes:
        
        # Evitar duplicados
        if include_path in processed_relations:
            continue

        # Procesar cada nivel del path (ej: 'author.posts.comments')
        path_parts = include_path.split('.')
        current_model = model_class
        current_option = None
        
        for i, part in enumerate(path_parts):
            # Verificar que la relación existe en el modelo actual
            if not hasattr(current_model, part):
                break
                
            relation: RelationshipProperty = getattr(current_model, part)
            
            if i == 0:
                # Primera relación desde el modelo principal
                # Determinar estrategia basándose en el tipo de relación
                if hasattr(relation, 'direction'):
                    # Para relaciones 1:N usar selectinload, para N:1 usar joinedload
                    if relation.direction.name in ['ONETOMANY', 'MANYTOMANY']:
                        current_option = selectinload(relation)
                    else:  # MANYTOONE, ONETOONE
                        current_option = joinedload(relation)
                else:
                    # Fallback a selectinload si no se puede determinar
                    current_option = selectinload(relation)
                
                # Actualizar el modelo actual para relaciones anidadas
                if hasattr(relation, 'mapper'):
                    current_model = relation.mapper.class_
            else:
                # Relaciones anidadas
                if current_option is not None and hasattr(current_model, part):
                    nested_relation: RelationshipProperty = getattr(current_model, part)
                    
                    # Determinar estrategia para relación anidada
                    if hasattr(nested_relation, 'direction'):
                        if nested_relation.direction.name in ['ONETOMANY', 'MANYTOMANY']:
                            current_option = current_option.selectinload(nested_relation)
                        else:
                            current_option = current_option.joinedload(nested_relation)
                    else:
                        current_option = current_option.selectinload(nested_relation)
                    
                    # Actualizar el modelo actual
                    if hasattr(nested_relation, 'mapper'):
                        current_model = nested_relation.mapper.class_
        
        # Añadir la opción completa
        if current_option is not None:
            options.append(current_option)
            processed_relations.add(include_path)
    
    return options

def load_relationships_from_dto(session: Session, instance: Any, dto: BaseModel, included: set=set()) -> set[str]:
    """
    Refresca recursivamente las relaciones en la instancia que están definidas en el DTO.

    - Solo considera relaciones de SQLAlchemy.
    - Recorre las relaciones anidadas si el DTO las define.

    Args:
        session: Sesión SQLAlchemy síncrona.
        instance: Objeto ORM ya añadido a la sesión y flush().
        dto: DTO que representa los datos creados.
    
    Returns:
        List[str]: Lista de nombres de relaciones que se han refrescado.
    """

    model = type(instance)
    mapper = class_mapper(model)

    for attr in mapper.relationships:
        relation_name = attr.key

        if not hasattr(dto, relation_name):
            continue

        subdto = getattr(dto, relation_name)
        if subdto is None:
            continue  # esta relación no se quiere incluir

        # Refrescar la relación
        session.refresh(instance, attribute_names=[relation_name])
        included.add(relation_name)
        subinstance = getattr(instance, relation_name, None)

        if subinstance is not None:
            if isinstance(subdto, list):
                for i, child_dto in enumerate(subdto):
                    if i < len(subinstance):  # por si la relación está incompleta
                        load_relationships_from_dto(session, subinstance[i], child_dto, included)
            elif isinstance(subdto, BaseModel):
                load_relationships_from_dto(session, subinstance, subdto, included)

    return included

