<?php

/**
 * Card Oracle Core Functions
 *
 * General core functions available on both the front-end and admin.
 *
 * @package CardOracle\Functions
 * @version 1.1.2
 */
// Exit if accessed directly.
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
define( 'DEFAULT_COUNTRY', 'US' );
/**
 * Create array of allowed html for wp_kses().
 *
 * @version 1.1.2
 * @return array
 */
function card_oracle_allowed_html() {
    // All allowed tags.
    return array(
        'DOCTYPE'    => array(),
        'a'          => array(
            'class' => array(),
            'href'  => array(),
            'rel'   => array(),
            'title' => array(),
        ),
        'abbr'       => array(
            'title' => array(),
        ),
        'audio'      => array(
            'autoplay' => array(),
            'controls' => array(),
            'loop'     => array(),
            'muted'    => array(),
            'preload'  => array(),
            'src'      => array(),
            'class'    => array(),
        ),
        'b'          => array(),
        'blockquote' => array(
            'cite' => array(),
        ),
        'body'       => array(),
        'br'         => array(),
        'center'     => array(),
        'cite'       => array(
            'title' => array(),
        ),
        'code'       => array(),
        'del'        => array(
            'datetime' => array(),
            'title'    => array(),
        ),
        'dd'         => array(),
        'div'        => array(
            'class' => array(),
            'title' => array(),
            'style' => array(),
        ),
        'dl'         => array(),
        'dt'         => array(),
        'em'         => array(),
        'h1'         => array(),
        'h2'         => array(),
        'h3'         => array(),
        'h4'         => array(),
        'h5'         => array(),
        'h6'         => array(),
        'head'       => array(),
        'html'       => array(
            'xmlns'   => array(),
            'xmlns:o' => array(),
            'xmlns:v' => array(),
        ),
        'i'          => array(),
        'img'        => array(
            'alt'     => array(),
            'class'   => array(),
            'height'  => array(),
            'loading' => array(),
            'sizes'   => array(),
            'src'     => array(),
            'srcset'  => array(),
            'width'   => array(),
        ),
        'input'      => array(
            'id'          => array(),
            'name'        => array(),
            'placeholder' => array(),
            'type'        => array(),
            'value'       => array(),
        ),
        'label'      => array(
            'for' => array(),
        ),
        'li'         => array(
            'class' => array(),
        ),
        'meta'       => array(
            'content'    => array(),
            'http-equiv' => array(),
            'name'       => array(),
        ),
        'ol'         => array(
            'class' => array(),
        ),
        'p'          => array(
            'class' => array(),
        ),
        'q'          => array(
            'cite'  => array(),
            'title' => array(),
        ),
        'span'       => array(
            'class' => array(),
            'title' => array(),
            'style' => array(),
        ),
        'strike'     => array(),
        'strong'     => array(),
        'style'      => array(),
        'table'      => array(
            'class' => array(),
        ),
        'tbody'      => array(),
        'td'         => array(
            'colspan' => array(),
            'rowspan' => array(),
            'style'   => array(),
            'valign'  => array(),
            'width'   => array(),
        ),
        'thead'      => array(),
        'title'      => array(),
        'tr'         => array(
            'height' => array(),
        ),
        'ul'         => array(
            'class' => array(),
        ),
    );
}

/**
 * Create the email.
 *
 * @since  0.22.0
 *
 * @param  string $title Text to display for the title.
 * @param  string $style Text to display for the title.
 * @param  string $body Text to display for the title.
 * @return string
 */
function card_oracle_create_email(  $title, $style, $body  ) {
    co_debug_log( 'card-oracle-core-functions.php:card_oracle_create_email: start: Title [' . $title . ']' );
    $email_text = sprintf(
        '<!DOCTYPE html><head><title>%1$s</title><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0 "><style>%2$s</style></head><body>%3$s</body></html>',
        $title,
        $style,
        $body
    );
    co_debug_log( 'card-oracle-core-functions.php:card_oracle_create_email: end' );
    return $email_text;
}

/**
 * Display the footer if any.
 *
 * @since  0.23.0
 *
 * @param  string $footer_text Text to display in the footer.
 * @return string
 */
function card_oracle_display_footer(  $footer_text  ) {
    $display_html = '';
    if ( !empty( $footer_text ) || 'yes' === get_option( 'card_oracle_powered_by' ) ) {
        $display_html .= '<div class="cotd-footer">';
        if ( !empty( $footer_text ) ) {
            $display_html .= '<div class="cotd-footer-text">' . wp_kses( $footer_text, card_oracle_allowed_html() ) . '</div>';
        }
        if ( 'yes' === get_option( 'card_oracle_powered_by' ) ) {
            // Use affiliate link if provided, otherwise default to ChilliChalli.com.
            $affiliate_link = get_option( 'card_oracle_affiliate_link' );
            $link_url = ( !empty( $affiliate_link ) ? esc_url( $affiliate_link ) : 'https://tarotcardoracle.com/turn-your-intuition-into-income-with-card-oracle/' );
            $allowed_powered_tags = array(
                'a'    => array(
                    'href'   => array(),
                    'title'  => array(),
                    'target' => array(),
                    'rel'    => array(),
                    'class'  => array(),
                ),
                'span' => array(
                    'class' => array(),
                ),
                'div'  => array(
                    'class' => array(),
                ),
            );
            $powered_badge = '<div class="cotd-powered-by">';
            $powered_badge .= '<a href="' . $link_url . '" target="_blank" rel="noopener" class="cotd-powered-by-link">';
            $powered_badge .= '<span class="cotd-powered-by-icon">&#x2728;</span> ';
            $powered_badge .= '<span class="cotd-powered-by-text">' . esc_html__( 'Powered by', 'card-oracle' ) . '</span> ';
            $powered_badge .= '<span class="cotd-powered-by-brand">Card Oracle</span>';
            $powered_badge .= '<span class="cotd-powered-by-cta">. ' . esc_html__( 'Try it for yourself now.', 'card-oracle' ) . '</span>';
            $powered_badge .= '</a>';
            $powered_badge .= '</div>';
            $display_html .= wp_kses( $powered_badge, $allowed_powered_tags );
        }
        $display_html .= '</div>';
    }
    return $display_html;
}

/**
 * Get From email address.
 *
 * @since 0.25.0
 * @return string From email address.
 */
function card_oracle_get_from_email_address() {
    co_debug_log( 'card-oracle-core-functions.php:card_oracle_get_from_email_address: start' );
    $from_email_name = ( get_option( 'card_oracle_from_email_name' ) ? get_option( 'card_oracle_from_email_name' ) : get_bloginfo( 'name' ) );
    $from_email = '<' . (( get_option( 'card_oracle_from_email' ) ? get_option( 'card_oracle_from_email' ) : get_bloginfo( 'admin_email' ) )) . '>';
    // Create the headers. Add From name and address if options are set.
    $from = 'From: ' . $from_email_name . ' ' . $from_email . "\r\n";
    co_debug_log( 'card-oracle-core-functions.php:card_oracle_get_from_email_address: from email address [' . $from . ']' );
    co_debug_log( 'card-oracle-core-functions.php:card_oracle_get_from_email_address: end' );
    return $from;
}

/**
 * Get PHP Arg Separator Output
 *
 * @since 0.25.0
 * @return string Arg separator output
 */
function card_oracle_get_php_arg_separator_output() {
    return ini_get( 'arg_separator.output' );
}

/**
 * Get array of environment information. Includes thing like software
 * versions, and various server settings.
 *
 * @version 1.0.3
 * @return array
 */
function card_oracle_get_environment_info() {
    global $wpdb, $co_logs;
    $md5_files = array();
    // Get Card Oracle Version and if Premium.
    $card_oracle_version = CARD_ORACLE_VERSION;
    // Load MD5 files from generated JSON file
    $md5_json_file = CARD_ORACLE_DIR . 'assets/data/md5-sums.json';
    $all_md5_files = array();
    if ( file_exists( $md5_json_file ) ) {
        $md5_json = file_get_contents( $md5_json_file );
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
        if ( false === $md5_json ) {
            co_debug_log( 'card-oracle-core-functions.php:card_oracle_get_environment_info: Unable to read MD5 sums file' );
            $md5_json = '';
        }
        $md5_data = json_decode( $md5_json, true );
        if ( is_array( $md5_data ) ) {
            foreach ( $md5_data as $file_data ) {
                $all_md5_files[] = array(
                    'name'     => $file_data['name'],
                    'filename' => CARD_ORACLE_DIR . $file_data['filename'],
                    'md5sum'   => $file_data['md5sum'],
                );
            }
        }
    } else {
        // Fallback to hardcoded values if JSON file doesn't exist
        $all_md5_files = array(array(
            'name'     => 'Card Oracle',
            'filename' => CARD_ORACLE_DIR . 'card-oracle.php',
            'md5sum'   => 'f148345032ffb740849e32752d62ef29',
        ), array(
            'name'     => 'Class Card Oracle',
            'filename' => CARD_ORACLE_DIR . 'includes/class-card-oracle.php',
            'md5sum'   => '43be047fe2713de2e6e428088f03b289',
        ));
    }
    foreach ( $all_md5_files as $md5_file ) {
        if ( file_exists( $md5_file['filename'] ) ) {
            array_push( $md5_files, $md5_file );
        }
    }
    // Figure out cURL version, if installed.
    $curl_version = '';
    if ( function_exists( 'curl_version' ) ) {
        $curl_version = curl_version();
        $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version'];
    } elseif ( extension_loaded( 'curl' ) ) {
        $curl_version = __( 'cURL installed but unable to retrieve version.', 'card-oracle' );
    }
    // WP memory limit.
    $wp_memory_limit = card_oracle_number_convert( WP_MEMORY_LIMIT );
    if ( function_exists( 'memory_get_usage' ) ) {
        $wp_memory_limit = max( $wp_memory_limit, card_oracle_number_convert( @ini_get( 'memory_limit' ) ) );
        // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    }
    $database_version = card_oracle_get_server_database_version();
    // Get domain name for email subject
    $parsed_url = parse_url( get_option( 'home' ) );
    $domain_name = ( isset( $parsed_url['host'] ) ? $parsed_url['host'] : 'unknown-domain' );
    // Additional WordPress constants and performance info
    $wp_constants = array(
        'WP_DEBUG'          => ( defined( 'WP_DEBUG' ) ? WP_DEBUG : false ),
        'WP_DEBUG_LOG'      => ( defined( 'WP_DEBUG_LOG' ) ? WP_DEBUG_LOG : false ),
        'WP_DEBUG_DISPLAY'  => ( defined( 'WP_DEBUG_DISPLAY' ) ? WP_DEBUG_DISPLAY : false ),
        'SCRIPT_DEBUG'      => ( defined( 'SCRIPT_DEBUG' ) ? SCRIPT_DEBUG : false ),
        'WP_CACHE'          => ( defined( 'WP_CACHE' ) ? WP_CACHE : false ),
        'AUTOSAVE_INTERVAL' => ( defined( 'AUTOSAVE_INTERVAL' ) ? AUTOSAVE_INTERVAL : 60 ),
        'WP_POST_REVISIONS' => ( defined( 'WP_POST_REVISIONS' ) ? WP_POST_REVISIONS : true ),
        'EMPTY_TRASH_DAYS'  => ( defined( 'EMPTY_TRASH_DAYS' ) ? EMPTY_TRASH_DAYS : 30 ),
    );
    // Performance information
    $memory_usage = array(
        'current_usage' => ( function_exists( 'memory_get_usage' ) ? size_format( memory_get_usage() ) : 'N/A' ),
        'peak_usage'    => ( function_exists( 'memory_get_peak_usage' ) ? size_format( memory_get_peak_usage() ) : 'N/A' ),
    );
    // OPcache status
    $opcache_status = array();
    if ( function_exists( 'opcache_get_status' ) ) {
        $opcache_info = opcache_get_status( false );
        $opcache_status = array(
            'enabled'             => ( $opcache_info ? 'Yes' : 'No' ),
            'cache_full'          => ( isset( $opcache_info['cache_full'] ) ? ( $opcache_info['cache_full'] ? 'Yes' : 'No' ) : 'N/A' ),
            'restart_pending'     => ( isset( $opcache_info['restart_pending'] ) ? ( $opcache_info['restart_pending'] ? 'Yes' : 'No' ) : 'N/A' ),
            'restart_in_progress' => ( isset( $opcache_info['restart_in_progress'] ) ? ( $opcache_info['restart_in_progress'] ? 'Yes' : 'No' ) : 'N/A' ),
        );
    } else {
        $opcache_status['enabled'] = 'N/A (OPcache not available)';
    }
    // Return all environment info. Described by JSON Schema.
    return array(
        'active_plugins'         => get_active_plugins(),
        'api_request_counts'     => $co_logs->get_log_count( 0, 'api_request' ),
        'card_oracle_options'    => get_card_oracle_options(),
        'card_oracle_version'    => $card_oracle_version,
        'curl_version'           => $curl_version,
        'database_prefix'        => $wpdb->prefix,
        'default_timezone'       => date_default_timezone_get(),
        'domain_name'            => $domain_name,
        'domdocument_enabled'    => class_exists( 'DOMDocument' ),
        'dropins_mu_plugins'     => get_dropins_mu_plugins(),
        'error_log_counts'       => $co_logs->get_log_count( 0, 'error' ),
        'external_object_cache'  => wp_using_ext_object_cache(),
        'hide_errors'            => !(defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY) || 0 === intval( ini_get( 'display_errors' ) ),
        'home_url'               => get_option( 'home' ),
        'inactive_plugins'       => get_inactive_plugins(),
        'language'               => get_locale(),
        'log_directory_writable' => (bool) @fopen( CARD_ORACLE_LOG_DIR . wp_hash( home_url( '/' ) ) . '-debug.log', 'a' ),
        'log_directory'          => CARD_ORACLE_LOG_DIR,
        'log_file'               => trailingslashit( CARD_ORACLE_LOG_DIR ) . wp_hash( home_url( '/' ) ) . '-debug.log',
        'max_upload_size'        => wp_max_upload_size(),
        'mbstring_enabled'       => extension_loaded( 'mbstring' ),
        'md5_files'              => $md5_files,
        'memory_usage'           => $memory_usage,
        'mysql_version_string'   => $database_version['string'],
        'mysql_version'          => $database_version['number'],
        'opcache_status'         => $opcache_status,
        'php_max_execution_time' => (int) ini_get( 'max_execution_time' ),
        'php_max_input_vars'     => (int) ini_get( 'max_input_vars' ),
        'php_post_max_size'      => card_oracle_number_convert( ini_get( 'post_max_size' ) ),
        'php_sendmail_path'      => ini_get( 'sendmail_path' ),
        'php_version'            => phpversion(),
        'post_type_counts'       => get_post_type_counts(),
        'rss_feed'               => card_oracle_check_feed(),
        'secure_connection'      => 'https' === substr( get_option( 'siteurl' ), 0, 5 ),
        'server_info'            => ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? card_oracle_sanitize_text( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '' ),
        'site_url'               => get_option( 'siteurl' ),
        'suhosin_installed'      => extension_loaded( 'suhosin' ),
        'wp_constants'           => $wp_constants,
        'wp_cron'                => !(defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON),
        'wp_debug_mode'          => defined( 'WP_DEBUG' ) && WP_DEBUG,
        'wp_memory_limit'        => $wp_memory_limit,
        'wp_multisite'           => is_multisite(),
        'wp_version'             => get_bloginfo( 'version' ),
    );
}

/**
 * Retrieves the country codes for payment providers.
 *
 * @since 0.25.0
 * @return array Country codes.
 */
function card_oracle_get_country_code() {
    global $co_logs;
    $country_array = array(
        DEFAULT_COUNTRY => 'United States',
    );
    $local_file = CARD_ORACLE_DIR . 'assets/data/country-code.json';
    $local_json = file_get_contents( $local_file );
    // @codingStandardsIgnoreLine
    if ( false === $local_json ) {
        // Log the failure.
        co_debug_log( 'card-oracle-core-functions.php:card_oracle_get_country_code: Cannot load file /admin/assets/data/country-code.json' );
        // translators: %s is a filename.
        $error = sprintf( __( 'Unable to access file [%s].', 'card-oracle' ), $local_file );
        $co_logs->add(
            'Local Demo Data',
            $error,
            null,
            'error'
        );
    } else {
        $json = json_decode( $local_json, true );
        if ( !is_array( $json ) ) {
            co_debug_log( 'card-oracle-core-functions.php:card_oracle_get_country_code: JSON decode failed for country codes file' );
            return $country_array;
        }
        foreach ( $json as $country ) {
            if ( true === $country['paypal'] ) {
                $country_array[$country['alpha-2']] = $country['name'];
            }
        }
    }
    return $country_array;
}

/**
 * Retrieves the MySQL server version. Based on $wpdb.
 *
 * @since 1.1.3
 * @return array Version information.
 */
function card_oracle_get_server_database_version() {
    global $wpdb;
    if ( empty( $wpdb->is_mysql ) ) {
        return array(
            'string' => '',
            'number' => '',
        );
    }
    // phpcs:disable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
    $server_info = ( $wpdb->use_mysqli ? mysqli_get_server_info( $wpdb->dbh ) : ($server_info = 'Unknown') );
    // phpcs:enable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
    return array(
        'string' => $server_info,
        'number' => preg_replace( '/([^\\d.]+).*/', '', $server_info ),
    );
}

/**
 * Checks the rss feed for daily readings exists
 *
 * @since  0.26.0
 * @return boolean
 */
function card_oracle_check_feed() {
    $name = 'tco';
    $registered = false;
    $rules = get_option( 'rewrite_rules' );
    if ( is_array( $rules ) ) {
        $feeds = array_keys( $rules, 'index.php?&feed=$matches[1]', true );
        foreach ( $feeds as $feed ) {
            if ( false !== strpos( $feed, $name ) ) {
                $registered = true;
            }
        }
    }
    return $registered;
}

/**
 * Display a delete button with Trash icon.
 *
 * @since  1.1.3
 *
 * @param  string $title Text to display for tool tip.
 * @param  string $name Name of the button element.
 * @param  string $value value of the button element.
 * @return string
 */
function card_oracle_delete_button(  $title, $name, $value  ) {
    return '<button title="' . esc_attr( $title ) . '" type="submit" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '"><span class="dashicons dashicons-trash"></span></button>';
}

/**
 * Display a Card Oracle help tip.
 *
 * @since  0.14.0
 *
 * @param  string $tip        Help tip text.
 * @param  bool   $allow_html Allow sanitized HTML if true or escape.
 * @return string
 */
function card_oracle_help_tip(  $tip, $allow_html = false  ) {
    $tip = ( $allow_html ? card_oracle_sanitize_tooltip( $tip ) : esc_attr( $tip ) );
    return '<div class="card-oracle-help-tip"><p>' . $tip . '</p></div>';
}

/**
 * Display a Card Oracle tool tip.
 *
 * @since  1.1.3
 *
 * @param  string $tip        Help tip text.
 * @param  bool   $allow_html Allow sanitized HTML if true or escape.
 * @return string
 */
function card_oracle_tool_tip(  $tip, $allow_html = false  ) {
    $tip = ( $allow_html ? card_oracle_sanitize_tooltip( $tip ) : esc_attr( $tip ) );
    return '<span title="' . $tip . '" class="dashicons dashicons-editor-help"></span>';
}

/**
 * Create a Card Oracle modal.
 *
 * @since  1.0.5
 *
 * @param  string $key Unique key to add to the id.
 * @param  string $content Content to display in the modal.
 * @param  string $link URL link.
 * @param  string $link_class CSS class to add to the <a> element.
 * @param  string $title Optional title to display in the modal header.
 *
 * @return string $output The html to create the modal.
 */
function card_oracle_modal(
    $key,
    $content,
    $link,
    $link_class = null,
    $title = ''
) {
    $output = '<div id="card-oracle-open-modal-' . esc_attr( $key ) . '" class="card-oracle-modal-dialog">';
    $output .= '<div class="card-oracle-modal-wide">';
    $output .= '<div class="card-oracle-modal-header">';
    if ( $title ) {
        $output .= '<h3 class="card-oracle-modal-title">' . esc_html( $title ) . '</h3>';
    }
    $output .= '<a href="#close" title="' . esc_attr__( 'Close', 'card-oracle' ) . '" aria-label="' . esc_attr__( 'Close modal', 'card-oracle' ) . '" class="card-oracle-modal-close">&times;</a>';
    $output .= '</div>';
    $output .= '<div id="card-oracle-modal-box">' . $content . '</div>';
    $output .= '</div></div>';
    $output .= '<a class="' . esc_attr( $link_class ) . '" href="#card-oracle-open-modal-' . esc_attr( $key ) . '">' . esc_html( $link ) . '</a>';
    return $output;
}

/**
 * Convert string values into ints. IE 40MB is 40 * 1024 * 1024.
 *
 * @since  1.1.6
 *
 * @param  string $value A size value 1MB/2GB/etc.
 * @return int
 */
function card_oracle_number_convert(  $value  ) {
    if ( is_numeric( $value ) ) {
        return $value + 0;
    }
    if ( !is_string( $value ) ) {
        return 0;
    }
    $trimmed = trim( $value );
    if ( '' === $trimmed ) {
        return 0;
    }
    if ( !preg_match( '/^\\s*([+-]?[0-9]+(?:\\.[0-9]+)?)\\s*([PTGMK])?/i', $trimmed, $matches ) ) {
        return 0;
    }
    $number = (float) $matches[1];
    $multiplier = ( isset( $matches[2] ) ? strtoupper( $matches[2] ) : '' );
    switch ( $multiplier ) {
        case 'P':
            $number *= 1024;
        // No break.
        case 'T':
            $number *= 1024;
        // No break.
        case 'G':
            $number *= 1024;
        // No break.
        case 'M':
            $number *= 1024;
        // No break.
        case 'K':
            $number *= 1024;
            break;
    }
    if ( !is_finite( $number ) ) {
        return PHP_INT_MAX;
    }
    return max( 0, (int) round( $number ) );
}

/**
 * Clean variables using sanitize_text_field. Arrays are cleaned recursively.
 * Non-scalar values are ignored.
 *
 * @since  1.1.3
 *
 * @param string|array $var Data to sanitize.
 * @return string|array
 */
function card_oracle_sanitize_text(  $var  ) {
    if ( is_array( $var ) ) {
        return array_map( 'card_oracle_sanitize_text', $var );
    }
    return ( is_scalar( $var ) ? sanitize_text_field( $var ) : $var );
}

/**
 * Sanitize a string destined to be a tooltip.
 *
 * @since  0.14.0 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr()
 * @param  string $var Data to sanitize.
 * @return string
 */
function card_oracle_sanitize_tooltip(  $var  ) {
    return htmlspecialchars( wp_kses( html_entity_decode( $var ), array(
        'br'     => array(),
        'em'     => array(),
        'strong' => array(),
        'small'  => array(),
        'span'   => array(),
        'ul'     => array(),
        'li'     => array(),
        'ol'     => array(),
        'p'      => array(),
    ) ) );
}

/**
 * Get array of layouts to display the deck to the users.
 *
 * @since  0.27.0
 *
 * @return array
 */
function get_deck_layouts() {
    $deck_layouts = array(
        'standard'    => 'Standard',
        'overlapping' => 'Overlapping',
    );
    return $deck_layouts;
}

/**
 * Provide the available display options for the question capture form.
 *
 * @since 1.2.0
 *
 * @return array Associative array with layout key => translated label.
 */
function card_oracle_get_question_layout_options() {
    $translate = function ( $text ) {
        return ( function_exists( 'esc_html__' ) ? esc_html__( $text, 'card-oracle' ) : $text );
    };
    return array(
        'card-oracle-form-left'          => $translate( 'Left' ),
        'card-oracle-form-center'        => $translate( 'Center' ),
        'card-oracle-form-right'         => $translate( 'Right' ),
        'card-oracle-form-inline-left'   => $translate( 'Inline Left' ),
        'card-oracle-form-inline-center' => $translate( 'Inline Center' ),
        'card-oracle-form-inline-right'  => $translate( 'Inline Right' ),
    );
}

/**
 * Sanitize a key with a lightweight fallback when WordPress helpers are unavailable.
 *
 * @since 1.2.0
 *
 * @param string $key Raw key to sanitize.
 * @return string Sanitized key.
 */
function card_oracle_sanitize_key(  $key  ) {
    $key = strtolower( (string) $key );
    return preg_replace( '/[^a-z0-9_\\-]/', '', $key );
}

/**
 * Get array of layouts to display the deck to the users.
 *
 * @since  1.0.4
 *
 * @return array
 */
function get_presentation_layouts() {
    $presentation_layouts = array(array(
        'uid'       => 'standard',
        'class'     => '',
        'label'     => __( 'Standard', 'card-oracle' ),
        'positions' => 0,
        'file'      => CARD_ORACLE_DIR . 'public/layouts/standard.php',
        'image'     => CARD_ORACLE_URL . 'assets/layouts/standard_layout.png',
    ), array(
        'uid'       => STANDARD_MOBILE,
        'class'     => '',
        'label'     => __( 'Mobile', 'card-oracle' ),
        'positions' => 0,
        'file'      => CARD_ORACLE_DIR . 'public/layouts/standard.php',
        'image'     => CARD_ORACLE_URL . 'assets/layouts/mobile_layout.png',
    ), array(
        'uid'       => 'three_layout_1',
        'class'     => 'three',
        'layout'    => '1',
        'file'      => CARD_ORACLE_EXTENDED_LAYOUT,
        'label'     => __( 'Three Card Layout 1', 'card-oracle' ),
        'image'     => CARD_ORACLE_URL . 'assets/layouts/three_card_layout-1.png',
        'positions' => 3,
    ));
    return $presentation_layouts;
}

/**
 * Get array of counts of post types. Readings, Positions, Cards, Descriptions.
 *
 * @return array
 */
function get_card_oracle_options() {
    global $wpdb;
    $all_options = wp_load_alloptions();
    $card_oracle_options = array();
    foreach ( $all_options as $name => $value ) {
        if ( strpos( $name, CARD_ORACLE_OPTION_PREFIX ) === 0 ) {
            $card_oracle_options[$name] = $value;
        }
    }
    ksort( $card_oracle_options );
    return $card_oracle_options;
}

/**
 * Get all the published cards for a specific reading
 *
 * @since  0.27.0
 * @param  string $reading_id This is the ID of the Reading.
 * @return array  All the Cards IDs in a Reading.
 */
/**
 * Get reading ID by name (slug or title)
 *
 * @since  1.1.9
 * @param  string $reading_name The reading name (slug or title).
 * @return int|null Reading ID if found, null otherwise.
 */
function get_reading_id_by_name(  $reading_name  ) {
    if ( !is_scalar( $reading_name ) ) {
        return null;
    }
    $reading_name = trim( (string) $reading_name );
    if ( '' === $reading_name ) {
        return null;
    }
    $slug = sanitize_title( $reading_name );
    $normalized_name = strtolower( $reading_name );
    $cache_group = 'card_oracle_reading_lookup';
    $cached_slug_key = '';
    $cached_title_key = '';
    $cache_enabled = function_exists( 'wp_cache_get' ) && function_exists( 'wp_cache_set' );
    if ( $cache_enabled ) {
        $prefix = card_oracle_get_reading_cache_prefix();
        if ( '' !== $slug ) {
            $cached_slug_key = $prefix . 'slug:' . md5( $slug );
            $cached_value = wp_cache_get( $cached_slug_key, $cache_group );
            if ( false !== $cached_value ) {
                return ( 0 === $cached_value ? null : (int) $cached_value );
            }
        }
        if ( '' !== $normalized_name ) {
            $cached_title_key = $prefix . 'title:' . md5( $normalized_name );
            $cached_value = wp_cache_get( $cached_title_key, $cache_group );
            if ( false !== $cached_value ) {
                return ( 0 === $cached_value ? null : (int) $cached_value );
            }
        }
    }
    if ( '' !== $slug ) {
        $reading = get_posts( array(
            'name'        => $slug,
            'post_type'   => 'co_readings',
            'post_status' => 'publish',
            'numberposts' => 1,
            'fields'      => 'ids',
        ) );
        if ( !empty( $reading ) ) {
            $reading_id = (int) $reading[0];
            card_oracle_cache_reading_lookup_result( $reading_id, $slug, $normalized_name );
            return $reading_id;
        }
    }
    // Secondary: Check if this is a numeric ID string.
    if ( is_numeric( $reading_name ) ) {
        $potential_id = absint( $reading_name );
        $post = get_post( $potential_id );
        if ( $post && 'co_readings' === $post->post_type && 'publish' === $post->post_status ) {
            card_oracle_cache_reading_lookup_result( $potential_id, $slug, $normalized_name );
            return $potential_id;
        }
    }
    // Tertiary: Try exact post_title match (legacy support).
    // Add a _doing_it_wrong() notice to encourage using slug or ID.
    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        // translators: %s is the reading name being looked up.
        _doing_it_wrong( __FUNCTION__, sprintf( 
            /* translators: %s is the reading name */
            __( 'Looking up reading by title "%s" instead of slug. Consider using the reading slug or ID for better performance and reliability.', 'card-oracle' ),
            esc_html( $reading_name )
         ), '1.2.0' );
    }
    $page = get_page_by_title( $reading_name, 'OBJECT', 'co_readings' );
    $reading_id = ( $page ? (int) $page->ID : null );
    card_oracle_cache_reading_lookup_result( $reading_id, $slug, $normalized_name );
    return $reading_id;
}

/**
 * Resolve reading ID from shortcode attributes
 *
 * @since  1.1.9
 * @param  array $atts Shortcode attributes.
 * @return int|null Reading ID if found, null otherwise.
 */
function resolve_reading_id_from_atts(  $atts  ) {
    // Check for ID first (backwards compatibility)
    if ( !empty( $atts['id'] ) ) {
        return absint( $atts['id'] );
    }
    // Check for name parameter
    if ( !empty( $atts['name'] ) ) {
        $reading_name = card_oracle_sanitize_text( $atts['name'] );
        return get_reading_id_by_name( $reading_name );
    }
    return null;
}

function get_cards_for_reading(  $reading_id  ) {
    $transient_id = card_oracle_reading_cards_transient_key( $reading_id );
    $cards = get_transient( $transient_id );
    // `false` means missing/expired; an empty array is a valid cached value
    // (e.g., a reading with no cards yet), so don't requery in that case.
    if ( false !== $cards ) {
        return $cards;
    }
    $cards = array();
    $meta_query = array(
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
        array(
            'key'   => CO_READING_ID,
            'value' => $reading_id,
        ),
    );
    if ( class_exists( 'WP_Query' ) ) {
        $query_args = array(
            'post_type'              => 'co_cards',
            'post_status'            => 'publish',
            'posts_per_page'         => -1,
            'orderby'                => 'ID',
            'order'                  => 'ASC',
            'no_found_rows'          => true,
            'update_post_meta_cache' => true,
            'update_post_term_cache' => false,
            'lazy_load_term_meta'    => false,
            'cache_results'          => true,
            'meta_query'             => $meta_query,
        );
        $query = new WP_Query($query_args);
        if ( !empty( $query->posts ) ) {
            foreach ( $query->posts as $post ) {
                $cards[] = $post->ID;
            }
        }
        if ( function_exists( 'update_post_thumbnail_cache' ) ) {
            update_post_thumbnail_cache( $query );
        }
        if ( function_exists( '_prime_post_caches' ) && !empty( $cards ) ) {
            _prime_post_caches( $cards, true, false );
        }
    } else {
        $args = array(
            'fields'                 => 'ids',
            'numberposts'            => -1,
            'order'                  => 'ASC',
            'orderby'                => 'ID',
            'post_type'              => 'co_cards',
            'post_status'            => 'publish',
            'cache_results'          => true,
            'update_post_meta_cache' => true,
            'update_post_term_cache' => false,
            'no_found_rows'          => true,
            'ignore_sticky_posts'    => true,
            'meta_query'             => $meta_query,
        );
        $cards = get_posts( $args );
        if ( function_exists( '_prime_post_caches' ) && !empty( $cards ) ) {
            _prime_post_caches( $cards, true, false );
        }
    }
    if ( function_exists( 'set_transient' ) ) {
        set_transient( $transient_id, $cards, HOUR_IN_SECONDS );
    }
    return $cards;
}

/**
 * Retrieve cache version prefix for reading lookup caching.
 *
 * @since 1.1.9
 * @return string Cache prefix including version.
 */
function card_oracle_get_reading_cache_prefix() {
    if ( !function_exists( 'wp_cache_get' ) || !function_exists( 'wp_cache_set' ) ) {
        return 'v1:';
    }
    $group = 'card_oracle_reading_lookup';
    $version = wp_cache_get( 'version', $group );
    if ( false === $version ) {
        $version = 1;
        wp_cache_set( 'version', $version, $group );
    }
    return 'v' . (int) $version . ':';
}

/**
 * Cache the reading lookup result for slug/title queries.
 *
 * @since 1.1.9
 * @param int|null $reading_id     Reading ID or null if not found.
 * @param string   $slug           Sanitized slug.
 * @param string   $normalized_name Lowercase trimmed reading name.
 * @return void
 */
function card_oracle_cache_reading_lookup_result(  $reading_id, $slug, $normalized_name  ) {
    if ( !function_exists( 'wp_cache_set' ) ) {
        return;
    }
    $group = 'card_oracle_reading_lookup';
    $prefix = card_oracle_get_reading_cache_prefix();
    $default_ttl = ( defined( 'HOUR_IN_SECONDS' ) ? HOUR_IN_SECONDS : 3600 );
    $ttl = ( function_exists( 'card_oracle_get_cache_ttl' ) ? card_oracle_get_cache_ttl() : $default_ttl );
    $value = ( null === $reading_id ? 0 : (int) $reading_id );
    if ( '' !== $slug ) {
        wp_cache_set(
            $prefix . 'slug:' . md5( $slug ),
            $value,
            $group,
            $ttl
        );
    }
    if ( '' !== $normalized_name ) {
        wp_cache_set(
            $prefix . 'title:' . md5( $normalized_name ),
            $value,
            $group,
            $ttl
        );
    }
}

/**
 * Invalidate cached reading lookups when readings change.
 *
 * @since 1.1.9
 * @param int $post_id Post ID whose cache should be invalidated.
 * @return void
 */
function card_oracle_invalidate_reading_lookup_cache(  $post_id = 0  ) {
    if ( $post_id && function_exists( 'get_post_type' ) && 'co_readings' !== get_post_type( $post_id ) ) {
        return;
    }
    if ( !function_exists( 'wp_cache_set' ) || !function_exists( 'wp_cache_get' ) ) {
        return;
    }
    $group = 'card_oracle_reading_lookup';
    $version = wp_cache_get( 'version', $group );
    if ( false === $version ) {
        $version = 1;
    }
    wp_cache_set( 'version', (int) $version + 1, $group );
}

add_action( 'clean_post_cache', 'card_oracle_invalidate_reading_lookup_cache' );
/**
 * Get all the published positions for a specific reading
 *
 * @since  1.0.3
 * @param string $reading_id This is the ID of the Reading.
 * @return array All the Position IDs in a Reading.
 */
function get_positions_for_reading(  $reading_id  ) {
    $transient_id = card_oracle_reading_positions_transient_key( $reading_id );
    $positions = ( function_exists( 'get_transient' ) ? get_transient( $transient_id ) : false );
    if ( false !== $positions ) {
        return $positions;
    }
    // The $positions is an array of all the positions in a reading, it consists of the position title and position ID.
    $args = array(
        'numberposts' => -1,
        'order'       => 'ASC',
        'orderby'     => 'card_order_clause',
        'post_type'   => 'co_positions',
        'post_status' => 'publish',
        'meta_query'  => array(
            'reading_clause'    => array(
                'key'   => CO_READING_ID,
                'value' => $reading_id,
            ),
            'card_order_clause' => array(
                'key'  => CO_CARD_ORDER,
                'type' => 'numeric',
            ),
        ),
    );
    $positions = get_posts( $args );
    if ( function_exists( 'set_transient' ) ) {
        set_transient( $transient_id, $positions, HOUR_IN_SECONDS );
    }
    return $positions;
}

/**
 * Per-reading cache key for the cards transient.
 *
 * @since 1.2.6
 * @param int $reading_id Reading post ID.
 * @return string Transient key.
 */
function card_oracle_reading_cards_transient_key(  $reading_id  ) {
    return 'co_cards_for_' . absint( $reading_id );
}

/**
 * Per-reading cache key for the positions transient.
 *
 * @since 1.2.6
 * @param int $reading_id Reading post ID.
 * @return string Transient key.
 */
function card_oracle_reading_positions_transient_key(  $reading_id  ) {
    return 'co_positions_for_' . absint( $reading_id );
}

/**
 * Invalidate the cards + positions transients for the affected reading.
 *
 * Wired to save_post and delete_post so any change to a co_readings,
 * co_cards, or co_positions post forces the next call to query the DB.
 *
 * @since 1.2.6
 * @param int $post_id Post being saved/deleted.
 * @return void
 */
function card_oracle_invalidate_reading_data_cache(  $post_id = 0  ) {
    if ( !function_exists( 'get_post_type' ) || !function_exists( 'delete_transient' ) ) {
        return;
    }
    if ( function_exists( 'wp_is_post_revision' ) && wp_is_post_revision( $post_id ) ) {
        return;
    }
    if ( function_exists( 'wp_is_post_autosave' ) && wp_is_post_autosave( $post_id ) ) {
        return;
    }
    $post_type = get_post_type( $post_id );
    if ( 'co_readings' === $post_type ) {
        delete_transient( card_oracle_reading_cards_transient_key( $post_id ) );
        delete_transient( card_oracle_reading_positions_transient_key( $post_id ) );
        return;
    }
    if ( 'co_cards' === $post_type || 'co_positions' === $post_type ) {
        $reading_id = ( function_exists( 'get_post_meta' ) ? (int) get_post_meta( $post_id, CO_READING_ID, true ) : 0 );
        if ( $reading_id > 0 ) {
            delete_transient( card_oracle_reading_cards_transient_key( $reading_id ) );
            delete_transient( card_oracle_reading_positions_transient_key( $reading_id ) );
        }
    }
}

if ( function_exists( 'add_action' ) ) {
    add_action( 'save_post', 'card_oracle_invalidate_reading_data_cache' );
    add_action( 'delete_post', 'card_oracle_invalidate_reading_data_cache' );
}
/**
 * Get array of counts of post types. Readings, Positions, Cards, Descriptions.
 *
 * @return array
 */
function get_post_type_counts() {
    global $wpdb;
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Static aggregate query, no user input.
    $post_type_counts = $wpdb->get_results( "SELECT post_type AS 'type', count(1) AS 'count' FROM {$wpdb->posts} GROUP BY post_type" );
    return ( is_array( $post_type_counts ) ? $post_type_counts : array() );
}

/**
 * Get a list of plugins active on the site.
 *
 * @return array
 */
function get_active_plugins() {
    require_once ABSPATH . 'wp-admin/includes/plugin.php';
    if ( !function_exists( 'get_plugin_data' ) ) {
        return array();
    }
    $active_plugins = (array) get_option( 'active_plugins', array() );
    if ( is_multisite() ) {
        $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
        $active_plugins = array_merge( $active_plugins, $network_activated_plugins );
    }
    $active_plugins_data = array();
    foreach ( $active_plugins as $plugin ) {
        $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
        $active_plugins_data[] = format_plugin_data( $plugin, $data );
    }
    return $active_plugins_data;
}

/**
 * Get a list of inplugins active on the site.
 *
 * @return array
 */
function get_inactive_plugins() {
    require_once ABSPATH . 'wp-admin/includes/plugin.php';
    if ( !function_exists( 'get_plugins' ) ) {
        return array();
    }
    $plugins = get_plugins();
    $active_plugins = (array) get_option( 'active_plugins', array() );
    if ( is_multisite() ) {
        $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
        $active_plugins = array_merge( $active_plugins, $network_activated_plugins );
    }
    $plugins_data = array();
    foreach ( $plugins as $plugin => $data ) {
        if ( in_array( $plugin, $active_plugins, true ) ) {
            continue;
        }
        $plugins_data[] = format_plugin_data( $plugin, $data );
    }
    return $plugins_data;
}

/**
 * Format plugin data, including data on updates, into a standard format.
 *
 * @since 3.6.0
 * @param string $plugin Plugin directory/file.
 * @param array  $data Plugin data from WP.
 * @return array Formatted data.
 */
function format_plugin_data(  $plugin, $data  ) {
    require_once ABSPATH . 'wp-admin/includes/update.php';
    if ( !function_exists( 'get_plugin_updates' ) ) {
        return array();
    }
    $version_latest = $data['Version'];
    return array(
        'plugin'            => $plugin,
        'name'              => $data['Name'],
        'version'           => $data['Version'],
        'version_latest'    => $version_latest,
        'url'               => $data['PluginURI'],
        'author_name'       => $data['AuthorName'],
        'author_url'        => esc_url_raw( $data['AuthorURI'] ),
        'network_activated' => $data['Network'],
    );
}

/**
 * Get a list of Dropins and MU plugins.
 *
 * @since 3.6.0
 * @return array
 */
function get_dropins_mu_plugins() {
    $dropins = get_dropins();
    $plugins = array(
        'dropins'    => array(),
        'mu_plugins' => array(),
    );
    foreach ( $dropins as $key => $dropin ) {
        $plugins['dropins'][] = array(
            'plugin' => $key,
            'name'   => $dropin['Name'],
        );
    }
    $mu_plugins = get_mu_plugins();
    foreach ( $mu_plugins as $plugin => $mu_plugin ) {
        $plugins['mu_plugins'][] = array(
            'plugin'      => $plugin,
            'name'        => $mu_plugin['Name'],
            'version'     => $mu_plugin['Version'],
            'url'         => $mu_plugin['PluginURI'],
            'author_name' => $mu_plugin['AuthorName'],
            'author_url'  => esc_url_raw( $mu_plugin['AuthorURI'] ),
        );
    }
    return $plugins;
}

/**
 * Retrieve posts from Card Oracle custom post types with flexible querying.
 *
 * Provides a unified interface for querying Card Oracle custom post types
 * (readings, positions, cards, descriptions) with support for ordering,
 * meta queries, and pagination. Used throughout the plugin for consistent
 * data retrieval.
 *
 * @since 1.0.4
 *
 * @param string $card_oracle_cpt The name of the custom post type to query.
 * @param string $order_by        Optional. Field to order results by. Default 'ID'.
 * @param string $order           Optional. Order direction 'ASC' or 'DESC'. Default 'ASC'.
 * @param array  $meta            Optional. Meta query parameters for filtering. Default empty array.
 * @param int    $numberposts     Optional. Number of posts to retrieve. -1 for all. Default -1.
 * @param int    $offset          Optional. Number of posts to skip. Default 0.
 *
 * @return WP_Post[] Array of post objects matching the query parameters.
 */
function get_card_oracle_posts_by_cpt(
    $card_oracle_cpt,
    $order_by = 'ID',
    $order = 'ASC',
    $meta = array(),
    $numberposts = -1,
    $offset = 0
) {
    // WordPress meta_query expects an array of arrays.
    // If $meta is a single clause (has 'key'), wrap it in an array.
    $meta_query = array();
    if ( !empty( $meta ) ) {
        if ( isset( $meta['key'] ) ) {
            // Single meta clause - wrap it in an array.
            $meta_query = array($meta);
        } else {
            // Already an array of clauses or complex query.
            $meta_query = $meta;
        }
    }
    $args = array(
        'numberposts' => $numberposts,
        'offset'      => $offset,
        'order'       => $order,
        'orderby'     => $order_by,
        'post_status' => 'publish',
        'post_type'   => $card_oracle_cpt,
        'meta_query'  => $meta_query,
    );
    return get_posts( $args );
}

/**
 * Should the reading be processed by the payment provider.
 *
 * @return boolean
 */
function card_oracle_process_order() {
    global $payment_options;
    $process_order = false;
    // if the price is set (not zero) verify the fields are set for the selected payment provider.
    if ( isset( $payment_options['price'] ) && 0 < $payment_options['price'] ) {
        switch ( $payment_options['provider'] ) {
            case 'paypal':
                // If the paypal email field is blank return false otherwise return true.
                $process_order = isset( $payment_options['paypal_email'] );
                break;
            case 'stripe':
                // If either of the secret or publishable keys are blank return false otherwise true.
                $process_order = isset( $payment_options['stripe_secret'] ) && isset( $payment_options['stripe_publishable'] );
                break;
            default:
                break;
        }
    }
    return $process_order;
}

/**
 * Generate HTML markup for card reading layouts.
 *
 * Creates the appropriate HTML structure for different card layout types including
 * standard grid, circular, overlapping, and spread layouts. Each layout type has
 * specific positioning, rotation, and z-index calculations for proper visual display.
 *
 * @since 1.0.0
 *
 * @param array  $card_ids      Array of card data including IDs, images, and upright status.
 * @param int    $card_count    Total number of cards to display in the layout.
 * @param string $card_back_url URL to the card back image used for all cards.
 * @param string $layout        Layout type: 'standard', 'circular', 'overlapping', or 'spread'.
 * @param int    $reading_id    Optional reading ID to retrieve layout-specific settings.
 *
 * @return string HTML markup for the specified card layout with proper styling and data attributes.
 */
function card_oracle_layout_html(
    $card_ids,
    $card_count,
    $card_back_url,
    $layout,
    $reading_id = null
) {
    $profiling_enabled = apply_filters( 'card_oracle_layout_profiling_enabled', defined( 'CARD_ORACLE_PROFILE_LAYOUT' ) && CARD_ORACLE_PROFILE_LAYOUT );
    if ( $profiling_enabled ) {
        card_oracle_register_layout_profiling();
        $profile_start = microtime( true );
    }
    // Nothing to render for a reading with no published cards. The circular and
    // spread layouts divide 360/40 by the card count, so bail before that maths
    // to avoid a DivisionByZeroError fatal on the front end.
    if ( $card_count < 1 ) {
        return '';
    }
    switch ( $layout ) {
        case 'circular':
            // Get the circular diameter setting
            $circular_diameter = 235;
            // Default value
            if ( $reading_id ) {
                $saved_diameter = get_post_meta( $reading_id, CO_CIRCULAR_DIAMETER, true );
                if ( !empty( $saved_diameter ) && is_numeric( $saved_diameter ) ) {
                    $circular_diameter = intval( $saved_diameter );
                }
            }
            $output = sprintf( '<div class="tarot-deck-circle-container"><div class="tarot-deck-circle" style="--circular-diameter: %d%%;"><ul class="tarot-spread-circle">', esc_attr( $circular_diameter ) );
            $angle = 360 / $card_count;
            for ($i = 0; $i < $card_count; $i++) {
                $image_class = 'card-oracle-image-hidden';
                $reversed = ( 1 === $card_ids[$i]['Upright'] ? '' : 'true' );
                if ( $reversed ) {
                    $image_class .= ' card-oracle-rotate-image';
                }
                $output .= sprintf(
                    '<li style="z-index: %1$d; transform: translate(0px) rotate(%2$ddeg);"><img style="z-index: %1$d;" id="back%3$s" src="%4$s" loading="lazy" data-value="%3$s" data-reversed="%5$s" alt="back of card" ><img style="z-index: %6$d;" id="card%3$s" class="%8$s" src="%7$s" loading="lazy" alt="card"></li>',
                    esc_attr( $i + 10 ),
                    // Fixed: Lower z-index values (10-based instead of 1000-based)
                    esc_attr( $i * $angle ),
                    esc_attr( $card_ids[$i]['ID'] ),
                    esc_url( $card_back_url ),
                    esc_attr( $reversed ),
                    esc_attr( $i + 20 ),
                    // Fixed: Lower z-index values (20-based instead of 2000-based)
                    esc_url( $card_ids[$i]['Image'] ),
                    esc_attr( $image_class )
                );
            }
            $output .= '</ul></div></div>';
            break;
        case 'overlapping':
            $output = '<div class="container"><div class="tarot-deck-overlap"><ul>';
            for ($i = 0; $i < $card_count; $i++) {
                $image_class = 'card-oracle-image-hidden';
                $reversed = ( 1 === $card_ids[$i]['Upright'] ? '' : 'true' );
                if ( $reversed ) {
                    $image_class .= ' card-oracle-rotate-image';
                }
                $output .= sprintf(
                    '<li><img id="back%1$s" src="%2$s" loading="lazy" data-value="%1$s" data-reversed="%3$s" alt="back of card"><img id="card%1$s" class="%5$s" src="%4$s" loading="lazy" alt="card"></li>',
                    esc_attr( $card_ids[$i]['ID'] ),
                    esc_url( $card_back_url ),
                    esc_attr( $reversed ),
                    esc_url( $card_ids[$i]['Image'] ),
                    esc_attr( $image_class )
                );
            }
            $output .= '</ul></div></div>';
            break;
        case 'spread':
            $output = '<div class="container"><div class="tarot-deck-overlap"><ul>';
            $angle = 40 / $card_count;
            for ($i = 0; $i < $card_count; $i++) {
                $rotate = -20 + $i * $angle;
                $image_class = 'card-oracle-image-hidden';
                $reversed = ( 1 === $card_ids[$i]['Upright'] ? '' : 'true' );
                if ( $reversed ) {
                    $image_class .= ' card-oracle-rotate-image';
                }
                $output .= sprintf(
                    '<li style="transform: translate(0px) rotate(%1$ddeg);"><img id="back%2$s" src="%3$s" loading="lazy" data-value="%2$s" data-reversed="%4$s" alt="back of card"><img id="card%2$s" class="%6$s" src="%5$s" loading="lazy" alt="card"></li>',
                    esc_attr( $rotate ),
                    esc_attr( $card_ids[$i]['ID'] ),
                    esc_url( $card_back_url ),
                    esc_attr( $reversed ),
                    esc_url( $card_ids[$i]['Image'] ),
                    esc_attr( $image_class )
                );
            }
            $output .= '</ul></div></div>';
            break;
        default:
            $output = '<div class="card-oracle-cards">';
            for ($i = 0; $i < $card_count; $i++) {
                $output .= '<div class="card-oracle-card"><div class="card-oracle-card-body"><div class="card-oracle-back">';
                // Set the data-value and the class based on whether is card is right side up or upside down.
                if ( 1 === $card_ids[$i]['Upright'] ) {
                    $reversed = '';
                    $class = 'card-oracle-front';
                } else {
                    $reversed = $card_ids[$i]['ID'];
                    $class = 'card-oracle-front-reverse';
                }
                $output .= sprintf(
                    '<button type="button" value="%1$s" id="btn%1$s" data-value="%2$s" onclick="this.disabled = true;" class="btn card-oracle-btn clicked"><img src="%3$s" loading="lazy" alt="back of card"/></button></div><div class="%4$s">',
                    esc_attr( $card_ids[$i]['ID'] ),
                    esc_attr( $reversed ),
                    esc_url( $card_back_url ),
                    esc_attr( $class )
                );
                $output .= get_the_post_thumbnail( $card_ids[$i]['ID'] );
                $output .= '</div></div></div>';
            }
            $output .= '</div>';
            break;
    }
    // Add shuffle button at the bottom of all layouts (if shuffle is enabled for this reading).
    $shuffle_enabled = true;
    if ( $reading_id ) {
        $shuffle_meta = get_post_meta( $reading_id, CO_SHUFFLE_CARDS, true );
        $shuffle_enabled = 'no' !== $shuffle_meta;
    }
    if ( $shuffle_enabled ) {
        $shuffle_id = uniqid( 'card-oracle-shuffle-', false );
        $output .= sprintf( '<div class="card-oracle-shuffle-container card-oracle-shuffle-bottom" data-shuffle-root="%1$s" aria-live="polite">', esc_attr( $shuffle_id ) );
        $output .= sprintf( '<button type="button" id="card-oracle-shuffle-btn-%1$s" class="card-oracle-shuffle-button" data-shuffle-button data-shuffle-id="%1$s" aria-describedby="%1$s-description %1$s-status" aria-pressed="false" aria-busy="false">', esc_attr( $shuffle_id ) );
        $output .= '<span class="card-oracle-shuffle-icon" aria-hidden="true">🔀</span> ';
        $output .= esc_html__( 'Shuffle Cards', 'card-oracle' );
        $output .= '</button>';
        $output .= sprintf( '<span id="%1$s-description" class="card-oracle-visually-hidden">%2$s</span>', esc_attr( $shuffle_id ), esc_html__( 'Shuffle the available cards to generate a new random order.', 'card-oracle' ) );
        $output .= sprintf(
            '<span id="%1$s-status" class="card-oracle-visually-hidden" data-shuffle-status data-status-ready="%2$s" data-status-busy="%3$s" data-status-complete="%4$s">%2$s</span>',
            esc_attr( $shuffle_id ),
            esc_attr( esc_html__( 'Ready to shuffle.', 'card-oracle' ) ),
            esc_attr( esc_html__( 'Shuffling cards…', 'card-oracle' ) ),
            esc_attr( esc_html__( 'Shuffle complete.', 'card-oracle' ) )
        );
        $output .= '</div>';
    }
    if ( $profiling_enabled ) {
        card_oracle_profile_layout_record( array(
            'layout'   => $layout,
            'duration' => microtime( true ) - $profile_start,
            'cards'    => $card_count,
            'reading'  => $reading_id,
        ) );
    }
    return $output;
}

/**
 * Provide a scoped place to store Gutenberg block overrides while rendering the shortcode.
 *
 * The overrides array is keyed by logical names that map to reading settings (deck layout,
 * presentation layout, etc.). Values should already be sanitized prior to storage. Overrides
 * are cleared automatically once rendering completes to avoid leaking state between requests.
 *
 * @since 1.2.0
 *
 * @param array $overrides Key/value pairs of override data.
 * @return void
 */
function card_oracle_set_block_overrides(  array $overrides  ) {
    $GLOBALS['card_oracle_block_overrides'] = array_filter( $overrides, function ( $value ) {
        return '' !== $value && null !== $value;
    } );
}

/**
 * Retrieve a single override value with an optional fallback.
 *
 * @since 1.2.0
 *
 * @param string $key     Override key to look up.
 * @param mixed  $default Fallback value when no override is present.
 * @return mixed
 */
function card_oracle_get_block_override(  $key, $default = null  ) {
    if ( empty( $GLOBALS['card_oracle_block_overrides'] ) || !is_array( $GLOBALS['card_oracle_block_overrides'] ) ) {
        return $default;
    }
    if ( array_key_exists( $key, $GLOBALS['card_oracle_block_overrides'] ) ) {
        return $GLOBALS['card_oracle_block_overrides'][$key];
    }
    return $default;
}

/**
 * Clear any block overrides that were previously stored for rendering.
 *
 * @since 1.2.0
 *
 * @return void
 */
function card_oracle_clear_block_overrides() {
    unset($GLOBALS['card_oracle_block_overrides']);
}

/**
 * Get Card Oracle settings with defaults
 *
 * Helper function to access Card Oracle settings from anywhere in the plugin.
 * Returns all settings merged with defaults to ensure no missing values.
 *
 * @since 1.1.9
 * @param string $key Optional. Specific setting key to retrieve. If empty, returns all settings.
 * @return mixed Settings array or specific setting value
 */
function card_oracle_get_setting(  $key = ''  ) {
    $defaults = array(
        'enable_feed_check' => false,
        'cache_ttl'         => 3600,
        'debug_logging'     => false,
        'default_deck'      => '',
    );
    $settings = get_option( 'card_oracle_settings', $defaults );
    $settings = wp_parse_args( $settings, $defaults );
    if ( !empty( $key ) ) {
        return ( isset( $settings[$key] ) ? $settings[$key] : (( isset( $defaults[$key] ) ? $defaults[$key] : null )) );
    }
    return $settings;
}

/**
 * Check if RSS feed checking is enabled
 *
 * @since 1.1.9
 * @return bool True if feed checking is enabled
 */
function card_oracle_is_feed_check_enabled() {
    return (bool) card_oracle_get_setting( 'enable_feed_check' );
}

/**
 * Get cache TTL setting
 *
 * @since 1.1.9
 * @return int Cache TTL in seconds
 */
function card_oracle_get_cache_ttl() {
    $ttl = (int) card_oracle_get_setting( 'cache_ttl' );
    // Ensure reasonable bounds
    return max( 60, min( 86400, $ttl ) );
}

/**
 * Check if debug logging is enabled
 *
 * @since 1.1.9
 * @return bool True if debug logging is enabled
 */
function card_oracle_is_debug_logging_enabled() {
    return (bool) card_oracle_get_setting( 'debug_logging' );
}

/**
 * Get default deck/reading ID
 *
 * @since 1.1.9
 * @return string Default deck ID, empty string if not set
 */
function card_oracle_get_default_deck() {
    return sanitize_text_field( card_oracle_get_setting( 'default_deck' ) );
}

/**
 * Register shutdown hook for layout profiling report.
 *
 * @since 1.2.0
 * @return void
 */
function card_oracle_register_layout_profiling() {
    static $registered = false;
    if ( $registered ) {
        return;
    }
    $registered = true;
    add_action( 'shutdown', 'card_oracle_emit_layout_profile', 20 );
}

/**
 * Record a layout profiling sample.
 *
 * @since 1.2.0
 * @param array $entry Profiling payload.
 * @return void
 */
function card_oracle_profile_layout_record(  $entry  ) {
    global $card_oracle_layout_profile;
    if ( !is_array( $card_oracle_layout_profile ) ) {
        $card_oracle_layout_profile = array(
            'calls'   => 0,
            'layouts' => array(),
        );
    }
    $layout = ( isset( $entry['layout'] ) ? $entry['layout'] : 'unknown' );
    $duration = ( isset( $entry['duration'] ) ? (float) $entry['duration'] : 0.0 );
    $card_count = ( isset( $entry['cards'] ) ? (int) $entry['cards'] : 0 );
    if ( !isset( $card_oracle_layout_profile['layouts'][$layout] ) ) {
        $card_oracle_layout_profile['layouts'][$layout] = array(
            'count'      => 0,
            'total_time' => 0.0,
            'max_time'   => 0.0,
            'cards'      => array(),
        );
    }
    ++$card_oracle_layout_profile['calls'];
    $record =& $card_oracle_layout_profile['layouts'][$layout];
    ++$record['count'];
    $record['total_time'] += $duration;
    $record['max_time'] = max( $record['max_time'], $duration );
    $record['cards'][] = $card_count;
}

/**
 * Emit a profiling report at shutdown when enabled.
 *
 * @since 1.2.0
 * @return void
 */
function card_oracle_emit_layout_profile() {
    global $card_oracle_layout_profile;
    if ( empty( $card_oracle_layout_profile['layouts'] ) ) {
        return;
    }
    $report = array();
    foreach ( $card_oracle_layout_profile['layouts'] as $layout => $data ) {
        $average = ( $data['count'] ? $data['total_time'] / $data['count'] : 0 );
        $cards_mean = ( $data['cards'] ? array_sum( $data['cards'] ) / count( $data['cards'] ) : 0 );
        $report[] = sprintf(
            'Layout %1$s: calls=%2$d total=%3$.2fms avg=%4$.2fms max=%5$.2fms avg-cards=%6$.2f',
            $layout,
            $data['count'],
            $data['total_time'] * 1000,
            $average * 1000,
            $data['max_time'] * 1000,
            $cards_mean
        );
    }
    $report = apply_filters( 'card_oracle_layout_profiling_report', $report, $card_oracle_layout_profile );
    if ( empty( $report ) ) {
        return;
    }
    foreach ( $report as $line ) {
        if ( function_exists( 'co_debug_log' ) ) {
            co_debug_log( $line );
        } else {
            error_log( '[Card Oracle Layout Profile] ' . $line );
        }
    }
    $card_oracle_layout_profile = array();
}

/**
 * Log debug message if debug logging is enabled
 *
 * @since 1.1.9
 * @param string $message Debug message to log
 * @param string $context Optional context for the log entry
 * @return void
 */
function card_oracle_debug_log(  $message, $context = ''  ) {
    if ( !card_oracle_is_debug_logging_enabled() ) {
        return;
    }
    $log_message = '[Card Oracle Debug]';
    if ( !empty( $context ) ) {
        $log_message .= ' [' . $context . ']';
    }
    $log_message .= ' ' . $message;
    error_log( $log_message );
}

/**
 * Scan for orphaned Card Oracle shortcodes in posts and pages.
 *
 * Finds all posts/pages containing Card Oracle shortcodes and validates
 * that each referenced reading exists and is published.
 *
 * @since 1.2.0
 *
 * @param bool $force_refresh Whether to bypass the cache and force a fresh scan.
 * @return array Array of orphaned shortcode data, each containing:
 *               - post_id: The post ID containing the shortcode
 *               - post_title: The post title
 *               - post_type: The post type (post, page, etc.)
 *               - edit_url: URL to edit the post
 *               - shortcode: The full shortcode text found
 *               - reason: Why the shortcode is considered orphaned
 */
function card_oracle_scan_orphaned_shortcodes(  $force_refresh = false  ) {
    $transient_key = 'card_oracle_orphaned_shortcodes';
    // Check for cached results unless force refresh is requested.
    if ( !$force_refresh ) {
        $cached = get_transient( $transient_key );
        if ( false !== $cached ) {
            return $cached;
        }
    }
    global $wpdb;
    $orphaned = array();
    // Query posts/pages containing Card Oracle shortcodes.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $posts_with_shortcodes = $wpdb->get_results( $wpdb->prepare(
        "SELECT ID, post_title, post_type, post_content\n\t\t\tFROM {$wpdb->posts}\n\t\t\tWHERE post_status = %s\n\t\t\tAND (post_type = %s OR post_type = %s)\n\t\t\tAND post_content LIKE %s",
        'publish',
        'post',
        'page',
        '%[card-oracle%'
    ) );
    if ( empty( $posts_with_shortcodes ) ) {
        set_transient( $transient_key, array(), HOUR_IN_SECONDS );
        return array();
    }
    // Regex to match all Card Oracle shortcode variants.
    // Uses \s* to handle cases with or without space before attributes.
    $shortcode_pattern = '/\\[(card-oracle(?:-daily|-random)?)\\s*([^\\]]*)\\]/i';
    foreach ( $posts_with_shortcodes as $post ) {
        // Find all shortcodes in this post's content.
        if ( preg_match_all(
            $shortcode_pattern,
            $post->post_content,
            $matches,
            PREG_SET_ORDER
        ) ) {
            foreach ( $matches as $match ) {
                $full_shortcode = $match[0];
                $shortcode_type = $match[1];
                $shortcode_attrs = $match[2];
                // Parse the shortcode attributes.
                $atts = shortcode_parse_atts( $shortcode_attrs );
                if ( empty( $atts ) || !is_array( $atts ) ) {
                    // No attributes found - shortcode is malformed.
                    $orphaned[] = array(
                        'post_id'    => $post->ID,
                        'post_title' => $post->post_title,
                        'post_type'  => $post->post_type,
                        'edit_url'   => get_edit_post_link( $post->ID, 'raw' ),
                        'shortcode'  => $full_shortcode,
                        'reason'     => __( 'No ID or name attribute specified', 'card-oracle' ),
                    );
                    continue;
                }
                // Resolve the reading ID using existing function.
                $reading_id = resolve_reading_id_from_atts( $atts );
                if ( null === $reading_id ) {
                    // Could not resolve reading ID.
                    $orphaned[] = array(
                        'post_id'    => $post->ID,
                        'post_title' => $post->post_title,
                        'post_type'  => $post->post_type,
                        'edit_url'   => get_edit_post_link( $post->ID, 'raw' ),
                        'shortcode'  => $full_shortcode,
                        'reason'     => __( 'No ID or name attribute specified', 'card-oracle' ),
                    );
                    continue;
                }
                // Check if the reading exists and is published.
                $reading_post = get_post( $reading_id );
                if ( !$reading_post ) {
                    $orphaned[] = array(
                        'post_id'    => $post->ID,
                        'post_title' => $post->post_title,
                        'post_type'  => $post->post_type,
                        'edit_url'   => get_edit_post_link( $post->ID, 'raw' ),
                        'shortcode'  => $full_shortcode,
                        'reason'     => sprintf( __( 'Reading ID %d does not exist', 'card-oracle' ), $reading_id ),
                    );
                    continue;
                }
                if ( 'co_readings' !== $reading_post->post_type ) {
                    $orphaned[] = array(
                        'post_id'    => $post->ID,
                        'post_title' => $post->post_title,
                        'post_type'  => $post->post_type,
                        'edit_url'   => get_edit_post_link( $post->ID, 'raw' ),
                        'shortcode'  => $full_shortcode,
                        'reason'     => sprintf( __( 'ID %d is not a reading', 'card-oracle' ), $reading_id ),
                    );
                    continue;
                }
                if ( 'publish' !== $reading_post->post_status ) {
                    $orphaned[] = array(
                        'post_id'    => $post->ID,
                        'post_title' => $post->post_title,
                        'post_type'  => $post->post_type,
                        'edit_url'   => get_edit_post_link( $post->ID, 'raw' ),
                        'shortcode'  => $full_shortcode,
                        'reason'     => sprintf( __( 'Reading ID %1$d is not published (status: %2$s)', 'card-oracle' ), $reading_id, $reading_post->post_status ),
                    );
                }
            }
        }
    }
    // Cache the results for 1 hour.
    set_transient( $transient_key, $orphaned, HOUR_IN_SECONDS );
    return $orphaned;
}

/**
 * Invalidate the orphaned shortcode cache.
 *
 * Called when readings or posts/pages are saved or deleted to ensure
 * the validation results stay current.
 *
 * @since 1.2.0
 *
 * @param int $post_id The post ID that was changed.
 * @return void
 */
function card_oracle_invalidate_orphaned_shortcode_cache(  $post_id = 0  ) {
    // Skip revisions and autosaves.
    if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
        return;
    }
    delete_transient( 'card_oracle_orphaned_shortcodes' );
}

// Register cache invalidation hooks for orphaned shortcode scanning.
add_action( 'save_post_co_readings', 'card_oracle_invalidate_orphaned_shortcode_cache' );
add_action( 'delete_post', 'card_oracle_invalidate_orphaned_shortcode_cache' );
add_action( 'save_post_post', 'card_oracle_invalidate_orphaned_shortcode_cache' );
add_action( 'save_post_page', 'card_oracle_invalidate_orphaned_shortcode_cache' );
/**
 * Clear daily and random card transients when the powered by setting changes.
 *
 * This ensures that changes to the "Powered by" setting take effect immediately
 * rather than waiting for the transient to expire.
 *
 * @since 1.2.0
 *
 * @param mixed $old_value The old option value.
 * @param mixed $new_value The new option value.
 * @return void
 */
function card_oracle_clear_card_transients_on_powered_by_change(  $old_value, $new_value  ) {
    if ( $old_value !== $new_value ) {
        delete_transient( CARD_ORACLE_DAILY_CARD );
        delete_transient( CARD_ORACLE_RANDOM_CARD );
    }
}

add_action(
    'update_option_card_oracle_powered_by',
    'card_oracle_clear_card_transients_on_powered_by_change',
    10,
    2
);
add_action(
    'update_option_card_oracle_affiliate_link',
    'card_oracle_clear_card_transients_on_powered_by_change',
    10,
    2
);
/**
 * Normalize integration provider slugs for renamed services.
 *
 * Lives in the non-premium core functions file so free users hit the same
 * passthrough behavior when the admin settings callback fires on admin_init.
 * Previously defined in card-oracle-core-functions__premium_only.php, which
 * fataled free admin loads.
 *
 * @since 1.1.9
 * @param string $integration Integration slug.
 * @return string
 */
function card_oracle_normalize_integration_slug(  $integration  ) {
    if ( empty( $integration ) ) {
        return $integration;
    }
    switch ( $integration ) {
        case 'sendinblue':
        case 'sendinblue_api':
        case 'brevo':
            return 'brevo_api';
        default:
            return $integration;
    }
}

/**
 * Retrieve Brevo options with a fallback to legacy SendinBlue keys.
 *
 * Companion to card_oracle_normalize_integration_slug(); moved here for the
 * same reason — callers in the non-premium admin settings flow rely on it.
 *
 * @since 1.1.9
 * @param string $option_name Brevo option name.
 * @return mixed
 */
function card_oracle_get_brevo_option(  $option_name  ) {
    $value = get_option( $option_name );
    if ( $value ) {
        return $value;
    }
    $legacy_option = str_replace( 'brevo', 'sendinblue', $option_name );
    if ( $legacy_option !== $option_name ) {
        $value = get_option( $legacy_option );
        if ( $value ) {
            update_option( $option_name, $value );
        }
    }
    return $value;
}

/**
 * Clear all Card Oracle caches in one operation.
 *
 * Removes the daily and random card transients, the orphaned shortcode
 * scan cache, and bumps the reading-lookup object-cache version so
 * cached slug/title lookups are invalidated. Intended to back a
 * user-facing "Clear cache" admin button.
 *
 * @since 1.2.6
 * @return array {
 *     @type int      $cleared Count of cache layers cleared.
 *     @type string[] $items   Human-readable labels of what was cleared.
 * }
 */
function card_oracle_clear_all_caches() {
    $items = array();
    if ( defined( 'CARD_ORACLE_DAILY_CARD' ) && delete_transient( CARD_ORACLE_DAILY_CARD ) ) {
        $items[] = __( 'Daily card transient', 'card-oracle' );
    }
    if ( defined( 'CARD_ORACLE_RANDOM_CARD' ) && delete_transient( CARD_ORACLE_RANDOM_CARD ) ) {
        $items[] = __( 'Random card transient', 'card-oracle' );
    }
    if ( delete_transient( 'card_oracle_orphaned_shortcodes' ) ) {
        $items[] = __( 'Orphaned shortcode scan', 'card-oracle' );
    }
    if ( function_exists( 'card_oracle_invalidate_reading_lookup_cache' ) ) {
        card_oracle_invalidate_reading_lookup_cache();
        $items[] = __( 'Reading lookup object cache', 'card-oracle' );
    }
    $reading_cache_cleared = card_oracle_purge_reading_data_transients();
    if ( $reading_cache_cleared > 0 ) {
        /* translators: %d: number of per-reading transients deleted */
        $items[] = sprintf( __( 'Per-reading cards & positions caches (%d)', 'card-oracle' ), $reading_cache_cleared );
    }
    return array(
        'cleared' => count( $items ),
        'items'   => $items,
    );
}

/**
 * Delete all per-reading transients written by get_cards_for_reading and
 * get_positions_for_reading. Returns the number of underlying option rows
 * removed (counts both the value and the timeout row WordPress stores).
 *
 * @since 1.2.6
 * @return int Number of option rows deleted.
 */
function card_oracle_purge_reading_data_transients() {
    global $wpdb;
    if ( !isset( $wpdb ) || !is_object( $wpdb ) || !method_exists( $wpdb, 'query' ) ) {
        return 0;
    }
    $deleted = $wpdb->query( 
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        "DELETE FROM {$wpdb->options}\n\t\t WHERE option_name LIKE '\\_transient\\_co\\_cards\\_for\\_%'\n\t\t    OR option_name LIKE '\\_transient\\_timeout\\_co\\_cards\\_for\\_%'\n\t\t    OR option_name LIKE '\\_transient\\_co\\_positions\\_for\\_%'\n\t\t    OR option_name LIKE '\\_transient\\_timeout\\_co\\_positions\\_for\\_%'"
     );
    return ( is_numeric( $deleted ) ? (int) $deleted : 0 );
}

/**
 * Rewrite an image URL's scheme to https when the global force-https option is on.
 *
 * Some hosts return attachment URLs as http even when the site itself runs on
 * https, which triggers mixed-content warnings on the public-facing reading.
 * This helper opts in to a simple set_url_scheme() rewrite for the cases where
 * the plugin emits an <img src>.
 *
 * @since 1.2.6
 * @param string $url Image URL (or any URL).
 * @return string URL with https scheme if the option is enabled and the input
 *                is a non-empty string; otherwise the input is returned unchanged.
 */
function card_oracle_maybe_force_https_image_url(  $url  ) {
    if ( !is_string( $url ) || '' === $url ) {
        return $url;
    }
    if ( !function_exists( 'get_option' ) || !function_exists( 'set_url_scheme' ) ) {
        return $url;
    }
    if ( 'yes' !== get_option( 'card_oracle_force_https_images' ) ) {
        return $url;
    }
    return set_url_scheme( $url, 'https' );
}

/**
 * Render a responsive <img> element for a card's featured image.
 *
 * Wraps wp_get_attachment_image() so callers get a fully-formed tag with src,
 * srcset, sizes, and loading="lazy" for free. When the global force-https
 * setting is enabled, every URL in src/srcset is rewritten via set_url_scheme
 * through the wp_get_attachment_image_attributes filter.
 *
 * @since 1.2.6
 *
 * @param int   $card_id WordPress post ID of a co_cards entry.
 * @param array $args    {
 *     Optional attributes.
 *     @type string $size    Image size keyword for wp_get_attachment_image(). Default 'large'.
 *     @type string $class   Extra class names. Default ''.
 *     @type string $alt     Alt text override. Default 'front of card'.
 * }
 * @return string Image HTML, or '' if the card has no featured image.
 */
function card_oracle_card_image_html(  $card_id, $args = array()  ) {
    if ( !function_exists( 'wp_get_attachment_image' ) ) {
        return '';
    }
    $attachment_id = ( function_exists( 'get_post_thumbnail_id' ) ? get_post_thumbnail_id( $card_id ) : 0 );
    if ( !$attachment_id ) {
        return '';
    }
    $defaults = array(
        'size'  => 'large',
        'class' => '',
        'alt'   => 'front of card',
    );
    $args = array_merge( $defaults, $args );
    $attrs = array(
        'alt'     => $args['alt'],
        'loading' => 'lazy',
    );
    if ( '' !== $args['class'] ) {
        $attrs['class'] = $args['class'];
    }
    return wp_get_attachment_image(
        $attachment_id,
        $args['size'],
        false,
        $attrs
    );
}

/**
 * Filter callback that rewrites http URLs in image attribute output to https.
 *
 * Registered against wp_get_attachment_image_attributes when the force-https
 * option is enabled. Handles both `src` and `srcset` (the latter has comma-
 * separated URL/descriptor pairs).
 *
 * @since 1.2.6
 *
 * @param string[] $attr Image attributes ready for output.
 * @return string[] Modified attributes.
 */
function card_oracle_filter_image_attrs_https(  $attr  ) {
    if ( !function_exists( 'set_url_scheme' ) ) {
        return $attr;
    }
    if ( isset( $attr['src'] ) && is_string( $attr['src'] ) ) {
        $attr['src'] = set_url_scheme( $attr['src'], 'https' );
    }
    if ( isset( $attr['srcset'] ) && is_string( $attr['srcset'] ) && '' !== $attr['srcset'] ) {
        $entries = array();
        foreach ( explode( ',', $attr['srcset'] ) as $entry ) {
            $entry = trim( $entry );
            if ( '' === $entry ) {
                continue;
            }
            $parts = preg_split( '/\\s+/', $entry, 2 );
            $parts[0] = set_url_scheme( $parts[0], 'https' );
            $entries[] = implode( ' ', $parts );
        }
        $attr['srcset'] = implode( ', ', $entries );
    }
    return $attr;
}

/**
 * Register the force-https filter on init when the option is enabled.
 *
 * @since 1.2.6
 * @return void
 */
function card_oracle_register_force_https_filter() {
    if ( !function_exists( 'get_option' ) || !function_exists( 'add_filter' ) ) {
        return;
    }
    if ( 'yes' === get_option( 'card_oracle_force_https_images' ) ) {
        add_filter( 'wp_get_attachment_image_attributes', 'card_oracle_filter_image_attrs_https' );
    }
}

if ( function_exists( 'add_action' ) ) {
    add_action( 'init', 'card_oracle_register_force_https_filter' );
}