from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from typing import Optional, List

from sqlalchemy.orm import Session

from api.v1.modules.a76.general_catalogs.fractions.previous_fractions.models import PreviousFraction
from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.a76.rule_octave.balances.models import OctaveBalance
from api.v1.modules.a76.rule_octave.country.models import CountryRuleOct
from api.v1.modules.a76.rule_octave.fractions.models import FractionRuleOctave
from api.v1.modules.a76.rule_octave.permissions.models import OctavePermission
from ....common.process.review_equivalence import _get_unit_equivalence
from core.exceptions import ErrorCollector

# Clarion date 80354 ≈ 2010-05-31 (see previous_fractions/models.py)
_CLARION_DATE_CUTOFF = date(2010, 5, 31)

# RFCs where fraction comparison uses only the first 8 characters
_RFC_FRACTION_8_CHARS = {"CLA940831AZ5", "CTE9801305I8"}


@dataclass
class OctavePermitEntry:
    """
    Entrada de permiso de Regla Octava para importación.
    Paridad: registro en QueuePermReglaOctDesc / QROImp (Clarion SCAII).
    """

    invoice_import: str     # FacturaImpo  — encabezado de la factura
    import_line: int        # LineaImpo
    part_number: str        # NumParte  (número de parte o clase)
    octave_permit: str      # PermisoROctava
    ro_line: int            # LineaRO
    origin_country: str     # PaisOrigen
    fraction_type: str      # TipoFracImpo
    import_fraction: str    # FraccionImpo
    sector: str             # Sector
    class_code: str         # Clase
    unit_of_measure: str    # UniMed  — se resuelve en _revisa_um
    quantity: Decimal       # Cantidad
    value: Decimal          # Valor
    quantity_used: Decimal  # CantUsada  (inicia en 0)
    value_used: Decimal     # ValorUsado (inicia en 0)


# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────

def _previous_fraction_exists(
    db: Session,
    tenant_id: str,
    company_id: str,
    previous_fraction: str,
    current_fraction: str,
) -> bool:
    """
    Verifica si existe un mapeo fracción-anterior → fracción-actual.
    Paridad: consulta a SFraccionesAnterioresN (Clarion SCAII).
    """
    return (
        db.query(PreviousFraction)
        .filter(
            PreviousFraction.tenant_id == tenant_id,
            PreviousFraction.company_id == company_id,
            PreviousFraction.previous_fraction == previous_fraction,
            PreviousFraction.current_fraction == current_fraction,
        )
        .count()
    ) > 0


# ─────────────────────────────────────────────────────────────────────────────
# REVISA_UM_REGLA_OCTAVA
# ─────────────────────────────────────────────────────────────────────────────

def _revisa_um_regla_octava(
    db: Session,
    line: LineItem,
    permission: OctavePermission,
    entry: OctavePermitEntry,
    company_rfc: str,
    tenant_id: str,
    company_id: str,
    errors: ErrorCollector,
) -> None:
    """
    Revisión de unidades de medida para Regla Octava.
    Paridad: REVISA_UM_REGLA_OCTAVA (Clarion SCAII).

    - Busca la fracción del permiso RO a nivel de línea.
    - Valida que la fracción de la partida coincida con la del permiso.
    - Determina la cantidad y UM a registrar en el entry, usando conversión si aplica.
    """
    ro_line = line.reference.ro_line if line.reference else 0
    if not ro_line:
        return

    line_fraction = (
        db.query(FractionRuleOctave)
        .filter(
            FractionRuleOctave.tenant_id == tenant_id,
            FractionRuleOctave.company_id == company_id,
            FractionRuleOctave.permission == line.octave_permit,
            FractionRuleOctave.line == ro_line,
        )
        .first()
    )

    if line_fraction is None:
        errors.add_error(
            field=f"line[{line.line_number}].octave_permit",
            message=(
                f"El Permiso de Regla Octava: {line.octave_permit}"
                f" con línea: {ro_line} no existe."
            ),
            solution=["Revisar el Permiso de Regla Octava en la Partida de Importación."],
            code="RO_FRACTION_NOT_FOUND",
        )
        return

    # ── Validar fracción ─────────────────────────────────────────────────────
    line_fraccion = (line.customs.fraction or "") if line.customs else ""
    ro_fraccion = line_fraction.fraction or ""

    use_8_chars = company_rfc.upper() in _RFC_FRACTION_8_CHARS
    frac_line = line_fraccion[:8] if use_8_chars else line_fraccion
    frac_ro = ro_fraccion[:8] if use_8_chars else ro_fraccion

    if frac_line != frac_ro:
        start_date = permission.start_date
        if start_date and hasattr(start_date, "date"):
            start_date = start_date.date()
        allow_previous = start_date is not None and start_date <= _CLARION_DATE_CUTOFF

        fraction_ok = False
        if allow_previous:
            fraction_ok = _previous_fraction_exists(
                db,
                tenant_id,
                company_id,
                previous_fraction=ro_fraccion[:8],
                current_fraction=line_fraccion[:8],
            )

        if not fraction_ok:
            errors.add_error(
                field=f"line[{line.line_number}].customs.fraction",
                message=f"La fracción: {frac_line} es diferente a la Fraccion: {frac_ro}",
                solution=["Revisar la Partida de Importación y el Permiso de Regla Octava."],
                code="RO_FRACTION_MISMATCH",
            )
            return

    # ── Determinar cantidad y valor según unidad de medida ───────────────────
    entry.unit_of_measure = line_fraction.unit_of_measure or ""
    line_um = line.unit_of_measure_info.code if line.unit_of_measure_info else ""
    quantity = Decimal(str(line.quantity.quantity or 0)) if line.quantity else Decimal(0)
    value_me = Decimal(str(line.financial.value_usd or 0)) if line.financial else Decimal(0)

    if line_fraction.unit_of_measure == line_um:
        entry.quantity = quantity
        entry.value = value_me
        return

    multi_divide, factor_conv = _get_unit_equivalence(
        db, line_um, line_fraction.unit_of_measure or "", tenant_id, company_id
    )

    if multi_divide == "M":
        entry.quantity = quantity * factor_conv
        entry.value = value_me
    elif multi_divide == "D":
        entry.quantity = quantity / factor_conv if factor_conv else Decimal(0)
        entry.value = value_me
    else:
        if line_fraction.unit_of_measure == "KGS":
            net_weight = (
                Decimal(str(line.quantity.net_weight or 0)) if line.quantity else Decimal(0)
            )
            entry.quantity = net_weight
            entry.value = value_me
        else:
            errors.add_error(
                field=f"line[{line.line_number}].unit_of_measure",
                message=(
                    f"Regla Octava: No hay equivalencia entre la U.M. del Permiso de RO: "
                    f"{line_fraction.unit_of_measure} y la U.M. de captura {line_um}."
                ),
                solution=["Capturar su equivalencia en el Catálogo de Equivalencias."],
                code="EQUIVALENCIA",
            )


# ─────────────────────────────────────────────────────────────────────────────
# LLENA_IMPO_PERMISO_REGLA_OCTAVA
# ─────────────────────────────────────────────────────────────────────────────

def llena_impo_permiso_regla_octava(
    db: Session,
    invoice: InvoiceHeader,
    line: LineItem,
    company_rfc: str,
    tenant_id: str,
    company_id: str,
    errors: ErrorCollector,
) -> Optional[OctavePermitEntry]:
    """
    Llena lo que se quiere importar con los permisos de Regla Octava.
    Paridad: LLENA_IMPO_PERMISO_REGLA_OCTAVA (Clarion SCAII).

    Retorna un OctavePermitEntry listo para agregar a la cola de permisos RO,
    o None si el permiso indicado en la partida no existe en el catálogo.
    """
    octave_permit_code = line.octave_permit or ""

    permission = (
        db.query(OctavePermission)
        .filter(
            OctavePermission.tenant_id == tenant_id,
            OctavePermission.company_id == company_id,
            OctavePermission.permission == octave_permit_code,
        )
        .first()
    )

    if permission is None:
        return None

    # NumParte: preferir número de parte, caer en código de clase
    part_number = ""
    if line.part_info and line.part_info.part_number:
        part_number = line.part_info.part_number
    elif line.class_info and line.class_info.class_code:
        part_number = line.class_info.class_code

    class_code = (line.class_info.class_code or "") if line.class_info else ""

    # Sector: preferir el de la partida, caer en el del permiso
    if line.customs and line.customs.sector:
        sector = line.customs.sector
    else:
        sector = permission.sector or ""

    ro_line = line.reference.ro_line if line.reference else 0

    entry = OctavePermitEntry(
        invoice_import=invoice.invoice_number or "",
        import_line=line.line_number,
        part_number=part_number,
        octave_permit=octave_permit_code,
        ro_line=ro_line or 0,
        origin_country=(line.customs.origin_country or "") if line.customs else "",
        fraction_type=(line.customs.fraction_type or "") if line.customs else "",
        import_fraction=(line.customs.fraction or "") if line.customs else "",
        sector=sector,
        class_code=class_code,
        unit_of_measure="",  # resuelto en _revisa_um_regla_octava
        quantity=Decimal(0),
        value=Decimal(0),
        quantity_used=Decimal(0),
        value_used=Decimal(0),
    )

    _revisa_um_regla_octava(
        db=db,
        line=line,
        permission=permission,
        entry=entry,
        company_rfc=company_rfc,
        tenant_id=tenant_id,
        company_id=company_id,
        errors=errors,
    )

    return entry


# ─────────────────────────────────────────────────────────────────────────────
# REVPERMISO_REGLA_OCTAVA
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class OctaveAvailableEntry:
    """
    Cupo disponible de una línea de permiso de Regla Octava.
    Paridad: registro en QueuePermReglaOctDis / QRODis (Clarion SCAII).
    """

    octave_permit: str          # Permiso
    fraction: str               # Fraccion
    ro_line: int                # LineaRO
    country_code: str           # ClaveM3  (PaisOrigen)
    sector: str                 # Sector  (del permiso)
    available_quantity: Decimal # CantidadDisponible  = CantidadCupo - CantUsada
    quantity_used: Decimal      # CantUsada  (inicia en 0)
    available_value: Decimal    # ValorDisponible  = ValorCupo - ValorUsada
    value_used: Decimal         # ValorUsado  (inicia en 0)


def _validate_fraction_ro(
    db: Session,
    line: LineItem,
    permission: OctavePermission,
    line_fraction: FractionRuleOctave,
    company_rfc: str,
    tenant_id: str,
    company_id: str,
    errors: ErrorCollector,
    octave_permit_code: str,
) -> bool:
    """
    Valida que la fracción de la partida coincida con la del permiso de RO.
    Reutilizada por REVPERMISO_REGLA_OCTAVA.
    Retorna True si es válida, False si se añadió un error.
    """
    line_fraccion = (line.customs.fraction or "") if line.customs else ""
    ro_fraccion = line_fraction.fraction or ""

    use_8_chars = company_rfc.upper() in _RFC_FRACTION_8_CHARS
    frac_line = line_fraccion[:8] if use_8_chars else line_fraccion
    frac_ro = ro_fraccion[:8] if use_8_chars else ro_fraccion

    if frac_line == frac_ro:
        return True

    start_date = permission.start_date
    if start_date and hasattr(start_date, "date"):
        start_date = start_date.date()
    allow_previous = start_date is not None and start_date <= _CLARION_DATE_CUTOFF

    if allow_previous and _previous_fraction_exists(
        db, tenant_id, company_id,
        previous_fraction=ro_fraccion[:8],
        current_fraction=line_fraccion[:8],
    ):
        return True

    errors.add_error(
        field=f"line[{line.line_number}].customs.fraction",
        message=f"La fracción: {frac_line} es diferente a la Fraccion: {frac_ro}.",
        solution=[f"Agregarla al Número de Permiso de Regla Octava: {octave_permit_code}."],
        code="PERMISO_REG_8VA",
    )
    return False


def revpermiso_regla_octava(
    db: Session,
    invoice: InvoiceHeader,
    line: LineItem,
    company_rfc: str,
    tenant_id: str,
    company_id: str,
    errors: ErrorCollector,
) -> Optional[OctaveAvailableEntry]:
    """
    Revisión del Permiso de Regla Octava.
    Paridad: REVPERMISO_REGLA_OCTAVA (Clarion SCAII).

    Valida:
    - Existencia del permiso en el catálogo.
    - Coherencia de sector (cuando fracción es PROSEC).
    - Rango de fechas del permiso vs fecha de la factura.
    - Existencia de la línea/fracción dentro del permiso.
    - Concordancia de fracción arancelaria.
    - Cupo disponible no agotado.
    - Existencia del país de origen dentro de la línea del permiso.

    Retorna un OctaveAvailableEntry si no hay errores (para ser agregado a la
    cola QueuePermReglaOctDis del proceso principal), o None en caso contrario.
    """
    octave_permit_code = (line.octave_permit or "").strip()
    ro_line = line.reference.ro_line if line.reference else 0
    origin_country = (line.customs.origin_country or "") if line.customs else ""
    fraction_type = (line.customs.fraction_type or "") if line.customs else ""
    line_sector = (line.customs.sector or "") if line.customs else ""
    invoice_date = invoice.invoice_date

    has_error = False

    if ro_line:
        if not octave_permit_code:
            # Tiene LineaRO pero no hay permiso capturado
            errors.add_error(
                field=f"line[{line.line_number}].octave_permit",
                message=(
                    "No existe capturado un Permiso de Regla Octava "
                    "y si una Línea de algún permiso de Regla Octava"
                ),
                solution=[
                    "Capturar un Permiso de Regla Octava para la Linea "
                    "correspondiente del Permiso de Regla Octava."
                ],
                code="PERMISO_REG_8VA",
            )
            return None

        # ── Buscar el permiso ─────────────────────────────────────────────
        permission = (
            db.query(OctavePermission)
            .filter(
                OctavePermission.tenant_id == tenant_id,
                OctavePermission.company_id == company_id,
                OctavePermission.permission == octave_permit_code,
            )
            .first()
        )

        if permission is None:
            errors.add_error(
                field=f"line[{line.line_number}].octave_permit",
                message=(
                    f"La Partida: {line.line_number} tiene el permiso "
                    f"{octave_permit_code} de Regla Octava y no esta dado de alta."
                ),
                solution=[
                    f"Dar de alta el permiso de Regla Octava o Borrar "
                    f"la partida: {line.line_number}."
                ],
                code="PERMISO_REG_8VA",
            )
            return None

        # ── Validar sector cuando la preferencia es PROSEC ────────────────
        if fraction_type == "PROSEC" and line_sector != (permission.sector or ""):
            errors.add_error(
                field=f"line[{line.line_number}].customs.sector",
                message=(
                    f"El sector: {line_sector} de la partida es diferente al sector: "
                    f"{permission.sector} del permiso de Regla Octava."
                ),
                solution=[
                    f"Cambiar de sector en la Partida o en el Permiso: {octave_permit_code}."
                ],
                code="PERMISO_REG_8VA",
            )
            has_error = True

        # ── Validar rango de fechas del permiso ───────────────────────────
        perm_start = permission.start_date
        perm_end = permission.end_date
        if perm_start and hasattr(perm_start, "date"):
            perm_start = perm_start.date()
        if perm_end and hasattr(perm_end, "date"):
            perm_end = perm_end.date()

        inv_date = invoice_date
        if inv_date and hasattr(inv_date, "date"):
            inv_date = inv_date.date()

        if inv_date and perm_start and perm_end:
            if inv_date < perm_start or inv_date > perm_end:
                errors.add_error(
                    field=f"line[{line.line_number}].octave_permit",
                    message=(
                        f"La fecha de la factura no corresponde al rango de fechas "
                        f"del Permiso: {octave_permit_code} de Regla Octava."
                    ),
                    solution=[
                        "Modificar el Rango de fechas del Permiso de Regla Octava "
                        "o seleccionar uno que en su rango entre la fecha de la Factura."
                    ],
                    code="PERMISO_REG_8VA",
                )
                has_error = True

        # ── Buscar línea/fracción dentro del permiso ──────────────────────
        line_fraction = (
            db.query(FractionRuleOctave)
            .filter(
                FractionRuleOctave.tenant_id == tenant_id,
                FractionRuleOctave.company_id == company_id,
                FractionRuleOctave.permission == octave_permit_code,
                FractionRuleOctave.line == ro_line,
            )
            .first()
        )

        if line_fraction is None:
            errors.add_error(
                field=f"line[{line.line_number}].octave_permit",
                message=(
                    f"No existe la Línea: {ro_line} amparada en el permiso "
                    f"seleccionado {octave_permit_code} de Regla Octava."
                ),
                solution=["Verificar los Permisos de Regla Octava"],
                code="PERMISO_REG_8VA",
            )
            return None

        # ── Validar fracción arancelaria ──────────────────────────────────
        if not _validate_fraction_ro(
            db, line, permission, line_fraction, company_rfc,
            tenant_id, company_id, errors, octave_permit_code,
        ):
            has_error = True

        # ── Validar cupo disponible ───────────────────────────────────────
        quota_qty = line_fraction.quota_quantity or Decimal(0)
        qty_used = line_fraction.quantity_used or Decimal(0)
        available_qty = quota_qty - qty_used

        if available_qty == 0:
            errors.add_error(
                field=f"line[{line.line_number}].octave_permit",
                message=(
                    "La Cantidad Cupo ya esta agotada, no se puede importar "
                    "más para este permiso de Regla Octava."
                ),
                solution=["Tramitar otro permiso o seleccione otro Permiso."],
                code="PERMISO_REG_8VA",
            )
            has_error = True

        # ── Validar país de origen dentro de la línea del permiso ─────────
        country = (
            db.query(CountryRuleOct)
            .filter(
                CountryRuleOct.tenant_id == tenant_id,
                CountryRuleOct.company_id == company_id,
                CountryRuleOct.permission == octave_permit_code,
                CountryRuleOct.line == ro_line,
                CountryRuleOct.fraction == (line_fraction.fraction or ""),
                CountryRuleOct.country_code == origin_country,
            )
            .first()
        )

        if country is None:
            errors.add_error(
                field=f"line[{line.line_number}].customs.origin_country",
                message=(
                    f"No existe la Línea: {ro_line} con país: {origin_country} "
                    f"amparada en el permiso seleccionado"
                ),
                solution=[
                    f"Agregar el País: {origin_country} al Número de Permiso "
                    f"de Regla Octava: {octave_permit_code}."
                ],
                code="PERMISO_REG_8VA",
            )
            has_error = True

        if has_error:
            return None

        # ── Construir entry de cupo disponible ────────────────────────────
        quota_val = line_fraction.quota_value or Decimal(0)
        val_used = line_fraction.value_used or Decimal(0)

        return OctaveAvailableEntry(
            octave_permit=octave_permit_code,
            fraction=line_fraction.fraction or "",
            ro_line=ro_line,
            country_code=origin_country,
            sector=permission.sector or "",
            available_quantity=available_qty,
            quantity_used=Decimal(0),
            available_value=quota_val - val_used,
            value_used=Decimal(0),
        )

    else:
        # ro_line == 0: si hay permiso capturado es un error
        if octave_permit_code:
            errors.add_error(
                field=f"line[{line.line_number}].octave_permit",
                message=(
                    f"No existe una Línea capturada para esta partida de importación "
                    f"y se tiene el Permiso de Regla Octava: {octave_permit_code}."
                ),
                solution=["Capturar una Linea de Permiso de Regla Octava."],
                code="PERMISO_REG_8VA",
            )

        return None


# ─────────────────────────────────────────────────────────────────────────────
# VALIDA_IMP_REGLA_OCTAVA
# ─────────────────────────────────────────────────────────────────────────────

def valida_imp_regla_octava(
    db: Session,
    desc_list: List[OctavePermitEntry],
    dis_dict: dict,
    tenant_id: str,
    company_id: str,
    errors: ErrorCollector,
) -> None:
    """
    Valida lo que se quiere importar contra los cupos disponibles de Regla Octava.
    Paridad: VALIDA_IMP_REGLA_OCTAVA (Clarion SCAII).

    Fase 1 — Asignación de saldos:
        Itera QueuePermReglaOctDesc (qué se quiere importar) contra
        QueuePermReglaOctDis (cupo disponible) y distribuye la cantidad/valor
        disponible entre las partidas, acumulando lo "usado" en cada lista.

    Fase 2 — Validación final:
        - Cantidad == 0: error (sin conversión de U.M.).
        - Cantidad – CantUsada != 0: error (cupo insuficiente).

    Parámetros:
        desc_list  Lista de OctavePermitEntry construida por llena_impo_permiso_regla_octava.
        dis_dict   Diccionario {(permiso, ro_line, country_code): OctaveAvailableEntry}
                   construido por revpermiso_regla_octava.
    """
    # ── Fase 1: Balancear saldos ──────────────────────────────────────────────
    desc_sorted = sorted(
        desc_list,
        key=lambda e: (e.octave_permit, e.ro_line, e.origin_country),
    )

    for desc in desc_sorted:
        key = (desc.octave_permit, desc.ro_line, desc.origin_country)
        dis = dis_dict.get(key)
        if dis is None:
            continue

        # BREAK: la partida ya está completamente consumida
        if (
            desc.quantity - desc.quantity_used == 0
            or desc.value - desc.value_used == 0
        ):
            continue

        # CYCLE: el cupo disponible ya está agotado
        if (
            dis.available_quantity - dis.quantity_used == 0
            or dis.available_value - dis.value_used == 0
        ):
            continue

        # ── Cantidad ──────────────────────────────────────────────────────
        qty_needed = desc.quantity - desc.quantity_used
        qty_avail = dis.available_quantity - dis.quantity_used

        if qty_needed <= qty_avail:
            desc.quantity_used += qty_needed
            dis.quantity_used += qty_needed
        else:
            desc.quantity_used += qty_avail
            dis.quantity_used += qty_avail

        # ── Valor ─────────────────────────────────────────────────────────
        val_needed = desc.value - desc.value_used
        val_avail = dis.available_value - dis.value_used

        if val_needed <= val_avail:
            # Asignación directa al total (no acumulativa) — paridad Clarion
            desc.value_used = desc.value
            dis.value_used += val_needed
        else:
            # Asignación directa al disponible — paridad Clarion
            desc.value_used = dis.available_value
            dis.value_used += val_avail

    # ── Fase 2: Validación final ──────────────────────────────────────────────
    for desc in desc_list:
        line_fraction = (
            db.query(FractionRuleOctave)
            .filter(
                FractionRuleOctave.tenant_id == tenant_id,
                FractionRuleOctave.company_id == company_id,
                FractionRuleOctave.permission == desc.octave_permit,
                FractionRuleOctave.line == desc.ro_line,
            )
            .first()
        )
        # line_fraction reservado para validación de costo unitario (comentada en Clarion)
        _ = line_fraction

        if desc.quantity == 0:
            errors.add_error(
                field=f"line[{desc.import_line}].octave_permit",
                message=(
                    "La Cantidad a Descargar del Permiso de Regla Octava es Cero "
                    "debido a que No hay conversión entre la U.M. de la Partida "
                    "vs la del Permiso"
                ),
                solution=[
                    "Revisar las U.M. de la Partida de Importación "
                    "y del Permiso de Regla Octava."
                ],
                code="PERMISO_REG_8VA",
            )
        elif desc.quantity - desc.quantity_used != 0:
            errors.add_error(
                field=f"line[{desc.import_line}].octave_permit",
                message=(
                    "No se puede importar más de este material "
                    "bajo el amparo de Regla Octava."
                ),
                solution=[
                    "Revisar la cantidad a Importar de la línea "
                    "o solicitar otro permiso."
                ],
                code="PERMISO_REG_8VA",
            )


# ─────────────────────────────────────────────────────────────────────────────
# LLENASALDOS_REGLA_OCTAVA
# ─────────────────────────────────────────────────────────────────────────────

def _llena_fraccion_regla_oct(sector: str) -> str:
    """
    Devuelve la fracción arancelaria de Regla Octava según el sector.
    Paridad: LLENA_FRACCION_REGLA_OCT (Clarion SCAII).

    El orden de comparación es deliberado: los prefijos más largos (XIX, XXIII)
    se evalúan antes que los más cortos (XX, II) para evitar falsos positivos,
    exactamente como advierte el comentario original del Clarion.
    """
    s = (sector or "").strip()

    if s == "I":        return "98020001"
    if s[:2] == "II":   return "98020002"
    if s == "III":      return "98020003"
    if s == "IV":       return "98020004"
    if s == "V":        return "98020005"
    if s == "VI":       return "98020006"
    if s == "VII":      return "98020007"
    if s == "VIII":     return "98020008"
    if s == "IX":       return "98020009"
    if s == "X":        return "98020010"
    if s == "XI":       return "98020011"
    if s == "XII":      return "98020012"
    if s == "XIII":     return "98020013"
    if s == "XIV":      return "98020014"
    if s == "XV":       return "98020015"
    if s == "XVI":      return "98020016"
    if s == "XVII":     return "98020017"
    if s == "XVIII":    return "98020018"
    if s[:3] == "XIX":  return "98020019"   # antes de XX
    if s == "XXI":      return "98020021"
    if s == "XXII":     return "98020022"
    if s[:5] == "XXIII": return "98020023"  # antes de XX
    if s == "XXIV":     return "98020024"
    if s == "XXV":      return "98020025"
    if s[:2] == "XX":   return "98020020"   # al último — paridad comentario Clarion
    return ""


def llenasaldos_regla_octava(
    db: Session,
    entry: "OctavePermitEntry",
    unit_cost_me: Decimal,
    tenant_id: str,
    company_id: str,
    sql_errors: list,
    consecutive_ref: list,
) -> None:
    """
    Llena / actualiza el saldo de Regla Octava en SSaldosReglaOctava.
    Paridad: LLENASALDOS_REGLA_OCTAVA (Clarion SCAII).

    - Si el registro no existe (ERRORCODE = 35 en Clarion → no encontrado):
        inserta uno nuevo con CantExitencia = quantity_used y
        ValorME = quantity_used * unit_cost_me.
    - Si ya existe: acumula CantExitencia y ValorME.

    consecutive_ref es una lista de un elemento [int] que actúa como
    contador mutable compartido con descuenta_cupo_r_octava.
    """
    fraction = _llena_fraccion_regla_oct(entry.sector)
    valor_me = entry.quantity_used * unit_cost_me

    existing = (
        db.query(OctaveBalance)
        .filter(
            OctaveBalance.tenant_id == tenant_id,
            OctaveBalance.company_id == company_id,
            OctaveBalance.invoice_import == entry.invoice_import,
            OctaveBalance.part_number == entry.part_number,
            OctaveBalance.origin_country == entry.origin_country,
            OctaveBalance.fraction_type == entry.fraction_type,
            OctaveBalance.sector == entry.sector,
            OctaveBalance.octave_permit == entry.octave_permit,
            OctaveBalance.origin == "TEM",
            OctaveBalance.system == "SCAF",
            OctaveBalance.line == entry.ro_line,
        )
        .first()
    )

    try:
        if existing is None:
            new_balance = OctaveBalance(
                tenant_id=tenant_id,
                company_id=company_id,
                invoice_import=entry.invoice_import,
                part_number=entry.part_number,
                origin_country=entry.origin_country,
                fraction_type=entry.fraction_type,
                sector=entry.sector,
                octave_permit=entry.octave_permit,
                origin="TEM",
                system="SCAF",
                line=entry.ro_line,
                ro_fraction=fraction,
                class_code=entry.class_code,
                import_fraction=entry.import_fraction,
                unit_of_measure=entry.unit_of_measure,
                quantity_stock=entry.quantity_used,
                value_me=valor_me,
            )
            db.add(new_balance)
            db.flush([new_balance])
        else:
            existing.quantity_stock = (existing.quantity_stock or Decimal(0)) + entry.quantity_used
            existing.value_me = (existing.value_me or Decimal(0)) + valor_me
            db.flush([existing])
    except Exception as exc:
        consecutive_ref[0] += 1
        action = "Agregar" if existing is None else "Actualizar"
        sql_errors.append({
            "consecutive": consecutive_ref[0],
            "error": (
                f"Error al {action} en (SSaldosReglaOctava) {exc}"
            ),
        })


# ─────────────────────────────────────────────────────────────────────────────
# DESCUENTA_CUPO_R_OCTAVA
# ─────────────────────────────────────────────────────────────────────────────

def descuenta_cupo_r_octava(
    db: Session,
    desc_list: List[OctavePermitEntry],
    tenant_id: str,
    company_id: str,
    sql_errors: list,
) -> None:
    """
    Descuenta los cupos de la Regla Octava en GFracROctava.
    Paridad: DESCUENTA_CUPO_R_OCTAVA (Clarion SCAII).

    Por cada entrada en desc_list que tenga cantidad y valor usados distintos
    de cero:
      1. Llama a llenasaldos_regla_octava para registrar/actualizar SSaldosReglaOctava.
      2. Actualiza FractionRuleOctave acumulando:
           - quantity_used += entry.quantity_used
           - value_used    += entry.quantity_used * unit_cost_me

    Los errores de actualización se acumulan en sql_errors como dicts con
    las claves 'consecutive' y 'error'.
    """
    consecutive_ref = [0]

    for entry in desc_list:
        if entry.quantity_used == 0 or entry.value_used == 0:
            continue

        fra_oct = (
            db.query(FractionRuleOctave)
            .filter(
                FractionRuleOctave.tenant_id == tenant_id,
                FractionRuleOctave.company_id == company_id,
                FractionRuleOctave.permission == entry.octave_permit,
                FractionRuleOctave.line == entry.ro_line,
            )
            .first()
        )

        if fra_oct is None:
            continue

        unit_cost_me = fra_oct.unit_cost_me or Decimal(0)

        llenasaldos_regla_octava(
            db=db,
            entry=entry,
            unit_cost_me=unit_cost_me,
            tenant_id=tenant_id,
            company_id=company_id,
            sql_errors=sql_errors,
            consecutive_ref=consecutive_ref,
        )

        fra_oct.quantity_used = (fra_oct.quantity_used or Decimal(0)) + entry.quantity_used
        fra_oct.value_used = (fra_oct.value_used or Decimal(0)) + entry.quantity_used * unit_cost_me

        try:
            db.flush([fra_oct])
        except Exception as exc:
            consecutive_ref[0] += 1
            sql_errors.append({
                "consecutive": consecutive_ref[0],
                "error": (
                    f"Error al regresar el Cupo en (Permiso de Regla Octava) {exc}"
                ),
            })
