<?php
/**
 * Aplica subidas y campos del paso PWA del instalador a settings.pwa.
 */

declare(strict_types=1);

require_once dirname(__DIR__) . '/includes/install_public.php';
require_once dirname(__DIR__) . '/includes/install_company_logo.php';

/** Tamaño máximo del archivo fuente del pack PWA (regla de negocio). */
const ANTARA_INSTALL_PWA_PACK_MAX_BYTES = 8 * 1024 * 1024;

/** Límite de píxeles descomprimidos (ancho × alto) para evitar abusos de memoria. */
const ANTARA_INSTALL_PWA_PACK_MAX_PIXELS = 4096 * 4096;

/** @var list<string> */
const ANTARA_INSTALL_PWA_PACK_MIME_WHITELIST = ['image/png', 'image/jpeg', 'image/webp'];

/** Claves de slot admitidas en pwa_crop_json (deben coincidir con el cliente del instalador). */
const ANTARA_INSTALL_PWA_PACK_SLOT_KEYS = ['icon192', 'icon512', 'maskable512', 'apple180', 'splash'];

/**
 * Reglas del pack de iconos para la vista del instalador (una sola fuente de verdad para textos numéricos).
 *
 * @return array{maxBytes: int, maxMb: int, accept: list<string>, maxPixels: int, slots: array<string, array{label: string, outW: int, outH: int, aspect: string}>}
 */
function antara_install_pwa_pack_rules_for_view(): array
{
    return [
        'maxBytes' => ANTARA_INSTALL_PWA_PACK_MAX_BYTES,
        'maxMb' => (int) round(ANTARA_INSTALL_PWA_PACK_MAX_BYTES / (1024 * 1024)),
        'accept' => [...ANTARA_INSTALL_PWA_PACK_MIME_WHITELIST],
        'maxPixels' => ANTARA_INSTALL_PWA_PACK_MAX_PIXELS,
        'slots' => [
            'icon192' => ['label' => 'Icono 192×192', 'outW' => 192, 'outH' => 192, 'aspect' => '1:1'],
            'icon512' => ['label' => 'Icono 512×512', 'outW' => 512, 'outH' => 512, 'aspect' => '1:1'],
            'maskable512' => ['label' => 'Maskable 512×512', 'outW' => 512, 'outH' => 512, 'aspect' => '1:1'],
            'apple180' => ['label' => 'Apple touch 180×180', 'outW' => 180, 'outH' => 180, 'aspect' => '1:1'],
            'splash' => ['label' => 'Arranque 1280×720 (16:9)', 'outW' => 1280, 'outH' => 720, 'aspect' => '16:9'],
        ],
    ];
}

/**
 * @return string MIME en minúsculas o cadena vacía si no se pudo detectar
 */
function antara_install_pwa_pack_detect_mime(string $tmpPath): string
{
    if (!function_exists('finfo_open')) {
        return '';
    }
    $fi = @finfo_open(FILEINFO_MIME_TYPE);
    if (!$fi) {
        return '';
    }
    $mime = (string) @finfo_file($fi, $tmpPath);
    finfo_close($fi);

    return strtolower($mime);
}

/**
 * Valida tamaño y MIME del logo de perfil bajo logos_sinapsys (reutilización PWA).
 *
 * @return array{ok: bool, error: string}
 */
function antara_install_pwa_pack_validate_profile_logo_file(string $absPath): array
{
    if ($absPath === '' || !is_file($absPath) || !is_readable($absPath)) {
        return ['ok' => false, 'error' => 'No se pudo leer la imagen de perfil.'];
    }
    $root = defined('GRID_ROOT') ? GRID_ROOT : dirname(__DIR__);
    $logoDir = realpath($root . DIRECTORY_SEPARATOR . 'logos_sinapsys');
    $realFile = realpath($absPath);
    if ($logoDir === false || $realFile === false || strpos($realFile, $logoDir) !== 0) {
        return ['ok' => false, 'error' => 'Ruta de imagen de perfil no válida.'];
    }
    $size = (int) @filesize($absPath);
    if ($size < 1) {
        return ['ok' => false, 'error' => 'El archivo de imagen está vacío.'];
    }
    if ($size > ANTARA_INSTALL_PWA_PACK_MAX_BYTES) {
        return ['ok' => false, 'error' => 'La imagen para iconos supera ' . (string) (int) round(ANTARA_INSTALL_PWA_PACK_MAX_BYTES / (1024 * 1024)) . ' MB.'];
    }
    $mime = antara_install_pwa_pack_detect_mime($absPath);
    if ($mime === '' || !in_array($mime, ANTARA_INSTALL_PWA_PACK_MIME_WHITELIST, true)) {
        return ['ok' => false, 'error' => 'La imagen de perfil debe ser PNG, JPEG o WebP para generar iconos PWA (no GIF).'];
    }

    return ['ok' => true, 'error' => ''];
}

/**
 * @return array{ok: bool, error: string}
 */
function antara_install_pwa_pack_validate_uploaded_file(string $tmpPath, int $size): array
{
    if ($size > ANTARA_INSTALL_PWA_PACK_MAX_BYTES) {
        return ['ok' => false, 'error' => 'La imagen para iconos supera ' . (string) (int) round(ANTARA_INSTALL_PWA_PACK_MAX_BYTES / (1024 * 1024)) . ' MB.'];
    }
    $mime = antara_install_pwa_pack_detect_mime($tmpPath);
    if ($mime === '' || !in_array($mime, ANTARA_INSTALL_PWA_PACK_MIME_WHITELIST, true)) {
        return ['ok' => false, 'error' => 'Formato no admitido en la imagen de iconos (PNG, JPEG o WebP).'];
    }

    return ['ok' => true, 'error' => ''];
}

/**
 * Cuadrado central máximo inscrito en WxH (misma fórmula que el cliente del instalador).
 *
 * @return array{x: int, y: int, width: int, height: int}
 */
function antara_install_pwa_default_square_rect(int $w, int $h): array
{
    $side = min($w, $h);

    return [
        'x' => (int) (($w - $side) / 2),
        'y' => (int) (($h - $side) / 2),
        'width' => $side,
        'height' => $side,
    ];
}

/**
 * Rectángulo 16:9 centrado de mayor área inscrito en WxH.
 *
 * @return array{x: int, y: int, width: int, height: int}
 */
function antara_install_pwa_default_splash_rect_16_9(int $w, int $h): array
{
    if ($w < 1 || $h < 1) {
        return ['x' => 0, 'y' => 0, 'width' => 1, 'height' => 1];
    }
    $rw = $w;
    $rh = (int) round($rw * 9 / 16);
    if ($rh > $h) {
        $rh = $h;
        $rw = (int) round($rh * 16 / 9);
        if ($rw > $w) {
            $rw = $w;
            $rh = (int) round($rw * 9 / 16);
        }
    }

    return [
        'x' => (int) (($w - $rw) / 2),
        'y' => (int) (($h - $rh) / 2),
        'width' => max(1, $rw),
        'height' => max(1, $rh),
    ];
}

/**
 * @param array<string, mixed>|null $decoded
 * @return array{errors: list<string>, crops: array<string, array{x: int, y: int, width: int, height: int}>|null}
 */
function antara_install_pwa_pack_parse_and_merge_crops(?array $decoded, int $imgW, int $imgH): array
{
    $errors = [];
    if ($imgW < 1 || $imgH < 1) {
        return ['errors' => ['Imagen origen inválida para recortes.'], 'crops' => null];
    }
    if ($imgW * $imgH > ANTARA_INSTALL_PWA_PACK_MAX_PIXELS) {
        return ['errors' => ['La imagen supera el tamaño máximo permitido en píxeles.'], 'crops' => null];
    }

    $sq = antara_install_pwa_default_square_rect($imgW, $imgH);
    $sp = antara_install_pwa_default_splash_rect_16_9($imgW, $imgH);

    $defaults = [
        'icon192' => $sq,
        'icon512' => $sq,
        'maskable512' => $sq,
        'apple180' => $sq,
        'splash' => $sp,
    ];

    if ($decoded === null) {
        return ['errors' => [], 'crops' => null];
    }

    $merged = [];
    foreach (ANTARA_INSTALL_PWA_PACK_SLOT_KEYS as $key) {
        $base = $defaults[$key];
        if (!isset($decoded[$key]) || !is_array($decoded[$key])) {
            $merged[$key] = $base;
            continue;
        }
        $row = $decoded[$key];
        foreach (['x', 'y', 'width', 'height'] as $f) {
            if (!array_key_exists($f, $row) || filter_var($row[$f], FILTER_VALIDATE_INT) === false) {
                $errors[] = 'Recorte inválido en la variante: ' . $key . '.';

                return ['errors' => $errors, 'crops' => null];
            }
        }
        $x = (int) $row['x'];
        $y = (int) $row['y'];
        $cw = (int) $row['width'];
        $ch = (int) $row['height'];
        if ($cw < 1 || $ch < 1 || $x < 0 || $y < 0 || $x + $cw > $imgW || $y + $ch > $imgH) {
            $errors[] = 'Las coordenadas de recorte están fuera de la imagen (' . $key . ').';

            return ['errors' => $errors, 'crops' => null];
        }
        $ratio = $cw / max(1, $ch);
        if ($key === 'splash') {
            $target = 16 / 9;
            if (abs($ratio - $target) > 0.08) {
                $errors[] = 'El recorte de arranque debe ser aproximadamente 16:9.';

                return ['errors' => $errors, 'crops' => null];
            }
        } else {
            if (abs($ratio - 1.0) > 0.02) {
                $errors[] = 'Los iconos requieren recorte cuadrado (' . $key . ').';

                return ['errors' => $errors, 'crops' => null];
            }
        }
        $merged[$key] = ['x' => $x, 'y' => $y, 'width' => $cw, 'height' => $ch];
    }

    return ['errors' => [], 'crops' => $merged];
}

/**
 * @return array{errors: list<string>, crops: array<string, array{x: int, y: int, width: int, height: int}>|null}
 */
function antara_install_pwa_pack_parse_crop_json_post(string $raw, int $imgW, int $imgH): array
{
    $raw = trim($raw);
    if ($raw === '') {
        return ['errors' => [], 'crops' => null];
    }
    try {
        $decoded = json_decode($raw, true, 8, JSON_THROW_ON_ERROR);
    } catch (\Throwable $e) {
        return ['errors' => ['El JSON de recortes PWA no es válido.'], 'crops' => null];
    }
    if (!is_array($decoded)) {
        return ['errors' => ['El JSON de recortes PWA debe ser un objeto.'], 'crops' => null];
    }

    return antara_install_pwa_pack_parse_and_merge_crops($decoded, $imgW, $imgH);
}

/**
 * Nombre de campo de subida por variante PWA (coincide con el instalador).
 */
function antara_install_pwa_pack_multislot_field(string $slotKey): string
{
    return 'pwa_slot_' . $slotKey;
}

/**
 * Valida y fusiona recortes cuando cada variante tiene su propia imagen (dimensiones por slot).
 *
 * @param array<string, mixed> $decoded
 * @param array<string, array{w: int, h: int}> $dimsBySlot
 *
 * @return array{errors: list<string>, crops: array<string, array{x: int, y: int, width: int, height: int}>|null}
 */
function antara_install_pwa_pack_parse_crops_multislot(array $decoded, array $dimsBySlot): array
{
    $errors = [];
    $merged = [];
    foreach ($dimsBySlot as $key => $dim) {
        if (!in_array($key, ANTARA_INSTALL_PWA_PACK_SLOT_KEYS, true)) {
            continue;
        }
        $imgW = (int) ($dim['w'] ?? 0);
        $imgH = (int) ($dim['h'] ?? 0);
        if ($imgW < 1 || $imgH < 1) {
            return ['errors' => ['Imagen origen inválida para la variante: ' . $key . '.'], 'crops' => null];
        }
        if ($imgW * $imgH > ANTARA_INSTALL_PWA_PACK_MAX_PIXELS) {
            return ['errors' => ['La imagen supera el tamaño máximo permitido en píxeles (' . $key . ').'], 'crops' => null];
        }

        $base = $key === 'splash'
            ? antara_install_pwa_default_splash_rect_16_9($imgW, $imgH)
            : antara_install_pwa_default_square_rect($imgW, $imgH);

        if (!isset($decoded[$key]) || !is_array($decoded[$key])) {
            $merged[$key] = $base;
            continue;
        }
        $row = $decoded[$key];
        foreach (['x', 'y', 'width', 'height'] as $f) {
            if (!array_key_exists($f, $row) || filter_var($row[$f], FILTER_VALIDATE_INT) === false) {
                $errors[] = 'Recorte inválido en la variante: ' . $key . '.';

                return ['errors' => $errors, 'crops' => null];
            }
        }
        $x = (int) $row['x'];
        $y = (int) $row['y'];
        $cw = (int) $row['width'];
        $ch = (int) $row['height'];
        if ($cw < 1 || $ch < 1 || $x < 0 || $y < 0 || $x + $cw > $imgW || $y + $ch > $imgH) {
            $errors[] = 'Las coordenadas de recorte están fuera de la imagen (' . $key . ').';

            return ['errors' => $errors, 'crops' => null];
        }
        $ratio = $cw / max(1, $ch);
        if ($key === 'splash') {
            $target = 16 / 9;
            if (abs($ratio - $target) > 0.08) {
                $errors[] = 'El recorte de arranque debe ser aproximadamente 16:9.';

                return ['errors' => $errors, 'crops' => null];
            }
        } elseif (abs($ratio - 1.0) > 0.02) {
            $errors[] = 'Los iconos requieren recorte cuadrado (' . $key . ').';

            return ['errors' => $errors, 'crops' => null];
        }
        $merged[$key] = ['x' => $x, 'y' => $y, 'width' => $cw, 'height' => $ch];
    }

    return ['errors' => [], 'crops' => $merged];
}

/**
 * Genera solo las variantes para las que hay una subida temporal distinta.
 *
 * @param array<string, string> $tmpBySlot
 * @param array<string, array{x: int, y: int, width: int, height: int}> $crops
 *
 * @return array{errors: list<string>, pwaPatch: array<string, string>}
 */
function antara_install_pwa_pack_generate_multislot(array $tmpBySlot, array $crops, string $destDir): array
{
    if (!function_exists('imagecreatefromstring') || !function_exists('imagepng')) {
        return ['errors' => ['El servidor necesita la extensión GD (PNG) para generar iconos.'], 'pwaPatch' => []];
    }

    $slotMap = [
        'icon192' => ['file' => 'icon-192.png', 'w' => 192, 'h' => 192, 'alpha' => true, 'rel' => 'icon192Rel'],
        'icon512' => ['file' => 'icon-512.png', 'w' => 512, 'h' => 512, 'alpha' => true, 'rel' => 'icon512Rel'],
        'maskable512' => ['file' => 'icon-maskable-512.png', 'w' => 512, 'h' => 512, 'alpha' => true, 'rel' => 'iconMaskable512Rel'],
        'apple180' => ['file' => 'apple-touch-180.png', 'w' => 180, 'h' => 180, 'alpha' => true, 'rel' => 'appleTouch180Rel'],
        'splash' => ['file' => 'splash-launch.png', 'w' => 1280, 'h' => 720, 'alpha' => true, 'rel' => 'splashLaunchRel'],
    ];

    $relFs = [
        'icon192Rel' => 'media/pwa/icon-192.png',
        'icon512Rel' => 'media/pwa/icon-512.png',
        'iconMaskable512Rel' => 'media/pwa/icon-maskable-512.png',
        'appleTouch180Rel' => 'media/pwa/apple-touch-180.png',
        'splashLaunchRel' => 'media/pwa/splash-launch.png',
    ];

    $writePng = static function ($im, string $path): bool {
        imagesavealpha($im, true);
        $ok = @imagepng($im, $path, 6);
        imagedestroy($im);

        return $ok;
    };

    $pwaPatch = [];
    foreach ($tmpBySlot as $key => $tmpPath) {
        if (!isset($slotMap[$key], $crops[$key])) {
            return ['errors' => ['Falta configuración interna para la variante ' . $key . '.'], 'pwaPatch' => []];
        }
        $meta = $slotMap[$key];
        $raw = @file_get_contents($tmpPath);
        if ($raw === false || $raw === '') {
            return ['errors' => ['No se pudo leer la imagen subida (' . $key . ').'], 'pwaPatch' => []];
        }
        $src = @imagecreatefromstring($raw);
        if ($src === false) {
            return ['errors' => ['No se pudo interpretar la imagen (' . $key . ').'], 'pwaPatch' => []];
        }
        $im = antara_install_pwa_pack_crop_scale_to($src, $crops[$key], $meta['w'], $meta['h'], $meta['alpha']);
        imagedestroy($src);
        if ($im === false) {
            return ['errors' => ['No se pudo generar la variante ' . $key . '.'], 'pwaPatch' => []];
        }
        $destFs = $destDir . DIRECTORY_SEPARATOR . $meta['file'];
        if (is_file($destFs)) {
            @unlink($destFs);
        }
        if (!$writePng($im, $destFs)) {
            return ['errors' => ['No se pudo guardar ' . $meta['file'] . '.'], 'pwaPatch' => []];
        }
        $rk = $meta['rel'];
        if (isset($relFs[$rk])) {
            $pwaPatch[$rk] = $relFs[$rk];
        }
    }

    return ['errors' => [], 'pwaPatch' => $pwaPatch];
}

/**
 * Procesa imagen maestra (pwa_icon_pack) + sustituciones opcionales por variante (pwa_slot_*).
 * Cada variante usa la subida del slot si existe; si no, la imagen maestra (o $forcedMasterPath en reutilización).
 *
 * @return array{errors: list<string>, pwaPatch: array<string, string>}
 */
function antara_install_pwa_pack_apply_master_and_overrides(array $post, array $files, string $dir, ?string $forcedMasterPath = null): array
{
    $masterTmp = $forcedMasterPath;
    if ($forcedMasterPath === null) {
        $packField = 'pwa_icon_pack';
        if (isset($files[$packField]) && is_array($files[$packField])) {
            $f = $files[$packField];
            if (($f['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
                if (($f['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
                    return ['errors' => ['Error al subir la imagen maestra para iconos PWA.'], 'pwaPatch' => []];
                }
                $tmp = (string) ($f['tmp_name'] ?? '');
                if ($tmp === '' || !is_uploaded_file($tmp)) {
                    return ['errors' => ['Subida de imagen maestra PWA no válida.'], 'pwaPatch' => []];
                }
                $size = (int) ($f['size'] ?? 0);
                $v = antara_install_pwa_pack_validate_uploaded_file($tmp, $size);
                if (!$v['ok']) {
                    return ['errors' => [$v['error']], 'pwaPatch' => []];
                }
                $masterTmp = $tmp;
            }
        }
    }

    $overrideTmp = [];
    foreach (ANTARA_INSTALL_PWA_PACK_SLOT_KEYS as $key) {
        $field = antara_install_pwa_pack_multislot_field($key);
        if (!isset($files[$field]) || !is_array($files[$field])) {
            continue;
        }
        $f = $files[$field];
        if (($f['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
            continue;
        }
        if (($f['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
            return ['errors' => ['Error al subir la imagen para ' . $key . '.'], 'pwaPatch' => []];
        }
        $tmp = (string) ($f['tmp_name'] ?? '');
        if ($tmp === '' || !is_uploaded_file($tmp)) {
            return ['errors' => ['Subida no válida para la variante ' . $key . '.'], 'pwaPatch' => []];
        }
        $size = (int) ($f['size'] ?? 0);
        $v = antara_install_pwa_pack_validate_uploaded_file($tmp, $size);
        if (!$v['ok']) {
            return ['errors' => [$v['error'] . ' (' . $key . ').'], 'pwaPatch' => []];
        }
        $overrideTmp[$key] = $tmp;
    }

    if ($masterTmp === null && $overrideTmp === []) {
        return ['errors' => [], 'pwaPatch' => []];
    }

    $tmpBySlot = [];
    foreach (ANTARA_INSTALL_PWA_PACK_SLOT_KEYS as $key) {
        if (isset($overrideTmp[$key])) {
            $tmpBySlot[$key] = $overrideTmp[$key];
        } elseif ($masterTmp !== null) {
            $tmpBySlot[$key] = $masterTmp;
        }
    }

    if ($tmpBySlot === []) {
        return ['errors' => [], 'pwaPatch' => []];
    }

    $raw = trim((string) ($post['pwa_crop_json'] ?? ''));
    $decoded = [];
    if ($raw !== '') {
        try {
            $decoded = json_decode($raw, true, 8, JSON_THROW_ON_ERROR);
        } catch (\Throwable $e) {
            return ['errors' => ['El JSON de recortes PWA no es válido.'], 'pwaPatch' => []];
        }
        if (!is_array($decoded)) {
            return ['errors' => ['El JSON de recortes PWA debe ser un objeto.'], 'pwaPatch' => []];
        }
    }

    if (!function_exists('imagecreatefromstring')) {
        return ['errors' => ['El servidor necesita GD para validar recortes.'], 'pwaPatch' => []];
    }

    $dimsBySlot = [];
    $dimCache = [];
    foreach ($tmpBySlot as $key => $tmpPath) {
        $cacheKey = $tmpPath;
        if (!isset($dimCache[$cacheKey])) {
            $probe = @imagecreatefromstring((string) @file_get_contents($tmpPath));
            if ($probe === false) {
                return ['errors' => ['No se pudo leer la imagen para validar recortes (' . $key . ').'], 'pwaPatch' => []];
            }
            $dimCache[$cacheKey] = ['w' => imagesx($probe), 'h' => imagesy($probe)];
            imagedestroy($probe);
        }
        $dimsBySlot[$key] = $dimCache[$cacheKey];
    }

    $parsed = antara_install_pwa_pack_parse_crops_multislot($decoded, $dimsBySlot);
    if ($parsed['errors'] !== [] || $parsed['crops'] === null) {
        return ['errors' => $parsed['errors'], 'pwaPatch' => []];
    }

    return antara_install_pwa_pack_generate_multislot($tmpBySlot, $parsed['crops'], $dir);
}

/**
 * @deprecated Usar antara_install_pwa_pack_apply_master_and_overrides
 *
 * @return array{errors: list<string>, pwaPatch: array<string, string>}
 */
function antara_install_pwa_pack_apply_multislot_uploads(array $post, array $files, string $dir): array
{
    return antara_install_pwa_pack_apply_master_and_overrides($post, $files, $dir, null);
}

/**
 * Generación legacy: un solo cuadrado central + splash con icono sobre fondo.
 *
 * @return list<string>
 */
function antara_install_pwa_pack_generate_legacy($src, string $destDir): array
{
    $errors = [];
    $w = imagesx($src);
    $h = imagesy($src);
    $side = min($w, $h);
    $sx = (int) (($w - $side) / 2);
    $sy = (int) (($h - $side) / 2);
    $sq = @imagecrop($src, ['x' => $sx, 'y' => $sy, 'width' => $side, 'height' => $side]);
    if ($sq === false) {
        return ['No se pudo recortar la imagen al cuadrado central.'];
    }
    imagesavealpha($sq, true);

    $writePng = static function ($im, string $path): bool {
        imagesavealpha($im, true);
        $ok = @imagepng($im, $path, 6);
        imagedestroy($im);

        return $ok;
    };

    $resizeSquare = static function ($from, int $px) {
        $im = imagecreatetruecolor($px, $px);
        if ($im === false) {
            return false;
        }
        imagealphablend($im, false);
        imagesavealpha($im, true);
        $transparent = imagecolorallocatealpha($im, 0, 0, 0, 127);
        imagefilledrectangle($im, 0, 0, $px, $px, $transparent);
        imagealphablend($im, true);
        imagecopyresampled($im, $from, 0, 0, 0, 0, $px, $px, imagesx($from), imagesy($from));

        return $im;
    };

    $r192 = $resizeSquare($sq, 192);
    $r512 = $resizeSquare($sq, 512);
    $rMask = $resizeSquare($sq, 512);
    $r180 = $resizeSquare($sq, 180);
    if ($r192 === false || $r512 === false || $rMask === false || $r180 === false) {
        imagedestroy($sq);

        return ['No se pudo preparar las variantes de tamaño de la imagen.'];
    }

    $tw = 1280;
    $th = 720;
    $splash = imagecreatetruecolor($tw, $th);
    if ($splash === false) {
        imagedestroy($sq);
        imagedestroy($r192);
        imagedestroy($r512);
        imagedestroy($rMask);
        imagedestroy($r180);

        return ['No se pudo crear la imagen de arranque.'];
    }
    $bg = imagecolorallocate($splash, 15, 23, 42);
    imagefilledrectangle($splash, 0, 0, $tw, $th, $bg);
    $sw = imagesx($sq);
    $sh = imagesy($sq);
    $scale = min($tw / $sw, $th / $sh) * 0.92;
    $nw = (int) max(1, round($sw * $scale));
    $nh = (int) max(1, round($sh * $scale));
    $ox = (int) (($tw - $nw) / 2);
    $oy = (int) (($th - $nh) / 2);
    imagecopyresampled($splash, $sq, $ox, $oy, 0, 0, $nw, $nh, $sw, $sh);

    imagedestroy($sq);

    $files = [
        'icon-192.png' => $r192,
        'icon-512.png' => $r512,
        'icon-maskable-512.png' => $rMask,
        'apple-touch-180.png' => $r180,
        'splash-launch.png' => $splash,
    ];
    foreach ($files as $name => $im) {
        $destFs = $destDir . DIRECTORY_SEPARATOR . $name;
        if (is_file($destFs)) {
            @unlink($destFs);
        }
        if (!$writePng($im, $destFs)) {
            $errors[] = 'No se pudo guardar ' . $name . '.';
        }
    }

    return $errors;
}

/**
 * Recorta y escala una región de $src a un lienzo WxH con canal alpha.
 *
 * @param resource|\GdImage $src
 * @param array{x: int, y: int, width: int, height: int} $rect
 * @return resource|\GdImage|false
 */
function antara_install_pwa_pack_crop_scale_to($src, array $rect, int $outW, int $outH, bool $transparentBg)
{
    $patch = @imagecrop($src, [
        'x' => $rect['x'],
        'y' => $rect['y'],
        'width' => $rect['width'],
        'height' => $rect['height'],
    ]);
    if ($patch === false) {
        return false;
    }
    $im = imagecreatetruecolor($outW, $outH);
    if ($im === false) {
        imagedestroy($patch);

        return false;
    }
    if ($transparentBg) {
        imagealphablend($im, false);
        imagesavealpha($im, true);
        $transparent = imagecolorallocatealpha($im, 0, 0, 0, 127);
        imagefilledrectangle($im, 0, 0, $outW, $outH, $transparent);
        imagealphablend($im, true);
    }
    imagecopyresampled($im, $patch, 0, 0, 0, 0, $outW, $outH, imagesx($patch), imagesy($patch));
    imagedestroy($patch);

    return $im;
}

/**
 * Genera iconos PWA a partir de una sola imagen.
 * Si $crops es null, usa el algoritmo histórico (cuadrado central + splash compuesto).
 * Si $crops es un array completo por slot, aplica recortes y escalados por variante.
 *
 * @param array<string, array{x: int, y: int, width: int, height: int}>|null $crops
 *
 * @return list<string> errores (vacío si OK)
 */
function antara_install_pwa_pack_generate(string $tmpPath, string $destDir, ?array $crops = null): array
{
    if (!function_exists('imagecreatefromstring') || !function_exists('imagepng')) {
        return ['El servidor necesita la extensión GD (PNG) para generar iconos desde una sola imagen.'];
    }
    $raw = @file_get_contents($tmpPath);
    if ($raw === false || $raw === '') {
        return ['No se pudo leer la imagen subida.'];
    }
    $src = @imagecreatefromstring($raw);
    if ($src === false) {
        return ['No se pudo interpretar la imagen (usa PNG, JPEG o WebP).'];
    }
    $w = imagesx($src);
    $h = imagesy($src);
    if ($w < 1 || $h < 1) {
        imagedestroy($src);

        return ['La imagen no tiene un tamaño válido.'];
    }
    if ($w * $h > ANTARA_INSTALL_PWA_PACK_MAX_PIXELS) {
        imagedestroy($src);

        return ['La imagen supera el tamaño máximo permitido en píxeles.'];
    }

    if ($crops === null) {
        $errs = antara_install_pwa_pack_generate_legacy($src, $destDir);
        imagedestroy($src);

        return $errs;
    }

    $writePng = static function ($im, string $path): bool {
        imagesavealpha($im, true);
        $ok = @imagepng($im, $path, 6);
        imagedestroy($im);

        return $ok;
    };

    $errors = [];
    $slotMap = [
        'icon192' => ['file' => 'icon-192.png', 'w' => 192, 'h' => 192, 'alpha' => true],
        'icon512' => ['file' => 'icon-512.png', 'w' => 512, 'h' => 512, 'alpha' => true],
        'maskable512' => ['file' => 'icon-maskable-512.png', 'w' => 512, 'h' => 512, 'alpha' => true],
        'apple180' => ['file' => 'apple-touch-180.png', 'w' => 180, 'h' => 180, 'alpha' => true],
        'splash' => ['file' => 'splash-launch.png', 'w' => 1280, 'h' => 720, 'alpha' => true],
    ];

    foreach ($slotMap as $key => $meta) {
        if (!isset($crops[$key])) {
            imagedestroy($src);
            $errors[] = 'Falta recorte interno para ' . $key . '.';

            return $errors;
        }
        $im = antara_install_pwa_pack_crop_scale_to($src, $crops[$key], $meta['w'], $meta['h'], $meta['alpha']);
        if ($im === false) {
            imagedestroy($src);
            $errors[] = 'No se pudo generar la variante ' . $key . '.';

            return $errors;
        }
        $destFs = $destDir . DIRECTORY_SEPARATOR . $meta['file'];
        if (is_file($destFs)) {
            @unlink($destFs);
        }
        if (!$writePng($im, $destFs)) {
            imagedestroy($src);
            $errors[] = 'No se pudo guardar ' . $meta['file'] . '.';

            return $errors;
        }
    }

    imagedestroy($src);

    return $errors;
}

/**
 * Genera el pack PWA desde una ruta legible (subida temporal o logo en disco) y devuelve parche de rutas relativas.
 *
 * @return array{errors: list<string>, pwaPatch: array<string, string>}
 */
function antara_install_pwa_pack_apply_source_path(string $sourcePath, array $post, string $dir): array
{
    $cropJsonRaw = (string) ($post['pwa_crop_json'] ?? '');
    $cropsMerged = null;
    if (trim($cropJsonRaw) !== '') {
        if (!function_exists('imagecreatefromstring')) {
            return ['errors' => ['El servidor necesita GD para validar recortes.'], 'pwaPatch' => []];
        }
        $probe = @imagecreatefromstring((string) @file_get_contents($sourcePath));
        if ($probe === false) {
            return ['errors' => ['No se pudo leer la imagen para validar recortes.'], 'pwaPatch' => []];
        }
        $iw = imagesx($probe);
        $ih = imagesy($probe);
        $parsed = antara_install_pwa_pack_parse_crop_json_post($cropJsonRaw, $iw, $ih);
        imagedestroy($probe);
        if ($parsed['errors'] !== []) {
            return ['errors' => $parsed['errors'], 'pwaPatch' => []];
        }
        $cropsMerged = $parsed['crops'];
    }
    $genErrs = antara_install_pwa_pack_generate($sourcePath, $dir, $cropsMerged);
    if ($genErrs !== []) {
        return ['errors' => $genErrs, 'pwaPatch' => []];
    }

    return [
        'errors' => [],
        'pwaPatch' => [
            'icon192Rel' => 'media/pwa/icon-192.png',
            'icon512Rel' => 'media/pwa/icon-512.png',
            'iconMaskable512Rel' => 'media/pwa/icon-maskable-512.png',
            'appleTouch180Rel' => 'media/pwa/apple-touch-180.png',
            'splashLaunchRel' => 'media/pwa/splash-launch.png',
        ],
    ];
}

/**
 * @param array<string, mixed> $post
 * @param array<string, mixed> $files $_FILES
 * @return array{errors: list<string>, pwa: array<string, mixed>}
 */
function antara_install_pwa_apply_from_post(array $post, array $files): array
{
    $errors = [];
    $root = defined('GRID_ROOT') ? GRID_ROOT : dirname(__DIR__);
    $dir = $root . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'pwa';
    if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
        return ['errors' => ['No se pudo crear la carpeta para recursos PWA.'], 'pwa' => []];
    }

    $enabled = !empty($post['pwa_enabled']);
    $name = trim((string)($post['pwa_name'] ?? ''));
    $short = trim((string)($post['pwa_short_name'] ?? ''));
    $desc = trim((string)($post['pwa_description'] ?? ''));

    if ($enabled && $name === '') {
        $errors[] = 'Si activas la PWA, indica el nombre de la aplicación.';
    }
    if ($name === '') {
        $name = 'Flexia';
    }
    if (strlen($name) > 120) {
        $errors[] = 'El nombre PWA es demasiado largo.';
    }
    if ($short !== '' && strlen($short) > 48) {
        $errors[] = 'El nombre corto PWA es demasiado largo.';
    }
    if (strlen($desc) > 400) {
        $errors[] = 'La descripción PWA es demasiado larga.';
    }

    $theme = trim((string)($post['pwa_theme_color'] ?? '#0ea5e9'));
    $bg = trim((string)($post['pwa_background_color'] ?? '#0f172a'));
    if (!preg_match('/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i', $theme)) {
        $errors[] = 'Color de tema no válido (usa formato #RGB o #RRGGBB).';
    }
    if (!preg_match('/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i', $bg)) {
        $errors[] = 'Color de fondo no válido (usa formato #RGB o #RRGGBB).';
    }

    $display = (string)($post['pwa_display'] ?? 'standalone');
    if (!in_array($display, ['standalone', 'fullscreen', 'minimal-ui', 'browser'], true)) {
        $display = 'standalone';
    }
    $orientation = (string)($post['pwa_orientation'] ?? 'any');
    if (!in_array($orientation, ['any', 'natural', 'portrait', 'landscape', 'portrait-primary', 'landscape-primary'], true)) {
        $orientation = 'any';
    }

    $auto = antara_install_pwa_default_scope_and_start();
    $startUrl = trim((string)($post['pwa_start_url'] ?? ''));
    if ($startUrl === '') {
        $startUrl = $auto['startUrl'];
    }
    $scope = trim((string)($post['pwa_scope'] ?? ''));
    if ($scope === '') {
        $scope = $auto['scope'];
    }
    if (strlen($startUrl) > 200 || preg_match('/[\s<>"\']/', $startUrl)) {
        $errors[] = 'La URL de inicio PWA no es válida.';
    }
    if (strlen($scope) > 200 || preg_match('/[\s<>"\']/', $scope)) {
        $errors[] = 'El alcance (scope) PWA no es válido.';
    }

    $langRaw = trim((string)($post['pwa_lang'] ?? 'es'));
    $lang = strtolower($langRaw);
    if (strlen($lang) > 12 || !preg_match('/^[a-z]{2}(-[a-z0-9]{2,8})?$/', $lang)) {
        $lang = 'es';
    }

    if ($errors !== []) {
        return ['errors' => $errors, 'pwa' => []];
    }

    $pwa = [
        'enabled' => $enabled,
        'name' => $name,
        'shortName' => $short !== '' ? $short : $name,
        'description' => $desc,
        'lang' => strtolower($lang),
        'themeColor' => strtolower($theme),
        'backgroundColor' => strtolower($bg),
        'display' => $display,
        'orientation' => $orientation,
        'startUrl' => $startUrl,
        'scope' => $scope,
        'icon192Rel' => trim((string)($post['pwa_icon192_rel'] ?? '')),
        'icon512Rel' => trim((string)($post['pwa_icon512_rel'] ?? '')),
        'iconMaskable512Rel' => trim((string)($post['pwa_maskable_rel'] ?? '')),
        'appleTouch180Rel' => trim((string)($post['pwa_apple_rel'] ?? '')),
        'splashLaunchRel' => trim((string)($post['pwa_splash_rel'] ?? '')),
    ];

    $iconMode = trim((string) ($post['pwa_icon_mode'] ?? 'upload'));
    if ($iconMode !== 'reuse_profile') {
        $iconMode = 'upload';
    }
    $reusePath = antara_install_company_logo_resolved_path();
    $packApplied = false;
    $profileImageSavedInSession = session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION['install_profile_image_saved']);

    if ($iconMode === 'reuse_profile') {
        if (!$profileImageSavedInSession) {
            $errors[] = 'La opción de reutilizar la imagen de perfil solo está disponible si cargaste una imagen en el paso anterior.';
        } elseif ($reusePath === null || !is_file($reusePath)) {
            $errors[] = 'No se encontró la imagen de perfil para reutilizar. Elige «Elegir otra imagen» o vuelve al paso anterior y guarda una imagen de perfil.';
        } else {
            $vLogo = antara_install_pwa_pack_validate_profile_logo_file($reusePath);
            if (!$vLogo['ok']) {
                $errors[] = $vLogo['error'];
            } else {
                $packRes = antara_install_pwa_pack_apply_master_and_overrides($post, $files, $dir, $reusePath);
                if ($packRes['errors'] !== []) {
                    $errors = array_merge($errors, $packRes['errors']);
                } else {
                    foreach ($packRes['pwaPatch'] as $pk => $pv) {
                        $pwa[$pk] = $pv;
                    }
                    $packApplied = true;
                }
            }
        }
    }

    if (!$packApplied && $iconMode === 'upload') {
        $multiRes = antara_install_pwa_pack_apply_master_and_overrides($post, $files, $dir, null);
        if ($multiRes['errors'] !== []) {
            $errors = array_merge($errors, $multiRes['errors']);
        } elseif ($multiRes['pwaPatch'] !== []) {
            foreach ($multiRes['pwaPatch'] as $pk => $pv) {
                $pwa[$pk] = $pv;
            }
            $packApplied = true;
        }
    }

    if ($errors !== []) {
        return ['errors' => $errors, 'pwa' => []];
    }

    return ['errors' => [], 'pwa' => $pwa];
}
