from sqlalchemy.orm import Session, attributes, relationship, sessionmaker,RelationshipProperty
from sqlalchemy.ext.declarative import declarative_base
from flask import Flask # Para tipado
from flask_sqlalchemy import SQLAlchemy # Para tipado
from typing import Callable, Type, Set, Dict, Any, Optional
from sqlalchemy.event import listen

# =================================================================
# 1. SISTEMA GENÉRICO DE REGISTRO DE EVENTOS POST-COMMIT
# =================================================================

# Registros de eventos
_POST_SAVE_REGISTRY: Dict[Type[Any], Dict[Type[Any], Callable]] = {}
_FK_ATTRIBUTE_MAP: Dict[Type[Any], str] = {}

_DB_INSTANCE = None
_FLASK_APP = None


def set_db_instance(db_instance: Any, flask_app: Any):
    """
    Función de INYECCIÓN DE DEPENDENCIA adaptada para Flask-SQLAlchemy.
    Recibe la instancia 'db' de Flask-SQLAlchemy y el objeto 'app' de Flask.
    """
    global _DB_INSTANCE, _FLASK_APP
    _DB_INSTANCE = db_instance
    _FLASK_APP = flask_app
    print("[INIT] Instancia de DB y App inyectada exitosamente para listeners.")

def get_parent_fk_attribute(ChildModel, ParentModel):
    """
    Intenta inferir el nombre del atributo de Foreign Key (FK) 
    en el modelo hijo que apunta al modelo padre.
    
    Ejemplo: en Image, devuelve 'object_id' que apunta a Object.
    """
    
    # 1. Iterar sobre las propiedades mapeadas del modelo hijo
    for prop in ChildModel.__mapper__.iterate_properties:
        
        # 2. Asegurarse de que es una propiedad de relación
        if isinstance(prop, RelationshipProperty):
            
            # 3. Verificar que esta relación apunta a la clase ParentModel
            # (Ej: Image.object apunta a Object)
            if prop.mapper.class_ is ParentModel:
                
                # 4. CRÍTICO: Verificar si la propiedad tiene pares locales/remotos.
                # Esta lista contiene tuplas (columna_local, columna_remota).
                if prop.local_remote_pairs:
                    # La columna_local (índice 0) es la FK en el modelo hijo (Image)
                    fk_column = prop.local_remote_pairs[0][0]
                    
                    # Devolvemos el nombre del atributo (ej: 'object_id')
                    return fk_column.key
    
    # Si fallamos en encontrar la FK:
    print(f"[ADVERTENCIA] Error al inferir FK para {ChildModel.__name__}: No se pudo encontrar la FK entre {ChildModel.__name__} y {ParentModel.__name__}")
    return None

def eventAfterSave(ParentModel: Type[Any], ChildModel: Type[Any], handler_function: Callable):
    """Registra una función para ser ejecutada después de un cambio en ChildModel."""
    if ParentModel not in _POST_SAVE_REGISTRY:
        _POST_SAVE_REGISTRY[ParentModel] = {}
        
    _POST_SAVE_REGISTRY[ParentModel][ChildModel] = handler_function
    print(f"[REGISTRO] '{ChildModel.__name__}' -> '{ParentModel.__name__}' usando '{handler_function.__name__}'.")

    if ChildModel not in _FK_ATTRIBUTE_MAP:
        try:
            fk_attr = get_parent_fk_attribute(ChildModel, ParentModel)
            _FK_ATTRIBUTE_MAP[ChildModel] = fk_attr
        except ValueError as e:
            print(f"[ADVERTENCIA] Error al inferir FK para {ChildModel.__name__}: {e}")

def _get_session_tracker(session: Session) -> Dict[Type[Any], Set[int]]:
    """Inicializa o devuelve el mapa de IDs para recálculo por sesión."""
    if not hasattr(session, '_recalc_ids_map'):
        session._recalc_ids_map = {parent_cls: set() for parent_cls in _POST_SAVE_REGISTRY}
    return session._recalc_ids_map

def generic_tracker(session: Session, flush_context, instances=None):
    """[before_flush listener] Inserta IDs de padres afectados en el tracker."""
    tracker = _get_session_tracker(session)
    all_changed = session.new | session.dirty | session.deleted

    for instance in all_changed:
        instance_cls = instance.__class__
        
        for ParentModel, ChildHandlers in _POST_SAVE_REGISTRY.items():
            if instance_cls in ChildHandlers:
                fk_attr = _FK_ATTRIBUTE_MAP.get(instance_cls)
                if not fk_attr: continue

                # 1. Rastrear ID actual
                current_parent_id = getattr(instance, fk_attr, None)
                if current_parent_id is not None:
                    tracker[ParentModel].add(current_parent_id)

                # 2. Rastrear ID anterior (para borrados o cambios de FK)
                if instance in session.deleted:
                    state = attributes.instance_state(instance)
                    old_parent_id = state.attrs[fk_attr].loaded_value
                    if old_parent_id is not None: tracker[ParentModel].add(old_parent_id)

                elif instance in session.dirty:
                    history = attributes.instance_state(instance).get_history(fk_attr, passive=True)
                    if history.deleted:
                        old_parent_id = history.deleted[0]
                        if old_parent_id is not None: tracker[ParentModel].add(old_parent_id)
                # print(f"[TRACKER] {instance_cls.__name__} ID {getattr(instance, 'id', 'new')} -> Parent ID(s) {tracker[ParentModel]}")

def init_listeners(app: Flask, db: SQLAlchemy):
    """
    Inicializa las referencias globales de Flask y SQLAlchemy.
    DEBE llamarse una vez, inmediatamente después de crear 'app' y 'db'.
    """
    global _FLASK_APP, _DB_INSTANCE
    _FLASK_APP = app
    _DB_INSTANCE = db
    print("[REGISTRO] Inicialización de listeners: App y DB registradas.")

# -------------------------------------------------------------
# Adaptación clave: generic_executor debe usar el contexto de Flask
# -------------------------------------------------------------

def generic_executor(session: Session): 
    """
    [after_commit listener]
    Abre una nueva sesión usando db.session dentro de un contexto de aplicación 
    y ejecuta los handlers.
    """
    db = _DB_INSTANCE
    app = _FLASK_APP
    
    if db is None or app is None:
        print("!!! [ERROR EN AFTER_COMMIT] Instancia de DB o App no está definida. Omisión.")
        return
        
    with app.app_context():
        tracker = session._recalc_ids_map if hasattr(session, '_recalc_ids_map') else {}
        has_tasks = any(len(ids) > 0 for ids in tracker.values())

        if not has_tasks:
            return

        print("\n" + "="*50)
        print(f"[AFTER_COMMIT] Ejecutando tareas de recálculo en contexto de Flask...")

        # Copiar y limpiar el tracker (previo al commit)
        ids_to_recalc = {parent_cls: ids.copy() for parent_cls, ids in tracker.items() if len(ids) > 0}
        for ids_set in tracker.values(): ids_set.clear()
        
        with app.app_context():
            new_session = db.session 
            
            try:
                for ParentModel, parent_ids_to_recalc in ids_to_recalc.items():
                    parents = new_session.query(ParentModel).filter(ParentModel.id.in_(parent_ids_to_recalc)).all()
                    
                    for parent_obj in parents:
                        child_handlers = _POST_SAVE_REGISTRY.get(ParentModel, {})
                        for ChildModel, handler in child_handlers.items():
                            print(f"  -> Ejecutando '{handler.__name__}' para {ParentModel.__name__} ID {parent_obj.id}")
                            
                            # *** CORRECCIÓN CRÍTICA AQUÍ ***
                            # Solo pasamos el objeto padre (parent_obj).
                            # El handler (update_qty_img) ahora debe esperar solo un argumento.
                            handler(parent_obj) # <--- ¡Cambiado!

                new_session.commit() 
                print("="*50)
                print("[AFTER_COMMIT] Todos los recálculos han sido confirmados.")

            except Exception as e:
                new_session.rollback()
                print(f"!!! [ERROR EN AFTER_COMMIT] Falló la transacción de recálculo: {e}")


# --- CONFIGURACIÓN DE EVENTOS DE SESIÓN ---
# **ESTO DEBE HACERSE AL CARGAR EL MÓDULO**
# Conecta la lógica genérica a la sesión de SQLAlchemy.
listen(Session, 'before_flush', generic_tracker)
listen(Session, 'after_commit', generic_executor)
