<?php
// =====================================================
// helpers/json.php — Decodificación JSON segura
// Resuelve el patrón global de json_decode($raw, true) accedido
// como array sin validar (null access errors silenciosos).
// =====================================================
//
// Uso:
//   require_once __DIR__ . '/../helpers/json.php';
//   $data = dm_json_decode($raw, ['context'=>'meta-action.duplicate']);
//   if (!is_array($data)) { /* logged automáticamente */ }
//
// Otros patrones útiles:
//   $data = dm_json_decode_or($raw, []);             // fallback a array vacío
//   $val  = dm_json_path($data, 'content.0.text', '');  // acceso seguro a path anidado
//   $extr = dm_json_extract_first($text);            // primer JSON dentro de un texto

if (!defined('DM_JSON_LOG_PATH')) {
    define('DM_JSON_LOG_PATH', dirname(__DIR__) . '/logs/json_errors.log');
}

/**
 * Decodifica JSON con validación. Devuelve array o null.
 * Loggea silenciosamente si falla (no throw).
 *
 * @param string|false|null $raw  Respuesta cruda (typical curl_exec return)
 * @param array $opts  ['context'=>'string para log', 'silent'=>bool]
 * @return array|null
 */
function dm_json_decode($raw, array $opts = []): ?array {
    if ($raw === false || $raw === null || $raw === '') {
        _dm_json_log('empty', $opts);
        return null;
    }
    if (!is_string($raw)) {
        _dm_json_log('not_string', $opts, gettype($raw));
        return null;
    }
    $data = json_decode($raw, true);
    if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
        _dm_json_log('invalid', $opts, json_last_error_msg(), $raw);
        return null;
    }
    if (!is_array($data)) {
        _dm_json_log('not_array', $opts, gettype($data));
        return null;
    }
    return $data;
}

/**
 * Igual que dm_json_decode pero con fallback obligatorio.
 * Útil para evitar null checks en código consumer.
 */
function dm_json_decode_or($raw, $fallback, array $opts = []) {
    $r = dm_json_decode($raw, $opts);
    return is_array($r) ? $r : $fallback;
}

/**
 * Acceso seguro a path anidado de un array.
 * Ejemplo: dm_json_path($r, 'error.message', 'Sin mensaje')
 *           dm_json_path($r, 'content.0.text', '')
 *
 * @param mixed  $data    Array o cualquier valor
 * @param string $path    Path con dots: 'a.b.0.c'
 * @param mixed  $default Devuelto si el path no existe o el tipo es incorrecto
 * @return mixed
 */
function dm_json_path($data, string $path, $default = null) {
    if (!is_array($data) || $path === '') return $default;
    $parts = explode('.', $path);
    $cur   = $data;
    foreach ($parts as $p) {
        if (is_array($cur) && array_key_exists($p, $cur)) {
            $cur = $cur[$p];
        } else if (is_array($cur) && ctype_digit((string)$p) && isset($cur[(int)$p])) {
            $cur = $cur[(int)$p];
        } else {
            return $default;
        }
    }
    return $cur;
}

/**
 * Extrae el primer objeto JSON dentro de un texto (ej: respuesta Claude con
 * markdown alrededor del JSON).
 * Devuelve array decodificado o null.
 */
function dm_json_extract_first(string $text, array $opts = []): ?array {
    $text = trim($text);
    if ($text === '') return null;

    // Caso 1: viene en bloque ```json ... ```
    if (preg_match('/```(?:json)?\s*([\s\S]*?)\s*```/i', $text, $m)) {
        $r = dm_json_decode($m[1], $opts + ['context'=>($opts['context'] ?? '') . ':code_block']);
        if ($r !== null) return $r;
    }

    // Caso 2: encontrar primer { ... } balanceado
    $start = strpos($text, '{');
    if ($start === false) return null;
    $depth   = 0;
    $in_str  = false;
    $esc     = false;
    $len     = strlen($text);
    for ($i = $start; $i < $len; $i++) {
        $c = $text[$i];
        if ($esc) { $esc = false; continue; }
        if ($c === '\\' && $in_str) { $esc = true; continue; }
        if ($c === '"') { $in_str = !$in_str; continue; }
        if ($in_str) continue;
        if ($c === '{') $depth++;
        else if ($c === '}') {
            $depth--;
            if ($depth === 0) {
                $candidate = substr($text, $start, $i - $start + 1);
                return dm_json_decode($candidate, $opts + ['context'=>($opts['context'] ?? '') . ':balanced']);
            }
        }
    }
    return null;
}

/**
 * Encode con flags seguros por defecto. Si falla, retorna '{}' y loggea.
 */
function dm_json_encode($value, int $flags = 0): string {
    $flags = $flags | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
    $r = json_encode($value, $flags);
    if ($r === false) {
        _dm_json_log('encode_failed', ['context'=>'dm_json_encode'], json_last_error_msg());
        return '{}';
    }
    return $r;
}

// ─── Log interno ───
function _dm_json_log(string $reason, array $opts = [], string $detail = '', string $raw = ''): void {
    if (!empty($opts['silent'])) return;
    $ctx  = $opts['context'] ?? '?';
    $line = sprintf("[%s] %s ctx=%s detail=%s%s\n",
        date('Y-m-d H:i:s'), $reason, $ctx, $detail,
        $raw ? ' raw=' . substr(preg_replace('/\s+/', ' ', $raw), 0, 200) : ''
    );
    @file_put_contents(DM_JSON_LOG_PATH, $line, FILE_APPEND | LOCK_EX);
}
