<?php
/**
 * Contentosapp Studio Options Helpers
 *
 * Thin wrappers over WordPress' option API. The `costudio_` prefix is the
 * single canonical naming scheme.
 *
 * The wrappers exist mostly to keep the option access pattern consistent and
 * to give us a single hook point if Phase B (key rotation) ever needs another
 * pass.
 *
 * @package Contentosapp_Studio
 * @since   3.2.0
 */

defined( 'ABSPATH' ) || exit;

if ( ! function_exists( 'costudio_get_option' ) ) {
	function costudio_get_option( string $key, $default = false ) {
		return get_option( $key, $default );
	}
}

if ( ! function_exists( 'costudio_update_option' ) ) {
	function costudio_update_option( string $key, $value, $autoload = null ) {
		if ( null === $autoload && 0 === strpos( $key, 'costudio_' ) ) {
			$autoload = false;
		}

		return update_option( $key, $value, $autoload );
	}
}

if ( ! function_exists( 'costudio_sslverify' ) ) {
	/**
	 * Whether SSL verification is enabled for outbound HTTPS calls.
	 *
	 * Defaults to true (secure). Hosts behind corporate proxies with broken
	 * cert chains, or development environments with self-signed certs, can
	 * override via the `costudio_sslverify` filter:
	 *
	 *     // Disable globally (last resort).
	 *     add_filter( 'costudio_sslverify', '__return_false' );
	 *
	 *     // Disable only for a specific callsite.
	 *     add_filter( 'costudio_sslverify', function ( $enabled, $context ) {
	 *         return $context === 'telemetry' ? false : $enabled;
	 *     }, 10, 2 );
	 *
	 * Loopback HTTP to the local site uses WordPress core's
	 * `https_local_ssl_verify` filter and is intentionally NOT routed through
	 * this helper.
	 *
	 * @param string $context Identifier of the callsite — useful for selective
	 *                        overrides. Known values: 'contentos_adapter',
	 *                        'telemetry', 'diagnose'.
	 * @return bool
	 */
	function costudio_sslverify( string $context = '' ): bool {
		return (bool) apply_filters( 'costudio_sslverify', true, $context );
	}
}

if ( ! function_exists( 'costudio_safe_http_url' ) ) {
	/**
	 * Normalize an outbound URL and keep only HTTP(S) targets.
	 *
	 * WordPress' wp_safe_remote_* functions perform the network-level SSRF
	 * checks. This helper rejects obviously unsafe/non-web schemes before a
	 * request is even attempted.
	 */
	function costudio_safe_http_url( string $url ): string {
		$url = trim( $url );
		if ( '' === $url ) {
			return '';
		}

		$url    = esc_url_raw( $url, array( 'http', 'https' ) );
		$scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );

		return in_array( $scheme, array( 'http', 'https' ), true ) ? $url : '';
	}
}

if ( ! function_exists( 'costudio_safe_remote_args' ) ) {
	/**
	 * Harden args for requests to URLs that may come from settings/content.
	 *
	 * Callers can still override redirection intentionally (e.g. 0 for manual
	 * HEAD redirect walking), but unsafe URL rejection should stay enabled.
	 */
	function costudio_safe_remote_args( array $args = array() ): array {
		$defaults = array(
			'reject_unsafe_urls' => true,
		);

		if ( ! array_key_exists( 'redirection', $args ) ) {
			$defaults['redirection'] = 3;
		}

		$args = array_merge( $defaults, $args );
		$args['reject_unsafe_urls'] = true;

		return $args;
	}
}

if ( ! function_exists( 'costudio_api_ajax_url' ) ) {
	/**
	 * Build the AJAX endpoint URL for the remote Contentosapp Studio / ContentOS API server.
	 *
	 * All plugin code that calls the remote admin-ajax.php endpoint on
	 * api.contentosapp.com must go through this helper so the path is defined
	 * in a single place. This is NOT the local WordPress admin-ajax.php — it
	 * targets an external service that manages quotas, licenses, prompts, and
	 * other hosted features.
	 *
	 * @param string $action WordPress admin-ajax action name (without ?action=).
	 * @return string Full URL to the remote AJAX endpoint.
	 */
	function costudio_api_ajax_url( string $action = '' ): string {
		$url = rtrim( COSTUDIO_API_URL, '/' ) . COSTUDIO_REMOTE_AJAX_PATH;
		if ( '' !== $action ) {
			$url = add_query_arg( 'action', sanitize_key( $action ), $url );
		}
		return $url;
	}
}

if ( ! function_exists( 'costudio_sanitize_block_markup' ) ) {
	/**
	 * Sanitize Gutenberg block markup coming from a request.
	 *
	 * Block markup carries block-delimiter HTML comments (`<!-- wp:* -->` /
	 * `<!-- /wp:* -->`) that wp_kses_post() would strip, so a generic KSES pass
	 * cannot be used here without corrupting the content. Instead this validates
	 * the payload at the point of read: enforce valid UTF-8, remove NULL and
	 * control characters (the real XSS/MITM-relevant tainting vectors for a
	 * stored block payload), and cap the length. WordPress re-sanitizes the
	 * markup through wp_kses_post() again at insert time for any user without
	 * the `unfiltered_html` capability, so the stored HTML is filtered downstream.
	 *
	 * @param mixed $value     Raw (already wp_unslash-ed) request value.
	 * @param int   $max_chars Hard length cap (0 = no cap).
	 * @return string Sanitized block markup, or '' for non-scalars.
	 */
	function costudio_sanitize_block_markup( $value, int $max_chars = 40000 ): string {
		if ( ! is_scalar( $value ) ) {
			return '';
		}

		$content = wp_check_invalid_utf8( (string) $value, true );
		$content = str_replace( "\0", '', $content );
		$content = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $content );
		$content = is_string( $content ) ? $content : '';

		if ( $max_chars > 0 && mb_strlen( $content ) > $max_chars ) {
			$content = mb_substr( $content, 0, $max_chars );
		}

		return $content;
	}
}

if ( ! function_exists( 'costudio_decode_json_payload' ) ) {
	/**
	 * Validate + decode a JSON request payload into an array.
	 *
	 * Used for structured settings sent as a JSON string (e.g. Brand Voice).
	 * Enforces valid UTF-8, strips NULL bytes, caps the length, and decodes.
	 * The CALLER is expected to run a structure-specific sanitizer on the
	 * returned array (e.g. Costudio_Brand_Voice::sanitize()), which enforces the
	 * allowed keys/types — JSON decoding alone is not the final sanitization.
	 *
	 * @param mixed $value     Raw (already wp_unslash-ed) request value.
	 * @param int   $max_chars Hard length cap (0 = no cap).
	 * @return array Decoded associative array, or [] when invalid.
	 */
	function costudio_decode_json_payload( $value, int $max_chars = 30000 ): array {
		if ( ! is_scalar( $value ) ) {
			return array();
		}

		$json = wp_check_invalid_utf8( (string) $value, true );
		$json = str_replace( "\0", '', $json );
		if ( $max_chars > 0 && mb_strlen( $json ) > $max_chars ) {
			$json = mb_substr( $json, 0, $max_chars );
		}

		$decoded = json_decode( $json, true );

		return is_array( $decoded ) ? $decoded : array();
	}
}

if ( ! function_exists( 'costudio_encryption_secret' ) ) {
	/**
	 * Return the primary per-install secret used for Contentosapp Studio encryption.
	 *
	 * AUTH_KEY remains first to preserve decryptability of existing values.
	 * When AUTH_KEY is missing, wp_salt('auth') gives us a WordPress-managed
	 * per-install secret instead of the old universal hardcoded fallback.
	 */
	function costudio_encryption_secret(): string {
		if ( defined( 'AUTH_KEY' ) && is_string( AUTH_KEY ) && '' !== AUTH_KEY ) {
			return AUTH_KEY;
		}

		if ( function_exists( 'wp_salt' ) ) {
			$salt = wp_salt( 'auth' );
			if ( is_string( $salt ) && '' !== $salt ) {
				return $salt;
			}
		}

		foreach ( array( 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT' ) as $constant ) {
			if ( defined( $constant ) && is_string( constant( $constant ) ) && '' !== constant( $constant ) ) {
				return (string) constant( $constant );
			}
		}

		if ( function_exists( 'costudio_log_debug' ) ) {
			costudio_log_debug( 'options.missing_auth_salts' );
		}

		return ( defined( 'ABSPATH' ) ? ABSPATH : __DIR__ ) . '|' . home_url();
	}
}
