# Archivo: ValidarScoreHelpers.py
# Ruta: App\SupyCtrol_Module\IngenieroControl\ValidarScoreHelpers.py
# Descripción: Módulo para validar el Score descargado desde SharePoint
# Autor: Equipo de Desarrollo IGSA
# Fecha: 2025

"""
Funciones auxiliares para validación de datos del Score
"""
import pandas as pd
import numpy as np
import re
from datetime import datetime
from decimal import Decimal, InvalidOperation

class ValidadorTiposDatos:
    """
    Clase para validar tipos de datos según el catálogo SQL Server
    """
    
    # COLUMNAS QUE NO PUEDEN ESTAR VACÍAS
    COLUMNAS_OBLIGATORIAS = [
        'OrderNum&Line',
        # Agrega aquí más columnas que no puedan estar vacías
        # 'OrderNumberLine',
        # 'Cliente',
        # etc...
    ]
    
    @staticmethod
    def extraer_info_tipo_sql(tipo_sql):
        """
        Extrae información del tipo de dato SQL Server
        
        Args:
            tipo_sql (str): Tipo SQL como 'NVARCHAR(50)', 'DECIMAL(18,2)', 'INT'
            
        Returns:
            tuple: (tipo_base, longitud, precision, escala)
        """
        tipo_sql = tipo_sql.upper().strip()
        
        # Patrones para diferentes tipos
        patron_con_longitud = r'(\w+)\((\d+)\)'  # NVARCHAR(50)
        patron_decimal = r'(\w+)\((\d+),(\d+)\)'  # DECIMAL(18,2)
        
        # Intentar match con DECIMAL
        match = re.match(patron_decimal, tipo_sql)
        if match:
            return match.group(1), None, int(match.group(2)), int(match.group(3))
        
        # Intentar match con longitud
        match = re.match(patron_con_longitud, tipo_sql)
        if match:
            return match.group(1), int(match.group(2)), None, None
        
        # Tipo sin parámetros (INT, BIT, etc.)
        return tipo_sql, None, None, None
    
    @staticmethod
    def validar_int(valor):
        """Valida si el valor es INT válido"""
        if pd.isna(valor) or valor == '':
            return True, None  # NULL es válido
        
        try:
            val_int = int(float(valor))  # Convertir primero a float por si viene como '123.0'
            if -2147483648 <= val_int <= 2147483647:
                return True, None
            else:
                return False, f"Valor fuera de rango INT: {valor}"
        except (ValueError, TypeError):
            return False, f"No es un número entero válido: {valor}"
    
    @staticmethod
    def validar_bigint(valor):
        """Valida si el valor es BIGINT válido"""
        if pd.isna(valor) or valor == '':
            return True, None
        
        try:
            val_int = int(float(valor))
            if -9223372036854775808 <= val_int <= 9223372036854775807:
                return True, None
            else:
                return False, f"Valor fuera de rango BIGINT: {valor}"
        except (ValueError, TypeError):
            return False, f"No es un número entero válido: {valor}"
    
    @staticmethod
    def validar_decimal(valor, precision, escala):
        """
        Valida si el valor cumple con DECIMAL(precision, escala)
        PERMITE valores con más decimales y los trunca mentalmente
        
        Args:
            valor: Valor a validar
            precision (int): Total de dígitos
            escala (int): Dígitos decimales
        """
        if pd.isna(valor) or valor == '':
            return True, None
        
        try:
            # Convertir a float
            valor_float = float(valor)
            
            # Convertir a string para analizar
            valor_str = str(valor_float)
            
            # Remover signo negativo para contar dígitos
            valor_abs = valor_str.replace('-', '').replace('+', '')
            
            # Separar parte entera y decimal
            if '.' in valor_abs:
                partes = valor_abs.split('.')
                digitos_enteros = len(partes[0])
                digitos_decimales = len(partes[1])
            else:
                digitos_enteros = len(valor_abs)
                digitos_decimales = 0
            
            # Validar parte entera
            if digitos_enteros > (precision - escala):
                return False, f"Parte entera excede precisión: {valor} (max {precision-escala} dígitos enteros)"
            
            # Para los decimales, PERMITIR más de los especificados
            # Solo advertir si excede MUCHO (más de 10 decimales extra)
            if digitos_decimales > (escala + 100):
                return False, f"Demasiados decimales: {valor} (tiene {digitos_decimales}, se esperaban max {escala})"
            
            # Si tiene más decimales de los esperados pero no excesivamente, es válido
            # (SQL Server truncará automáticamente al insertar)
            if digitos_decimales > escala:
                # Solo log, no error
                pass
            
            return True, None
            
        except Exception as e:
            return False, f"Error validando decimal: {str(e)}"
    
    @staticmethod
    def validar_nvarchar(valor, longitud):
        """Valida si el valor cumple con NVARCHAR(longitud)"""
        if pd.isna(valor) or valor == '':
            return True, None
        
        try:
            valor_str = str(valor).strip()
            if len(valor_str) <= longitud:
                return True, None
            else:
                return False, f"Excede longitud máxima de {longitud}: '{valor_str[:50]}...' (longitud: {len(valor_str)})"
        except Exception as e:
            return False, f"Error validando NVARCHAR: {str(e)}"
    
    @staticmethod
    def validar_datetime(valor):
        """Valida si el valor es DATETIME válido o un valor especial que representa NULL"""
        if pd.isna(valor) or valor == '':
            return True, None
        
        # Valores especiales que representan NULL o ausencia de fecha
        valores_null_fecha = [
            '00/01/1900',
            '00-01-1900',
            '1900-01-00',
            '00:00:00',
            '0000-00-00',
            '00/00/0000',
            '01/01/1900',  # Algunos sistemas usan esta como fecha "vacía"
        ]
        
        # Convertir a string para comparar
        valor_str = str(valor).strip()
        
        # Si es uno de los valores especiales que representan NULL, es válido
        if valor_str in valores_null_fecha:
            return True, None
        
        # Si empieza con "00/" o "00:", probablemente es un valor NULL
        if valor_str.startswith('00/') or valor_str.startswith('00:') or valor_str.startswith('00-'):
            return True, None
        
        try:
            # Intentar convertir con pandas (maneja múltiples formatos)
            fecha_parseada = pd.to_datetime(valor, errors='raise')
            
            # Validar que no sea una fecha muy antigua (antes de 1900)
            if fecha_parseada.year < 1900:
                return False, f"Fecha muy antigua (antes de 1900): {valor}"
            
            return True, None
        except:
            return False, f"No es una fecha válida: {valor}"
        
    @staticmethod
    def validar_bit(valor):
        """Valida si el valor es BIT válido (0, 1, True, False)"""
        if pd.isna(valor) or valor == '':
            return True, None
        
        if valor in [0, 1, '0', '1', True, False, 'True', 'False', 'true', 'false']:
            return True, None
        else:
            return False, f"No es un valor BIT válido: {valor}"
    
    @classmethod
    def validar_por_tipo_sql(cls, valor, tipo_sql, nombre_columna=None):
        """
        Valida un valor según su tipo SQL Server
        INCLUYE VALIDACIÓN DE COLUMNAS OBLIGATORIAS (NOT NULL)
        
        Args:
            valor: Valor a validar
            tipo_sql (str): Tipo SQL como 'INT', 'NVARCHAR(50)', etc.
            nombre_columna (str): Nombre de la columna (para validar si es obligatoria)
            
        Returns:
            tuple: (es_valido: bool, mensaje_error: str o None)
        """
        # PRIMERO: Verificar si la columna es obligatoria
        esta_vacio = pd.isna(valor) or valor == '' or (isinstance(valor, str) and valor.strip() == '')
        
        if esta_vacio and nombre_columna and nombre_columna in cls.COLUMNAS_OBLIGATORIAS:
            return False, f"⚠️ Campo obligatorio vacío: '{nombre_columna}' no puede estar vacío"
        
        # Si está vacío pero NO es obligatoria, es válido
        if esta_vacio:
            return True, None
        
        # Si tiene valor, validar según el tipo
        tipo_base, longitud, precision, escala = cls.extraer_info_tipo_sql(tipo_sql)
        
        # Mapeo de tipos SQL a funciones de validación
        if tipo_base == 'INT':
            return cls.validar_int(valor)
        
        elif tipo_base == 'BIGINT':
            return cls.validar_bigint(valor)
        
        elif tipo_base in ['DECIMAL', 'NUMERIC']:
            return cls.validar_decimal(valor, precision, escala)
        
        elif tipo_base in ['NVARCHAR', 'VARCHAR', 'CHAR', 'NCHAR']:
            return cls.validar_nvarchar(valor, longitud)
        
        elif tipo_base in ['DATETIME', 'DATETIME2', 'DATE', 'SMALLDATETIME']:
            return cls.validar_datetime(valor)
        
        elif tipo_base == 'BIT':
            return cls.validar_bit(valor)
        
        else:
            # Tipo no soportado, pero no marcamos como error
            return True, None
        
        
def limpiar_dataframe(df):
    """
    Limpia y prepara el DataFrame según las especificaciones
    
    Args:
        df (pd.DataFrame): DataFrame a limpiar
        
    Returns:
        tuple: (pd.DataFrame limpio, list de filas eliminadas con detalle)
    """
    filas_eliminadas_detalle = []
    
    # 4.3.1 Borrar espacios en blanco innecesarios
    for col in df.columns:
        if df[col].dtype == 'object':  # Solo columnas de texto
            df[col] = df[col].apply(lambda x: x.strip() if isinstance(x, str) else x)
    
    # 4.3.2 Reducir números a 18 dígitos (solo para columnas numéricas)
    for col in df.columns:
        if df[col].dtype in ['float64', 'int64']:
            df[col] = df[col].apply(lambda x: round(x, 18) if pd.notna(x) else x)
    
    # 4.3.3 Identificar y registrar filas donde OrderNum&Line está vacío
    columna_obligatoria = 'OrderNum&Line'  
    
    if columna_obligatoria in df.columns:
        filas_antes = len(df)
        
        # Identificar índices de filas a eliminar
        mask_vacias = df[columna_obligatoria].isna() | (df[columna_obligatoria] == '')
        indices_vacias = df[mask_vacias].index.tolist()
        
        # Registrar detalles de cada fila eliminada
        for idx in indices_vacias:
            filas_eliminadas_detalle.append({
                'fila': idx + 2,  # +2 porque Excel empieza en 1 y tiene encabezado
                'columna': columna_obligatoria,
                'valor': '[VACÍO]',
                'tipo_esperado': 'NOT NULL',
                'error': f"⚠️ Eliminar Fila completa: '{columna_obligatoria}' está vacío (campo obligatorio)"
            })
        
        # Eliminar filas vacías
        df = df[~mask_vacias]
        
        filas_despues = len(df)
        filas_eliminadas = filas_antes - filas_despues
        print(f"Filas eliminadas por {columna_obligatoria} nulo/vacío: {filas_eliminadas}")
    
    return df, filas_eliminadas_detalle