<?php

namespace Dobare\Settings\API;

use Dobare\Abstracts\Dobare_REST_Controller;
use Dobare\Utils\Dobare_Utils;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

class Dobare_Plugin_Settings_REST_Controller extends Dobare_REST_Controller
{
	/**
	 * Option prefixes owned by the Dobare plugin.
	 *
	 * @var string[]
	 */
	private const OPTION_PREFIXES = array(
		'DOBARE-',
		'DOB-',
		'dobare_',
	);

	/**
	 * Mapping between stored option keys and public field names.
	 *
	 * @var array<string,string>
	 */
	private const OPTION_FIELD_MAP = array(
		'DOBARE-ONBOARDED'                             => 'onboardingCompleted',
		'DOBARE-sync_order_status'                    => 'syncOrderStatus',
		'DOBARE-credit_with_discount_enabled'         => 'creditWithDiscountEnabled',
		'DOBARE-minimum_cart_total_for_credit'        => 'minimumCartSubtotal',
		'DOBARE-lead_magnet_without_login_plugin_enabled' => 'leadMagnetWithoutLoginEnabled',
		'DOBARE-custom_lead_magnet_enabled'           => 'customLeadMagnetEnabled',
		'DOBARE-custom_lead_magnet_html'              => 'customLeadMagnetHtmlDesktop',
		'DOBARE-custom_lead_magnet_css'               => 'customLeadMagnetCssDesktop',
		'DOBARE-custom_lead_magnet_mobile_html'       => 'customLeadMagnetHtmlMobile',
		'DOBARE-custom_lead_magnet_mobile_css'        => 'customLeadMagnetCssMobile',
		'DOBARE-customer_credit_indicator_enabled'    => 'customerCreditWidgetEnabled',
		'DOBARE-customer_credit_indicator_position'   => 'customerCreditWidgetPosition',
		'dobare_sync_customers_progress'              => 'customerSyncProgress',
	);

	/**
	 * Fields that should be cast to boolean before returning.
	 *
	 * @var string[]
	 */
	private const BOOLEAN_FIELDS = array(
		'onboardingCompleted',
		'creditWithDiscountEnabled',
		'customerCreditWidgetEnabled',
		'leadMagnetWithoutLoginEnabled',
		'customLeadMagnetEnabled',
		'abandonedCartEnabled',
		'leadMagnetAdvancedScopeCss',
		'leadMagnetOverrideScopeCss',
	);

	/**
	 * Exact-sensitive keys (normalized) that must never be exposed.
	 *
	 * @var string[]
	 */
	private const SENSITIVE_KEYS = array(
		'dobare_api_key',
		'dobare_old_api_key',
		'api-key',
		'api_key',
		'jwt_key',
		'jwt-secret',
		'jwt_token',
		'access_token',
		'refresh_token',
		'secret',
		'license_key',
		'hash',
		'password',
	);

	/**
	 * Keyword fragments that hint at sensitive data.
	 *
	 * @var string[]
	 */
	private const SENSITIVE_FRAGMENTS = array(
		'api',
		'token',
		'secret',
		'key',
		'password',
		'hash',
		'jwt',
		'nonce',
		'credential',
	);

	public function __construct()
	{
		$this->resource_name = 'plugin-settings';
	}

	/**
	 * Register Dobare public settings routes.
	 *
	 * @return void
	 */
	public function register_routes()
	{
		register_rest_route($this->namespace, '/' . $this->resource_name . '/', array(
			'methods'             => 'GET',
			'callback'            => array($this, 'get_plugin_settings'),
			// Endpoint is intentionally public; expose filter for site owners who want to restrict it.
			'permission_callback' => array($this, 'plugin_settings_permissions_check'),
		));
	}

	/**
	 * Public permission check with filter for hardening.
	 *
	 * @param WP_REST_Request $request Request instance.
	 *
	 * @return bool|\WP_Error
	 */
	public function plugin_settings_permissions_check(WP_REST_Request $request)
	{
		/**
		 * Filter whether the public settings endpoint should respond.
		 *
		 * @param bool|\WP_Error   $allowed Whether the request should proceed.
		 * @param WP_REST_Request  $request Current request.
		 */
		$allowed = apply_filters('dobare_public_settings_permissions_check', true, $request);

		return $allowed instanceof WP_Error ? $allowed : (bool) $allowed;
	}

	/**
	 * Provide all plugin settings (with sensitive data filtered/masked).
	 *
	 * @param WP_REST_Request $request Request instance.
	 *
	 * @return WP_REST_Response
	 */
	public function get_plugin_settings(WP_REST_Request $request)
	{
		$options  = $this->collect_plugin_options();
		$warnings = array();
		$sanitized_options = array();

		foreach ($options as $option_name => $value) {
			$field_name = $this->get_public_field_name($option_name, $warnings);
			if (empty($field_name)) {
				continue;
			}

			$sanitized_value = $this->scrub_option($option_name, $value, $warnings);
			$sanitized_options[$field_name] = $this->normalize_public_value($field_name, $sanitized_value);
		}

		/**
		 * Filter the public settings payload before it is returned.
		 *
		 * @param array $sanitized_options Masked options keyed by raw option name.
		 * @param array $raw_options       Original option values (potentially sensitive).
		 * @param array $warnings          Warning strings collected during masking.
		 */
		$filtered_options = apply_filters('dobare_public_settings_options', $sanitized_options, $options, $warnings);

		$payload = array(
			'active'         => $this->is_plugin_active(),
			'plugin_version' => defined('DOBARE_VERSION') ? DOBARE_VERSION : null,
			'options'        => $filtered_options,
			'warnings'       => array_values(array_unique($warnings)),
		);

		$response_body = Dobare_Utils::normalize_response_body($payload, 200);

		return new WP_REST_Response($response_body, 200);
	}

	/**
	 * Query the wp_options table for Dobare owned options.
	 *
	 * @return array<string, mixed>
	 */
	private function collect_plugin_options(): array
	{
		global $wpdb;

		if (!($wpdb instanceof \wpdb)) {
			return array();
		}

		$like_clauses = array();
		$like_values  = array();
		foreach (self::OPTION_PREFIXES as $prefix) {
			$like_clauses[] = 'option_name LIKE %s';
			$like_values[]  = $prefix . '%';
		}

		if (empty($like_clauses)) {
			return array();
		}

		$sql = 'SELECT option_name, option_value FROM ' . $wpdb->options . ' WHERE ' . implode(' OR ', $like_clauses);
		$query = $wpdb->prepare($sql, $like_values);

		if ($query === false) {
			return array();
		}

		$rows = $wpdb->get_results($query, ARRAY_A);
		if (!is_array($rows)) {
			return array();
		}

		$options = array();
		foreach ($rows as $row) {
			if (!isset($row['option_name'])) {
				continue;
			}
			$name = (string) $row['option_name'];
			$value = array_key_exists('option_value', $row) ? maybe_unserialize($row['option_value']) : null;
			$options[$name] = $value;
		}

		return $options;
	}

	/**
	 * Map raw option name to public field name while filtering sensitive entries.
	 *
	 * @param string $option_name Raw option key from wp_options.
	 * @param array  $warnings    Warning accumulator.
	 *
	 * @return string|null
	 */
	private function get_public_field_name(string $option_name, array &$warnings): ?string
	{
		$normalized_key = $this->normalize_option_key($option_name);

		if ($this->is_sensitive_key($normalized_key)) {
			$this->warn_sensitive_path($option_name, $warnings);

			return null;
		}

		if (isset(self::OPTION_FIELD_MAP[$option_name])) {
			return self::OPTION_FIELD_MAP[$option_name];
		}

		$field_name = $this->generate_field_name($normalized_key);

		/**
		 * Filter the computed public field name.
		 *
		 * @param string|null $field_name     Newly generated field name.
		 * @param string      $option_name    Raw option key.
		 * @param string      $normalized_key Normalized key without prefixes.
		 */
		$field_name = apply_filters('dobare_public_settings_field_name', $field_name, $option_name, $normalized_key);

		return is_string($field_name) && $field_name !== '' ? $field_name : null;
	}

	/**
	 * Apply masking rules for a non-sensitive option.
	 *
	 * @param string $option_name Complete option key stored in wp_options.
	 * @param mixed  $value       Option value.
	 * @param array  $warnings    Warning list (passed by reference).
	 *
	 * @return mixed
	 */
	private function scrub_option(string $option_name, $value, array &$warnings)
	{
		return $this->scrub_value($value, array($option_name), $warnings);
	}

	/**
	 * Normalize outgoing values (type casting, etc.).
	 *
	 * @param string $field_name Public field name.
	 * @param mixed  $value      Sanitized value.
	 *
	 * @return mixed
	 */
	private function normalize_public_value(string $field_name, $value)
	{
		if (in_array($field_name, self::BOOLEAN_FIELDS, true)) {
			return $this->cast_to_boolean($value);
		}

		return $value;
	}

	/**
	 * Convert mixed values into strict boolean.
	 *
	 * @param mixed $value Original value.
	 *
	 * @return bool
	 */
	private function cast_to_boolean($value): bool
	{
		if (is_bool($value)) {
			return $value;
		}

		if (is_numeric($value)) {
			return (int) $value !== 0;
		}

		if (is_string($value)) {
			$value = strtolower(trim($value));

			if ($value === '' || $value === '0' || $value === 'false' || $value === 'no' || $value === 'off') {
				return false;
			}

			return true;
		}

		return (bool) $value;
	}

	/**
	 * Recursively scrub arrays / objects.
	 *
	 * @param mixed $value   Value to inspect.
	 * @param array $path    Breadcrumbs for warning context.
	 * @param array $warnings Warning list (passed by reference).
	 *
	 * @return mixed
	 */
	private function scrub_value($value, array $path, array &$warnings)
	{
		if (is_array($value)) {
			$clean = array();
			foreach ($value as $key => $item) {
				$clean[$key] = $this->scrub_nested_value($key, $item, $path, $warnings);
			}

			return $clean;
		}

		if (is_object($value)) {
			$object_vars = get_object_vars($value);

			return $this->scrub_value($object_vars, $path, $warnings);
		}

		return $value;
	}

	/**
	 * Scrub nested values by key.
	 *
	 * @param mixed  $key       Nested key (string|int).
	 * @param mixed  $value     Nested value.
	 * @param array  $path      Breadcrumbs for warning context.
	 * @param array  $warnings  Warning accumulator.
	 *
	 * @return mixed
	 */
	private function scrub_nested_value($key, $value, array $path, array &$warnings)
	{
		$child_path = $path;
		$child_path[] = (string) $key;

		if (is_string($key)) {
			$normalized_child_key = $this->normalize_option_key($key);
			if ($this->is_sensitive_key($normalized_child_key)) {
				$this->warn_sensitive_path(implode(' > ', $child_path), $warnings);

				return $this->mask_value($value);
			}
		}

		return $this->scrub_value($value, $child_path, $warnings);
	}

	/**
	 * Normalize an option key by stripping plugin prefixes and lowercasing.
	 *
	 * @param string $key Original key.
	 *
	 * @return string
	 */
	private function normalize_option_key(string $key): string
	{
		$normalized = $key;

		foreach (self::OPTION_PREFIXES as $prefix) {
			if (stripos($normalized, $prefix) === 0) {
				$normalized = substr($normalized, strlen($prefix));
				break;
			}
		}

		return strtolower($normalized);
	}

	/**
	 * Convert normalized keys to camelCase field names.
	 *
	 * @param string $normalized_key Prefix-stripped key.
	 *
	 * @return string|null
	 */
	private function generate_field_name(string $normalized_key): ?string
	{
		if ($normalized_key === '') {
			return null;
		}

		$parts = preg_split('/[^a-z0-9]+/i', $normalized_key);
		$parts = array_values(array_filter($parts, static function ($part) {
			return $part !== '';
		}));

		if (empty($parts)) {
			return null;
		}

		$field = strtolower(array_shift($parts));
		foreach ($parts as $part) {
			$field .= ucfirst(strtolower($part));
		}

		return $field;
	}

	/**
	 * Detect whether a key represents sensitive data.
	 *
	 * @param string $normalized_key Lower-cased, prefix-stripped key.
	 *
	 * @return bool
	 */
	private function is_sensitive_key(string $normalized_key): bool
	{
		if ($normalized_key === '') {
			return false;
		}

		if (in_array($normalized_key, self::SENSITIVE_KEYS, true)) {
			return true;
		}

		foreach (self::SENSITIVE_FRAGMENTS as $fragment) {
			if (strpos($normalized_key, $fragment) !== false) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Replace sensitive values with a masked placeholder.
	 *
	 * @param mixed $value Original value.
	 *
	 * @return string
	 */
	private function mask_value($value): string
	{
		if (is_string($value)) {
			$length = strlen($value);

			if ($length <= 4) {
				return str_repeat('*', $length ?: 4);
			}

			return substr($value, 0, 2) . '...' . substr($value, -2);
		}

		return '[masked]';
	}

	/**
	 * Log and store warnings for masked data paths.
	 *
	 * @param string $path     Human readable identifier.
	 * @param array  $warnings Warning accumulator.
	 *
	 * @return void
	 */
	private function warn_sensitive_path(string $path, array &$warnings): void
	{
		$message = sprintf('[Dobare] Public settings endpoint filtered sensitive data at "%s"', $path);
		error_log($message);
		$warnings[] = $message;

		do_action('dobare_public_settings_masked_value', $path);
	}

	/**
	 * Detect whether the plugin is active at runtime.
	 *
	 * @return bool
	 */
	private function is_plugin_active(): bool
	{
		$plugin_basename = function_exists('plugin_basename') ? plugin_basename(DOBARE_MAIN_PLUGIN_FILE) : 'dobare/dobare.php';

		if (function_exists('is_plugin_active')) {
			return (bool) is_plugin_active($plugin_basename);
		}

		$active_plugins = (array) get_option('active_plugins', array());

		return in_array($plugin_basename, $active_plugins, true);
	}
}

