
from datetime import date, datetime
from decimal import Decimal
from typing import List

from sqlalchemy.orm import Session

from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.invoices.models import Currency, InvoiceHeader, InvoiceStatus
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.a76.items.common.fractions import search_fraction_preference
from api.v1.modules.a76.general_catalogs.company.models import Company
from core.exceptions import ErrorCollector
from .pre_validators import pre_validators
from .sub_process.review_classes import review_classes
from .sub_process.review_exchange_rate import review_exchange_rate
from .sub_process.review_weights import review_weights_kgs, review_weights_lbs
from .sub_process.review_series import review_series
from .sub_process.review_rule_octave import (
    llena_impo_permiso_regla_octava,
    revpermiso_regla_octava,
    valida_imp_regla_octava,
    descuenta_cupo_r_octava,
)
from ...common.process.review_uma import revisa_uma
from .sub_process.assing_values import assign_values_lines, assign_values_invoice
from .sub_process.assing_values_def_mex import (
    assign_values_iva_lines,
    assign_values_invoice_totals,
)
from ..balance.create_balance_entries import create_balance_entries



def _validate_lines(
    db: Session,
    invoice: InvoiceHeader,
    lines: List[LineItem],
    tenant_id: str,
    company_id: str,
    errors: ErrorCollector,
) -> tuple[list, dict]:
    """Recorre cada partida y ejecuta las validaciones individuales."""
    company = db.get(Company, invoice.company_id)
    company_rfc = (company.rfc or "").strip().upper() if company else ""
    # Deduplicación de cupos disponibles: (permiso, ro_line, pais) → OctaveAvailableEntry
    octave_available: dict = {}
    # Partidas a descargar: se pasa a valida_imp_regla_octava
    octave_desc: list = []
    for line in lines:
        # Validar costo unitario capturado en partidas principales
        if (line.financial and line.fa_data) and not line.fa_data.is_subitem and (line.financial.unit_cost_capture or Decimal(0)) == 0:
            errors.add_error(
                field=f"line[{line.line_number}].unit_cost_capture",
                message="No existe el costo unitario para la Partida.",
                solution=[
                    f"Entrar a la partida: {line.line_number} y capturar el Costo Unitario."
                ],
                code="UNIT_COST_REQUIRED",
            )

        # Validar clase habilitada/deshabilitada
        if line.class_id is not None:
            cls: Class | None = db.get(Class, line.class_id)
            if cls and cls.is_active is False:
                errors.add_error(
                    field=f"line[{line.line_number}].class",
                    message=(
                        f"El número de parte: {cls.class_code} esta desactivado, no se pueden hacer movimientos."
                    ),
                    solution=["Seleccionar un número de parte activo."],
                    code="CLASS_DISABLED",
                )

        # Validar número de parte habilitado/deshabilitado
        if line.part_number_id is not None:
            part: Part | None = db.get(Part, line.part_number_id)
            if part and part.is_active is False:
                errors.add_error(
                    field=f"line[{line.line_number}].part_number",
                    message=(
                        f"El número de parte: {part.part_number} esta desactivado, no se pueden hacer movimientos."
                    ),
                    solution=["Seleccionar un número de parte activo."],
                    code="PART_DISABLED",
                )

        search_fraction_preference(
            db=db,
            country=line.customs.origin_country or "",
            fraccion=line.customs.fraction if line.customs else "",
            fraction_type=line.customs.fraction_type if line.customs else "",
            sector=line.customs.sector if line.customs else "",
            invoice_date=invoice.invoice_date,
            errors=errors,
        )        
        
        review_series(db, line, company_rfc, errors)

        # Validación de la Regla Octava
        if line.octave_permit:
            if not company.prosec:
                errors.add_error(
                    field=f"line[{line.line_number}].octave_permit",
                    message="No se puede hacer uso de la Regla Octava, ...",
                    solution=["Borrar el permiso o dar de alta el permiso PROSEC..."],
                    code="OCTAVA_SIN_PROSEC",
                )
            else:
                desc_entry = llena_impo_permiso_regla_octava(
                    db=db,
                    invoice=invoice,
                    line=line,
                    company_rfc=company_rfc,
                    tenant_id=tenant_id,
                    company_id=company_id,
                    errors=errors,
                )
                if desc_entry is not None:
                    octave_desc.append(desc_entry)
                available = revpermiso_regla_octava(
                    db=db,
                    invoice=invoice,
                    line=line,
                    company_rfc=company_rfc,
                    tenant_id=tenant_id,
                    company_id=company_id,
                    errors=errors,
                )
                if available is not None:
                    key = (available.octave_permit, available.ro_line, available.country_code)
                    octave_available.setdefault(key, available)

        revisa_uma(db=db, line=line, tenant_id=tenant_id, company_id=company_id, errors=errors)

    return octave_desc, octave_available


def _validate_sisimp_limits(
    invoice: InvoiceHeader,
    errors: ErrorCollector,
) -> None:
    """
    Valida los límites de cantidad, peso y valor configurados en SisImp.

    TODO: Leer los parámetros SisImp desde la configuración del sistema:
          - SisImp:CantLimiteMin / SisImp:CantLimite
          - SisImp:PesoLimiteMin / SisImp:PesoLimite
          - SisImp:ValorLimiteMin / SisImp:ValorLimite
    Una vez disponibles, usar invoice.financials.total_quantity, net_weight y value_mc.
    """
    # TODO: Implementar cuando SisImp esté disponible en la configuración del tenant
    pass


def _update_invoice_totals(invoice: InvoiceHeader, lines: List[LineItem]) -> None:
    """
    Copia los totales calculados de financials/logistics al encabezado de la factura
    y calcula IVA, incrementables y valores de aduanas.

    TODO: Validar SSisGen:ActSeguridad para asignar el usuario que actualizó.
    TODO: Validar SSisGen:CalValBaseTCPed para registrar el mensaje de procesamiento.
    """
    tc = Decimal(str(invoice.financials.exchange_rate or 0))
    tc_mm = Decimal(str(invoice.financials.exchange_rate_mm or 0))

    # Calcular IVA sólo para facturas con fecha posterior al corte (78165 en Clarion =     2004-06-01 aprox.)
    iva_factor = Decimal(str(invoice.financials.iva_factor or 0)) if invoice.financials.iva_factor else Decimal(0)
    if invoice.invoice_date and invoice.invoice_date >= date(2014, 12, 31):
        invoice.financials.iva_mn = float(Decimal(str(invoice.financials.value_mn or 0)) * iva_factor / 100)
        invoice.financials.iva_me = float(Decimal(str(invoice.financials.value_me or 0)) * iva_factor / 100)
    else:
        invoice.financials.iva_mn = 0.0
        invoice.financials.iva_me = 0.0

    # Calcular total de incrementables por tipo de moneda
    freight = Decimal(str(invoice.financials.freight or 0))
    insurance = Decimal(str(invoice.financials.insurance or 0))
    packaging = Decimal(str(invoice.financials.packaging or 0))
    other = Decimal(str(invoice.financials.other_increments or 0))
    base_increm = freight + insurance + packaging + other

    if invoice.financials.currency == Currency.FOREIGN:  # ME
        val_seguro = Decimal(str(invoice.financials.total_increments_me or 0)) - base_increm
        invoice.financials.total_increments_me = float(base_increm + val_seguro)
        invoice.financials.total_increments_mn = float(Decimal(str(invoice.financials.total_increments_me)) * tc)

    elif invoice.financials.currency == Currency.LOCAL:  # MN
        val_seguro = Decimal(str(invoice.financials.total_increments_mn or 0)) - base_increm
        invoice.financials.total_increments_mn = float(base_increm + val_seguro)
        invoice.financials.total_increments_me = float(
            (Decimal(str(invoice.financials.total_increments_mn)) / tc) if tc else Decimal(0)
        )

    elif invoice.financials.currency == Currency.MANUAL:  # MC
        val_seguro = (
            (Decimal(str(invoice.financials.total_increments_me or 0)) / tc_mm) if tc_mm else Decimal(0)
        ) - base_increm
        invoice.financials.total_increments_me = float((base_increm + val_seguro) * tc_mm)
        invoice.financials.total_increments_mn = float(Decimal(str(invoice.financials.total_increments_me)) * tc)

    # Marcar la factura como procesada
    invoice.status = InvoiceStatus.PROCESSED
    invoice.party_count = len(lines)

    # TODO: SSisGen:ActSeguridad = 1 → invoice.updated_by = current_user
    # TODO: SSisGen:CalValBaseTCPed = 1 →
    #       invoice.process_log = "Se Actualizó con el Tipo de Cambio de la Fecha de Pago de Pedimento."


def main_process(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id: str) -> dict:
    """
    Proceso principal para importar facturas.

    Flujo (porta la rutina principal del legacy SCAII):
    1. Validación previa de datos (pre_validators)
    2. Tipo de cambio del pedimento (TODO: SSisGen:CalValBaseTCPed)
    3. Revisión de clases, tipo de cambio y pesos
    4. Asignación de valores por partida y totalización
    5. Validaciones per-línea (costo, clase, número de parte, Regla Octava, UMA)
    6. Validación de límites SisImp (TODO)
    7. Si no hay errores: actualizar totales e incrementables en la factura y hacer commit
    8. Si hay errores: rollback (SQLAlchemy lo maneja con la excepción)
    """
    errors = ErrorCollector()

    # Paso 1: Validación previa
    lines = pre_validators(db, invoice, tenant_id, company_id, errors)
    if not lines:
        errors.add_error(
            field="line_items",
            message="La factura debe contener al menos una partida para ser importada",
            solution=["Agregue partidas a la factura antes de intentar importarla"],
            code="NO_LINE_ITEMS",
        )
    errors.raise_if_errors()

    # TODO: SSisGen:CalValBaseTCPed = 1 → obtener tipo de cambio de la fecha de pago del pedimento
    #       y asignarlo a invoice.financials.exchange_rate antes de continuar.
    #       invoice.which_exchange_rate = 'TCPED'  (o 'TCFAC' si CalValBaseTCPed = 0)

    # Paso 2: Revisión de clases y fracciones
    review_classes(db, invoice, lines, tenant_id, company_id, errors)
    review_exchange_rate(db, invoice, errors)

    if invoice.logistics and invoice.logistics.weight_type == "kgs":
        review_weights_kgs(db, lines, tenant_id, company_id, errors)
    elif invoice.logistics and invoice.logistics.weight_type == "lbs":
        review_weights_lbs(db, lines, tenant_id, company_id, errors)    

    # Paso 3: Asignación de valores por partida y totalización de factura
    # Para IMPO DEF / Compras Mexicanas se usa la versión con IVA por partida.    
    invoice_type = (invoice.invoice_type or "").strip().upper()   
    if invoice_type in {"DEF", "MEX"}:
        assign_values_iva_lines(invoice, lines)
        assign_values_invoice_totals(invoice, lines)
    else:
        assign_values_lines(invoice, lines)
        assign_values_invoice(invoice, lines)

    # Paso 4: Validaciones per-línea
    octave_desc, octave_available = _validate_lines(db, invoice, lines, tenant_id, company_id, errors)

    company = db.get(Company, invoice.company_id)

    if company.prosec and octave_desc:
        valida_imp_regla_octava(
            db=db,
            desc_list=octave_desc,
            dis_dict=octave_available,
            tenant_id=tenant_id,
            company_id=company_id,
            errors=errors,
        )

    # Paso 5: Límites de SisImp
    _validate_sisimp_limits(invoice, errors)

    errors.raise_if_errors()

    # Paso 6: Descontar cupos de Regla Octava
    sql_errors: list = []
    if octave_desc:
        descuenta_cupo_r_octava(
            db=db,
            desc_list=octave_desc,
            tenant_id=tenant_id,
            company_id=company_id,
            sql_errors=sql_errors,
        )

    # Paso 7: Actualizar totales, IVA e incrementables y marcar como procesada
    _update_invoice_totals(invoice, lines)

    # Paso 8: Generar saldos en a24.balance_movement (una entrada por partida)
    if invoice_type not in {"DEF", "MEX"}:
        create_balance_entries(db, invoice, lines)

    db.flush()