from typing import List

from sqlalchemy import func, or_
from sqlalchemy.orm import Session

from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.general_catalogs.fractions.historical_tariff_fractions.models import (
    HistoricalTariffFraction,
)
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.items.models import LineItem
from core.exceptions import ErrorCollector


def _fraction_exists_via_sitar(fraction_code: str) -> bool:
    """Returns True if the fraction exists in SITAR (fracciones o fracciones-anteriores).

    Fraction format: first 8 chars = base fraction, chars 9-10 = NICO/country (optional).
    Falls back to False if SITAR is not configured or request fails.
    """
    if not fraction_code:
        return True

    base_frac = fraction_code[:8].strip()
    nico = fraction_code[8:10].strip() if len(fraction_code) > 8 else ""

    try:
        from api.v1.modules.sitar.fracciones.service import FraccionesService
        from api.v1.modules.sitar.fracciones_anteriores.service import (
            FraccionesAnterioresService,
        )

        # 1. Buscar en fracciones arancelarias (SITAR)
        results = FraccionesService.search_sync(
            fraccion=base_frac,
            nico=nico if nico else None,
            limit=1,
        )
        if results:
            return True

        # 2. Buscar en fracciones anteriores / histórico (SITAR)
        hist_results = FraccionesAnterioresService.search_sync(
            fraccion_anterior=base_frac,
            limit=1,
        )
        return len(hist_results) > 0

    except (ValueError, Exception):
        # SITAR no configurado o error de red: se usa fallback a BD local
        return False


def _fraction_exists_in_local_db(db: Session, fraction_code: str) -> bool:
    """Fallback: valida contra TariffFraction e HistoricalTariffFraction locales."""
    if not fraction_code:
        return True

    base_frac = fraction_code[:8]
    nico = fraction_code[8:10] if len(fraction_code) > 8 else ""

    tariff_q = db.query(TariffFraction).filter(
        func.left(TariffFraction.code, 8) == base_frac
    )
    if nico:
        tariff_q = tariff_q.filter(TariffFraction.nico == nico)
    else:
        tariff_q = tariff_q.filter(
            or_(TariffFraction.nico.is_(None), TariffFraction.nico == "")
        )
    if tariff_q.first() is not None:
        return True

    hist_q = db.query(HistoricalTariffFraction).filter(
        HistoricalTariffFraction.historical_fraction == base_frac
    )
    if nico:
        hist_q = hist_q.filter(HistoricalTariffFraction.country == nico)
    else:
        hist_q = hist_q.filter(
            or_(
                HistoricalTariffFraction.country.is_(None),
                HistoricalTariffFraction.country == "",
            )
        )
    return hist_q.first() is not None


def _fraction_exists_in_catalog(db: Session, fraction_code: str) -> bool:
    """Returns True if the fraction exists in SITAR (fracciones) or fallback a BD local.

    Usa las funciones de SITAR de fracciones como fuente principal.
    Si SITAR no está configurado o falla, valida contra TariffFraction e HistoricalTariffFraction.
    """
    if _fraction_exists_via_sitar(fraction_code):
        return True
    return _fraction_exists_in_local_db(db, fraction_code)


def _validate_line_fraction(
    db: Session, line: LineItem, errors: ErrorCollector
) -> None:
    """Adds a FRACCION error if the line's fraction does not exist in either catalog."""
    fraction = line.customs.fraction if line.customs else None
    if not fraction:
        return

    if _fraction_exists_in_catalog(db, fraction):
        return

    class_code = line.class_info.class_code if line.class_info else ""
    errors.add_error(
        field="fraction",
        message=(
            f"La Factura contiene la fraccion: {fraction} asociada al Clase {class_code} "
            "que no existe en el catálogo de fracciones"
        ),
        solution=["Agregar la fracción a fracciones históricas."],
        code="FRACCION",
    )


def review_classes(
    db: Session,
    invoice: InvoiceHeader,
    lines: List[LineItem],
    tenant_id: str,
    company_id: str,
    errors: ErrorCollector,
) -> None:
    """
    Validates class and fraction integrity for all line items of an invoice.

    Logic ported from legacy REVISA_CLASE routine:
    1. Identify line items with no assigned class (class_id IS NULL).
    2. If rule_3121_parties_ii: validate that every line has container_parts_ii set.
    3a. If no class errors: validate fractions for ALL lines.
    3b. If class errors exist: add a CLASE error per invalid line and validate
        fractions only for those lines.
    4. If the invoice has not been reviewed by the company, flag lines whose
       class requires physical review (Class.physical_review == 1).
    """
    # 1. Lines whose class is not assigned / not found in the catalog
    invalid_class_lines = [line for line in lines if line.class_id is None]
    has_class_errors = bool(invalid_class_lines)

    # 2. Container validation (Regla 3.1.21 Partes II)
    if invoice.compliance_mx.rule_3121_parties_ii:
        for line in lines:
            if not line.container_parts_ii:
                class_code = line.class_info.class_code if line.class_info else ""
                errors.add_error(
                    field="container_parts_ii",
                    message=f"La clase: {class_code} no tiene contenedor asignado",
                    solution=[f"Capturar el contenedor en la partida: {line.line_number}"],
                    code="CONTENEDOR",
                )

    # 3. Fraction and class validation
    if not has_class_errors:
        # All classes are valid — validate fractions for every line
        for line in lines:
            _validate_line_fraction(db, line, errors)
    else:
        # Some classes are missing — report CLASE errors and validate
        # fractions only for the affected lines
        for line in invalid_class_lines:
            errors.add_error(
                field="class",
                message=f"La clase no existe en catálogo de clases (línea: {line.line_number})",
                solution=[
                    f"Borrar la partida: {line.line_number}, "
                    "o dar de alta la clase en el catálogo de Clases"
                ],
                code="CLASE",
            )
            _validate_line_fraction(db, line, errors)

    # 4. Physical review check
    if not invoice.compliance_mx.was_reviewed_by_company:
        review_required_lines = (
            db.query(LineItem)
            .join(Class, LineItem.class_id == Class.id)
            .filter(
                LineItem.invoice_id == invoice.id,
                LineItem.tenant_id == tenant_id,
                LineItem.company_id == company_id,
                Class.physical_review == 1,
            )
            .all()
        )
        for line in review_required_lines:
            cls = db.get(Class, line.class_id)
            class_code = cls.class_code if cls else ""
            errors.add_error(
                field="physical_review",
                message=(
                    f"La Clase: {class_code} en la Linea: {line.line_number} "
                    "No ha sido Revisada."
                ),
                solution=[
                    f"Revisar el Equipo de la Partida: {line.line_number} "
                    "y Asignar Como Revisado a Nivel Factura."
                ],
                code="CLASE",
            )
    