<?php
/**
 * [1.5.406] ARMAZENAMENTO S3 (compatível: AWS S3, Cloudflare R2, Backblaze B2,
 * DigitalOcean Spaces, Wasabi, Contabo…) — sem SDKs, assinatura AWS SigV4 em
 * PHP puro sobre wp_remote_request.
 *
 * O que faz quando LIGADO:
 *   - Os uploads do 1MZ (comprovativos, KYC, demos, capas, fotos de filmagem,
 *     contratos PDF) sobem para o bucket e as URLs guardadas passam a apontar
 *     para o S3 (ou para o domínio público/CDN configurado).
 *   - Opcional: apagar a cópia local depois do envio (poupa disco no site).
 *   - Apagar ficheiros (rmbz_destruir_ficheiro_fisico) também apaga no S3.
 *
 * Segredos: Access Key/Secret guardados ENCRIPTADOS (AES-256 com as salts do
 * WP, como a chave da IA) ou definidos no wp-config.php:
 *   define( 'RMBZ_S3_KEY_ID', '...' );
 *   define( 'RMBZ_S3_SECRET', '...' );
 *
 * Configuração: WP Admin → 1MZ → Configurações → aba "☁️ S3".
 */

if ( ! defined( 'ABSPATH' ) ) exit;

/* ====================================================================== */
/* CONFIG                                                                  */
/* ====================================================================== */

/** Settings guardadas (option) — o secret vai encriptado. */
function rmbz_s3_settings() {
    $s = (array) get_option( 'rmbz_s3_settings', [] );
    return wp_parse_args( $s, [
        'enabled'      => 0,
        'endpoint'     => '',   // ex: https://s3.eu-central-003.backblazeb2.com  |  https://<acct>.r2.cloudflarestorage.com
        'region'       => 'us-east-1',
        'bucket'       => '',
        'prefix'       => '1mz', // pasta base dentro do bucket
        'public_url'   => '',   // base pública (CDN/domínio) ex: https://cdn.1musicmoz.com — vazio = endpoint/bucket
        'key_id'       => '',
        'secret'       => '',   // encriptado (enc:/b64:)
        'acl_public'   => 0,    // enviar x-amz-acl: public-read (AWS/B2; R2 não usa)
        'offload_all'  => 1,    // 1 = todos os uploads dos fluxos 1MZ; 0 = só writes diretos (contratos/fotos)
        'delete_local' => 0,    // apagar a cópia local depois de subir
    ] );
}

/** Config resolvida (constantes do wp-config têm prioridade). */
function rmbz_s3_cfg() {
    $s = rmbz_s3_settings();
    $key    = defined( 'RMBZ_S3_KEY_ID' ) ? (string) RMBZ_S3_KEY_ID : (string) $s['key_id'];
    $secret = defined( 'RMBZ_S3_SECRET' ) ? (string) RMBZ_S3_SECRET
        : ( function_exists( 'rmbz_ai_decrypt' ) ? rmbz_ai_decrypt( (string) $s['secret'] ) : (string) $s['secret'] );
    return [
        'enabled'      => ! empty( $s['enabled'] ),
        'endpoint'     => untrailingslashit( (string) $s['endpoint'] ),
        'region'       => (string) ( $s['region'] ?: 'us-east-1' ),
        'bucket'       => (string) $s['bucket'],
        'prefix'       => trim( (string) $s['prefix'], '/' ),
        'public_url'   => untrailingslashit( (string) $s['public_url'] ),
        'key'          => trim( $key ),
        'secret'       => trim( $secret ),
        'acl_public'   => ! empty( $s['acl_public'] ),
        'offload_all'  => ! empty( $s['offload_all'] ),
        'delete_local' => ! empty( $s['delete_local'] ),
    ];
}

/** Está tudo configurado e ligado? */
function rmbz_s3_enabled() {
    $c = rmbz_s3_cfg();
    return $c['enabled'] && $c['endpoint'] !== '' && $c['bucket'] !== '' && $c['key'] !== '' && $c['secret'] !== '';
}

/* ====================================================================== */
/* CLIENTE SigV4 (path-style: endpoint/bucket/key)                         */
/* ====================================================================== */

/**
 * Pedido assinado (AWS Signature V4) ao S3.
 * @return array|WP_Error resposta do wp_remote_request.
 */
function rmbz_s3_request( $method, $key, $body = '', $extra_headers = [] ) {
    $c = rmbz_s3_cfg();
    $key = ltrim( (string) $key, '/' );

    $host = wp_parse_url( $c['endpoint'], PHP_URL_HOST );
    if ( ! $host ) return new WP_Error( 's3_bad_endpoint', 'Endpoint S3 inválido.' );

    // Path-style — funciona em todos os fornecedores compatíveis.
    $uri = '/' . $c['bucket'] . '/' . str_replace( '%2F', '/', rawurlencode( $key ) );
    $url = $c['endpoint'] . '/' . $c['bucket'] . '/' . str_replace( '%2F', '/', rawurlencode( $key ) );

    $amz_date   = gmdate( 'Ymd\THis\Z' );
    $date_stamp = gmdate( 'Ymd' );
    $payload_hash = hash( 'sha256', (string) $body );

    $headers = array_merge( [
        'host'                 => $host,
        'x-amz-content-sha256' => $payload_hash,
        'x-amz-date'           => $amz_date,
    ], array_change_key_case( (array) $extra_headers, CASE_LOWER ) );
    ksort( $headers );

    $canonical_headers = '';
    $signed_names = [];
    foreach ( $headers as $k => $v ) {
        $canonical_headers .= $k . ':' . trim( (string) $v ) . "\n";
        $signed_names[] = $k;
    }
    $signed_headers = implode( ';', $signed_names );

    $canonical_request = strtoupper( $method ) . "\n" . $uri . "\n" . '' /* query */ . "\n"
        . $canonical_headers . "\n" . $signed_headers . "\n" . $payload_hash;

    $scope = $date_stamp . '/' . $c['region'] . '/s3/aws4_request';
    $string_to_sign = "AWS4-HMAC-SHA256\n" . $amz_date . "\n" . $scope . "\n" . hash( 'sha256', $canonical_request );

    $k_date    = hash_hmac( 'sha256', $date_stamp, 'AWS4' . $c['secret'], true );
    $k_region  = hash_hmac( 'sha256', $c['region'], $k_date, true );
    $k_service = hash_hmac( 'sha256', 's3', $k_region, true );
    $k_signing = hash_hmac( 'sha256', 'aws4_request', $k_service, true );
    $signature = hash_hmac( 'sha256', $string_to_sign, $k_signing );

    $auth = 'AWS4-HMAC-SHA256 Credential=' . $c['key'] . '/' . $scope
          . ', SignedHeaders=' . $signed_headers . ', Signature=' . $signature;

    // Headers do pedido (sem o host — o WP põe-no).
    $req_headers = [ 'Authorization' => $auth ];
    foreach ( $headers as $k => $v ) {
        if ( $k === 'host' ) continue;
        $req_headers[ $k ] = $v;
    }

    return wp_remote_request( $url, [
        'method'  => strtoupper( $method ),
        'headers' => $req_headers,
        'body'    => $body === '' ? null : $body,
        'timeout' => 30,
    ] );
}

/**
 * Envia bytes para o bucket. @return array [ 'success', 'url', 'message' ]
 */
function rmbz_s3_put( $key, $bytes, $mime = 'application/octet-stream' ) {
    if ( ! rmbz_s3_enabled() ) return [ 'success' => false, 'url' => '', 'message' => 'S3 desligado.' ];
    $c = rmbz_s3_cfg();
    $headers = [ 'content-type' => $mime ];
    if ( $c['acl_public'] ) $headers['x-amz-acl'] = 'public-read';

    $res = rmbz_s3_request( 'PUT', $key, (string) $bytes, $headers );
    if ( is_wp_error( $res ) ) {
        return [ 'success' => false, 'url' => '', 'message' => $res->get_error_message() ];
    }
    $code = (int) wp_remote_retrieve_response_code( $res );
    if ( $code < 200 || $code >= 300 ) {
        $body = substr( (string) wp_remote_retrieve_body( $res ), 0, 300 );
        return [ 'success' => false, 'url' => '', 'message' => 'HTTP ' . $code . ' — ' . $body ];
    }
    return [ 'success' => true, 'url' => rmbz_s3_public_url( $key ), 'message' => 'OK' ];
}

/** Apaga um objeto do bucket. */
function rmbz_s3_delete( $key ) {
    if ( ! rmbz_s3_enabled() ) return false;
    $res = rmbz_s3_request( 'DELETE', $key );
    if ( is_wp_error( $res ) ) return false;
    $code = (int) wp_remote_retrieve_response_code( $res );
    return $code >= 200 && $code < 300;
}

/** URL pública de um objeto (CDN/domínio configurado, senão endpoint/bucket). */
function rmbz_s3_public_url( $key ) {
    $c = rmbz_s3_cfg();
    $key = ltrim( (string) $key, '/' );
    $base = $c['public_url'] !== '' ? $c['public_url'] : $c['endpoint'] . '/' . $c['bucket'];
    return $base . '/' . str_replace( '%2F', '/', rawurlencode( $key ) );
}

/** Se a URL for do nosso S3, devolve a key do objeto; senão ''. */
function rmbz_s3_key_from_url( $url ) {
    $c = rmbz_s3_cfg();
    $url = (string) $url;
    $bases = [];
    if ( $c['public_url'] !== '' ) $bases[] = $c['public_url'] . '/';
    if ( $c['endpoint'] !== '' && $c['bucket'] !== '' ) $bases[] = $c['endpoint'] . '/' . $c['bucket'] . '/';
    foreach ( $bases as $base ) {
        if ( strpos( $url, $base ) === 0 ) {
            return rawurldecode( substr( $url, strlen( $base ) ) );
        }
    }
    return '';
}

/** Key com prefixo + ano/mês (organização tipo uploads do WP). */
function rmbz_s3_build_key( $filename ) {
    $c = rmbz_s3_cfg();
    $parts = [];
    if ( $c['prefix'] !== '' ) $parts[] = $c['prefix'];
    $parts[] = wp_date( 'Y' );
    $parts[] = wp_date( 'm' );
    $parts[] = sanitize_file_name( (string) $filename );
    return implode( '/', $parts );
}

/* ====================================================================== */
/* INTEGRAÇÃO                                                              */
/* ====================================================================== */

/**
 * Sobe um ficheiro LOCAL já gravado para o S3. Devolve a URL S3 (e apaga a
 * cópia local se configurado), ou '' se desligado/falhou (fica o local).
 * Usado pelos writes diretos: contratos PDF, fotos de filmagem, etc.
 */
function rmbz_s3_maybe_offload( $local_path, $mime = '' ) {
    if ( ! rmbz_s3_enabled() ) return '';
    if ( ! is_file( $local_path ) ) return '';
    $bytes = file_get_contents( $local_path );
    if ( $bytes === false ) return '';
    if ( $mime === '' && function_exists( 'wp_check_filetype' ) ) {
        $ft = wp_check_filetype( basename( $local_path ) );
        $mime = $ft['type'] ?: 'application/octet-stream';
    }
    $res = rmbz_s3_put( rmbz_s3_build_key( basename( $local_path ) ), $bytes, $mime );
    if ( empty( $res['success'] ) ) {
        error_log( '[rmbz_s3] Falha no offload de ' . basename( $local_path ) . ': ' . $res['message'] );
        return '';
    }
    if ( rmbz_s3_cfg()['delete_local'] ) {
        @unlink( $local_path );
    }
    return (string) $res['url'];
}

/**
 * O upload pertence a um fluxo do 1MZ? (admin-post/ajax com action a começar
 * por "rmbz_" ou "onemz_", ou pedido REST da API 1mz). Evita mexer nos
 * uploads da biblioteca de média do WordPress.
 */
function rmbz_s3_is_1mz_context() {
    $action = isset( $_REQUEST['action'] ) ? (string) $_REQUEST['action'] : '';
    if ( $action !== '' && ( strpos( $action, 'rmbz_' ) === 0 || strpos( $action, 'onemz_' ) === 0 ) ) return true;
    if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
        $route = isset( $GLOBALS['wp']->query_vars['rest_route'] ) ? (string) $GLOBALS['wp']->query_vars['rest_route'] : (string) ( $_SERVER['REQUEST_URI'] ?? '' );
        if ( strpos( $route, '/1mz/' ) !== false || strpos( $route, '1mz/v1' ) !== false ) return true;
    }
    return false;
}

/**
 * Filtro wp_handle_upload — apanha TODOS os uploads dos fluxos 1MZ
 * (comprovativos, KYC, demos, capas…) e envia para o S3, reescrevendo a URL.
 */
add_filter( 'wp_handle_upload', function ( $upload, $context = 'upload' ) {
    if ( ! is_array( $upload ) || ! empty( $upload['error'] ) ) return $upload;
    if ( ! rmbz_s3_enabled() ) return $upload;
    $c = rmbz_s3_cfg();
    if ( ! $c['offload_all'] ) return $upload;
    if ( ! rmbz_s3_is_1mz_context() ) return $upload; // média do WP fica local

    $s3_url = rmbz_s3_maybe_offload( (string) ( $upload['file'] ?? '' ), (string) ( $upload['type'] ?? '' ) );
    if ( $s3_url !== '' ) {
        $upload['url'] = $s3_url;
    }
    return $upload;
}, 20, 2 );

/**
 * Apagar no S3 quando o plugin destrói um ficheiro por URL. Corre no hook
 * próprio para não depender da ordem de load (admin-handlers pode não estar
 * carregado quando este ficheiro é lido).
 */
add_action( 'rmbz_s3_delete_by_url', function ( $file_url ) {
    $key = rmbz_s3_key_from_url( (string) $file_url );
    if ( $key !== '' ) rmbz_s3_delete( $key );
} );

/* ====================================================================== */
/* SETTINGS UI (aba "☁️ S3" nas Configurações 1MZ)                          */
/* ====================================================================== */

function rmbz_s3_masked_secret() {
    $c = rmbz_s3_cfg();
    if ( $c['secret'] === '' ) return '';
    return '••••••••••••' . substr( $c['secret'], -4 );
}

function rmbz_s3_render_settings_body() {
    if ( ! current_user_can( 'manage_options' ) ) return;
    $s = rmbz_s3_settings();
    $c = rmbz_s3_cfg();
    $const_creds = defined( 'RMBZ_S3_KEY_ID' ) || defined( 'RMBZ_S3_SECRET' );
    $msg = isset( $_GET['s3msg'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['s3msg'] ) ) : '';
    ?>
    <?php if ( $msg !== '' ) : ?>
        <div class="notice <?php echo strpos( $msg, '✅' ) === 0 ? 'notice-success' : 'notice-warning'; ?> is-dismissible"><p><?php echo esc_html( $msg ); ?></p></div>
    <?php endif; ?>

    <div class="card" style="max-width:720px; padding:20px;">
        <h2 style="margin-top:0;">☁️ Armazenamento S3</h2>
        <p style="color:#5f6368; font-size:13px;">
            Envia os ficheiros do 1MZ (comprovativos, fotos KYC, demos, capas, fotos de filmagem e contratos PDF)
            para um bucket compatível com S3 — AWS S3, Cloudflare R2, Backblaze B2, DigitalOcean Spaces, Wasabi…
            As URLs guardadas passam a apontar para o bucket (ou para o teu CDN/domínio público).
        </p>

        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
            <input type="hidden" name="action" value="rmbz_s3_save_settings">
            <?php wp_nonce_field( 'rmbz_s3_save_settings' ); ?>

            <table class="form-table">
                <tr>
                    <th>Ativar S3</th>
                    <td><label><input type="checkbox" name="s3_enabled" value="1" <?php checked( ! empty( $s['enabled'] ) ); ?>> Enviar os ficheiros do 1MZ para o bucket</label></td>
                </tr>
                <tr>
                    <th>Endpoint</th>
                    <td>
                        <input type="url" name="s3_endpoint" value="<?php echo esc_attr( (string) $s['endpoint'] ); ?>" class="regular-text" placeholder="https://s3.eu-central-003.backblazeb2.com">
                        <p class="description">AWS: <code>https://s3.REGIAO.amazonaws.com</code> · R2: <code>https://ACCOUNT.r2.cloudflarestorage.com</code> · B2: <code>https://s3.REGIAO.backblazeb2.com</code></p>
                    </td>
                </tr>
                <tr>
                    <th>Região</th>
                    <td><input type="text" name="s3_region" value="<?php echo esc_attr( (string) $s['region'] ); ?>" class="regular-text" placeholder="us-east-1"><p class="description">R2 usa <code>auto</code>; B2 usa a região do endpoint (ex.: <code>eu-central-003</code>).</p></td>
                </tr>
                <tr>
                    <th>Bucket</th>
                    <td><input type="text" name="s3_bucket" value="<?php echo esc_attr( (string) $s['bucket'] ); ?>" class="regular-text" placeholder="1mz-ficheiros"></td>
                </tr>
                <tr>
                    <th>Pasta base (prefixo)</th>
                    <td><input type="text" name="s3_prefix" value="<?php echo esc_attr( (string) $s['prefix'] ); ?>" class="regular-text" placeholder="1mz"><p class="description">Os objetos ficam em <code>prefixo/ANO/MES/ficheiro</code>.</p></td>
                </tr>
                <tr>
                    <th>URL pública (CDN)</th>
                    <td>
                        <input type="url" name="s3_public_url" value="<?php echo esc_attr( (string) $s['public_url'] ); ?>" class="regular-text" placeholder="https://cdn.1musicmoz.com">
                        <p class="description">Opcional — domínio público/CDN à frente do bucket. Vazio = <code>endpoint/bucket</code>. ⚠️ O bucket (ou o CDN) tem de servir os ficheiros publicamente para o site/apps os mostrarem.</p>
                    </td>
                </tr>
                <tr>
                    <th>Access Key ID</th>
                    <td>
                        <?php if ( $const_creds ) : ?>
                            <em>Definido no wp-config.php (<code>RMBZ_S3_KEY_ID</code>) — tem prioridade.</em>
                        <?php else : ?>
                            <input type="text" name="s3_key_id" value="<?php echo esc_attr( (string) $s['key_id'] ); ?>" class="regular-text" autocomplete="off">
                        <?php endif; ?>
                    </td>
                </tr>
                <tr>
                    <th>Secret Access Key</th>
                    <td>
                        <?php if ( $const_creds ) : ?>
                            <em>Definido no wp-config.php (<code>RMBZ_S3_SECRET</code>) — tem prioridade.</em>
                        <?php else : ?>
                            <input type="password" name="s3_secret" value="" class="regular-text" autocomplete="new-password"
                                   placeholder="<?php echo esc_attr( rmbz_s3_masked_secret() ?: 'Cola aqui a secret key' ); ?>">
                            <p class="description">Guardada <strong>encriptada</strong>. Deixa em branco para manter a atual.</p>
                        <?php endif; ?>
                    </td>
                </tr>
                <tr>
                    <th>Opções</th>
                    <td>
                        <label style="display:block; margin-bottom:6px;"><input type="checkbox" name="s3_acl_public" value="1" <?php checked( ! empty( $s['acl_public'] ) ); ?>> Enviar <code>x-amz-acl: public-read</code> (AWS/B2 com ACLs; o R2 ignora/rejeita — desliga lá)</label>
                        <label style="display:block; margin-bottom:6px;"><input type="checkbox" name="s3_offload_all" value="1" <?php checked( ! empty( $s['offload_all'] ) ); ?>> Enviar TODOS os uploads dos fluxos 1MZ (comprovativos, KYC, demos, capas…) — desligado envia só contratos e fotos de filmagem</label>
                        <label style="display:block;"><input type="checkbox" name="s3_delete_local" value="1" <?php checked( ! empty( $s['delete_local'] ) ); ?>> Apagar a cópia local depois de subir (poupa disco; garante primeiro que o bucket serve os ficheiros)</label>
                    </td>
                </tr>
            </table>
            <p>
                <button type="submit" class="button button-primary">💾 Guardar configuração</button>
            </p>
        </form>

        <hr>
        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-top:12px;">
            <input type="hidden" name="action" value="rmbz_s3_test">
            <?php wp_nonce_field( 'rmbz_s3_test' ); ?>
            <button type="submit" class="button" <?php disabled( ! rmbz_s3_enabled() && ! ( $c['endpoint'] && $c['bucket'] && $c['key'] && $c['secret'] ) ); ?>>🧪 Testar ligação (envia e apaga um ficheiro de teste)</button>
            <span style="font-size:12px; color:#5f6368; margin-left:8px;">Faz PUT + GET público + DELETE de <code>teste-1mz.txt</code> no bucket.</span>
        </form>
    </div>
    <?php
}

// Guardar settings.
add_action( 'admin_post_rmbz_s3_save_settings', function () {
    if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Sem permissão.' );
    check_admin_referer( 'rmbz_s3_save_settings' );

    $s = rmbz_s3_settings();
    $s['enabled']      = ! empty( $_POST['s3_enabled'] ) ? 1 : 0;
    $s['endpoint']     = esc_url_raw( wp_unslash( (string) ( $_POST['s3_endpoint'] ?? '' ) ) );
    $s['region']       = sanitize_text_field( wp_unslash( (string) ( $_POST['s3_region'] ?? 'us-east-1' ) ) );
    $s['bucket']       = sanitize_text_field( wp_unslash( (string) ( $_POST['s3_bucket'] ?? '' ) ) );
    $s['prefix']       = sanitize_text_field( wp_unslash( (string) ( $_POST['s3_prefix'] ?? '1mz' ) ) );
    $s['public_url']   = esc_url_raw( wp_unslash( (string) ( $_POST['s3_public_url'] ?? '' ) ) );
    $s['acl_public']   = ! empty( $_POST['s3_acl_public'] ) ? 1 : 0;
    $s['offload_all']  = ! empty( $_POST['s3_offload_all'] ) ? 1 : 0;
    $s['delete_local'] = ! empty( $_POST['s3_delete_local'] ) ? 1 : 0;
    if ( isset( $_POST['s3_key_id'] ) ) {
        $s['key_id'] = sanitize_text_field( wp_unslash( (string) $_POST['s3_key_id'] ) );
    }
    // Secret: só substitui quando vem valor novo (vazio/mascarado = manter).
    if ( isset( $_POST['s3_secret'] ) ) {
        $new = trim( (string) wp_unslash( $_POST['s3_secret'] ) );
        if ( $new !== '' && strpos( $new, '•' ) === false ) {
            $s['secret'] = function_exists( 'rmbz_ai_encrypt' ) ? rmbz_ai_encrypt( $new ) : $new;
        }
    }
    update_option( 'rmbz_s3_settings', $s, false );
    wp_redirect( add_query_arg( 's3msg', rawurlencode( '✅ Configuração S3 guardada.' ),
        admin_url( 'admin.php?page=rmbz-settings&tab=s3' ) ) );
    exit;
} );

// Testar ligação: PUT + GET público + DELETE.
add_action( 'admin_post_rmbz_s3_test', function () {
    if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Sem permissão.' );
    check_admin_referer( 'rmbz_s3_test' );
    $back = admin_url( 'admin.php?page=rmbz-settings&tab=s3' );

    $c = rmbz_s3_cfg();
    if ( $c['endpoint'] === '' || $c['bucket'] === '' || $c['key'] === '' || $c['secret'] === '' ) {
        wp_redirect( add_query_arg( 's3msg', rawurlencode( '⚠️ Preenche endpoint, bucket e credenciais primeiro.' ), $back ) ); exit;
    }

    $key = ( $c['prefix'] !== '' ? $c['prefix'] . '/' : '' ) . 'teste-1mz.txt';
    $put = rmbz_s3_put( $key, '1MZ For Artists — teste S3 em ' . gmdate( 'c' ), 'text/plain' );
    if ( empty( $put['success'] ) ) {
        wp_redirect( add_query_arg( 's3msg', rawurlencode( '⚠️ PUT falhou: ' . $put['message'] ), $back ) ); exit;
    }

    // GET público (sem assinatura) — verifica se o bucket/CDN serve os ficheiros.
    $pub = wp_remote_get( $put['url'], [ 'timeout' => 20 ] );
    $pub_code = is_wp_error( $pub ) ? 0 : (int) wp_remote_retrieve_response_code( $pub );

    $deleted = rmbz_s3_delete( $key );

    $msg = '✅ PUT OK (' . $put['url'] . ')';
    $msg .= $pub_code === 200
        ? ' · Leitura pública OK'
        : ' · ⚠️ Leitura pública devolveu HTTP ' . $pub_code . ' — o site/apps não vão conseguir mostrar os ficheiros (torna o bucket público, liga o ACL public-read ou usa um CDN/URL pública)';
    $msg .= $deleted ? ' · DELETE OK' : ' · ⚠️ DELETE falhou';
    wp_redirect( add_query_arg( 's3msg', rawurlencode( $msg ), $back ) );
    exit;
} );
