<?php
/**
 * Persistencia y validación de personalización del explorador (Flexia).
 * Un solo archivo JSON: config/explorer_customization.json
 */

if (!defined('GRID_ROOT')) {
    define('GRID_ROOT', dirname(__DIR__));
}

/** @return string */
function grid_explorer_customization_config_path(): string {
    return GRID_ROOT . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'explorer_customization.json';
}

/** @return string */
function grid_explorer_customization_media_root(): string {
    return GRID_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'explorer' . DIRECTORY_SEPARATOR . 'customization';
}

/**
 * @return array<string, mixed>
 */
function grid_explorer_customization_defaults(): array {
    return [
        'version' => 1,
        'global' => [
            'explorer_theme' => 'default',
            'folder_view_mode' => 'icons_md',
            'icon_size' => 'medium',
            'default_folder_icon' => '',
            'default_preview_type' => 'none',
            'allow_gif_preview' => true,
            'allow_video_preview' => true,
            'explorer_chrome' => [
                'type' => 'none',
                'color' => '',
                'src' => '',
            ],
            'extensions' => [],
        ],
        'folders' => [
            'files' => [],
            'cache' => [],
        ],
        'files' => [
            'files' => [],
            'cache' => [],
        ],
    ];
}

/**
 * Normaliza ruta relativa (sin .., sin barra inicial).
 */
function grid_ec_normalize_rel_path(string $path): string {
    $path = str_replace(["\0", '\\'], ['', '/'], $path);
    $path = ltrim($path, '/');
    if (strpos($path, '..') !== false) {
        return '';
    }
    $parts = array_values(array_filter(explode('/', $path), static function ($p) {
        return $p !== '' && $p !== '.' && $p !== '..';
    }));
    return implode('/', $parts);
}

function grid_ec_allowed_bucket(string $b): bool {
    return $b === 'files' || $b === 'cache';
}

/**
 * Valida ruta relativa de medio (icono/preview/fondo) bajo media/explorer/.
 *
 * @return string|null ruta saneada o null
 */
function grid_ec_sanitize_media_src(?string $src): ?string {
    if ($src === null || $src === '') {
        return '';
    }
    $s = str_replace(["\0", '\\'], ['', '/'], trim((string)$src));
    $s = ltrim($s, '/');
    if ($s === '' || strpos($s, '..') !== false) {
        return null;
    }
    $allowedPrefixes = [
        'media/explorer/customization/',
        'media/explorer/files/',
    ];
    foreach ($allowedPrefixes as $pre) {
        if (strncasecmp($s, $pre, strlen($pre)) === 0) {
            return $s;
        }
    }
    return null;
}

/**
 * @param array<string, mixed> $iconVal string URL/clase o array legacy
 */
function grid_ec_sanitize_icon($iconVal): string {
    if ($iconVal === null) {
        return '';
    }
    if (is_array($iconVal)) {
        $iconVal = isset($iconVal['class']) ? (string)$iconVal['class'] : (isset($iconVal['url']) ? (string)$iconVal['url'] : '');
    }
    $icon = trim((string)$iconVal);
    if ($icon === '') {
        return '';
    }
    if (preg_match('#^https?://#i', $icon)) {
        return '';
    }
    if (strpos($icon, '<') !== false || stripos($icon, 'javascript:') !== false) {
        return '';
    }
    if (preg_match('#^media/#i', $icon)) {
        $ok = grid_ec_sanitize_media_src($icon);
        return $ok !== null ? $ok : '';
    }
    $icon = preg_replace('/\s+/', ' ', $icon);
    if (strlen($icon) > 200) {
        return '';
    }
    if (preg_match('/^fa[srlb]?\s+fa-[\w\-]+$/i', $icon) || preg_match('/^(fas|far|fal|fab)\s+fa-[\w\-]+$/i', $icon)) {
        return $icon;
    }
    return '';
}

/**
 * @param array<string, mixed>|string|null $entry
 * @return array{errors: list<string>, data: array<string, mixed>}
 */
function grid_ec_sanitize_folder_or_file_entry($entry): array {
    $errors = [];
    if ($entry === null) {
        return ['errors' => [], 'data' => []];
    }
    if (!is_array($entry)) {
        return ['errors' => ['Entrada inválida'], 'data' => []];
    }
    $out = [];
    if (isset($entry['icon'])) {
        $out['icon'] = grid_ec_sanitize_icon($entry['icon']);
    }
    if (isset($entry['color'])) {
        $c = trim((string)$entry['color']);
        if ($c === '' || preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $c)) {
            $out['color'] = $c;
        } else {
            $errors[] = 'Color inválido';
        }
    }
    if (isset($entry['background'])) {
        $bg = grid_ec_sanitize_media_src((string)$entry['background']);
        if ($bg === null && $entry['background'] !== '' && $entry['background'] !== null) {
            $errors[] = 'Fondo: ruta no permitida';
        } else {
            $out['background'] = $bg ?? '';
        }
    }
    if (isset($entry['preview_type'])) {
        $pt = strtolower(trim((string)$entry['preview_type']));
        $allowed = ['none', 'image', 'gif', 'video'];
        $out['preview_type'] = in_array($pt, $allowed, true) ? $pt : 'none';
    }
    if (isset($entry['preview_src'])) {
        $ps = grid_ec_sanitize_media_src((string)$entry['preview_src']);
        if ($ps === null && $entry['preview_src'] !== '' && $entry['preview_src'] !== null) {
            $errors[] = 'Preview: ruta no permitida';
        } else {
            $out['preview_src'] = $ps ?? '';
        }
    }
    if (isset($entry['view_mode'])) {
        $vm = preg_replace('/[^a-z0-9_\-]/i', '', (string)$entry['view_mode']);
        $out['view_mode'] = substr($vm, 0, 64);
    }
    if (isset($entry['card_style'])) {
        $out['card_style'] = substr(preg_replace('/[^a-z0-9_\-]/i', '', (string)$entry['card_style']), 0, 64);
    }
    return ['errors' => $errors, 'data' => $out];
}

/**
 * @param array<string, mixed> $global
 * @return array{errors: list<string>, global: array<string, mixed>}
 */
function grid_ec_validate_global(array $global): array {
    $errors = [];
    $def = grid_explorer_customization_defaults()['global'];
    $g = array_merge($def, $global);
    $g['explorer_theme'] = substr(preg_replace('/[^a-z0-9_\-]/i', '', (string)($g['explorer_theme'] ?? 'default')), 0, 64) ?: 'default';
    $g['folder_view_mode'] = substr(preg_replace('/[^a-z0-9_\-]/i', '', (string)($g['folder_view_mode'] ?? 'icons_md')), 0, 64) ?: 'icons_md';
    $sizes = ['small', 'medium', 'large', 'xlarge'];
    $is = strtolower((string)($g['icon_size'] ?? 'medium'));
    $g['icon_size'] = in_array($is, $sizes, true) ? $is : 'medium';
    $g['default_folder_icon'] = grid_ec_sanitize_icon($g['default_folder_icon'] ?? '');
    $pt = strtolower((string)($g['default_preview_type'] ?? 'none'));
    $g['default_preview_type'] = in_array($pt, ['none', 'image', 'gif', 'video'], true) ? $pt : 'none';
    $g['allow_gif_preview'] = !empty($g['allow_gif_preview']);
    $g['allow_video_preview'] = !empty($g['allow_video_preview']);
    if (isset($g['explorer_chrome']) && is_array($g['explorer_chrome'])) {
        $ch = $g['explorer_chrome'];
        $type = strtolower(trim((string)($ch['type'] ?? 'none')));
        if (!in_array($type, ['none', 'solid', 'image', 'gif', 'video'], true)) {
            $type = 'none';
        }
        $color = trim((string)($ch['color'] ?? ''));
        if ($color !== '' && !preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color)) {
            $color = '';
        }
        $src = '';
        if (!empty($ch['src'])) {
            $s = grid_ec_sanitize_media_src((string)$ch['src']);
            if ($s === null) {
                $errors[] = 'explorer_chrome.src no permitido';
            } else {
                $src = $s;
            }
        }
        $g['explorer_chrome'] = ['type' => $type, 'color' => $color, 'src' => $src];
    } else {
        $g['explorer_chrome'] = $def['explorer_chrome'];
    }
    $ext = $g['extensions'] ?? [];
    if (!is_array($ext)) {
        $ext = [];
    }
    $g['extensions'] = [];
    foreach ($ext as $k => $v) {
        if (is_int($k)) {
            continue;
        }
        $ek = strtolower(preg_replace('/[^a-z0-9]/i', '', (string)$k));
        if ($ek === '' || strlen($ek) > 16) {
            continue;
        }
        if (!is_array($v)) {
            continue;
        }
        $san = grid_ec_sanitize_folder_or_file_entry($v);
        $errors = array_merge($errors, $san['errors']);
        $g['extensions'][$ek] = $san['data'];
    }
    return ['errors' => $errors, 'global' => $g];
}

/**
 * @param array<string, mixed> $data
 * @return array{errors: list<string>, data: array<string, mixed>}
 */
function grid_explorer_customization_validate(array $data): array {
    $errors = [];
    $defaults = grid_explorer_customization_defaults();
    $out = $defaults;
    if (isset($data['version'])) {
        $out['version'] = max(1, (int)$data['version']);
    }
    if (isset($data['global']) && is_array($data['global'])) {
        $vg = grid_ec_validate_global($data['global']);
        $errors = array_merge($errors, $vg['errors']);
        $out['global'] = $vg['global'];
    }
    foreach (['folders', 'files'] as $top) {
        if (!isset($data[$top]) || !is_array($data[$top])) {
            continue;
        }
        foreach (['files', 'cache'] as $bucket) {
            if (!isset($data[$top][$bucket]) || !is_array($data[$top][$bucket])) {
                continue;
            }
            foreach ($data[$top][$bucket] as $rel => $entry) {
                $relKey = grid_ec_normalize_rel_path((string)$rel);
                if ($relKey === '' && (string)$rel !== '') {
                    continue;
                }
                if (!is_array($entry)) {
                    continue;
                }
                $san = grid_ec_sanitize_folder_or_file_entry($entry);
                $errors = array_merge($errors, $san['errors']);
                if (!empty($san['data'])) {
                    $out[$top][$bucket][$relKey] = $san['data'];
                }
            }
        }
    }
    return ['errors' => $errors, 'data' => $out];
}

/**
 * Lee y fusiona con defaults (sin escribir).
 *
 * @return array<string, mixed>
 */
function grid_explorer_customization_read(): array {
    $path = grid_explorer_customization_config_path();
    $defaults = grid_explorer_customization_defaults();
    if (!is_file($path)) {
        return $defaults;
    }
    $raw = @file_get_contents($path);
    if ($raw === false || $raw === '') {
        return $defaults;
    }
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        return $defaults;
    }
    $v = grid_explorer_customization_validate($decoded);
    return $v['data'];
}

/**
 * Merge profundo controlado: patch puede contener global, folders.files, folders.cache, files.files, files.cache.
 * Valores null en patch eliminan la clave hija indicada.
 *
 * @param array<string, mixed> $current
 * @param array<string, mixed> $patch
 * @return array<string, mixed>
 */
function grid_explorer_customization_merge_patch(array $current, array $patch): array {
    $merged = $current;
    if (isset($patch['global']) && is_array($patch['global'])) {
        $merged['global'] = array_replace_recursive($merged['global'], $patch['global']);
    }
    foreach (['folders', 'files'] as $top) {
        if (!isset($patch[$top]) || !is_array($patch[$top])) {
            continue;
        }
        foreach (['files', 'cache'] as $bucket) {
            if (!isset($patch[$top][$bucket]) || !is_array($patch[$top][$bucket])) {
                continue;
            }
            if (!isset($merged[$top][$bucket]) || !is_array($merged[$top][$bucket])) {
                $merged[$top][$bucket] = [];
            }
            foreach ($patch[$top][$bucket] as $rel => $entry) {
                $relKey = grid_ec_normalize_rel_path((string)$rel);
                if ($relKey === '' && (string)$rel !== '' && (string)$rel !== '0') {
                    continue;
                }
                if ($entry === null) {
                    unset($merged[$top][$bucket][$relKey]);
                    continue;
                }
                if (!is_array($entry)) {
                    continue;
                }
                $prev = isset($merged[$top][$bucket][$relKey]) && is_array($merged[$top][$bucket][$relKey])
                    ? $merged[$top][$bucket][$relKey]
                    : [];
                $merged[$top][$bucket][$relKey] = array_replace_recursive($prev, $entry);
                if (empty($merged[$top][$bucket][$relKey])) {
                    unset($merged[$top][$bucket][$relKey]);
                }
            }
        }
    }
    return $merged;
}

function grid_explorer_customization_write(array $data): bool {
    $dir = dirname(grid_explorer_customization_config_path());
    if (!is_dir($dir)) {
        if (!@mkdir($dir, 0755, true)) {
            return false;
        }
    }
    $path = grid_explorer_customization_config_path();
    $tmp = $path . '.tmp.' . bin2hex(random_bytes(4));
    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    if ($json === false) {
        return false;
    }
    if (@file_put_contents($tmp, $json) === false) {
        @unlink($tmp);
        return false;
    }
    if (!@rename($tmp, $path)) {
        @unlink($tmp);
        return false;
    }
    return true;
}

/**
 * @return array{success: bool, error?: string, data?: array<string, mixed>}
 */
function grid_explorer_customization_apply_patch(array $patch): array {
    $current = grid_explorer_customization_read();
    $merged = grid_explorer_customization_merge_patch($current, $patch);
    $v = grid_explorer_customization_validate($merged);
    if (!empty($v['errors'])) {
        return ['success' => false, 'error' => implode('; ', $v['errors'])];
    }
    if (!grid_explorer_customization_write($v['data'])) {
        return ['success' => false, 'error' => 'No se pudo guardar explorer_customization.json'];
    }
    if (function_exists('error_log')) {
        error_log('[explorer_customization] save ok');
    }
    return ['success' => true, 'data' => $v['data']];
}

/**
 * @return array{success: bool, error?: string, relPath?: string}
 */
function grid_explorer_customization_handle_upload(string $category): array {
    $allowedCat = ['icons', 'previews', 'backgrounds', 'videos', 'gifs'];
    if (!in_array($category, $allowedCat, true)) {
        return ['success' => false, 'error' => 'Categoría no válida'];
    }
    $root = grid_explorer_customization_media_root();
    $sub = $category === 'gifs' ? 'gifs' : $category;
    $destDir = $root . DIRECTORY_SEPARATOR . $sub;
    if (!is_dir($destDir)) {
        if (!@mkdir($destDir, 0755, true)) {
            return ['success' => false, 'error' => 'No se pudo crear directorio de destino'];
        }
    }
    if (!isset($_FILES['file']) || !is_array($_FILES['file'])) {
        return ['success' => false, 'error' => 'Archivo no recibido'];
    }
    if (($_FILES['file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return ['success' => false, 'error' => 'Error de subida'];
    }
    $tmp = (string)($_FILES['file']['tmp_name'] ?? '');
    if ($tmp === '' || !is_uploaded_file($tmp)) {
        return ['success' => false, 'error' => 'Subida inválida'];
    }
    $orig = basename((string)($_FILES['file']['name'] ?? 'upload'));
    $orig = preg_replace('/[^a-zA-Z0-9._\-]/', '_', $orig);
    if ($orig === '' || $orig === '.' || $orig === '..') {
        return ['success' => false, 'error' => 'Nombre inválido'];
    }
    $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
    $maxBytes = ($category === 'videos') ? (50 * 1024 * 1024) : (12 * 1024 * 1024);
    $size = (int)($_FILES['file']['size'] ?? 0);
    if ($size <= 0 || $size > $maxBytes) {
        return ['success' => false, 'error' => 'Tamaño no permitido'];
    }
    $allowedExt = [
        'icons' => ['svg', 'png', 'webp', 'jpg', 'jpeg', 'gif'],
        'previews' => ['png', 'webp', 'jpg', 'jpeg', 'gif', 'webp'],
        'gifs' => ['gif'],
        'backgrounds' => ['png', 'webp', 'jpg', 'jpeg', 'gif', 'webp'],
        'videos' => ['mp4', 'webm'],
    ];
    $extList = $allowedExt[$category] ?? [];
    if (!in_array($ext, $extList, true)) {
        return ['success' => false, 'error' => 'Extensión no permitida'];
    }
    $finfo = @finfo_open(FILEINFO_MIME_TYPE);
    $mime = $finfo ? finfo_file($finfo, $tmp) : '';
    if ($finfo) {
        finfo_close($finfo);
    }
    $mime = strtolower((string)$mime);
    $mimeMap = [
        'png' => ['image/png'],
        'jpg' => ['image/jpeg'],
        'jpeg' => ['image/jpeg'],
        'gif' => ['image/gif'],
        'webp' => ['image/webp'],
        'svg' => ['image/svg+xml'],
        'mp4' => ['video/mp4'],
        'webm' => ['video/webm'],
    ];
    $expected = $mimeMap[$ext] ?? [];
    if ($expected !== [] && !in_array($mime, $expected, true) && $mime !== 'application/octet-stream' && $mime !== '') {
        return ['success' => false, 'error' => 'MIME no coincide con extensión'];
    }
    if ($ext === 'svg') {
        $svg = @file_get_contents($tmp);
        if ($svg === false || stripos($svg, '<script') !== false || stripos($svg, 'javascript:') !== false) {
            return ['success' => false, 'error' => 'SVG no permitido'];
        }
    }
    $base = pathinfo($orig, PATHINFO_FILENAME);
    $base = substr(preg_replace('/[^a-zA-Z0-9_\-]/', '_', $base), 0, 80);
    $final = $base . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
    $dest = $destDir . DIRECTORY_SEPARATOR . $final;
    if (!@move_uploaded_file($tmp, $dest)) {
        return ['success' => false, 'error' => 'No se pudo mover el archivo'];
    }
    $relWeb = 'media/explorer/customization/' . $sub . '/' . $final;
    return ['success' => true, 'relPath' => $relWeb];
}
