"""
void_balance_entries
Cancels every open ENTRY balance of an import invoice by inserting a matching
ENTRY_VOID movement for each one.

Design rules preserved:
  1. NEVER update or delete balance_movement rows — only INSERT.
  2. Net balance after void = SUM(ENTRY qty) - SUM(ENTRY_VOID qty) = 0.
  3. order_peps is set to the new movement's id (post-flush, globally monotonic).

Called when an import invoice is un-processed (reverted) so that the lots can
no longer be consumed by export discharges.  A subsequent re-process will
insert fresh ENTRY rows with up-to-date values.

Guard:
  If any ENTRY has already been partially or fully consumed (i.e. there exist
  CONSUMPTION/WASTE/SCRAP/DESTRUCTION movements against it), the void is
  blocked and a ``ValueError`` is raised — you cannot un-process an invoice
  whose materials are already in use.
"""

from decimal import Decimal
from typing import List

from sqlalchemy import select, func, case
from sqlalchemy.orm import Session

from api.v1.modules.a24.balance_movements.models import (
    BalanceMovement,
    MovementType,
    NEGATIVE_MOVEMENTS,
    USED_MOVEMENTS,
)
from api.v1.modules.a76.invoices.models import InvoiceHeader


def void_balance_entries(
    db: Session,
    invoice: InvoiceHeader,
) -> List[BalanceMovement]:
    """
    Inserts ``ENTRY_VOID`` movements that cancel every open ENTRY for the
    given import invoice.

    Parameters
    ----------
    db      : active SQLAlchemy session (inside the revert transaction)
    invoice : the import invoice being un-processed

    Returns
    -------
    List of the newly created ENTRY_VOID BalanceMovement objects.

    Raises
    ------
    ValueError
        If any lot of the invoice has already been (partially) consumed by
        an export, waste, scrap or destruction.  In that case the invoice
        cannot be un-processed without first cancelling those discharges.
    """
    # ── 1. Fetch all ENTRY movements for this invoice ────────────────────────
    entries: List[BalanceMovement] = (
        db.execute(
            select(BalanceMovement).where(
                BalanceMovement.import_invoice_id == invoice.id,
                BalanceMovement.movement_type == MovementType.ENTRY,
            )
        )
        .scalars()
        .all()
    )

    if not entries:
        return []

    import_line_ids = [e.import_item_line_id for e in entries]

    # ── 2. Guard: check no lot has been consumed ─────────────────────────────
    sign_expr = case(
        (BalanceMovement.movement_type.in_(NEGATIVE_MOVEMENTS), -1),
        else_=1,
    )
    # "used" must be NET of returns:
    #   + consumption/waste/scrap/destruction
    #   - return
    # so an already reverted discharge does not block import un-processing.
    used_net_expr = case(
        (BalanceMovement.movement_type.in_(USED_MOVEMENTS), BalanceMovement.quantity),
        (BalanceMovement.movement_type == MovementType.RETURN, -BalanceMovement.quantity),
        else_=Decimal(0),
    )

    lot_summary = (
        db.execute(
            select(
                BalanceMovement.import_item_line_id,
                func.sum(sign_expr * BalanceMovement.quantity).label("balance"),
                func.sum(used_net_expr).label("used_net"),
            )
            .where(
                BalanceMovement.import_item_line_id.in_(import_line_ids),
            )
            .group_by(BalanceMovement.import_item_line_id)
        )
        .all()
    )

    consumed_lots = [row for row in lot_summary if (row.used_net or 0) > 0]
    if consumed_lots:
        lot_ids = ", ".join(str(r.import_item_line_id) for r in consumed_lots)
        raise ValueError(
            f"No se puede des-procesar la factura '{invoice.invoice_number}': "
            f"los siguientes lotes ya tienen consumos registrados y deben "
            f"cancelarse primero (item_line ids: {lot_ids})."
        )

    # ── 3. Build the ENTRY_VOID map: one void per LOT ────────────────────────
    # Map lot_id → open balance (should equal the original ENTRY qty since no
    # consumptions exist, but we use the actual net balance to be safe).
    balance_map: dict[int, Decimal] = {
        row.import_item_line_id: Decimal(str(row.balance or 0))
        for row in lot_summary
    }
    # Pick one representative ENTRY per lot to copy informational fields.
    entry_by_lot: dict[int, BalanceMovement] = {}
    for e in entries:
        if e.import_item_line_id not in entry_by_lot:
            entry_by_lot[e.import_item_line_id] = e

    operation_date = (
        invoice.invoice_date.date()
        if hasattr(invoice.invoice_date, "date")
        else invoice.invoice_date
    )

    voids: List[BalanceMovement] = []
    for lot_id, entry in entry_by_lot.items():
        open_qty = balance_map.get(lot_id, Decimal(0))
        if open_qty <= 0:
            continue

        void_mov = BalanceMovement(
            tenant_id=invoice.tenant_id,
            company_id=invoice.company_id,
            import_invoice_id=invoice.id,
            import_item_line_id=lot_id,
            part_number_id=entry.part_number_id,
            movement_type=MovementType.ENTRY_VOID,
            quantity=open_qty,
            value_me=entry.value_me,
            value_mn=entry.value_mn,
            net_weight=entry.net_weight,
            source_invoice_id=None,
            source_item_line_id=None,
            order_peps=0,          # set after flush
            operation_date=operation_date,
            notes=(
                f"Anulación de entrada por des-procesamiento de "
                f"factura {invoice.invoice_number} (entry id={entry.id})"
            ),
        )
        db.add(void_mov)
        voids.append(void_mov)

    if voids:
        db.flush()
        for mov in voids:
            mov.order_peps = mov.id

    return voids
