from decimal import Decimal
from sqlalchemy import func, exists
from sqlalchemy.orm import Session
from ...common.common_validators import count_items
from core.exceptions import ErrorCollector

from ...models import LineItem
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
from ...models import LineItem
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.packages.models import Package
from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
    american_fraction_ad_valorem_from_row,
    resolve_american_fraction_from_sitar,
    store_canonical_american_code,
)
from api.v1.modules.a76.app_settings.service import AppSettingsService
from .common import validate_common


def _normalize_weight_type(logistics) -> str:
    """Paridad con CSV: enum o string a 'KGS'/'LBS' para comparaciones."""
    if logistics is None:
        return "KGS"
    wt = getattr(logistics, "weight_type", None) or "KGS"
    if hasattr(wt, "value"):
        wt = wt.value
    weight_str = str(wt).upper() if wt else "KGS"
    return weight_str if weight_str in ("KGS", "LBS") else "KGS"


def validate_create(
    db: Session,
    line: LineItem,
    tenant_id: int,
    company_id: int,
    errors: ErrorCollector,
    line_number: int,
):
    """
    Validates and calculates fields for a new line item before DB creation.
    Works with Pydantic schemas, modifying them in-place.

    Args:
        line: LineItemCreate schema with nested data (financial, quantity, customs, etc.)
        invoice_id: ID of the invoice this line belongs to
        fa_data: FaLineItemCreateDTO or None (None for INV system)
    """    

    # Access fa_data safely
    fa_data: FaLineItem = getattr(line, "fa_data", None)

    # Required field validations
    if not line.class_id:
        errors.add_required_error(field=f"line[{line_number}].class_id")

    if not line.quantity or line.quantity.quantity is None:
        errors.add_required_error(field=f"line[{line_number}].quantity.quantity")
    elif line.quantity.quantity <= 0:
        errors.add_error(
            field=f"line[{line_number}].quantity.quantity",
            message=f"La cantidad debe ser mayor a cero (recibido: {line.quantity.quantity})",
            solution=["Capturar una cantidad válida mayor a cero."],
            code="INVALID_QUANTITY",
            value=float(line.quantity.quantity)
        )

    # 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

    # Parámetro SCAF: Calcular costo unitario en base a valor total
    calc_costo_unit = find_in_obj(settings, 'calcularcostounitarioenbaseavalortotal') or \
                      find_in_obj(settings, 'calcularcostounitarioenbaseavalortotalscaf')

    if fa_data and not fa_data.is_subitem:
        # Si el parámetro está apagado (o no existe), el costo unitario es obligatorio
        if str(calc_costo_unit) != "1":
            if (
                not line.financial
                or not line.financial.unit_cost_capture
                or line.financial.unit_cost_capture <= 0
            ):
                errors.add_required_error(
                    field=f"line[{line_number}].financial.unit_cost_capture"
                )

    if not line.quantity or line.quantity.net_weight is None:
        errors.add_required_error(field=f"line[{line_number}].quantity.net_weight")
    elif line.quantity.net_weight <= 0:
        errors.add_error(
            field=f"line[{line_number}].quantity.net_weight",
            message=f"El peso neto debe ser mayor a cero (recibido: {line.quantity.net_weight})",
            solution=["Capturar un peso neto válido mayor a cero."],
            code="INVALID_NET_WEIGHT",
            value=float(line.quantity.net_weight)
        )

    if not line.customs or not line.customs.origin_country:
        errors.add_required_error(field=f"line[{line_number}].customs.origin_country")

    if not line.customs or not line.customs.fraction_type:
        errors.add_required_error(field=f"line[{line_number}].customs.fraction_type")

    # FA-specific validations
    if fa_data:
        if (
            fa_data.is_subitem and fa_data.contains_subitems
        ) and not fa_data.subitem_number:
            errors.add_required_error(
                field=f"line[{line_number}].fa_data.subitem_number"
            )

        # Validar que si es un subitem, existe un item principal correspondiente
        if (
            fa_data.is_subitem
            and fa_data.subitem_number
            and fa_data.subitem_number != 0
        ):
            principal_item_exists = db.query(
                exists().where(
                    (LineItem.id == FaLineItem.id)
                    & (LineItem.id == LineItem.id)
                    & (LineItem.invoice_id == line.invoice_id)
                    & (LineItem.line_number == line_number)
                    & (FaLineItem.is_subitem == False)
                    & (FaLineItem.contains_subitems == True)
                    & (LineItem.tenant_id == tenant_id)
                    & (LineItem.company_id == company_id)
                )
            ).scalar()

            if not principal_item_exists:
                errors.add_error(
                    field=f"line[{line_number}]",
                    message=f"No existe un item principal registrado para esta linea {line_number} con subitem {fa_data.subitem_number}",
                    solution=[
                        "Registrar el item principal correspondiente a esta linea antes de registrar subitems."
                    ],
                    code="SUBITEM_WITHOUT_PRINCIPAL_ITEM",
                )

        if fa_data.is_subitem and (
            fa_data.subitem_number == 0 or not fa_data.subitem_number
        ):
            errors.add_error(
                field=f"line[{line_number}]",
                message=f"El número de subitem no puede ser 0 si la línea es un subitem.",
                solution=["Asignar un número de subitem mayor a 0 para esta línea."],
                code="SUBITEM_NUMBER_INVALID",
            )

    validate_common(db, line, tenant_id, company_id, errors, line_number)

    if not errors.has_errors():
        # Ensure nested objects exist for calculations
        from ...schemas import (
            LineFinancialCreate,
            LineQuantityCreate,
            LineCustomCreate,
            LineDescriptionCreate,
        )

        if line.financial is None:
            line.financial = LineFinancialCreate()
        if line.quantity is None:
            line.quantity = LineQuantityCreate()
        if line.customs is None:
            line.customs = LineCustomCreate()
        if line.description is None:
            line.description = LineDescriptionCreate()

        # Obtener la factura para acceder a tipo de cambio, moneda y peso
        invoice: InvoiceHeader = (
            db.query(InvoiceHeader)
            .filter(
                InvoiceHeader.id == line.invoice_id,
                InvoiceHeader.tenant_id == tenant_id,
                InvoiceHeader.company_id == company_id,
            )
            .first()
        )

        if not invoice or not invoice.financials or not invoice.logistics:
            errors.add_error(
                field=f"line[{line_number}]",
                message="No se pudo obtener información de la factura",
                solution=[
                    "Verificar que la factura existe y tiene datos financieros y logísticos"
                ],
                code="INVOICE_DATA_MISSING",
            )
            return

        # Obtener la clase para valores por defecto
        class_info: Class = (
            db.query(Class)
            .filter(
                Class.id == line.class_id,
                Class.tenant_id == tenant_id,
                Class.company_id == company_id,
            )
            .first()
        )

        # ==========================================
        # ASIGNAR TIPO DE CAMBIO
        # ==========================================
        exchange_rate = invoice.financials.exchange_rate or Decimal("1.0")

        # ==========================================
        # ASIGNAR UNIDAD DE MEDIDA
        # ==========================================
        # Si no se proporcionó unidad de medida, usar la de la clase
        if not line.unit_of_measure and class_info:
            line.unit_of_measure = class_info.unit_of_measure

        # ==========================================
        # ASIGNAR TIPOS DE MONEDA Y CALCULAR COSTOS
        # ==========================================
        currency_type = invoice.financials.currency_type
        unit_cost_capture = line.financial.unit_cost_capture or Decimal("0")

        # Calcular costos según tipo de moneda
        if currency_type == "USD" or currency_type == "ME":  # Moneda Extranjera (ME)
            line.financial.unit_cost_capture = unit_cost_capture
            line.financial.unit_cost_usd = unit_cost_capture
            line.financial.unit_cost_mxn = unit_cost_capture * exchange_rate
        elif currency_type == "MXN" or currency_type == "MN":  # Moneda Nacional (MN)
            line.financial.unit_cost_capture = unit_cost_capture
            line.financial.unit_cost_usd = (
                unit_cost_capture / exchange_rate if exchange_rate else Decimal("0")
            )
            line.financial.unit_cost_mxn = unit_cost_capture
        # Si es otro tipo de moneda, dejamos el costo como está

        # Calcular valores totales basados en cantidad y costo unitario
        quantity = line.quantity.quantity or Decimal("0")
        
        # Valor Comercial
        if line.financial.unit_cost_usd is not None:
            line.financial.value_usd = line.financial.unit_cost_usd * quantity
        if line.financial.unit_cost_mxn is not None:
            line.financial.value_mxn = line.financial.unit_cost_mxn * quantity
            
        # Valor Aduanas (asumiendo que es igual al Valor Comercial por defecto)
        line.financial.customs_value_usd = line.financial.value_usd
        line.financial.customs_value_mxn = line.financial.value_mxn
        
        # Valor MP Temp (Materia Prima Temporal)
        line.financial.value_temp_material_usd = line.financial.value_usd
        line.financial.value_temp_material_mxn = line.financial.value_mxn

        # value_mc: paridad con calculate_values y cargas CSV
        currency = getattr(invoice.financials, "currency", None)
        if currency == "foreign":
            line.financial.value_mc = line.financial.value_usd
        elif currency == "local":
            line.financial.value_mc = line.financial.value_usd
        elif currency == "manual":
            line.financial.value_mc = unit_cost_capture * quantity
        else:
            line.financial.value_mc = line.financial.value_usd

        # ==========================================
        # VALIDAR Y CONVERTIR PESOS NETOS
        # ==========================================
        invoice_weight_type = _normalize_weight_type(invoice.logistics)
        quantity = line.quantity.quantity or Decimal("0")
        net_weight_input = line.quantity.net_weight or Decimal("0")

        # UOM peso: aceptar 24/"24" y 25/"25" (paridad con CSV)
        uom = line.unit_of_measure
        unit_is_kgs = uom is not None and (str(uom) == "24" or uom == 24)
        unit_is_lbs = uom is not None and (str(uom) == "25" or uom == 25)

        # Calcular peso neto en kilogramos (estándar interno)
        if unit_is_kgs:
            if invoice_weight_type.lower() == "kgs":
                line.quantity.net_weight = quantity
            else:  # invoice en libras
                line.quantity.net_weight = quantity * Decimal("2.204624")
        elif unit_is_lbs:
            if invoice_weight_type.lower() == "kgs":
                line.quantity.net_weight = quantity / Decimal("2.204624")
            else:  # invoice en libras
                line.quantity.net_weight = quantity
        else:
            # Otra unidad de medida - usar peso capturado y convertir si es necesario
            if invoice_weight_type.lower() == "kgs":
                # El peso capturado está en kilos
                line.quantity.net_weight = net_weight_input
            else:
                # El peso capturado está en libras, convertir a kilos
                line.quantity.net_weight = net_weight_input / Decimal("2.204624")

        # ==========================================
        # CALCULAR PESO BRUTO
        # ==========================================
        gross_weight_input = line.quantity.gross_weight
        package_quantity = line.quantity.package_quantity or 0
        package_weight_unit = Decimal("0")

        # Obtener peso unitario del bulto si existe
        if line.quantity.package_id:
            package: Package = (
                db.query(Package)
                .filter(
                    Package.id == line.quantity.package_id,
                    Package.tenant_id == tenant_id,
                    Package.company_id == company_id,
                )
                .first()
            )
            if package and package.weight_unit:
                package_weight_unit = package.weight_unit

        # Si no se proporcionó peso bruto, calcularlo
        if not gross_weight_input or gross_weight_input == 0:
            if invoice_weight_type.lower() == "kgs":
                line.quantity.gross_weight = line.quantity.net_weight + (
                    package_weight_unit * package_quantity
                )
            else:  # libras
                line.quantity.gross_weight = line.quantity.net_weight + (
                    (package_weight_unit * Decimal("2.204624")) * package_quantity
                )
        else:
            # Convertir peso bruto capturado según tipo de factura
            if invoice_weight_type.lower() == "kgs":
                line.quantity.gross_weight = gross_weight_input
            else:  # libras
                line.quantity.gross_weight = gross_weight_input / Decimal("2.204624")

        # ==========================================
        # VALIDAR PESO BRUTO < PESO NETO
        # ==========================================
        if line.quantity.gross_weight < line.quantity.net_weight:
            line.quantity.gross_weight = line.quantity.net_weight + (
                package_weight_unit * package_quantity
            )

        # ==========================================
        # ASIGNAR DESCRIPCIÓN DE BULTOS
        # ==========================================
        if package_quantity and package_quantity > 0 and line.quantity.package_id:
            package: Package = (
                db.query(Package)
                .filter(
                    Package.id == line.quantity.package_id,
                    Package.tenant_id == tenant_id,
                    Package.company_id == company_id,
                )
                .first()
            )
            if package:
                line.description.package_description = package.description_es
        else:
            line.quantity.package_quantity = 0
            line.quantity.package_id = None
            line.description.package_description = None

        # ==========================================
        # ASIGNAR FRACCIÓN AMERICANA POR DEFECTO
        # ==========================================
        if not line.customs.american_fraction and class_info and class_info.us_fraction:
            line.customs.american_fraction = class_info.us_fraction

        # Ad valorem desde catálogo SITAR (fracciones-usa)
        if line.customs.american_fraction:
            resolved = resolve_american_fraction_from_sitar(str(line.customs.american_fraction))
            if resolved:
                sitar_row, canon = resolved
                line.customs.american_fraction = store_canonical_american_code(canon)
                line.customs.advalorem_american = american_fraction_ad_valorem_from_row(
                    sitar_row
                )

        # ==========================================
        # ASIGNAR DESCRIPCIONES POR DEFECTO
        # ==========================================
        if not line.description.description_spanish and class_info:
            line.description.description_spanish = class_info.description_es

        if not line.description.description_english and class_info:
            line.description.description_english = class_info.description_en

        # ==========================================
        # NORMALIZAR CAMPOS DE TEXTO
        # ==========================================
        # Convertir a mayúsculas campos que lo requieran
        if line.description.brand:
            line.description.brand = line.description.brand.upper().strip()

        if line.description.model:
            line.description.model = line.description.model.upper().strip()

        # ==========================================
        # CUMPLIMIENTO MEXICANO (Valores por defecto gestionados en apply_calculations)
        # ==========================================
