from sqlalchemy import exists
from sqlalchemy.orm import Session
from api.v1.modules.a76.invoices.common.common_validators import invoice_id_required
from core.exceptions import ErrorCollector
from sqlalchemy import func

from ...common.fractions import search_fraction_preference
from ...common.common_validators import item_exists
from ...models import LineItem
from ...line_customs.models import FractionType, LineCustom
from api.v1.modules.a76.items.schemas import LineItemCreate
from api.v1.modules.a76.items.exports.validators.calculations import apply_calculations
from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
from api.v1.modules.a76.general_catalogs.packages.models import Package
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
    resolve_american_fraction_from_sitar,
    store_canonical_american_code,
)
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
from api.v1.modules.public.reference_data.countries.models import Country
from api.v1.modules.a76.general_catalogs.sectors.models import Sector
from api.v1.modules.public.reference_data.valuation_methods.models import (
    ValuationMethod,
)
from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a76.app_settings.service import AppSettingsService
from api.v1.modules.a76.general_catalogs.company.models import Company


def validate_common(
    db: Session,
    line: LineItemCreate,
    tenant_id: int,
    company_id: int,
    errors: ErrorCollector,
    line_number: int,
):
    invoice: InvoiceHeader = invoice_id_required(
        db, line.invoice_id, tenant_id, company_id, errors
    )
    line_item: LineItem = item_exists(db, line.line_number, tenant_id, company_id)

    fecha_factura = invoice.invoice_date if invoice else None
    fraction = None

    class_ = db.query(Class).filter(Class.id == line.class_id).first()
    if not class_:
        errors.add_error(
            field=f"line[{line_number}].class_id",
            message="La clase especificada no existe.",
            solution=["Darla de alta en el catalogo de clases."],
            code="CLASS_NOT_FOUND",
        )
    else:
        if not line.unit_of_measure and not class_.unit_of_measure:
            errors.add_error(
                field=f"line[{line_number}].unit_of_measure",
                message="La unidad de medida es obligatoria para la clase especificada.",
                solution=["Proporciona una unidad de medida valida."],
                code="UNIT_OF_MEASURE_REQUIRED",
            )

        if not line.customs.fraction:
            if not line_item:
                if not class_.fraction:
                    errors.add_error(
                        field=f"line[{line_number}].customs.fraction",
                        message="La fracción arancelaria es obligatoria para la clase especificada.",
                        solution=["Proporciona una fracción arancelaria valida."],
                        code="FRACTION_REQUIRED",
                    )
                else:
                    fraction = class_.fraction
            else:
                if not line.customs.fraction:
                    if not class_.fraction:
                        errors.add_error(
                            field=f"line[{line_number}].customs.fraction",
                            message="La fracción arancelaria es obligatoria para la clase especificada.",
                            solution=["Proporciona una fracción arancelaria valida."],
                            code="FRACTION_REQUIRED",
                        )
                    else:
                        fraction = class_.fraction
                else:
                    if line_item:
                        fraction = line.customs.fraction

        if not line.description.description_spanish and not class_.description_es:
            errors.add_error(
                field=f"line[{line_number}].description.description_spanish",
                message="La descripción en español es obligatoria para la clase especificada.",
                solution=["Proporciona una descripción en español valida."],
                code="DESCRIPTION_SPANISH_REQUIRED",
            )

        if not line.description.description_english and not class_.description_en:
            errors.add_error(
                field=f"line[{line_number}].description.description_english",
                message="La descripción en inglés es obligatoria para la clase especificada.",
                solution=["Proporciona una descripción en inglés valida."],
                code="DESCRIPTION_ENGLISH_REQUIRED",
            )

    if line.quantity.quantity and line.quantity.quantity <= 0:
        errors.add_error(
            field=f"line[{line_number}].quantity.quantity",
            message="La cantidad debe ser mayor a cero.",
            solution=["Proporciona una cantidad valida."],
            code="QUANTITY_MUST_BE_GREATER_THAN_ZERO",
        )

    if line.unit_of_measure:
        um = (
            db.query(func.count(UnitOfMeasure.id))
            .filter(
                UnitOfMeasure.id == line.unit_of_measure,
                UnitOfMeasure.tenant_id == tenant_id,
                UnitOfMeasure.company_id == company_id,
            )
            .scalar()
        )
        if um == 0:
            errors.add_error(
                field=f"line[{line_number}].unit_of_measure",
                message="La unidad de medida especificada no existe.",
                solution=["Proporciona una unidad de medida valida."],
                code="UNIT_OF_MEASURE_NOT_FOUND",
            )

    if line.quantity.package_id:
        package = (
            db.query(func.count(Package.id))
            .filter(
                Package.id == line.quantity.package_id,
                Package.tenant_id == tenant_id,
                Package.company_id == company_id,
            )
            .scalar()
        )
        if package == 0:
            errors.add_error(
                field=f"line[{line_number}].quantity.package_id",
                message="El paquete especificado no existe.",
                solution=["Proporciona un paquete valido."],
                code="PACKAGE_NOT_FOUND",
            )
        if not line.quantity.package_quantity:
            errors.add_error(
                field=f"line[{line_number}].quantity.package_quantity",
                message="La cantidad de paquetes es obligatoria cuando se proporciona el paquete.",
                solution=["Proporciona una cantidad de paquetes valida."],
                code="PACKAGE_QUANTITY_REQUIRED",
            )
        if line.quantity.package_quantity <= 0:
            errors.add_error(
                field=f"line[{line_number}].quantity.package_quantity",
                message="La cantidad de paquetes debe ser mayor a cero.",
                solution=["Proporciona una cantidad de paquetes valida."],
                code="PACKAGE_QUANTITY_MUST_BE_GREATER_THAN_ZERO",
            )
    else:
        if line.quantity.package_quantity and (line.quantity.package_quantity > 0 and not line.quantity.package_id): 
            errors.add_error(
                field=f"line[{line_number}].quantity.package_id",
                message="El paquete es obligatorio cuando se proporciona la cantidad de paquetes.",
                solution=["Proporciona un paquete valido."],
                code="PACKAGE_ID_REQUIRED",
            )

    # ==========================================
    # FA DATA: PROCEDENCIA Y REFERENCIA DE IMPORTACIÓN (Col. C / D / E / H)
    # ==========================================
    fa_data: FaLineItem = getattr(line, "fa_data", None)

    if fa_data and fa_data.movement_type_import:
        # Col. C: debe ser 'TEM' o 'DEF'
        if fa_data.movement_type_import.upper() not in ("TEM", "DEF"):
            errors.add_error(
                field=f"line[{line_number}].fa_data.movement_type_import",
                message=f"La Procedencia '{fa_data.movement_type_import}' no es válida. Debe ser 'TEM' o 'DEF'.",
                solution=["Capturar una procedencia válida como TEM o DEF."],
                code="MOVEMENT_TYPE_IMPORT_INVALID",
            )
        elif fa_data.search_invoice:
            tipo_label = "Temporales" if fa_data.movement_type_import.upper() == "TEM" else "Definitivas"
            tipo_label_sg = "Temporal" if fa_data.movement_type_import.upper() == "TEM" else "Definitiva"

            # Col. D: validar que la factura de importación exista
            import_invoice: InvoiceHeader = (
                db.query(InvoiceHeader)
                .filter(
                    InvoiceHeader.invoice_number == fa_data.search_invoice,
                    InvoiceHeader.invoice_type == fa_data.movement_type_import.upper(),
                    InvoiceHeader.tenant_id == tenant_id,
                    InvoiceHeader.company_id == company_id,
                )
                .first()
            )
            if not import_invoice:
                errors.add_error(
                    field=f"line[{line_number}].fa_data.search_invoice",
                    message=f"La Factura '{fa_data.search_invoice}' de Importación {tipo_label_sg} no existe.",
                    solution=[f"Capturar un Número de Factura que exista en el Catálogo de Importaciones {tipo_label}."],
                    code="IMPORT_INVOICE_NOT_FOUND",
                )
            elif fa_data.search_line:
                # Col. D + E: validar que la línea de importación exista dentro de esa factura
                import_line: LineItem = (
                    db.query(LineItem)
                    .filter(
                        LineItem.invoice_id == import_invoice.id,
                        LineItem.line_number == fa_data.search_line,
                        LineItem.tenant_id == tenant_id,
                        LineItem.company_id == company_id,
                    )
                    .first()
                )
                if not import_line:
                    errors.add_error(
                        field=f"line[{line_number}].fa_data.search_line",
                        message=f"La Factura '{fa_data.search_invoice}' con línea '{fa_data.search_line}' de Importación {tipo_label_sg} no existe.",
                        solution=["Capturar un Número de Factura con diferente línea que esté en el Catálogo de Importaciones correspondiente."],
                        code="IMPORT_LINE_NOT_FOUND",
                    )
                elif (
                    # Col. H: valida unidad de medida sólo cuando hay descarga
                    fa_data.discharge is True
                    and line.unit_of_measure
                    and import_line.unit_of_measure
                    and line.unit_of_measure != import_line.unit_of_measure
                ):
                    errors.add_error(
                        field=f"line[{line_number}].unit_of_measure",
                        message=(
                            f"La Unidad de Medida '{line.unit_of_measure}' es diferente de "
                            f"'{import_line.unit_of_measure}', que es la U.M. de la Factura "
                            f"'{fa_data.search_invoice}' con línea '{fa_data.search_line}' "
                            f"de Importación {tipo_label_sg}."
                        ),
                        solution=["Capturar la Unidad de Medida correcta para el descargo de esta línea de Importación."],
                        code="UNIT_OF_MEASURE_MISMATCH",
                    )

    country = None
    fraction_type = None
    sector = None
    if fraction:
        fraction = line.customs.fraction if line.customs.fraction else fraction

    country = line.customs.origin_country
    if line_item and line_item.customs:
        country = (
            line_item.customs.origin_country
            if line_item.customs.origin_country
            else country
        )

    fraction_type = line.customs.fraction_type.upper()
    if line_item and line_item.customs:
        fraction_type = (
            line_item.customs.fraction_type
            if line_item.customs.fraction_type
            else fraction_type
        )

    sector = line.customs.sector
    if line_item and line_item.customs:
        sector = line_item.customs.sector if line_item.customs.sector else sector

    country_m3 = db.query(Country.m3_key).filter(Country.m3_key == country).scalar()
    if not country_m3:
        country_m3 = (
            db.query(Country.m3_key).filter(Country.ame_key == country).scalar()
        )

    country = country_m3
    if not country:
        errors.add_error(
            field=f"line[{line_number}].customs.origin_country",
            message="El país de origen especificado no existe.",
            solution=["Proporciona un país de origen valido."],
            code="ORIGIN_COUNTRY_NOT_FOUND",
        )
    else:
        if fraction_type.strip().upper() not in vars(FractionType).values():
            valid_types = [
                v
                for k, v in vars(FractionType).items()
                if not k.startswith("_") and isinstance(v, str)
            ]
            errors.add_error(
                field=f"line[{line_number}].customs.fraction_type",
                message="El tipo de fracción especificado no es válido.",
                solution=[
                    f"Proporciona un tipo de fracción válido. Valores permitidos: {', '.join(valid_types)}"
                ],
                code="FRACTION_TYPE_INVALID",
                value=fraction_type,
            )
        else:
            if fraction_type.strip().upper() == FractionType.PROSEC and not sector:
                errors.add_error(
                    field=f"line[{line_number}].customs.sector",
                    message="El sector es obligatorio cuando el tipo de fracción es 'PROSEC'.",
                    solution=["Proporciona un sector valido."],
                    code="SECTOR_REQUIRED_FOR_PROSEC",
                )
            elif fraction_type.strip().upper() != FractionType.PROSEC and sector:
                errors.add_error(
                    field=f"line[{line_number}].customs.sector",
                    message="El sector solo es aplicable cuando el tipo de fracción es 'PROSEC'.",
                    solution=[
                        "Elimina el sector o cambia el tipo de fracción a 'PROSEC'."
                    ],
                    code="SECTOR_ONLY_FOR_PROSEC",
                )
            elif fraction_type.strip().upper() == FractionType.PROSEC and sector:
                sector_db: Sector = (
                    db.query(Sector).filter(
                        Sector.key == sector,
                        Sector.tenant_id == tenant_id,
                        Sector.company_id == company_id,
                    ).scalar()
                )
                if sector_db:
                    errors.add_error(
                        field=f"line[{line_number}].customs.sector",
                        message="El sector especificado no existe.",
                        solution=["Proporciona un sector valido."],
                        code="SECTOR_NOT_FOUND",
                    )
                else:
                    if not sector_db.authorized:
                        errors.add_error(
                            field=f"line[{line_number}].customs.sector",
                            message="El sector especificado no está autorizado.",
                            solution=["Proporciona un sector autorizado."],
                            code="SECTOR_NOT_AUTHORIZED",
                        )

                company_db = db.query(Company).filter(Company.id == company_id).first()
                if not company_db.prosec:
                    errors.add_error(
                        field=f"line[{line_number}].customs.sector",
                        message=" La empresa no cuenta con autorización PROSEC.",
                        solution=[
                            "Accese a los datos de la empresa y selecione la opción Pertenece al Programa de Promoción Sectorial y capture el número de permiso PROSEC."
                        ],
                        code="COMPANY_NOT_AUTHORIZED_FOR_PROSEC",
                    )

            if fraction:
                search_fraction_preference(
                    db=db,
                    country=country,
                    fraccion=fraction,
                    fraction_type=fraction_type,
                    sector=sector,
                    invoice_date=fecha_factura,
                    errors=errors,
                )

    if line.customs.american_fraction:
        resolved = resolve_american_fraction_from_sitar(str(line.customs.american_fraction))
        if not resolved:
            errors.add_error(
                field=f"line[{line_number}].customs.american_fraction",
                message="La fracción americana especificada no existe.",
                solution=["Proporciona una fracción americana valida."],
                code="AMERICAN_FRACTION_NOT_FOUND",
            )
        else:
            _, canon = resolved
            line.customs.american_fraction = store_canonical_american_code(canon)

    if line.order:
        if len(line.order) > 20:
            errors.add_error(
                field=f"item.order",
                message="El campo orden no debe exceder los 20 caracteres.",
                solution=["Proporciona un valor valido para el campo orden."],
                code="ORDER_EXCEEDS_MAX_LENGTH",
            )

    unit_of_measure = line.unit_of_measure or (
        class_.unit_of_measure if class_ else None
    )
    
    # 0. Obtener parámetros de configuración para validaciones dinámicas
    settings = AppSettingsService.get_resolved_settings(db, int(tenant_id), int(company_id))
    
    def find_in_obj(obj, target_key):
        if isinstance(obj, dict):
            for k, v in obj.items():
                if k.lower() == target_key.lower():
                    return v
                res = find_in_obj(v, target_key)
                if res is not None: return res
        elif hasattr(obj, '__dict__'):
            dict_rep = obj.__dict__
            for k, v in dict_rep.items():
                if k.startswith('_'): continue
                if k.lower() == target_key.lower():
                    return v
                res = find_in_obj(v, target_key)
                if res is not None: return res
        return None

    # Resolver el CÓDIGO de la unidad de medida para validación (Blindaje con STRIP y UPPER)
    uom_code = ""
    if line.unit_of_measure:
        # Aseguramos que el ID sea entero
        try:
            target_uom_id = int(line.unit_of_measure)
        except:
            target_uom_id = line.unit_of_measure
            
        uom_rec = db.query(UnitOfMeasure).filter(
            UnitOfMeasure.id == target_uom_id,
            UnitOfMeasure.tenant_id == tenant_id,
            UnitOfMeasure.company_id == company_id
        ).first()
        if uom_rec and uom_rec.code:
            uom_code = str(uom_rec.code).strip().upper()
    elif class_ and class_.unit_of_measure:
        uom_code = str(class_.unit_of_measure).strip().upper()

    # Búsqueda robusta del parámetro (soporta alias técnicos)
    validar_dec_pza = find_in_obj(settings, 'validadecencant')
    if validar_dec_pza is None:
        validar_dec_pza = find_in_obj(settings, 'validadecencantscaii')

    # EXTRACCIÓN ROBUSTA DE LA CANTIDAD
    qty_val = 0
    if hasattr(line, 'quantity') and line.quantity:
        qty_val = getattr(line.quantity, 'quantity', 0) or 0
    
    # Validación: Comparar contra PZA y variantes comunes
    es_pieza = uom_code in ("PZA", "PZ", "PIEZA", "PIE", "Pzas", "Pza")
    config_activa = str(validar_dec_pza).lower() in ("1", "true")

    if es_pieza and config_activa:
        # SI LLEGAMOS AQUÍ Y HAY DECIMALES, VAMOS A FORZAR UN ERROR QUE DETENGA TODO
        if float(qty_val) % 1 != 0:
            raise ValueError(f"CRITICAL_VALIDATION: La unidad es {uom_code} y la cantidad {qty_val} tiene decimales. El proceso DEBE detenerse.")

    if es_pieza and config_activa and float(qty_val) % 1 != 0:
        errors.add_error(
            field=f"line[{line_number}].quantity.quantity",
            message=f"La cantidad ({qty_val}) no puede tener decimales cuando la unidad es PZA.",
            solution=["Captura una cantidad entera."],
            code="QUANTITY_INTEGER_REQUIRED",
        )

    if line.valuation_method:
        valuation_method_exists = db.query(
            exists().where(ValuationMethod.key == line.valuation_method)
        ).scalar()
        if not valuation_method_exists:
            errors.add_error(
                field=f"line[{line_number}].valuation_method",
                message="El método de valoración especificado no existe.",
                solution=["Proporciona un método de valoración valido."],
                code="VALUATION_METHOD_NOT_FOUND",
            )

    if line.part_number_id:
        part_exists = db.query(exists().where(Part.id == line.part_number_id)).scalar()
        if not part_exists:
            errors.add_error(
                field=f"line[{line_number}].part_number_id",
                message="El número de parte especificado no existe.",
                solution=["Proporciona un número de parte valido."],
                code="PART_NUMBER_NOT_FOUND",
            )

    apply_calculations(db, line, tenant_id, company_id, line_number)