<?php
/**
 * Detección de anomalías por sensor (job batch).
 *
 * - Lee últimas N lecturas por sensor activo.
 * - Aplica MAD robusto.
 * - Genera filas en `ai_insights` con type='anomaly'.
 *
 * Programar en cron cada 5–15 min:
 *   *\/10 * * * * php /ruta/bin/ai_anomaly_detect >> logs/ai.log 2>&1
 */

if (php_sapi_name() !== 'cli') {
    die("Este script se debe ejecutar por consola.\n");
}

define('BASE_PATH', dirname(__DIR__) . DIRECTORY_SEPARATOR);

require BASE_PATH . 'core/helpers/Env.php';
Env::load(BASE_PATH . '.env');

require BASE_PATH . 'app/config/Config.php';
require BASE_PATH . 'app/config/ConfigEnv.php';

if (file_exists(BASE_PATH . 'vendor/autoload.php')) {
    require BASE_PATH . 'vendor/autoload.php';
}

if (class_exists('ConfigEnv') && method_exists('ConfigEnv', 'applyEnvironmentOverrides')) {
    ConfigEnv::applyEnvironmentOverrides();
}

use Klee\Core\Ai\AnomalyDetector;

// ---- Conexión PDO ----------------------------------------------------------
try {
    $connection = ConfigEnv::getConnectionConfig('klee');
    $dsn = "mysql:host={$connection['host']};dbname={$connection['dbname']};charset=utf8mb4";
    $pdo = new PDO($dsn, $connection['user'], $connection['password'], array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ));
} catch (Exception $e) {
    fwrite(STDERR, "[ai_anomaly_detect] Error conexión BD: " . $e->getMessage() . "\n");
    exit(1);
}

// ---- Parámetros ------------------------------------------------------------
$WINDOW_POINTS = (int)(getenv('AI_ANOMALY_WINDOW') ?: 288); // 24h @ 5min
$THRESHOLD = (float)(getenv('AI_ANOMALY_THRESHOLD') ?: 3.5);
$MIN_RUN = (int)(getenv('AI_ANOMALY_MIN_RUN') ?: 3);
$MODEL_TAG = 'mad-ewma-v1';
$tenantId = (int)(getenv('TENANT_ID') ?: 1);

$detector = new AnomalyDetector($THRESHOLD, $MIN_RUN);

// ---- Sensores activos ------------------------------------------------------
$sensores = $pdo->query(
    "SELECT Id, IdEstacion, Codigo, Nombre, Variable, Unidad
     FROM sensores WHERE Estado = 1"
)->fetchAll(PDO::FETCH_ASSOC);

$nombreEstacionStmt = $pdo->prepare("SELECT Nombre FROM estaciones WHERE Id = :id LIMIT 1");
$lecturasStmt = $pdo->prepare(
    "SELECT Valor, Fecha, Hora FROM lecturas
     WHERE IdEstacion = :est AND IdSensor = :sen
     ORDER BY Fecha DESC, Hora DESC
     LIMIT :lim"
);

$totalSensores = count($sensores);
$totalAnomalias = 0;
$totalInsertados = 0;
$saltadosPocaData = 0;

foreach ($sensores as $sensor) {
    $lecturasStmt->bindValue(':est', (int)$sensor['IdEstacion'], PDO::PARAM_INT);
    $lecturasStmt->bindValue(':sen', (int)$sensor['Id'], PDO::PARAM_INT);
    $lecturasStmt->bindValue(':lim', $WINDOW_POINTS, PDO::PARAM_INT);
    $lecturasStmt->execute();
    $rows = $lecturasStmt->fetchAll(PDO::FETCH_ASSOC);

    if (count($rows) < 30) {
        $saltadosPocaData++;
        continue;
    }

    // Orden cronológico ascendente para que idx represente la posición temporal.
    $rows = array_reverse($rows);
    $values = array();
    foreach ($rows as $r) {
        if ($r['Valor'] === null || $r['Valor'] === '') {
            continue;
        }
        $values[] = (float)$r['Valor'];
    }
    if (count($values) < 30) {
        $saltadosPocaData++;
        continue;
    }

    $runs = $detector->detect($values);
    if (empty($runs)) {
        continue;
    }

    foreach ($runs as $run) {
        $totalAnomalias++;
        $scores = array_column($run, 'score');
        $maxScore = max($scores);
        $severity = AnomalyDetector::severityFromScore($maxScore);

        $firstIdx = $run[0]['idx'];
        $lastIdx = $run[count($run) - 1]['idx'];
        $windowFrom = $rows[$firstIdx]['Fecha'] . ' ' . $rows[$firstIdx]['Hora'];
        $windowTo = $rows[$lastIdx]['Fecha'] . ' ' . $rows[$lastIdx]['Hora'];

        $unidad = trim((string)$sensor['Unidad']);
        $nombre = $sensor['Nombre'] ?: $sensor['Codigo'] ?: ('Sensor #' . $sensor['Id']);
        $title = sprintf(
            'Anomalía detectada en %s (z=%.2f, %d puntos)',
            $nombre,
            $maxScore,
            count($run)
        );
        $msg = sprintf(
            'Patrón anómalo en variable "%s" entre %s y %s. Valor pico: %s%s.',
            $sensor['Variable'] ?: 's/v',
            $windowFrom,
            $windowTo,
            $run[array_search($maxScore, $scores)]['value'],
            $unidad !== '' ? ' ' . $unidad : ''
        );

        $payload = array(
            'sensor' => array(
                'id' => (int)$sensor['Id'],
                'codigo' => $sensor['Codigo'],
                'nombre' => $sensor['Nombre'],
                'variable' => $sensor['Variable'],
                'unidad' => $sensor['Unidad'],
            ),
            'estacion_id' => (int)$sensor['IdEstacion'],
            'window_points' => $WINDOW_POINTS,
            'threshold' => $THRESHOLD,
            'run' => $run,
            'max_score' => $maxScore,
        );

        try {
            $id = AiInsightsModel::createIfNotExists(array(
                'TenantId' => $tenantId,
                'Type' => 'anomaly',
                'EntityType' => 'sensor',
                'EntityId' => (int)$sensor['Id'],
                'Score' => $maxScore,
                'Severity' => $severity,
                'Title' => $title,
                'Message' => $msg,
                'Payload' => $payload,
                'Model' => $MODEL_TAG,
                'WindowFrom' => $windowFrom,
                'WindowTo' => $windowTo,
            ));
            if ($id !== null) {
                $totalInsertados++;
            }
        } catch (Throwable $e) {
            fwrite(STDERR, "[ai_anomaly_detect] Sensor {$sensor['Id']}: " . $e->getMessage() . "\n");
        }
    }
}

printf(
    "[ai_anomaly_detect] %s | sensores=%d, sin_data=%d, anomalias=%d, insertados=%d\n",
    date('Y-m-d H:i:s'),
    $totalSensores,
    $saltadosPocaData,
    $totalAnomalias,
    $totalInsertados
);

exit(0);
