import logging
from typing import Any, Dict, List, Optional

from fastapi import HTTPException
from sqlalchemy import or_, insert as sa_insert, update as sa_update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, defer, joinedload, load_only

# Importamos el modelo PRINCIPAL
from .models import Part

# Importamos TODOS los DTOs necesarios
from .dto import (
    PartCreateDTO,
    PartUpdateDTO,
    PartResponseDTO,
    PartBasicDTO # ¡Importante tener este!
)

logger = logging.getLogger(__name__)


def _apply_part_currency_defaults(data: dict) -> None:
    """Moneda obligatoria en partes: si no viene, extranjera USD / tipo EX."""
    ck = data.get("currency_key")
    ct = data.get("currency_type")
    if ck is None or (isinstance(ck, str) and not str(ck).strip()):
        data["currency_key"] = "USD"
    if ct is None or (isinstance(ct, str) and not str(ct).strip()):
        data["currency_type"] = "EX"


class PartService:
    """Servicio para gestión de Partes (Anexo 76 + Anexo 24)"""

    # Estos campos están en el modelo pero NO en la DB todavía, faltan las migraciones del usuario
    MISSING_INV_COLUMNS = []

    # Campos que SÍ existen en la DB (Verificados con \d a24.inv_partes)
    SAFE_INV_COLUMNS = [
        "id", "part_type", "material_type", "reference_number", "flex_reference_number",
        "equivalent_uom", "conversion_factor", "stock_uom", "alternate_uom", "conversion_uom",
        "added_value", "added_value_type", "assigned_client", "supplier_code", "is_textile",
        "bom_version", "is_repair", "is_hazardous", "emergency_number", "danger_class",
        "packaging_group", "width", "thickness", "specification", "total_value", "direct_labor",
        "general_expenses", "total_expenses", "depreciation", "tooling", "material_consumed",
        "profit", "us_fraction_alt", "ca_fraction", "ad_valorem_us", "nafta_result",
        "nafta_percentage", "dta", "dtb", "dtg", "tenant_id", "company_id",
        "substitute_part", "complementary_part", "preference_part", "use_alternate_quantity",
        "un_number", "shipping_name", "hazard_notes", "repair_unit_cost", "repair_added_value",
        "fraction_9801", "immex_type", "disable_movements", "pga_program_code", "usmca_fraction",
        "scrap_part_number", "waste_part_number", "scrap_description_en", "scrap_description_es",
        "scrap_export_fraction", "scrap_us_fraction", "equivalent_uom_2", "conversion_factor_2",
        "has_auxiliary", "auxiliary_uom", "auxiliary_conversion", "auxiliary_unit_cost",
        "mex_packing", "sales_order", "use_rule_8", "sector", "origin_country", "fraction_type",
        "non_discharge_clients", "agency_code_definition", "carta_porte", 
        "client_part_names", "part_identifiers", "substitute_parts", "aphis_data"
    ]

    @classmethod
    def get_all(
        cls,
        db: Session,
        tenant_id: int,
        company_id: Optional[int],
        skip: int = 0,
        limit: int = 100,
        filters: Optional[Dict[str, Any]] = None,
        sort_by: Optional[str] = None,
        sort_order: Optional[str] = "asc",
    ) -> tuple[List[Part], int]:
        
        query = db.query(Part).filter(Part.tenant_id == tenant_id)
        
        if company_id is not None:
            query = query.filter(Part.company_id == company_id)

        # Cargar inv_data de forma segura (Solo las columnas que existen)
        from api.v1.modules.a24.inv.inv_parts.models import InvPart
        from api.v1.modules.a24.fa.fa_parts.models import FaPart
        
        # Usamos joinedload + load_only para ser 100% seguros de qué columnas se piden
        load_inv_opt = joinedload(Part.inv_data).load_only(*[getattr(InvPart, c) for c in cls.SAFE_INV_COLUMNS])
        load_fa_opt = joinedload(Part.fa_data)
        query = query.options(load_inv_opt, load_fa_opt)

        if filters:
            logger.info(f"Applying filters to Part list: {filters}")
            if filters.get("q"):
                search = f"%{filters['q']}%"
                query = query.filter(
                    or_(
                        Part.part_number.ilike(search),
                        Part.description_spanish.ilike(search),
                        Part.description_english.ilike(search),
                        Part.commercial_part_number.ilike(search)
                    )
                )
            # Otros filtros...
 
        # Apply sorting
        if sort_by:
            column = getattr(Part, sort_by, None)
            if column:
                if sort_order == "desc":
                    query = query.order_by(column.desc())
                else:
                    query = query.order_by(column.asc())
        else:
            # Default sorting: Newest first
            query = query.order_by(Part.id.desc())

        total = query.count()
        items = query.offset(skip).limit(limit).all()

        # FIX PROACTIVO: Inyectar None en campos inexistentes para evitar que Pydantic dispare la carga perezosa
        for item in items:
            if item.inv_data:
                for col in cls.MISSING_INV_COLUMNS:
                    item.inv_data.__dict__[col] = None

        return items, total

    @classmethod
    def get_by_id(cls, db: Session, part_id: int, tenant_id: int, company_id: int) -> Optional[Part]:
        query = db.query(Part).filter(
            Part.id == part_id,
            Part.tenant_id == tenant_id,
            Part.company_id == company_id
        )

        # Cargar inv_data de forma segura y fa_data
        from api.v1.modules.a24.inv.inv_parts.models import InvPart
        from api.v1.modules.a24.fa.fa_parts.models import FaPart
        load_inv_opt = joinedload(Part.inv_data).load_only(*[getattr(InvPart, c) for c in cls.SAFE_INV_COLUMNS])
        load_fa_opt = joinedload(Part.fa_data)
        query = query.options(load_inv_opt, load_fa_opt)

        item = query.first()
        
        # FIX PROACTIVO: Inyectar None
        if item and item.inv_data:
            for col in cls.MISSING_INV_COLUMNS:
                item.inv_data.__dict__[col] = None
                
        return item

    @classmethod
    def create(cls, db: Session, part_data: PartCreateDTO, tenant_id: int, company_id: int) -> Part:
        # 1. Preparar datos
        data = part_data.model_dump()
        _apply_part_currency_defaults(data)
        print(f"DEBUG: PartService.create - full data: {data}")
        
        # Separar datos anidados
        fa_dict = data.pop('fa_data', None)
        inv_dict = data.pop('inv_data', None)
        
        # Extraer BOM items si existen en inv_data
        bom_items_data = None
        if inv_dict:
            bom_items_data = inv_dict.pop('bom_items', None)
        
        # Extraer datos de países si existen en inv_data
        countries_data = None
        aphis_records_data = None
        if inv_dict:
            countries_data = inv_dict.pop('countries', None)
            aphis_records_data = inv_dict.pop('aphis_records', None)
        
        # Inyectar IDs de contexto (Seguridad Multi-tenant)
        data['company_id'] = company_id
        data['tenant_id'] = tenant_id

        # 2. Verificar duplicados (Usando la UniqueConstraint del modelo)
        existing = db.query(Part).filter(
            Part.tenant_id == tenant_id,
            Part.company_id == company_id,
            Part.part_number == data['part_number']
        ).first()

        if existing:
            raise HTTPException(
                status_code=400, 
                detail=f"El número de parte '{data['part_number']}' ya existe."
            )

        # 3. Crear objeto Part
        db_part = Part(**data)

        # 4. Crear relaciones (Anexo 24)
        # Importamos aquí para evitar ciclos, usando las rutas de tu modelo
        if fa_dict:
            from api.v1.modules.a24.fa.fa_parts.models import FaPart
            # Importante: Pasar tenant/company también al hijo
            db_part.fa_data = FaPart(**fa_dict, tenant_id=tenant_id, company_id=company_id)
        
        # Filtrar campos que no existen en la DB (Safe filtering) para insert Core
        safe_inv_data = None
        if inv_dict:
            safe_inv_data = {k: v for k, v in inv_dict.items() if k not in cls.MISSING_INV_COLUMNS}
            
        # 5. Agregar la parte primero para obtener el ID (flush)
        db.add(db_part)
        db.flush()  # Obtener el ID generado sin hacer commit

        # 5b. Insertar INV Data vía Core (Evita que el ORM use columnas inexistentes)
        if safe_inv_data is not None:
            from api.v1.modules.a24.inv.inv_parts.models import InvPart
            db.execute(
                sa_insert(InvPart.__table__).values(
                    id=db_part.id,
                    tenant_id=tenant_id,
                    company_id=company_id,
                    **safe_inv_data
                )
            )
        
        # Ahora crear BOM items con el parent_part_id
        if bom_items_data:
            from api.v1.modules.a24.inv.bom.models import BillOfMaterial
            for item in bom_items_data:
                # El DTO puede traer component_part_number pero el modelo usa IDs
                # En creación asumimos que enviamos component_part_id
                item.pop('id', None)  # Limpiar ID si viene
                item.pop('component_part_number', None)  # Limpiar part_number
                item['parent_part_id'] = db_part.id  # Asignar el parent_part_id
                bom_obj = BillOfMaterial(
                    **item,
                    tenant_id=tenant_id,
                    company_id=company_id
                )
                db_part.bom_items.append(bom_obj)

        # 6. Crear relaciones con países
        if countries_data:
            from api.v1.modules.a24.inv.part_countries.models import PartCountry
            for country in countries_data:
                country_obj = PartCountry(
                    part_id=db_part.id if db_part.id else None,  # Se asignará después del flush
                    **country,
                    tenant_id=tenant_id,
                    company_id=company_id
                )
                db_part.inv_countries.append(country_obj)

        # 7. Crear registros Aphis
        if aphis_records_data:
            from api.v1.modules.a24.inv.inv_aphis.models import (
                InvPartAphisGeneral, InvPartAphisCharacteristic, InvPartAphisStypePitems,
                InvPartAphisLpcos, InvPartAphisEntities, InvPartAphisContainers, InvPartAphisRouting
            )
            for a_data in aphis_records_data:
                a_data.pop('id', None)
                char_data = a_data.pop('characteristics', [])
                stype_data = a_data.pop('stype_pitems', [])
                lpco_data = a_data.pop('lpcos', [])
                entity_data = a_data.pop('entities', [])
                container_data = a_data.pop('containers', [])
                routing_data = a_data.pop('routing', [])
                
                aphis_obj = InvPartAphisGeneral(
                    **a_data, 
                    inv_part_id=db_part.id, 
                    tenant_id=tenant_id, 
                    company_id=company_id
                )
                
                for c_data in char_data:
                    c_data.pop('id', None); c_data.pop('aphis_general_id', None)
                    aphis_obj.characteristics.append(InvPartAphisCharacteristic(**c_data, tenant_id=tenant_id, company_id=company_id))
                    
                for s_data in stype_data:
                    s_data.pop('id', None); s_data.pop('aphis_general_id', None)
                    aphis_obj.stype_pitems.append(InvPartAphisStypePitems(**s_data, tenant_id=tenant_id, company_id=company_id))

                for l_data in lpco_data:
                    l_data.pop('id', None); l_data.pop('aphis_general_id', None)
                    aphis_obj.lpcos.append(InvPartAphisLpcos(**l_data, tenant_id=tenant_id, company_id=company_id))

                for e_data in entity_data:
                    e_data.pop('id', None); e_data.pop('aphis_general_id', None)
                    aphis_obj.entities.append(InvPartAphisEntities(**e_data, tenant_id=tenant_id, company_id=company_id))

                for con_data in container_data:
                    con_data.pop('id', None); con_data.pop('aphis_general_id', None)
                    aphis_obj.containers.append(InvPartAphisContainers(**con_data, tenant_id=tenant_id, company_id=company_id))

                for r_data in routing_data:
                    r_data.pop('id', None); r_data.pop('aphis_general_id', None)
                    aphis_obj.routing.append(InvPartAphisRouting(**r_data, tenant_id=tenant_id, company_id=company_id))
                    
                db.add(aphis_obj)

        try:
            db.commit()  # db.add ya se hizo en el flush anterior
            db.refresh(db_part)
            return db_part

        except IntegrityError as e:
            db.rollback()
            err_msg = str(e.orig)
            logger.exception("Error DB creando parte")
            
            # Mensajes específicos para BOM
            if "inv_bom_component" in err_msg or "fk_inv_bom_component" in err_msg:
                raise HTTPException(
                    400, 
                    "Error en BOM: La parte componente no existe. Verifique que haya seleccionado una parte válida."
                )
            if "inv_bom_parent" in err_msg or "fk_inv_bom_parent" in err_msg:
                raise HTTPException(
                    400, 
                    "Error en BOM: La parte padre no existe. Contacte al administrador."
                )
            
            if "foreign key" in err_msg:
                if "unit_of_measure" in err_msg:
                    raise HTTPException(400, "La Unidad de Medida no existe en el catálogo.")
                if "currency_key" in err_msg:
                    raise HTTPException(400, "La Moneda no existe en el catálogo.")
                if "part_class" in err_msg:
                    raise HTTPException(400, "La Clase no existe para este cliente.")
                
                # El error genérico si falla Company/Client
                raise HTTPException(400, "Error de referencia: Verifique Cliente, Compañía o Catálogos.")
            
            raise HTTPException(400, f"Error al guardar la parte: {err_msg}")

    @classmethod
    def update(cls, db: Session, part_id: int, tenant_id: int, part_data: PartUpdateDTO, company_id: int) -> Optional[Part]:
        print(f"DEBUG: PartService.update - part_id={part_id}, data={part_data.model_dump(exclude_unset=True)}")
        db_part = PartService.get_by_id(db, part_id, tenant_id, company_id)
        if not db_part:
            return None

        data = part_data.model_dump(exclude_unset=True)
        if "currency_key" in data and (
            data["currency_key"] is None
            or (isinstance(data["currency_key"], str) and not data["currency_key"].strip())
        ):
            data["currency_key"] = "USD"
        if "currency_type" in data and (
            data["currency_type"] is None
            or (isinstance(data["currency_type"], str) and not data["currency_type"].strip())
        ):
            data["currency_type"] = "EX"
        fa_dict = data.pop('fa_data', None)
        inv_dict = data.pop('inv_data', None)
        
        bom_items_data = None
        if inv_dict:
            bom_items_data = inv_dict.pop('bom_items', None)
            
        countries_data = None
        aphis_records_data = None
        if inv_dict:
            countries_data = inv_dict.pop('countries', None)
            aphis_records_data = inv_dict.pop('aphis_records', None)

        # Actualizar campos directos
        for key, value in data.items():
            setattr(db_part, key, value)

        # Actualizar FA Data
        if fa_dict is not None:
            if db_part.fa_data:
                for k, v in fa_dict.items():
                    setattr(db_part.fa_data, k, v)
            else:
                from api.v1.modules.a24.fa.fa_parts.models import FaPart
                db_part.fa_data = FaPart(**fa_dict, tenant_id=tenant_id, company_id=company_id)

        # Actualizar INV Data vía Core
        if inv_dict is not None:
            from api.v1.modules.a24.inv.inv_parts.models import InvPart
            safe_inv_dict = {k: v for k, v in inv_dict.items() if k in cls.SAFE_INV_COLUMNS}
            
            # Verificar si ya existe el registro en inv_partes
            has_inv = db.query(InvPart.id).filter(InvPart.id == part_id).first() is not None
            
            if has_inv:
                if safe_inv_dict:
                    db.execute(
                        sa_update(InvPart.__table__)
                        .where(InvPart.__table__.c.id == part_id)
                        .values(**safe_inv_dict)
                    )
            else:
                db.execute(
                    sa_insert(InvPart.__table__)
                    .values(
                        id=part_id, 
                        tenant_id=tenant_id, 
                        company_id=company_id, 
                        **safe_inv_dict
                    )
                )
            # Expirar para que se recargue con el load_only fix si se accede
            db.expire(db_part, ["inv_data"])

        # Actualizar BOM items
        if bom_items_data is not None:
            from api.v1.modules.a24.inv.bom.models import BillOfMaterial
            # Estrategia: Reemplazo total por ahora para simplicidad (típico en BOMs de formularios)
            # Si se requiere edición fina por ID se puede implementar luego
            db_part.bom_items = []
            for item in bom_items_data:
                # Limpiar ID si viene para que SQLAlchemy cree nuevos o los maneje
                item.pop('id', None)
                item.pop('component_part_number', None)
                # Asignar el parent_part_id (la parte que se está editando)
                item['parent_part_id'] = db_part.id
                bom_obj = BillOfMaterial(
                    **item,
                    tenant_id=tenant_id,
                    company_id=company_id
                )
                db_part.bom_items.append(bom_obj)

        # Actualizar relaciones con países
        if countries_data is not None:
            from api.v1.modules.a24.inv.part_countries.models import PartCountry
            # Estrategia: Reemplazo total (típico en formularios)
            db_part.inv_countries = []
            for country in countries_data:
                country_obj = PartCountry(
                    part_id=db_part.id,
                    **country,
                    tenant_id=tenant_id,
                    company_id=company_id
                )
                db_part.inv_countries.append(country_obj)

        # Actualizar registros Aphis (Esquema relacional)
        if aphis_records_data is not None:
            from api.v1.modules.a24.inv.inv_aphis.models import (
                InvPartAphisGeneral,
                InvPartAphisCharacteristic,
                InvPartAphisStypePitems,
                InvPartAphisLpcos,
                InvPartAphisEntities,
                InvPartAphisContainers,
                InvPartAphisRouting
            )
            if db_part.inv_data:
                # Reemplazo total de registros Aphis para esta parte
                db_part.inv_data.aphis_records = []
                for a_data in aphis_records_data:
                    # Limpiar IDs y sub-datos para recreación
                    a_data.pop('id', None)
                    
                    char_data = a_data.pop('characteristics', [])
                    stype_data = a_data.pop('stype_pitems', [])
                    lpco_data = a_data.pop('lpcos', [])
                    entity_data = a_data.pop('entities', [])
                    container_data = a_data.pop('containers', [])
                    routing_data = a_data.pop('routing', [])
                    
                    aphis_obj = InvPartAphisGeneral(
                        **a_data,
                        tenant_id=tenant_id,
                        company_id=company_id
                    )
                    
                    for c_data in char_data:
                        c_data.pop('id', None)
                        c_data.pop('aphis_general_id', None)
                        aphis_obj.characteristics.append(
                            InvPartAphisCharacteristic(**c_data, tenant_id=tenant_id, company_id=company_id)
                        )
                        
                    for s_data in stype_data:
                        s_data.pop('id', None)
                        s_data.pop('aphis_general_id', None)
                        aphis_obj.stype_pitems.append(
                            InvPartAphisStypePitems(**s_data, tenant_id=tenant_id, company_id=company_id)
                        )

                    for l_data in lpco_data:
                        l_data.pop('id', None)
                        l_data.pop('aphis_general_id', None)
                        aphis_obj.lpcos.append(
                            InvPartAphisLpcos(**l_data, tenant_id=tenant_id, company_id=company_id)
                        )

                    for e_data in entity_data:
                        e_data.pop('id', None)
                        e_data.pop('aphis_general_id', None)
                        aphis_obj.entities.append(
                            InvPartAphisEntities(**e_data, tenant_id=tenant_id, company_id=company_id)
                        )

                    for con_data in container_data:
                        con_data.pop('id', None)
                        con_data.pop('aphis_general_id', None)
                        aphis_obj.containers.append(
                            InvPartAphisContainers(**con_data, tenant_id=tenant_id, company_id=company_id)
                        )

                    for r_data in routing_data:
                        r_data.pop('id', None)
                        r_data.pop('aphis_general_id', None)
                        aphis_obj.routing.append(
                            InvPartAphisRouting(**r_data, tenant_id=tenant_id, company_id=company_id)
                        )
                        
                    db_part.inv_data.aphis_records.append(aphis_obj)

        try:
            db.commit()
            db.refresh(db_part)
            return db_part
        except IntegrityError as e:
            db.rollback()
            err_msg = str(e.orig)
            logger.exception("Error DB actualizando parte")
            
            # Mensajes de error específicos para BOM
            if "inv_bom_component" in err_msg or "fk_inv_bom_component" in err_msg:
                raise HTTPException(
                    400, 
                    "Error en BOM: La parte componente no existe en la base de datos. Verifique que haya seleccionado una parte válida."
                )
            if "inv_bom_parent" in err_msg or "fk_inv_bom_parent" in err_msg:
                raise HTTPException(
                    400, 
                    "Error en BOM: La parte padre no existe. Contacte al administrador."
                )
            
            raise HTTPException(400, f"Error actualizando: {err_msg}")

    @staticmethod
    def delete(db: Session, part_id: int, tenant_id: int, company_id: int) -> bool:
        db_part = PartService.get_by_id(db, part_id, tenant_id, company_id)
        if not db_part: return False
        
        try:
            db.delete(db_part)
            db.commit()
            return True
        except Exception:
            db.rollback()
            raise HTTPException(500, "Error eliminando parte")