"""
Mapeo fila CSV → datos para Part. Helpers de normalización de valores.
Clarion: ME/USD, MN/MXP, MC/ClaveMoneda; apóstrofes omitidos en NUMPARTE; RFC excepción rellena desde Class.
"""
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Optional, Set


def clean_dict(data_dict: dict) -> dict:
    """Convierte strings vacíos a None; mantiene 0 en campos no-_id. Igual que a76/invoices."""
    cleaned = {}
    for key, value in data_dict.items():
        if isinstance(value, str) and not value.strip():
            cleaned[key] = None
        elif value == 0 and (key.endswith("_id") or key == "remesa"):
            cleaned[key] = None
        else:
            cleaned[key] = value
    return cleaned


def _str_or_none(val: Any, max_len: Optional[int] = None) -> Optional[str]:
    if val is None:
        return None
    s = str(val).strip()
    if not s:
        return None
    if max_len and len(s) > max_len:
        return s[:max_len]
    return s


def _decimal_or_none(val: Any) -> Optional[Decimal]:
    if val is None or val == "":
        return None
    try:
        return Decimal(str(val))
    except (InvalidOperation, ValueError, TypeError):
        return None


def _bool_from_row(val: Any) -> bool:
    if val is None or val == "":
        return True
    s = str(val).strip().upper()
    if s in ("0", "F", "FALSE", "NO", "N"):
        return False
    return True


def _normalize_num_parte(val: Optional[str], max_len: int = 70) -> Optional[str]:
    """NUMPARTE: mayúsculas, sin apóstrofes (Clarion: Se Omitirá el Apostrofe)."""
    if val is None:
        return None
    s = str(val).strip().replace("'", "")
    if not s:
        return None
    s = s.upper()
    if max_len and len(s) > max_len:
        return s[:max_len]
    return s


def _currency_from_row(
    row_norm: Dict[str, Any],
    valid_currency_codes: Set[str],
) -> tuple:
    """
    Deriva currency_type y currency_key desde TIPOMONEDA (Col G) y CLAVEMONEDA (Col H).
    Clarion: G vacío -> ME/USD; G=ME -> USD; G=MN -> MXP; G=MC -> H obligatoria.
    """
    g = (row_norm.get("TIPOMONEDA") or row_norm.get("MONEDA") or "").strip().upper()
    h = (row_norm.get("CLAVEMONEDA") or "").strip().upper()
    if not g:
        return "ME", "USD"
    if g == "ME":
        return "ME", "USD"
    if g == "MN":
        return "MN", "MXP"
    if g == "MC" and h and h in valid_currency_codes:
        return "MC", h
    if h and h in valid_currency_codes:
        return g[:2], h
    return None, None


def row_to_part_data(
    row_norm: Dict[str, Any],
    valid_class_codes: Set[str],
    valid_uom_codes: Set[str],
    valid_currency_codes: Set[str],
) -> Dict[str, Any]:
    """
    Mapea una fila normalizada del CSV a un diccionario de datos para Part.
    Clarion: NUMPARTE sin apóstrofes; Tipo Moneda ME/MN/MC con Clave; PAIS, PREFERENCIA, SECTOR, RUTA IMAGEN.
    """
    part_number = _normalize_num_parte(row_norm.get("NUMPARTE"), 70)
    commercial = _str_or_none(row_norm.get("NUMPARTECOM"), 70)
    desc_es = _str_or_none(row_norm.get("DESCRIPCIONE"), 500)
    desc_en = _str_or_none(row_norm.get("DESCRIPCIONI"), 500)
    part_class = _str_or_none(row_norm.get("CLASE"), 8)
    if part_class and part_class.upper() not in valid_class_codes:
        part_class = None
    unit_of_measure = _str_or_none(row_norm.get("UNIMED"), 5)
    if unit_of_measure and unit_of_measure.upper() not in valid_uom_codes:
        unit_of_measure = None

    currency_type, currency_key = _currency_from_row(row_norm, valid_currency_codes)
    if currency_key and currency_key not in valid_currency_codes:
        currency_key = None
        currency_type = None

    unit_cost = _decimal_or_none(row_norm.get("COSTOUNIT"))
    unit_weight = _decimal_or_none(row_norm.get("PESOUNIT"))
    weight_type = _str_or_none(row_norm.get("TIPOPESO"), 6)
    if weight_type and weight_type.upper() not in ("KILOS", "LIBRAS"):
        weight_type = "KILOS"
    fraction = _str_or_none(row_norm.get("FRACCION"), 10)
    us_fraction = _str_or_none(row_norm.get("FRACCIONAME"), 16)
    part_photo = _str_or_none(row_norm.get("RUTAIMAGEN"), 255)

    fda_key = _str_or_none(row_norm.get("FDAKEY"), 20)
    fcc_key = _str_or_none(row_norm.get("FCCKEY"), 30)
    license_code = _str_or_none(row_norm.get("LICENCIA"), 3)
    eccn = _str_or_none(row_norm.get("ECCN"), 20)
    export_code = _str_or_none(row_norm.get("EXPORTCODE"), 2)
    exclusion_symbol = _str_or_none(row_norm.get("EXCLUSION"), 19)
    is_active = _bool_from_row(row_norm.get("ACTIVO"))

    return {
        "part_number": part_number,
        "commercial_part_number": commercial,
        "description_spanish": desc_es,
        "description_english": desc_en,
        "part_class": part_class,
        "unit_of_measure": unit_of_measure,
        "unit_cost": unit_cost,
        "currency_type": currency_type,
        "currency_key": currency_key,
        "unit_weight": unit_weight,
        "weight_type": weight_type,
        "fraction": fraction,
        "us_fraction": us_fraction,
        "part_photo": part_photo,
        "fda_key": fda_key,
        "fcc_key": fcc_key,
        "license_code": license_code,
        "eccn": eccn,
        "export_code": export_code,
        "exclusion_symbol": exclusion_symbol,
        "is_active": is_active,
    }


def row_to_part_data_merge_existing(
    row_norm: Dict[str, Any],
    existing_data: Dict[str, Any],
    valid_class_codes: Set[str],
    valid_uom_codes: Set[str],
    valid_currency_codes: Set[str],
) -> Dict[str, Any]:
    """
    Para modo Actualizar (parcial): valores del CSV si no vacíos, sino los de la parte existente (Clarion VALIDA_PARCIAL_PARTES).
    """
    data = row_to_part_data(
        row_norm, valid_class_codes, valid_uom_codes, valid_currency_codes
    )
    if not data.get("part_number"):
        return data
    merge_keys = (
        "description_spanish",
        "description_english",
        "part_class",
        "unit_of_measure",
        "unit_cost",
        "currency_type",
        "currency_key",
        "unit_weight",
        "weight_type",
        "fraction",
        "us_fraction",
        "part_photo",
    )
    for key in merge_keys:
        val = data.get(key)
        if val is None or (isinstance(val, str) and not val.strip()):
            data[key] = existing_data.get(key)
    return data


def apply_rfc_exception_from_class(
    data: Dict[str, Any],
    class_obj: Any,
) -> Dict[str, Any]:
    """
    Clarion LLENA_PARTES: si empresa CLA/CTE y part_class informado, sobrescribir desde la clase:
    UniMed, DescripcionE, DescripcionI, Fraccion, FraccionAme, TipoFraccion='GENERAL'.
    """
    data["unit_of_measure"] = getattr(class_obj, "unit_of_measure", None) or data.get("unit_of_measure")
    data["description_spanish"] = getattr(class_obj, "description_es", None) or data.get("description_spanish")
    data["description_english"] = getattr(class_obj, "description_en", None) or data.get("description_english")
    data["fraction"] = getattr(class_obj, "fraction", None) or data.get("fraction")
    data["us_fraction"] = getattr(class_obj, "us_fraction", None) or data.get("us_fraction")
    return data
