"""Generadores didácticos del capítulo 4. Sin entrenamiento ni escritura automática."""
import numpy as np

def generar_xor(n=600, ruido=0.05, semilla=42):
    """X (n,2), y binario. La etiqueta depende de los signos antes de perturbar X."""
    if n < 1 or ruido < 0:
        raise ValueError('n debe ser positivo y ruido no negativo')
    rng = np.random.default_rng(semilla)
    limpio = rng.uniform(-1, 1, size=(n, 2))
    y = (limpio[:, 0] * limpio[:, 1] > 0).astype(np.int64)
    return limpio + rng.normal(0, ruido, limpio.shape), y

def generar_espirales(n_por_clase=200, clases=3, vueltas=1.5, ruido=0.08, semilla=42):
    """Brazos entrelazados; ruido gaussiano en las dos coordenadas."""
    if n_por_clase < 1 or clases < 2 or vueltas <= 0 or ruido < 0:
        raise ValueError('Revisar tamaño, clases, vueltas y ruido')
    rng = np.random.default_rng(semilla)
    grupos, etiquetas = [], []
    for k in range(clases):
        r = rng.uniform(.15, 1, n_por_clase)
        angulo = 2 * np.pi * vueltas * r + 2 * np.pi * k / clases
        X = np.column_stack([r * np.cos(angulo), r * np.sin(angulo)])
        grupos.append(X + rng.normal(0, ruido, X.shape))
        etiquetas.append(np.full(n_por_clase, k, dtype=np.int64))
    X, y = np.vstack(grupos), np.concatenate(etiquetas)
    orden = rng.permutation(len(y))
    return X[orden], y[orden]

def generar_regresion(n=400, relacion='seno', ruido=.3, heterocedastico=False, semilla=42):
    """Devuelve X (n,1), y, media conocida y desviación condicional sigma."""
    if n < 1 or ruido < 0:
        raise ValueError('Revisar n y ruido')
    rng = np.random.default_rng(semilla)
    x = rng.uniform(-3, 3, n)
    funciones = {'lineal': lambda v: 1 + 2*v,
                 'cuadratica': lambda v: .6*v**2 - 1,
                 'seno': lambda v: np.sin(2*v) + .4*v,
                 'por_tramos': lambda v: np.where(v < 0, -.5*v, 1.5*v)}
    if relacion not in funciones:
        raise ValueError('Relación no disponible')
    media = funciones[relacion](x)
    sigma = ruido * (1 + np.abs(x)) if heterocedastico else np.full(n, ruido)
    y = media + sigma * rng.normal(size=n)
    return x[:, None], y, media, sigma

def invertir_etiquetas(y, proporcion=.1, semilla=42):
    """Invierte exactamente round(proporcion*n) etiquetas binarias, sin modificar y."""
    original = np.asarray(y)
    if not 0 <= proporcion <= 1 or not np.isin(original, [0, 1]).all():
        raise ValueError('Se requieren etiquetas binarias y proporción entre 0 y 1')
    rng = np.random.default_rng(semilla)
    mascara = np.zeros(len(original), dtype=bool)
    mascara[rng.choice(len(original), round(proporcion*len(original)), replace=False)] = True
    cambiado = original.copy()
    cambiado[mascara] = 1 - cambiado[mascara]
    return cambiado, mascara

def agregar_faltantes(X, proporcion=.1, semilla=42):
    """Ausencia independiente por celda (MCAR); devuelve copia float y máscara."""
    if not 0 <= proporcion <= 1:
        raise ValueError('Proporción fuera de [0,1]')
    rng = np.random.default_rng(semilla)
    copia = np.asarray(X, dtype=float).copy()
    mascara = rng.random(copia.shape) < proporcion
    copia[mascara] = np.nan
    return copia, mascara

def generar_serie(n=480, periodo=24, pendiente=.003, amplitud=1.2,
                  ruido=.2, rho=.6, cambio=None, salto=0., semilla=42):
    """Serie, media conocida y componentes. ruido es sigma de las innovaciones AR(1)."""
    if n < 2 or periodo < 2 or ruido < 0 or abs(rho) >= 1:
        raise ValueError('Revisar n, periodo, ruido y estacionariedad del componente AR')
    if cambio is not None and not 0 <= cambio < n:
        raise ValueError('El cambio debe estar dentro de la serie')
    rng = np.random.default_rng(semilla)
    t = np.arange(n)
    tendencia = pendiente * t
    estacional = amplitud * np.sin(2*np.pi*t/periodo)
    nivel = np.where(t >= cambio, salto, 0.) if cambio is not None else np.zeros(n)
    innovaciones = rng.normal(0, ruido, n)
    error = np.empty(n)
    error[0] = rng.normal(0, ruido / np.sqrt(1-rho**2))
    for j in range(1, n):
        error[j] = rho*error[j-1] + innovaciones[j]
    media = tendencia + estacional + nivel
    return {'t': t, 'y': media + error, 'media': media, 'tendencia': tendencia,
            'estacional': estacional, 'nivel': nivel, 'error': error}

def generar_imagenes(n_por_clase=40, lado=32, ruido=.15, semilla=42):
    """Cuatro formas. X float32 (N,1,H,W) en [0,1], y int64, orden aleatorio."""
    if n_por_clase < 1 or lado < 16 or ruido < 0:
        raise ValueError('Revisar cantidad, tamaño y ruido')
    rng = np.random.default_rng(semilla)
    yy, xx = np.mgrid[:lado, :lado]
    imagenes, etiquetas = [], []
    for k in range(4):
        for _ in range(n_por_clase):
            cx, cy = rng.uniform(.35*lado, .65*lado, 2)
            grosor = rng.uniform(.035*lado, .08*lado)
            if k == 0:
                forma = np.abs(xx-cx) <= grosor
            elif k == 1:
                forma = np.abs(yy-cy) <= grosor
            elif k == 2:
                forma = np.abs((xx-cx)-(yy-cy))/np.sqrt(2) <= grosor
            else:
                radio = rng.uniform(.18*lado, .26*lado)
                forma = np.abs(np.hypot(xx-cx, yy-cy)-radio) <= grosor
            imagenes.append(np.clip(forma.astype(float) + rng.normal(0, ruido, (lado, lado)), 0, 1))
            etiquetas.append(k)
    X = np.asarray(imagenes, dtype=np.float32)[:, None]
    y = np.asarray(etiquetas, dtype=np.int64)
    orden = rng.permutation(len(y))
    return X[orden], y[orden]

def generar_tabla_categorica(n=600, semilla=42):
    """Devuelve columnas mixtas, y binario y probabilidad conocida de una simulación."""
    if n < 1:
        raise ValueError('n debe ser positivo')
    rng = np.random.default_rng(semilla)
    canal = rng.choice(['web', 'tienda', 'telefono'], size=n, p=[.5,.3,.2])
    segmento = rng.choice(['nuevo', 'recurrente'], size=n, p=[.6,.4])
    intensidad = rng.normal(size=n)
    efecto_canal = np.select([canal == 'web', canal == 'tienda'], [.5, -.3], default=0.)
    logit = -1 + .8*intensidad + efecto_canal + .7*(segmento == 'recurrente')
    probabilidad = 1 / (1 + np.exp(-logit))
    y = rng.binomial(1, probabilidad).astype(np.int64)
    return {'canal': canal, 'segmento': segmento, 'intensidad': intensidad}, y, probabilidad
