<?php

namespace SiipAvailableIps;

/**
 * Servicio de verificación de IPs mediante ping
 * Implementa ping paralelo para verificar disponibilidad real de IPs
 */
class PingService
{
    private $logger;
    private $timeout;
    private $batchSize;

    /**
     * Constructor
     * 
     * @param object|null $logger Logger para registrar operaciones
     * @param int $timeout Timeout en segundos para cada ping (default: 1)
     * @param int $batchSize Número de IPs a procesar en paralelo (default: 30)
     */
    public function __construct($logger = null, $timeout = 1, $batchSize = 30)
    {
        $this->logger = $logger;
        $this->timeout = $timeout;
        $this->batchSize = $batchSize;
    }

    /**
     * Verifica si el comando ping está disponible en el sistema
     * 
     * @return bool True si ping está disponible
     */
    public function isAvailable()
    {
        $output = [];
        $returnCode = 0;
        
        // Intentar ejecutar ping
        exec('which ping 2>&1', $output, $returnCode);
        
        return $returnCode === 0;
    }

    /**
     * Realiza ping a una sola IP
     * 
     * @param string $ip Dirección IP a verificar
     * @param int|null $timeout Timeout en segundos (usa el default si es null)
     * @return array ['ip' => string, 'responding' => bool, 'time' => float|null]
     */
    public function pingIp($ip, $timeout = null)
    {
        if ($timeout === null) {
            $timeout = $this->timeout;
        }

        $startTime = microtime(true);
        
        // Comando ping: -c 1 (1 paquete), -W timeout (espera en segundos)
        // 2>&1 redirige stderr a stdout
        $command = sprintf('ping -c 1 -W %d %s 2>&1', $timeout, escapeshellarg($ip));
        
        $output = [];
        $returnCode = 0;
        
        exec($command, $output, $returnCode);
        
        $executionTime = microtime(true) - $startTime;
        
        // returnCode 0 = ping exitoso (IP responde)
        // returnCode != 0 = ping falló (IP no responde o error)
        $responding = ($returnCode === 0);
        
        if ($this->logger) {
            $status = $responding ? 'RESPONDE' : 'NO RESPONDE';
            $this->logger->appendLog(sprintf(
                'Ping a %s: %s (%.2fs)',
                $ip,
                $status,
                $executionTime
            ));
        }
        
        return [
            'ip' => $ip,
            'responding' => $responding,
            'time' => $executionTime
        ];
    }

    /**
     * Realiza ping a múltiples IPs en paralelo
     * 
     * @param array $ips Array de direcciones IP a verificar
     * @param int|null $timeout Timeout en segundos (usa el default si es null)
     * @return array Array con resultados: ['ip' => string, 'responding' => bool, 'time' => float]
     */
    public function pingMultipleIps($ips, $timeout = null)
    {
        if (empty($ips)) {
            return [];
        }

        if ($timeout === null) {
            $timeout = $this->timeout;
        }

        if (!$this->isAvailable()) {
            if ($this->logger) {
                $this->logger->appendLog('ADVERTENCIA: Comando ping no disponible en el sistema');
            }
            
            // Retornar todas las IPs como no respondiendo
            return array_map(function($ip) {
                return [
                    'ip' => $ip,
                    'responding' => false,
                    'time' => null,
                    'error' => 'Ping no disponible'
                ];
            }, $ips);
        }

        $totalIps = count($ips);
        $results = [];
        
        if ($this->logger) {
            $this->logger->appendLog(sprintf(
                'Iniciando verificación por ping de %d IPs (lotes de %d)',
                $totalIps,
                $this->batchSize
            ));
        }

        // Procesar en lotes para evitar sobrecarga
        $batches = array_chunk($ips, $this->batchSize);
        
        foreach ($batches as $batchIndex => $batch) {
            if ($this->logger) {
                $this->logger->appendLog(sprintf(
                    'Procesando lote %d/%d (%d IPs)',
                    $batchIndex + 1,
                    count($batches),
                    count($batch)
                ));
            }
            
            $batchResults = $this->pingBatch($batch, $timeout);
            $results = array_merge($results, $batchResults);
        }

        return $results;
    }

    /**
     * Realiza ping a un lote de IPs en paralelo
     * 
     * @param array $ips Array de IPs del lote
     * @param int $timeout Timeout en segundos
     * @return array Resultados del lote
     */
    private function pingBatch($ips, $timeout)
    {
        $startTime = microtime(true);
        $tempFiles = [];
        $processes = [];

        // Iniciar procesos de ping en paralelo
        foreach ($ips as $ip) {
            // Crear archivo temporal para guardar resultado
            $tempFile = tempnam(sys_get_temp_dir(), 'ping_');
            $tempFiles[$ip] = $tempFile;
            
            // Comando ping con salida a archivo temporal
            $command = sprintf(
                'ping -c 1 -W %d %s > %s 2>&1 ; echo $? >> %s &',
                $timeout,
                escapeshellarg($ip),
                escapeshellarg($tempFile),
                escapeshellarg($tempFile)
            );
            
            // Ejecutar en background
            exec($command);
            $processes[$ip] = true;
        }

        // Esperar a que todos los procesos terminen
        // Timeout total = timeout de ping + 1 segundo de margen
        $maxWait = $timeout + 1;
        $waited = 0;
        $sleepInterval = 0.1; // 100ms
        
        while ($waited < $maxWait && !empty($processes)) {
            usleep($sleepInterval * 1000000); // Convertir a microsegundos
            $waited += $sleepInterval;
            
            // Verificar si los archivos tienen resultados COMPLETOS
            foreach ($processes as $ip => $pending) {
                if (file_exists($tempFiles[$ip])) {
                    $content = file_get_contents($tempFiles[$ip]);
                    // Buscamos si hay al menos una línea y si la última línea parece un código de salida (número)
                    // El comando es: ping ... ; echo $?
                    // Así que esperamos ver output del ping y luego un número al final.
                    // A veces el archivo puede estar vacío o incompleto.
                    
                    if (!empty($content)) {
                        $lines = explode("\n", trim($content));
                        $lastLine = end($lines);
                        
                        // Si la última línea es un número (0-255), asumimos que terminó
                        if (is_numeric($lastLine)) {
                            unset($processes[$ip]);
                        }
                    }
                }
            }
        }

        // Procesar resultados
        $results = [];
        foreach ($ips as $ip) {
            $tempFile = $tempFiles[$ip];
            $responding = false;
            
            if (file_exists($tempFile)) {
                $content = file_get_contents($tempFile);
                $lines = explode("\n", trim($content));
                
                // El último valor es el código de retorno
                $returnCode = end($lines);
                $responding = (trim($returnCode) === '0');
                
                // Limpiar archivo temporal
                @unlink($tempFile);
            }
            
            $results[] = [
                'ip' => $ip,
                'responding' => $responding,
                'time' => null // No calculamos tiempo individual en modo paralelo
            ];
        }

        $executionTime = microtime(true) - $startTime;
        
        if ($this->logger) {
            $respondingCount = count(array_filter($results, function($r) {
                return $r['responding'];
            }));
            
            $this->logger->appendLog(sprintf(
                'Lote completado: %d/%d IPs responden (%.2fs)',
                $respondingCount,
                count($ips),
                $executionTime
            ));
        }

        return $results;
    }

    /**
     * Procesa resultados de ping y separa IPs que responden vs. no responden
     * 
     * @param array $results Resultados de pingMultipleIps()
     * @return array ['responding' => [...], 'not_responding' => [...], 'stats' => [...]]
     */
    public function processPingResults($results)
    {
        $responding = [];
        $notResponding = [];
        
        foreach ($results as $result) {
            if ($result['responding']) {
                $responding[] = $result['ip'];
            } else {
                $notResponding[] = $result['ip'];
            }
        }
        
        return [
            'responding' => $responding,
            'not_responding' => $notResponding,
            'stats' => [
                'total_checked' => count($results),
                'responding' => count($responding),
                'not_responding' => count($notResponding)
            ]
        ];
    }
}
