"""
GENERAIMPODEFINITIVA
Generates a definitive import invoice header from the export invoice when
processing a regime-change (cambio de régimen) export.

If an invoice with the same number already exists as a definitive import,
the step is skipped (idempotent).  After creating the header the function
calls the appropriate lines sub-routine based on ``generate_desc_parties``:

  'Todas'  → generate_definitive_import_all_lines
  other    → generate_definitive_import_discharged_lines

Legacy equivalent
-----------------
GENERAIMPODEFINITIVA Routine
  Access:QFacImpDef.TryFetch(EqiFID:FKFacImpoDef)
  If ErrorCode() = 35 Then   ← not found → create
    INSERT INTO QFacImpDef (...)
  End
  IF EqiFex:GenPartidas = 'Todas' THEN
      DO GENERAIMPODEFINITIVA_PARTIDAS_TODAS
  ELSE
      DO GENERAIMPODEFINITIVA_PARTIDAS_CON_DESCARGA
  END
"""

from decimal import Decimal
from typing import List
from sqlalchemy import select
from sqlalchemy.orm import Session

from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceStatus, OperationType
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
from api.v1.modules.a76.items.line_customs.models import LineCustom
from api.v1.modules.a76.items.line_descriptions.models import LineDescription
from api.v1.modules.a76.items.series.models import Serie
from core.exceptions import ErrorCollector
from .discharge_types import DownloadEntry


# ---------------------------------------------------------------------------
# Header creation
# ---------------------------------------------------------------------------

def _create_definitive_import_header(
    db: Session,
    export_invoice: InvoiceHeader,
) -> InvoiceHeader:
    """
    Inserts a new InvoiceHeader of type 'IMD' (Importación Definitiva) cloning
    the relevant fields from the export invoice.

    Clarion field mapping (EqiFex → EqiFID):
      FacturaExpo            → invoice_number
      FechaFactura           → invoice_date / updated_date
      TipoCambio             → financials.exchange_rate
      TipoPeso               → logistics.weight_type
      Proveedor              → provider_id
      VendidoConsignado      → sold_to_header  ('Vendido a:')
      VendidoA               → sold_to_id
      EnviadoTransferido     → shipped_to_header ('Enviado a:')
      EnviadoA               → shipped_to_id
      AAduanal               → customs_broker_id
      AAduanalAme            → customs_broker_us_id
      Aduana_Cruce           → compliance_mx.aduana
      Cant_Partidas          → party_count
      Transportista          → logistics.carrier_id (approx)
      Incoterm               → logistics.incoterm
      Precinto               → logistics.precinto (approx)
      SubEmpresa             → sub_company (approx)
      Flete / Seguros / etc. → financials.*
      Observaciones          → notes
      TipoMoneda             → financials.currency_type
      ClaveMoneda            → financials.currency (approx)
      ModTrans               → logistics.transport_mode (approx)
      Ped_Pendiente_Asignar  → compliance_mx.is_pedimento_pending
      PedimentoExpo          → compliance_mx.pedimento_id (approx)
      Remesa                 → compliance_mx.remesa
      Estatus                → status = PENDING ('NA')
      TipoDoc                → invoice_type = 'IMD'
      ProvImpoDefCR          → 'C'  (fixed — always definitiva por cambio de régimen)
      Sujecion               → 'MaqEquipo' (fixed)
    """
    exp = export_invoice
    exp_fin = exp.financials
    exp_log = exp.logistics
    exp_comp = exp.compliance_mx

    def_invoice = InvoiceHeader(
        tenant_id=exp.tenant_id,
        company_id=exp.company_id,
        system=exp.system,
        operation_type=OperationType.IMP,
        invoice_type="DEF",
        invoice_number=exp.invoice_number,
        invoice_date=exp.invoice_date,
        party_count=exp.party_count,
        generate_id=False,
        status=InvoiceStatus.PENDING,

        # Comments (Mapped from original OBSERVACIONE/I)
        observation_es=exp.observation_es,
        observation_en=exp.observation_en,
    )
    db.add(def_invoice)
    db.flush()  # get def_invoice.id before creating child records

    # ── Financials ────────────────────────────────────────────────────────
    if exp_fin is not None:
        from api.v1.modules.a76.invoices.models import InvoiceFinancials
        def_fin = InvoiceFinancials(
            tenant_id=exp.tenant_id,
            company_id=exp.company_id,
            invoice_id=def_invoice.id,
            currency=exp_fin.currency,
            currency_type=exp_fin.currency_type,
            exchange_rate=exp_fin.exchange_rate,
            freight=exp_fin.freight,
            insurance=exp_fin.insurance,
            insurance_value=exp_fin.insurance_value,
            packaging=exp_fin.packaging,
            other_increments=exp_fin.other_increments,
        )
        db.add(def_fin)

    # ── Compliance / pedimento ────────────────────────────────────────────
    if exp_comp is not None:
        from api.v1.modules.a76.invoices.models import InvoiceComplianceMx
        def_comp = InvoiceComplianceMx(
            tenant_id=exp.tenant_id,
            company_id=exp.company_id,
            invoice_id=def_invoice.id,
            aduana=exp_comp.aduana,
            remesa=exp_comp.remesa,
            pedimento_id=exp_comp.pedimento_id,
            is_pedimento_pending=exp_comp.is_pedimento_pending,
            # Reubicados aquí (Correcto en el esquema unificado)
            provider_id=exp_comp.provider_id,
            sold_to_id=exp_comp.sold_to_id,
            shipped_to_id=exp_comp.shipped_to_id,
            customs_broker_id=exp_comp.customs_broker_id,
            customs_broker_us_id=exp_comp.customs_broker_us_id,
            provider_header=exp_comp.provider_header,
            sold_to_header=exp_comp.sold_to_header,
            shipped_to_header=exp_comp.shipped_to_header,
        )
        db.add(def_comp)

    # ── Logistics ─────────────────────────────────────────────────────────
    if exp_log is not None:
        from api.v1.modules.a76.invoices.models import InvoiceLogistics
        def_log = InvoiceLogistics(
            tenant_id=exp.tenant_id,
            company_id=exp.company_id,
            invoice_id=def_invoice.id,
            weight_type=exp_log.weight_type,
            incoterm=exp_log.incoterm,
        )
        db.add(def_log)

    db.flush()
    return def_invoice


# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------

def is_regime_change_detected(invoice: InvoiceHeader, settings: any = None) -> bool:
    """
    Detecta si la factura debe activar la generación de Importación Definitiva (DEF).
    Usa el parámetro 'incambioregdesc' como interruptor maestro.
    """
    # 0. Interruptor Maestro (Configuración Global)
    # Soporta Dict de JSON o Objetos Pydantic/SQLA
    if settings:
        # Búsqueda exhaustiva y recursiva del parámetro maestro
        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__'):
                # Soporte para objetos Pydantic/SQLAlchemy
                dict_rep = obj.__dict__ if hasattr(obj, '__dict__') else {}
                if hasattr(obj, 'dict') and callable(getattr(obj, 'dict')):
                    try: dict_rep = obj.dict()
                    except: pass
                elif hasattr(obj, 'model_dump') and callable(getattr(obj, 'model_dump')):
                    try: dict_rep = obj.model_dump()
                    except: pass
                
                for k, v in dict_rep.items():
                    # Ignorar propiedades privadas de SQLAlchemy
                    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

        # Buscamos el valor en todo el árbol de configuración
        val = find_in_obj(settings, 'incambioregdesc')
        
        # LOG DE EMERGENCIA en consola para debugging
        print(f"[REGIME_CHANGE_DETECT] Factura: {invoice.invoice_number}, Valor de incambioregdesc: {val}")

        # Comprobación segura (1, '1', True, 'true')
        master_on = str(val).strip().lower() in ['1', 'true']
        
        if not master_on:
            return False

    # 1. Export + Pedimento F4 (Art. 114)
    op_type = str(invoice.operation_type or "").lower()
    if op_type == "exp":
        if invoice.compliance_mx:
            ped = invoice.compliance_mx.pedimento
            if ped and (ped.pedimento_code or "").strip().upper() == "F4":
                return True

    # 2. Tipo de Factura 'CR' o Flag Manual
    if (invoice.invoice_type or "").strip().upper() == "CR":
        return True
    
    if invoice.compliance_mx and invoice.compliance_mx.is_regime_change:
        return True
            
    return False


def generate_definitive_import(
    db: Session,
    invoice: InvoiceHeader,
    to_discharge: List[DownloadEntry],
    settings: any,
    errors: ErrorCollector,
) -> tuple[InvoiceHeader, bool]:
    """
    GENERAIMPODEFINITIVA
    Creates a definitive import invoice header from ``invoice`` (export) when
    processing a regime-change export.

    If a definitive import with the same ``invoice_number`` already exists,
    the function is a no-op and returns the existing record.

    After creating (or finding) the header, delegates to the lines sub-routine:
      generate_desc_parties == 'Todas'  → TODO: GENERAIMPODEFINITIVA_PARTIDAS_TODAS
      otherwise                         → TODO: GENERAIMPODEFINITIVA_PARTIDAS_CON_DESCARGA

    Parameters
    ----------
    db      : active SQLAlchemy session
    invoice : the export invoice being processed (regime-change type)
    errors  : shared error collector

    Returns
    -------
    The existing or newly created definitive import InvoiceHeader, or None if
    a non-blocking SQL error occurred.
    """
    # ── 1. Check whether the definitive import already exists ────────────────
    existing: InvoiceHeader | None = db.execute(
        select(InvoiceHeader).where(
            InvoiceHeader.tenant_id == invoice.tenant_id,
            InvoiceHeader.company_id == invoice.company_id,
            InvoiceHeader.invoice_number == invoice.invoice_number,
            InvoiceHeader.invoice_type == "DEF",
        )
    ).scalar_one_or_none()

    if existing is not None:
        return existing, False

    # ── 2. Create the definitiva header ──────────────────────────────────
    def_invoice = _create_definitive_import_header(db, invoice)
    return def_invoice, True


# ---------------------------------------------------------------------------
# Shared helper — copies one export line into the definitive import invoice
# (EqiPex → EqiPdf, identical body in both PARTIDAS_CON_DESCARGA and
# PARTIDAS_TODAS Clarion routines)
# ---------------------------------------------------------------------------

def _copy_line_to_definitive(
    db: Session,
    export_line: LineItem,
    def_invoice: InvoiceHeader,
    def_line_number: int,
    custom_qty: Decimal | None = None,
) -> None:
    """
    Copies a single export LineItem (and its series) into a new definitive
    import LineItem under ``def_invoice``.

    If ``custom_qty`` is provided, the function scales weights and values
    proportionally (Regime Change with Discharge logic).
    """
    from api.v1.modules.a76.items.line_financials.models import LineFinancial
    from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem

    # Calculate proportionality ratio if custom_qty is provided
    original_qty = export_line.quantity.quantity if export_line.quantity else Decimal(1)
    if original_qty == 0:
        original_qty = Decimal(1)
    
    ratio = Decimal(1)
    if custom_qty is not None:
        ratio = custom_qty / original_qty

    def_line = LineItem(
        tenant_id=def_invoice.tenant_id,
        company_id=def_invoice.company_id,
        invoice_id=def_invoice.id,
        line_number=def_line_number,
        part_number_id=export_line.part_number_id,
        class_id=export_line.class_id,
        unit_of_measure=export_line.unit_of_measure,
    )
    db.add(def_line)
    db.flush()  # get def_line.id

    if export_line.quantity:
        src_q = export_line.quantity
        qty_val = custom_qty if custom_qty is not None else src_q.quantity
        
        db.add(LineQuantity(
            item_line_id=def_line.id,
            quantity=qty_val,
            net_weight=(src_q.net_weight * ratio) if src_q.net_weight is not None else None,
            gross_weight=(src_q.gross_weight * ratio) if src_q.gross_weight is not None else None,
            package_quantity=int(src_q.package_quantity * ratio) if src_q.package_quantity is not None else None,
            package_id=src_q.package_id,
        ))

    if export_line.financial:
        src_f = export_line.financial
        
        # Helper to scale optional Decimal fields
        def scale(val: Decimal | None) -> Decimal | None:
            return (val * ratio) if val is not None else None

        db.add(LineFinancial(
            item_line_id=def_line.id,
            unit_cost_capture=src_f.unit_cost_capture,
            unit_cost_usd=src_f.unit_cost_usd,
            unit_cost_mxn=src_f.unit_cost_mxn,
            # Scale total values
            value_mxn=scale(src_f.value_mxn),
            value_usd=scale(src_f.value_usd),
            customs_value_mxn=scale(src_f.customs_value_mxn),
            customs_value_usd=scale(src_f.customs_value_usd),
            value_added_mxn=scale(src_f.value_added_mxn),
            value_added_usd=scale(src_f.value_added_usd),
        ))

    if export_line.customs:
        src_c = export_line.customs
        db.add(LineCustom(
            item_line_id=def_line.id,
            fraction=src_c.fraction,
            fraction_type=src_c.fraction_type,
            rate=src_c.rate,
            sector=src_c.sector,
            origin_country=src_c.origin_country,
        ))

    if export_line.description:
        src_d = export_line.description
        db.add(LineDescription(
            item_line_id=def_line.id,
            description_spanish=src_d.description_spanish,
            extra_description=src_d.extra_description,
            description_english=src_d.description_english,
            package_description=src_d.package_description,
            brand=src_d.brand,
            model=src_d.model,
            has_serial=src_d.has_serial,
        ))

    db.add(FaLineItem(
        id=def_line.id,
        tenant_id=def_line.tenant_id,
        company_id=def_line.company_id,
        is_subitem=False,
        contains_subitems=False,
        subitem_number=0,
    ))

    # Series: QSeriesExpo → QSeriesDef
    export_series: list[Serie] = (
        db.execute(select(Serie).where(Serie.line_item_id == export_line.id))
        .scalars()
        .all()
    )
    for serie in export_series:
        db.add(Serie(
            tenant_id=def_invoice.tenant_id,
            company_id=def_invoice.company_id,
            line_item_id=def_line.id,
            row=serie.row,
            serial_numbers=serie.serial_numbers,  # SerieImpo ← SerieExpo
            model=serie.model,                     # ModeloImpo ← ModeloExpo
            brand=serie.brand,                     # ParteImpo ← ParteExpo
        ))


# ---------------------------------------------------------------------------
# GENERAIMPODEFINITIVA_PARTIDAS_CON_DESCARGA
# ---------------------------------------------------------------------------

def generate_definitive_import_discharged_lines(
    db: Session,
    export_invoice: InvoiceHeader,
    def_invoice: InvoiceHeader,
    to_discharge: List[DownloadEntry],
    errors: ErrorCollector,
) -> None:
    """
    GENERAIMPODEFINITIVA_PARTIDAS_CON_DESCARGA
    Creates definitive import lines only for the lots consumed in the discharge,
    effectively splitting export lines if they came from multiple import batches.

    Clarion: Sort(QueADescargar, FacturaImpo, LineaImpo) → loop
    """
    # Sort by import lot origin to match legacy behavior
    sorted_entries = sorted(
        to_discharge,
        key=lambda e: (e.import_invoice, e.import_line),
    )
    
    def_line_number = 0
    for entry in sorted_entries:
        export_line: LineItem | None = db.get(LineItem, entry.line_item_id)
        if export_line is None:
            continue
            
        # If the entry has LOTS assigned (PEPS), create one IMD line per lot consumed
        lots_to_transfer = [lot for lot in entry.available_lots if lot.consumed_qty > 0]
        
        if lots_to_transfer:
            for lot in lots_to_transfer:
                def_line_number += 1
                _copy_line_to_definitive(
                    db, 
                    export_line, 
                    def_invoice, 
                    def_line_number, 
                    custom_qty=lot.consumed_qty
                )
        else:
            # Fallback: if no lot info but entry exists, use entry quantity
            def_line_number += 1
            _copy_line_to_definitive(
                db, 
                export_line, 
                def_invoice, 
                def_line_number, 
                custom_qty=entry.quantity
            )

    db.flush()


# ---------------------------------------------------------------------------
# GENERAIMPODEFINITIVA_PARTIDAS_TODAS
# ---------------------------------------------------------------------------

def generate_definitive_import_all_lines(
    db: Session,
    export_invoice: InvoiceHeader,
    def_invoice: InvoiceHeader,
    errors: ErrorCollector,
) -> None:
    """
    GENERAIMPODEFINITIVA_PARTIDAS_TODAS
    Creates definitive import lines for ALL lines of the export invoice,
    sorted by line_number (QueuePartidaID sorted by LineaExpo).

    Clarion: Sort(QueuePartidaID, LineaExpo) → loop over all export lines
    """
    export_lines: list[LineItem] = (
        db.execute(
            select(LineItem)
            .where(LineItem.invoice_id == export_invoice.id)
            .order_by(LineItem.line_number)
        )
        .scalars()
        .all()
    )

    def_line_number = 0
    for export_line in export_lines:
        def_line_number += 1
        _copy_line_to_definitive(db, export_line, def_invoice, def_line_number)

    db.flush()
