<?php
/**
 * Utility functions for Blaminhor Essentials
 *
 * @package BlaminhorEssentials
 */

// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Sanitize and validate email address
 *
 * @param string $email Email address.
 * @return string|false
 */
function blaminhor_essentials_sanitize_email( $email ) {
    $email = sanitize_email( $email );
    return is_email( $email ) ? $email : false;
}

/**
 * Get plugin option with default
 *
 * @param string $key     Option key.
 * @param mixed  $default Default value.
 * @return mixed
 */
function blaminhor_essentials_get_option( $key, $default = null ) {
    $options = get_option( 'blaminhor_essentials_options', array() );
    return isset( $options[ $key ] ) ? $options[ $key ] : $default;
}

/**
 * Update plugin option
 *
 * @param string $key   Option key.
 * @param mixed  $value Option value.
 * @return bool
 */
function blaminhor_essentials_update_option( $key, $value ) {
    $options         = get_option( 'blaminhor_essentials_options', array() );
    $options[ $key ] = $value;
    return update_option( 'blaminhor_essentials_options', $options );
}

/**
 * Encrypt sensitive data
 *
 * @param string $data Data to encrypt.
 * @return string
 */
function blaminhor_essentials_encrypt( $data ) {
    if ( empty( $data ) ) {
        return '';
    }

    $key = blaminhor_essentials_get_encryption_key();

    if ( function_exists( 'openssl_encrypt' ) ) {
        $iv     = openssl_random_pseudo_bytes( 16 );
        $cipher = openssl_encrypt( $data, 'AES-256-CBC', $key, 0, $iv );
        return base64_encode( $iv . $cipher );
    }

    // Fallback: simple obfuscation (not as secure)
    return base64_encode( $data );
}

/**
 * Decrypt sensitive data
 *
 * @param string $data Data to decrypt.
 * @return string
 */
function blaminhor_essentials_decrypt( $data ) {
    if ( empty( $data ) ) {
        return '';
    }

    $key = blaminhor_essentials_get_encryption_key();

    if ( function_exists( 'openssl_encrypt' ) ) {
        $decoded = base64_decode( $data );
        $iv      = substr( $decoded, 0, 16 );
        $cipher  = substr( $decoded, 16 );
        return openssl_decrypt( $cipher, 'AES-256-CBC', $key, 0, $iv );
    }

    // Fallback: simple de-obfuscation
    return base64_decode( $data );
}

/**
 * Get encryption key
 *
 * @return string
 */
function blaminhor_essentials_get_encryption_key() {
    if ( defined( 'BLAMINHOR_ESSENTIALS_ENCRYPTION_KEY' ) ) {
        return BLAMINHOR_ESSENTIALS_ENCRYPTION_KEY;
    }

    if ( defined( 'AUTH_KEY' ) ) {
        return AUTH_KEY;
    }

    return 'blaminhor-essentials-default-key';
}

/**
 * Log message to debug.log if WP_DEBUG is enabled
 *
 * @param mixed  $message Message to log.
 * @param string $level   Log level.
 */
function blaminhor_essentials_log( $message, $level = 'info', $module_id = 'core', $context = array() ) {
    // 1. Forward to the structured cross-module logger when the admin has
    //    enabled it via Blaminhor → Logs. Always safe to call: the logger
    //    silent-no-ops when disabled, when the module is filtered out, or
    //    when the level is below the configured threshold.
    if ( class_exists( 'Blaminhor_Essentials_Logs' ) ) {
        $event = is_array( $context ) && isset( $context['event'] ) ? (string) $context['event'] : 'log';
        if ( isset( $context['event'] ) ) {
            unset( $context['event'] );
        }
        $printable = ( is_array( $message ) || is_object( $message ) )
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Debug logging on user opt-in
            ? print_r( $message, true )
            : (string) $message;
        Blaminhor_Essentials_Logs::instance()->record( $module_id, $level, $event, $printable, is_array( $context ) ? $context : array() );
    }

    // 2. Keep the historical behaviour: also write to the PHP error_log when
    //    WP_DEBUG is on. This is independent of the structured logger so
    //    developers using the classic WP_DEBUG_LOG workflow keep what they had.
    if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
        return;
    }

    if ( is_array( $message ) || is_object( $message ) ) {
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Intentional debug logging function
        $message = print_r( $message, true );
    }

    $log_message = sprintf(
        '[Blaminhor Essentials][%s][%s] %s',
        strtoupper( (string) $level ),
        (string) $module_id,
        $message
    );

    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging function
    error_log( $log_message );
}

/**
 * Check if current user can manage plugin
 *
 * @return bool
 */
function blaminhor_essentials_current_user_can_manage() {
    return current_user_can( 'manage_options' );
}

/**
 * Generate a secure random token
 *
 * @param int $length Token length.
 * @return string
 */
function blaminhor_essentials_generate_token( $length = 32 ) {
    if ( function_exists( 'random_bytes' ) ) {
        return bin2hex( random_bytes( $length / 2 ) );
    }
    return wp_generate_password( $length, false );
}

/**
 * Get list of post types available for duplication
 *
 * @return array
 */
function blaminhor_essentials_get_post_types() {
    $post_types = get_post_types(
        array(
            'show_ui' => true,
        ),
        'objects'
    );

    $excluded = array( 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation' );

    foreach ( $excluded as $type ) {
        unset( $post_types[ $type ] );
    }

    return apply_filters( 'blaminhor_essentials_post_types', $post_types );
}

/**
 * Get list of taxonomies available for duplication
 *
 * @return array
 */
function blaminhor_essentials_get_taxonomies() {
    $taxonomies = get_taxonomies(
        array(
            'show_ui' => true,
        ),
        'objects'
    );

    $excluded = array( 'nav_menu', 'link_category', 'post_format', 'wp_theme', 'wp_template_part_area' );

    foreach ( $excluded as $tax ) {
        unset( $taxonomies[ $tax ] );
    }

    return apply_filters( 'blaminhor_essentials_taxonomies', $taxonomies );
}

/**
 * Render admin notice
 *
 * @param string $message Notice message.
 * @param string $type    Notice type (success, error, warning, info).
 * @param bool   $dismissible Whether notice is dismissible.
 */
function blaminhor_essentials_admin_notice( $message, $type = 'info', $dismissible = true ) {
    $classes = array( 'notice', 'notice-' . $type );

    if ( $dismissible ) {
        $classes[] = 'is-dismissible';
    }

    printf(
        '<div class="%1$s"><p>%2$s</p></div>',
        esc_attr( implode( ' ', $classes ) ),
        wp_kses_post( $message )
    );
}

/**
 * Validate nonce for AJAX requests
 *
 * @param string $action Nonce action.
 * @param string $nonce_name Nonce field name.
 * @return bool
 */
function blaminhor_essentials_verify_ajax_nonce( $action, $nonce_name = 'nonce' ) {
    if ( ! isset( $_REQUEST[ $nonce_name ] ) ) {
        return false;
    }

    return wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_name ] ) ), $action );
}

/**
 * Validate a database table name
 *
 * Ensures table name starts with WordPress prefix and contains only safe characters.
 *
 * @param string $table_name Table name to validate.
 * @return bool True if valid, false otherwise.
 */
function blaminhor_essentials_validate_table_name( $table_name ) {
    global $wpdb;

    // Must start with WordPress prefix.
    if ( strpos( $table_name, $wpdb->prefix ) !== 0 ) {
        return false;
    }

    // Must contain only alphanumeric characters and underscores.
    if ( preg_match( '/[^a-zA-Z0-9_]/', $table_name ) ) {
        return false;
    }

    return true;
}

/**
 * Ensure a protected directory exists
 *
 * Creates the directory if it doesn't exist and adds security files (.htaccess, index.php).
 *
 * @param string $dir_path Directory path.
 * @return bool True on success, false on failure.
 */
function blaminhor_essentials_ensure_protected_directory( $dir_path ) {
    // Create directory if it doesn't exist.
    if ( ! file_exists( $dir_path ) ) {
        if ( ! wp_mkdir_p( $dir_path ) ) {
            return false;
        }
    }

    // Initialize WP_Filesystem.
    global $wp_filesystem;
    if ( empty( $wp_filesystem ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
        WP_Filesystem();
    }

    // Add .htaccess for Apache.
    $htaccess_path = trailingslashit( $dir_path ) . '.htaccess';
    if ( ! file_exists( $htaccess_path ) ) {
        $wp_filesystem->put_contents( $htaccess_path, "Order deny,allow\nDeny from all", FS_CHMOD_FILE );
    }

    // Add index.php for security.
    $index_path = trailingslashit( $dir_path ) . 'index.php';
    if ( ! file_exists( $index_path ) ) {
        $wp_filesystem->put_contents( $index_path, '<?php // Silence is golden', FS_CHMOD_FILE );
    }

    return true;
}

/**
 * Get the plugin upload directory path
 *
 * @param string $subdir Optional subdirectory.
 * @return string Directory path.
 */
function blaminhor_essentials_get_upload_dir( $subdir = '' ) {
    $upload_dir = wp_upload_dir();
    $base_dir   = trailingslashit( $upload_dir['basedir'] ) . 'blaminhor-essentials';

    if ( ! empty( $subdir ) ) {
        $base_dir = trailingslashit( $base_dir ) . ltrim( $subdir, '/' );
    }

    return $base_dir;
}

/**
 * Format file size in human-readable format
 *
 * @param int $bytes File size in bytes.
 * @param int $decimals Number of decimal places.
 * @return string Formatted size.
 */
function blaminhor_essentials_format_size( $bytes, $decimals = 2 ) {
    if ( $bytes >= 1073741824 ) {
        return number_format( $bytes / 1073741824, $decimals ) . ' GB';
    } elseif ( $bytes >= 1048576 ) {
        return number_format( $bytes / 1048576, $decimals ) . ' MB';
    } elseif ( $bytes >= 1024 ) {
        return number_format( $bytes / 1024, $decimals ) . ' KB';
    }
    return $bytes . ' B';
}

/**
 * Sanitize a URL for database storage
 *
 * @param string $url URL to sanitize.
 * @return string Sanitized URL.
 */
function blaminhor_essentials_sanitize_url( $url ) {
    // First, trim whitespace.
    $url = trim( $url );

    // If it's a relative URL (starts with /), keep it as is but sanitize.
    if ( strpos( $url, '/' ) === 0 ) {
        return sanitize_text_field( $url );
    }

    // For absolute URLs, use esc_url_raw.
    return esc_url_raw( $url );
}

/**
 * Check if the current request is an AJAX request
 *
 * @return bool
 */
function blaminhor_essentials_is_ajax() {
    return wp_doing_ajax();
}

/**
 * Send a standardized AJAX success response
 *
 * @param string $message Success message.
 * @param array  $data    Additional data.
 */
function blaminhor_essentials_ajax_success( $message, $data = array() ) {
    $response = array_merge(
        array( 'message' => $message ),
        $data
    );
    wp_send_json_success( $response );
}

/**
 * Send a standardized AJAX error response
 *
 * @param string $message Error message.
 * @param array  $data    Additional data.
 */
function blaminhor_essentials_ajax_error( $message, $data = array() ) {
    $response = array_merge(
        array( 'message' => $message ),
        $data
    );
    wp_send_json_error( $response );
}

/**
 * One-shot cleanup of accumulated backslashes in module option values.
 *
 * Until v1.9.10, several modules sanitized $_POST values without calling
 * wp_unslash() first. Each successive save therefore added a backslash in
 * front of apostrophes / quotes / backslashes inside text fields. This
 * routine strips ONE level of slashes on the stored module options on first
 * load of the patched version. The "_done" flag prevents it from running
 * again on subsequent loads. Encrypted relay passwords are skipped — they
 * are base64 and contain no real backslashes; their cleartext was slashed
 * before encryption, so users with quote-bearing passwords will have to
 * re-enter them once after the upgrade.
 */
function blaminhor_essentials_migrate_strip_accumulated_slashes() {
    $flag = 'blaminhor_essentials_backslash_cleanup_done';
    if ( '2' === get_option( $flag, '' ) ) {
        return;
    }

    // Real option names used by the module base class (load_settings/save_settings).
    // The Blaminhor_Essentials_Module::get_option_key() getter still returns the
    // "_settings" suffix but is unused by the actual persistence layer.
    $affected_options = array(
        'blaminhor_essentials_module_smtp',
        'blaminhor_essentials_module_seo-manager',
        'blaminhor_essentials_module_maintenance',
        'blaminhor_essentials_module_duplicator',
        'blaminhor_essentials_module_backup',
    );

    foreach ( $affected_options as $option_name ) {
        $value = get_option( $option_name, null );
        if ( ! is_array( $value ) ) {
            continue;
        }
        $cleaned = blaminhor_essentials_strip_slashes_recursive( $value, array( 'password' ) );
        if ( $cleaned !== $value ) {
            update_option( $option_name, $cleaned );
        }
    }

    // SMTP relay passwords need a dedicated round-trip because they're stored
    // encrypted: pre-1.9.10 versions encrypted the slashed cleartext, which
    // makes SMTP authentication silently fail when the password contains an
    // apostrophe, quote or backslash.
    blaminhor_essentials_migrate_unslash_smtp_passwords();

    update_option( $flag, '2', false );
}

/**
 * Decrypt → stripslashes → re-encrypt SMTP relay passwords stored in their
 * slashed form by pre-1.9.10 releases. Skips passwords that have nothing to
 * clean to leave legitimate values untouched.
 */
function blaminhor_essentials_migrate_unslash_smtp_passwords() {
    $smtp = get_option( 'blaminhor_essentials_module_smtp' );
    if ( ! is_array( $smtp ) || empty( $smtp['relays'] ) || ! is_array( $smtp['relays'] ) ) {
        return;
    }

    $changed = false;
    foreach ( $smtp['relays'] as $i => $relay ) {
        if ( empty( $relay['password'] ) ) {
            continue;
        }
        $plain = blaminhor_essentials_decrypt( $relay['password'] );
        if ( ! is_string( $plain ) || '' === $plain ) {
            continue;
        }
        $stripped = stripslashes( $plain );
        if ( $stripped === $plain ) {
            continue;
        }
        $smtp['relays'][ $i ]['password'] = blaminhor_essentials_encrypt( $stripped );
        $changed = true;
    }

    if ( $changed ) {
        update_option( 'blaminhor_essentials_module_smtp', $smtp );
    }
}

/**
 * Recursively strip one level of slashes from string values in an array.
 *
 * @param mixed $value     Value (array or scalar).
 * @param array $skip_keys Array keys whose values must be left untouched
 *                         (e.g. encrypted blobs whose cleartext may
 *                         legitimately contain backslashes).
 * @return mixed
 */
function blaminhor_essentials_strip_slashes_recursive( $value, $skip_keys = array() ) {
    if ( is_array( $value ) ) {
        $result = array();
        foreach ( $value as $k => $v ) {
            if ( in_array( $k, $skip_keys, true ) ) {
                $result[ $k ] = $v;
                continue;
            }
            $result[ $k ] = blaminhor_essentials_strip_slashes_recursive( $v, $skip_keys );
        }
        return $result;
    }
    if ( is_string( $value ) ) {
        return stripslashes( $value );
    }
    return $value;
}
