"""
Service para fracciones arancelarias
Catálogo de referencia global (no tenant-scoped)
"""

from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
import zlib
import logging
import re

from .models import TariffFraction
from .dto import TariffFractionCreateDTO, TariffFractionUpdateDTO
from api.v1.modules.sitar.fracciones.service import FraccionesService
from api.v1.modules.sitar.fracciones.schemas import FraccionesResponse
from api.v1.modules.sitar.fracciones_usa.service import FraccionesUSAService
from api.v1.modules.sitar.fracciones_usa.schemas import FraccionesUSAResponse

logger = logging.getLogger(__name__)


class TariffFractionMapper:
    """Helper to map Sitar responses to Local domain objects"""

    @staticmethod
    def _digits_only(value: Optional[str]) -> str:
        return re.sub(r"\D", "", (value or "").strip())

    @staticmethod
    def _format_mx_fraction(code: str) -> str:
        if code.isdigit() and len(code) == 8:
            return f"{code[:2]}.{code[2:4]}.{code[4:6]}.{code[6:]}"
        if code.isdigit() and len(code) == 6:
            return f"{code[:2]}.{code[2:4]}.{code[4:]}"
        return code

    @staticmethod
    def _format_usa_fraction(code: str) -> str:
        if code.isdigit() and len(code) == 10:
            return f"{code[:4]}.{code[4:6]}.{code[6:8]}.{code[8:]}"
        if code.isdigit() and len(code) == 8:
            return f"{code[:4]}.{code[4:6]}.{code[6:]}"
        return code

    @staticmethod
    def _normalized_pair(raw_code: Optional[str], raw_fraction: Optional[str], formatter) -> Tuple[str, str]:
        """Return (code_without_separators, formatted_fraction)."""
        code = TariffFractionMapper._digits_only(raw_code)
        fraction = (raw_fraction or "").strip()
        if not code:
            code = TariffFractionMapper._digits_only(fraction)
        if not fraction:
            fraction = formatter(code)
        elif "." not in fraction and "-" not in fraction:
            fraction = formatter(TariffFractionMapper._digits_only(fraction))
        if not fraction:
            fraction = formatter(code)
        return code, fraction

    @staticmethod
    def to_domain(fraccion: FraccionesResponse) -> TariffFraction:
        # Generate ID: Use SYSID if available, else composite hash of code + nico
        if fraccion.SYSID:
            fake_id = fraccion.SYSID
        else:
            # Composite key for uniqueness if SYSID missing
            unique_str = f"{fraccion.FRACCION}-{fraccion.NICO}"
            fake_id = zlib.crc32(unique_str.encode('utf-8'))
        
        code_val, fraction_val = TariffFractionMapper._normalized_pair(
            fraccion.FRACCION, fraccion.FRACCIONPUNTO, TariffFractionMapper._format_mx_fraction
        )
        description_val = fraccion.DESCRIPCION if fraccion.DESCRIPCION else "(Sin descripción)"

        tf = TariffFraction(
            id=fake_id,
            code=code_val,
            fraction=fraction_val,
            description=description_val,
            nico=fraccion.NICO,
            # MAP CHANGE: UMT now maps to Abbreviation (e.g., Pza, Kg)
            umt=fraccion.UMABREVIACION,
            adv_impo=fraccion.ADVIMPOTXT,
            adv_expo=fraccion.ADVEXPOTXT
        )
        # Dynamically attach non-model attributes for DTO
        tf.dof = fraccion.DOF
        tf.aplica_ieps = fraccion.APLICAIEPS
        # MAP CHANGE: New field for the numeric code (e.g., 01, 06)
        tf.um_code = fraccion.UMCLAVE
        
        return tf

    @staticmethod
    def to_domain_usa(item: FraccionesUSAResponse) -> TariffFraction:
        """Map US Fraction to Domain"""
        code_val, fraction_val = TariffFractionMapper._normalized_pair(
            item.FRACCION_SIN_PUNTO,
            item.FRACCION_CON_PUNTO or item.FRACCION_MOSTRAR,
            TariffFractionMapper._format_usa_fraction,
        )
        return TariffFraction(
            id=item.CONSECUTIVO,
            code=code_val,
            fraction=fraction_val,
            description=item.DESCRIPCION or "(Sin descripción)",
            nico=None,  # Not applicable
            umt=item.UNIDADCANTIDAD,
            adv_impo=item.TARIFA1,
            adv_expo=item.TARIFA2
        )


class TariffFractionService:
    """Service para gestionar fracciones arancelarias (catálogo global)"""


    @staticmethod
    def to_domain_usa_local(item: Any) -> TariffFraction:
        """Map Local US Fraction (ORM) to Domain"""
        # Formatter helper (simple logic: add dots every 2/4 chars? or just return as is?)
        # US format: 1234.56.78.90. For now return as is or use helper if available.
        # item is USTariffFraction (imported inside method to avoid circular import if needed, or assumed available)
        
        # Remove formatting (e.g. dots) for the 'code' property
        code_str = str(item.code)
        clean_code = code_str.replace(".", "").replace("-", "")

        return TariffFraction(
            id=item.id,
            code=clean_code,
            fraction=code_str,
            description=item.description or "(Sin descripción)",
            nico=None,
            umt=item.unit_of_measure,
            adv_impo=str(item.ad_valorem) if item.ad_valorem is not None else None,
            adv_expo=None
        )

    @staticmethod
    async def _fetch_usa_catalog_from_sitar(
        skip: int,
        limit: int,
        filters: Optional[Dict[str, Any]],
    ) -> Tuple[List[TariffFraction], int]:
        """SITAR fracciones-usa for catalog 'usa' and 'american' (read-only, same source)."""
        try:
            usa_service = FraccionesUSAService.get_instance()
        except ValueError:
            logger.warning("SITAR USA not configured (missing env)")
            return [], 0

        search_term = None
        search_description = None

        if filters and filters.get("search"):
            term = filters["search"]
            clean_term = term.replace(".", "")
            if clean_term.isdigit() and len(clean_term) >= 4:
                search_term = term
            else:
                search_description = term

        try:
            usa_items, total = await usa_service.search_with_total(
                fraccion=search_term,
                descripcion=search_description,
                skip=skip,
                limit=limit,
            )
            items = [TariffFractionMapper.to_domain_usa(item) for item in usa_items]
            return items, total
        except Exception as e:
            import traceback

            logger.error("Error fetching USA fractions from SITAR: %s", e)
            logger.error(traceback.format_exc())
            return [], 0

    @staticmethod
    async def get_all(
        db: Session,
        skip: int = 0,
        limit: int = 100,
        filters: Optional[Dict[str, Any]] = None,
        catalog: str = "mex",
        tenant_id: Optional[int] = None,
        company_id: Optional[int] = None,
    ) -> Tuple[List[TariffFraction], int]:
        """
        Obtiene fracciones arancelarias.
        Estrategia:
        - MEX: Sitar API (fracciones)
        - USA / AMERICAN: Sitar API (fracciones-usa), solo lectura
        """

        if catalog in ("american", "usa"):
            return await TariffFractionService._fetch_usa_catalog_from_sitar(
                skip, limit, filters
            )

        # MEX (SITAR) CATALOG HANDLING
        try:
            sitar_service = FraccionesService.get_instance()
            
            # Map filters
            sitar_fraccion = None
            sitar_nico = None
            sitar_description = None
            
            # Legacy parity: base query is always Nivel = 5 unless caller explicitly requests another level.
            level_filter = 5
            if filters and filters.get("level") is not None:
                level_filter = filters["level"]
            # UI compatibility: level -1 means "sin filtro de nivel".
            if level_filter == -1:
                level_filter = None
            
            if filters:
                if filters.get("search"):
                    term = str(filters["search"]).strip()
                    # Legacy-like behavior:
                    # - Numeric search targets fracción first.
                    # - Text search targets descripción.
                    clean_term = term.replace(".", "")
                    if clean_term.isdigit():
                       sitar_fraccion = clean_term
                    else:
                        sitar_description = term
                
                if filters.get("code"):
                     sitar_fraccion = filters["code"]
                if filters.get("fraction"):
                     sitar_fraccion = filters["fraction"] 
                if filters.get("nico"):
                     sitar_nico = filters["nico"]
            if sitar_fraccion is not None:
                sitar_fraccion = str(sitar_fraccion).replace(".", "").strip()

            # Note: Sitar search might not return total count. 
            # We fetch page items. Pagination might be tricky if Sitar doesn't return total.
            # Assuming Sitar returns a list.
            sitar_items, total = await sitar_service.search_with_total(
                fraccion=sitar_fraccion,
                nico=sitar_nico,
                description=sitar_description,
                nivel=level_filter,  # Dynamic level
                skip=skip,
                limit=limit,
            )

            # STRICT API USAGE: 
            # We do NOT fallback to local DB on empty list, as user requested strict API consumption.
            # We also do NOT attempt enrichment as codes mismatch (API uses '010191A' vs Local '01012101').

            # Map items
            items = [TariffFractionMapper.to_domain(item) for item in sitar_items]
            # Legacy browse behavior: keep table in ascending fracción order.
            items = sorted(items, key=lambda row: ((row.code or ""), (row.nico or "")))

            # total comes from SITAR PaginatedFraccionesResponse (matches API-wide count for the query).
            return items, total

        except Exception as e:
            logger.error(f"Error fetching from Sitar API: {e}")
            # STRICT API USAGE: Propagate error, do NOT fallback to local DB.
            raise e

    @staticmethod
    async def _get_all_local_async(
        db: Session,
        skip: int = 0,
        limit: int = 100,
        filters: Optional[Dict[str, Any]] = None,
    ) -> Tuple[List[TariffFraction], int]:
        """Lógica original de consulta local"""
        query = db.query(TariffFraction)

        # Aplicar filtros
        if filters:
            # Si hay un filtro 'search', buscar en múltiples campos
            if filters.get("search"):
                search_term = f"%{filters['search']}%"
                query = query.filter(
                    TariffFraction.code.ilike(search_term) |
                    TariffFraction.fraction.ilike(search_term) |
                    TariffFraction.description.ilike(search_term) |
                    TariffFraction.nico.ilike(search_term) |
                    TariffFraction.umt.ilike(search_term)
                )
            else:
                # Filtros individuales
                if filters.get("code"):
                    query = query.filter(TariffFraction.code.ilike(f"%{filters['code']}%"))
                if filters.get("fraction"):
                    query = query.filter(TariffFraction.fraction.ilike(f"%{filters['fraction']}%"))
                if filters.get("description"):
                    query = query.filter(TariffFraction.description.ilike(f"%{filters['description']}%"))
                if filters.get("nico"):
                    query = query.filter(TariffFraction.nico.ilike(f"%{filters['nico']}%"))
                if filters.get("umt"):
                    query = query.filter(TariffFraction.umt.ilike(f"%{filters['umt']}%"))

        total = query.count()
        items = query.offset(skip).limit(limit).all()

        return items, total

    @staticmethod
    def get_by_id(
        db: Session,
        tariff_fraction_id: int,
    ) -> Optional[TariffFraction]:
        """
        Obtiene por ID. 
        Como Sitar no usa estos IDs, consultamos Local DB directamente para compatibilidad legacy.
        Si se necesitara obtener detalle de Sitar, se requeriría otro identificador (Code).
        """
        return (
            db.query(TariffFraction)
            .filter(TariffFraction.id == tariff_fraction_id)
            .first()
        )

    # WRITE OPERATIONS - DEPRECATED / LOCAL ONLY (Optional: Remove or Keep for Fallback Maintenance)
    
    @staticmethod
    def get_by_code(
        db: Session,
        code: str,
    ) -> Optional[TariffFraction]:
        return (
            db.query(TariffFraction)
            .filter(TariffFraction.code == code)
            .first()
        )

    @staticmethod
    def create(
        db: Session,
        tariff_fraction_data: TariffFractionCreateDTO,
    ) -> TariffFraction:
        try:
            tariff_fraction = TariffFraction(
                **tariff_fraction_data.model_dump(),
            )
            db.add(tariff_fraction)
            db.commit()
            db.refresh(tariff_fraction)
            return tariff_fraction
        except IntegrityError as e:
            db.rollback()
            logger.error(f"Error creating tariff fraction: {e}")
            raise HTTPException(
                status_code=400,
                detail="Tariff fraction with this code already exists",
            )

    @staticmethod
    def update(
        db: Session,
        tariff_fraction_id: int,
        tariff_fraction_data: TariffFractionUpdateDTO,
    ) -> Optional[TariffFraction]:
        tariff_fraction = TariffFractionService.get_by_id(
            db, tariff_fraction_id
        )
        
        if not tariff_fraction:
            return None

        try:
            update_data = tariff_fraction_data.model_dump(exclude_unset=True)
            for key, value in update_data.items():
                setattr(tariff_fraction, key, value)

            db.commit()
            db.refresh(tariff_fraction)
            return tariff_fraction
        except IntegrityError as e:
            db.rollback()
            logger.error(f"Error updating tariff fraction: {e}")
            raise HTTPException(
                status_code=400,
                detail="Error updating tariff fraction",
            )

    @staticmethod
    def delete(
        db: Session,
        tariff_fraction_id: int,
    ) -> bool:
        tariff_fraction = TariffFractionService.get_by_id(
            db, tariff_fraction_id
        )
        
        if not tariff_fraction:
            return False

        try:
            db.delete(tariff_fraction)
            db.commit()
            return True
        except IntegrityError as e:
            db.rollback()
            logger.error(f"Error deleting tariff fraction: {e}")
            raise HTTPException(
                status_code=400,
                detail="Cannot delete tariff fraction - may be in use",
            )
