<?php
// =====================================================
// helpers/http.php — Cliente HTTP centralizado
// SSL verify ON por defecto, retry, timeout, error mapping
// Reemplaza todos los curl manuales con CURLOPT_SSL_VERIFYPEER=>false
// =====================================================
//
// Uso básico:
//   require_once __DIR__ . '/../helpers/http.php';
//   $r = dm_http_get('https://api.example.com/data', ['key'=>'val'], ['Authorization: Bearer xxx']);
//   if ($r['ok']) { $data = $r['data']; } else { error_log($r['error']); }
//
// Respuesta SIEMPRE tiene esta estructura:
//   ['ok' => bool, 'status' => int, 'data' => array|null, 'error' => string|null,
//    'error_code' => string|null, 'raw' => string|null]
//
// error_code posibles:
//   'network'         — curl falló (timeout, DNS, conexión)
//   'json_invalid'    — respuesta no es JSON parseable
//   'http_4xx'        — error cliente (400-499)
//   'http_5xx'        — error servidor (500-599)
//   'auth_failed'     — 401/403 específico
//   'rate_limit'      — 429 específico
//   'not_found'       — 404 específico
//   'server_overload' — 529 (Anthropic) / 503 (otros)

if (!defined('DM_HTTP_DEFAULT_TIMEOUT'))   define('DM_HTTP_DEFAULT_TIMEOUT', 25);
if (!defined('DM_HTTP_DEFAULT_RETRIES'))   define('DM_HTTP_DEFAULT_RETRIES', 2);
if (!defined('DM_HTTP_DEFAULT_USERAGENT')) define('DM_HTTP_DEFAULT_USERAGENT', 'SuperCerebro/Brain HTTP Client');

/**
 * Ejecuta una request HTTP con SSL verify ON, retry y manejo de errores.
 *
 * @param string $method GET|POST|PUT|DELETE|PATCH
 * @param string $url    URL completa
 * @param array  $opts {
 *   @var array       $query    Parámetros GET (se concatenan a URL)
 *   @var array|string $body    Body para POST/PUT (array→JSON, string→raw)
 *   @var array       $headers  Headers (formato ["Key: Value", ...])
 *   @var int         $timeout  Timeout en segundos (default 25)
 *   @var int         $retries  Reintentos exponenciales para 5xx/timeout (default 2)
 *   @var bool        $as_form  Si true, body como form-encoded (default false=JSON)
 *   @var bool        $decode   Decodificar respuesta como JSON (default true)
 *   @var bool        $follow   Seguir redirects (default false — más seguro)
 *   @var string      $cacert   Path opcional a CA bundle custom
 *   @var bool        $debug    Si true, incluye 'raw' en respuesta (no usar prod salvo debug)
 * }
 * @return array {ok, status, data, error, error_code, raw}
 */
function dm_http_request(string $method, string $url, array $opts = []): array {
    $timeout = (int)($opts['timeout'] ?? DM_HTTP_DEFAULT_TIMEOUT);
    $retries = (int)($opts['retries'] ?? DM_HTTP_DEFAULT_RETRIES);
    $headers = $opts['headers'] ?? [];
    $query   = $opts['query']   ?? [];
    $body    = $opts['body']    ?? null;
    $as_form = !empty($opts['as_form']);
    $decode  = !isset($opts['decode']) || $opts['decode'] === true;
    $follow  = !empty($opts['follow']);
    $cacert  = $opts['cacert']  ?? null;
    $debug   = !empty($opts['debug']);

    if (!empty($query)) {
        $url .= (str_contains($url, '?') ? '&' : '?') . http_build_query($query);
    }

    $method  = strtoupper($method);
    $attempt = 0;
    $last    = ['ok'=>false, 'status'=>0, 'data'=>null, 'error'=>'unknown', 'error_code'=>'network', 'raw'=>null];

    while ($attempt <= $retries) {
        $ch = curl_init($url);
        $curl_opts = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => $timeout,
            CURLOPT_CONNECTTIMEOUT => min(10, $timeout),
            CURLOPT_SSL_VERIFYPEER => true,         // ← FIX SEGURIDAD
            CURLOPT_SSL_VERIFYHOST => 2,            // ← validar hostname
            CURLOPT_FOLLOWLOCATION => $follow,      // default false (no redirect hijack)
            CURLOPT_MAXREDIRS      => $follow ? 3 : 0,
            CURLOPT_USERAGENT      => DM_HTTP_DEFAULT_USERAGENT,
            CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
        ];

        if ($cacert && is_file($cacert)) {
            $curl_opts[CURLOPT_CAINFO] = $cacert;
        }

        if ($method !== 'GET') {
            $curl_opts[CURLOPT_CUSTOMREQUEST] = $method;
        }

        // Body
        if ($body !== null) {
            if (is_array($body)) {
                if ($as_form) {
                    $curl_opts[CURLOPT_POSTFIELDS] = http_build_query($body);
                    $headers = _dm_http_ensure_header($headers, 'Content-Type', 'application/x-www-form-urlencoded');
                } else {
                    $curl_opts[CURLOPT_POSTFIELDS] = json_encode($body, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
                    $headers = _dm_http_ensure_header($headers, 'Content-Type', 'application/json');
                }
            } else {
                $curl_opts[CURLOPT_POSTFIELDS] = (string)$body;
            }
        }

        if (!empty($headers)) {
            $curl_opts[CURLOPT_HTTPHEADER] = array_values($headers);
        }

        curl_setopt_array($ch, $curl_opts);
        $raw      = curl_exec($ch);
        $status   = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err_no   = curl_errno($ch);
        $err_str  = curl_error($ch);
        curl_close($ch);

        // Network error
        if ($raw === false || $err_no !== 0) {
            $last = [
                'ok'=>false, 'status'=>0, 'data'=>null,
                'error'=>"Conexión fallida: {$err_str}",
                'error_code'=>'network',
                'raw'=>$debug ? null : null,
            ];
            // Retry en errores de red transitorios
            if ($attempt < $retries && in_array($err_no, [CURLE_OPERATION_TIMEDOUT, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT], true)) {
                usleep(_dm_http_backoff_us($attempt));
                $attempt++;
                continue;
            }
            return $last;
        }

        // Status mapping
        $data = null;
        if ($decode && $raw !== '') {
            $data = json_decode($raw, true);
            if (!is_array($data)) {
                // Si pidió decode pero no es JSON, marcamos pero seguimos
                $data = null;
            }
        }

        if ($status >= 200 && $status < 300) {
            return [
                'ok'=>true, 'status'=>$status, 'data'=>$data,
                'error'=>null, 'error_code'=>null,
                'raw'=>$debug ? $raw : null,
            ];
        }

        // Error de HTTP
        $error_code = _dm_http_classify_status($status);
        $msg        = _dm_http_extract_error_msg($data, $raw, $status);

        $last = [
            'ok'=>false, 'status'=>$status, 'data'=>$data,
            'error'=>$msg, 'error_code'=>$error_code,
            'raw'=>$debug ? $raw : null,
        ];

        // Retry para 5xx, 429 (rate limit), 529 (overload)
        if ($attempt < $retries && in_array($status, [500,502,503,504,429,529], true)) {
            usleep(_dm_http_backoff_us($attempt));
            $attempt++;
            continue;
        }

        return $last;
    }

    return $last;
}

/** Atajo GET */
function dm_http_get(string $url, array $query = [], array $headers = [], array $opts = []): array {
    return dm_http_request('GET', $url, array_merge($opts, ['query'=>$query, 'headers'=>$headers]));
}

/** Atajo POST JSON */
function dm_http_post(string $url, $body = null, array $headers = [], array $opts = []): array {
    return dm_http_request('POST', $url, array_merge($opts, ['body'=>$body, 'headers'=>$headers]));
}

/** Atajo POST form-encoded */
function dm_http_post_form(string $url, array $body = [], array $headers = [], array $opts = []): array {
    return dm_http_request('POST', $url, array_merge($opts, ['body'=>$body, 'headers'=>$headers, 'as_form'=>true]));
}

/** Atajo PUT */
function dm_http_put(string $url, $body = null, array $headers = [], array $opts = []): array {
    return dm_http_request('PUT', $url, array_merge($opts, ['body'=>$body, 'headers'=>$headers]));
}

/** Atajo DELETE */
function dm_http_delete(string $url, array $query = [], array $headers = [], array $opts = []): array {
    return dm_http_request('DELETE', $url, array_merge($opts, ['query'=>$query, 'headers'=>$headers]));
}

// ─── Helpers internos ───

function _dm_http_classify_status(int $status): string {
    if ($status === 401 || $status === 403) return 'auth_failed';
    if ($status === 404) return 'not_found';
    if ($status === 429) return 'rate_limit';
    if ($status === 529) return 'server_overload';
    if ($status === 503) return 'server_overload';
    if ($status >= 500)  return 'http_5xx';
    if ($status >= 400)  return 'http_4xx';
    return 'unknown';
}

function _dm_http_extract_error_msg($data, $raw, int $status): string {
    if (is_array($data)) {
        // Patrones comunes
        if (isset($data['error']['message'])) return (string)$data['error']['message'];
        if (isset($data['error_description'])) return (string)$data['error_description'];
        if (isset($data['message']))           return (string)$data['message'];
        if (isset($data['error']) && is_string($data['error'])) return (string)$data['error'];
    }
    return "HTTP {$status}";
}

function _dm_http_backoff_us(int $attempt): int {
    // Exponential backoff con jitter: 0.5s, 1s, 2s, 4s... + 0-200ms random
    $base_ms = 500 * (1 << $attempt);
    $jitter  = random_int(0, 200);
    return ($base_ms + $jitter) * 1000;
}

function _dm_http_ensure_header(array $headers, string $key, string $value): array {
    $key_l = strtolower($key);
    foreach ($headers as $h) {
        if (str_starts_with(strtolower($h), $key_l . ':')) return $headers;
    }
    $headers[] = "{$key}: {$value}";
    return $headers;
}
