import  numpy   as  np

def calcular_pendiente(df, x_col, y_col):
    """
    Calcular pendiente de ajuste lineal 
    """
    x   =   df[x_col].values
    y   =   df[y_col].values

    m, b    =   np.polyfit(x, y, 1)

    return m, b

def filtrar_por_limites(df, x_col, v_min=None, v_max=None):

    out =   df.copy()

    if v_min is not None:
        out =   out[out[x_col] >= v_min]
    if v_max is not None:
        out =   out[out[x_col] <= v_max]
    
    return out.reset_index(drop=True)

def calcular_r2(df, x_col, y_col):
    """
    Calcular coeficiente de determinación R^2.
    """

    x   =   df[x_col].values
    y   =   df[y_col].values

    m, b    =   np.polyfit(x, y, 1)

    y_pred  =   m * x + b

    ss_res  =   np.sum((y - y_pred) ** 2)
    ss_tot  =   np.sum((y - np.mean(y)) ** 2)

    r2  =   1 - (ss_res / ss_tot)

    return r2

def derivada_numerica(df, x_col, y_col):
    """
    Calcula primera derivada númerica dy/dx
    """

    x   =   np.asanyarray(df[x_col].values, dtype=float)
    y   =   np.asanyarray(df[y_col].values, dtype=float)

    idx     =   np.argsort(x)
    x       =   x[idx]
    y       =   y[idx]

    return x, y, np.gradient(y,x)

def segunda_derivada(df, x_col, y_col):
    """
    Calcula segunda derivada númerica d^2y/d^2x
    """

    x   =   np.asanyarray(df[x_col].values, dtype=float)
    y   =   np.asanyarray(df[y_col].values, dtype=float)

    idx     =   np.argsort(x)
    x       =   x[idx]
    y       =   x[idx]

    dy_dx   =   np.gradient(y, x)
    d2y_dx2 =   np.gradient(dy_dx, x)

    return x, y, dy_dx, d2y_dx2

def find_local_peaks(signal):
    """
    Encuentra indices de maximos locales simples
    """
    peaks   =   []

    for i in range(1, len(signal) - 1):
        if signal[i] > signal[i - 1] and signal[i] > signal[i + 1]:
            peaks.append(i)

    return np.array(peaks, dtype=int)

def variación_resistencia(df, col_res="Resistencia"):
    """
    Calcula variación relativa de resistencia.
    """
    R   =   df[col_res].values

    R0  =   R[0]

    delta_R =   (R - R0) / R0

    return delta_R

def comparar_pendientes(data, rondas, experimentos, x_col, y_col):
    """
    Compara pendientes entre diferentes experimentos o rondas
    """

    resultados = {}

    for r in rondas:
        for e in experimentos:
            df      =   data[(r, e)]
            m, b    =   calcular_pendiente(df, x_col, y_col)
            resultados[(r, e)]  =   m
    
    return resultados

def zonas_manual(df, x_col, limites):
    """
    Divide el dataset en zonas usando límites definidos manualmente
    """ 
    zonas   =   {}

    for i in range(len(limites) - 1):

        v_min   =   limites[i]
        v_max   =   limites[i + 1]

        zona_df =   df[(df[x_col] >= v_min) & (df[x_col] < v_max)]

        zonas[f"Zona_{i+1}"]    =   zona_df
    
    return  zonas

def detectar_zonas_auto(df, x_col, y_col, n_ruido=8):

    """
    Detecta automaticamente los limites de 4 zonas:

    1) No conduccion 
    2) Conduccion
    3) Generacion H
    4) Saturacion
    """

    x, y, dy_dx, d2y_d2x = segunda_derivada(df, x_col=x_col, y_col=y_col)

    #===============================
    #Estimacion automatica del ruido 
    #===============================
    n_ruido     =   min(n_ruido, len(y))
    ruido_base  =   np.std(y[:n_ruido])

    #Umbral fisico basado en ruido experimental
    tol_corriente   =   max(3 * ruido_base, 1e-12)

    #=============================
    # Fin de no conduccion
    #=============================
    idx_no_conduccion = None
    for i in range(len(y)):
        if np.abs(y[i]) > tol_corriente:
            idx_no_conduccion = i
            break

    if idx_no_conduccion is None:
        return{
            "Zona no Conduccion": None,
            "Zona Conduccion": None,
            "Generacion": None,
            "Saturacion": None,
        }
    
    #=====================
    # Picos de Curvatura
    #=====================
    abs_d2  =   np.abs(d2y_d2x)
    peaks   =   find_local_peaks(abs_d2)

    # Nos quedamos solo con picos posteriores a no conduccion
    peaks   =   peaks[peaks > idx_no_conduccion]

    # Si no hay suficientes picos, usamos la estrategia de respaldo
    if len(peaks) < 2:
        idx_conduccion = idx_no_conduccion

        # Generación: punto donde la pendiente alcanza su maximo crecimiento relativo
        idx_generacion  =   np.argmax(np.abs(dy_dx[idx_no_conduccion:])) + idx_no_conduccion
        # Saturación: máximo de segunda derivada despues de generacion
        idx_saturacion  =   np.argmax(abs_d2[idx_generacion]) + idx_generacion
    else:
        # Primer pico importante de curvatura - transicion a conduccion
        idx_conduccion  =   peaks[0]

        # Segundo pico importante - inicio de generacion 
        idx_generacion  =   peaks[1]

        # Saturacion: buscar el mayro pico de curvatura despues de generacion 
        peaks_after_gen = peaks[peaks > idx_generacion]
        if len(peaks_after_gen) > 0:
            idx_saturacion  =   peaks_after_gen[np.argmax(abs_d2[peaks_after_gen])]
        else:
            idx_saturacion  =   np.argmax(abs_d2[idx_generacion:]) + idx_generacion
    
    limites =   {
        "Zona_no_conduccion":   float(x[idx_no_conduccion]),
        "Zona_conduccion"   :   float(x[idx_conduccion]),
        "Zona_Generacion"   :   float(x[idx_generacion]),
        "Zona_Saturacion"   :   float(x[idx_saturacion]),
    }

    return limites

