<?php

namespace Limb_Chatbot\Includes\Database_Strategies;

use Limb_Chatbot\Includes\Database_Strategy_Interface;
use Limb_Chatbot\Includes\Traits\SingletonTrait;

/**
 * Class WP_Options
 *
 * Implements a database strategy for managing WordPress options via the Options API.
 * Supports creating, updating, finding, querying, deleting, and checking existence of options.
 *
 * Utilizes WordPress functions get_option, add_option, update_option, and delete_option.
 *
 * @since 1.0.0
 */
class WP_Options extends Database_Strategy implements Database_Strategy_Interface {

	use SingletonTrait;

	/**
	 * Create a new option if it does not already exist.
	 *
	 * @param array $data Associative array with keys:
	 *                    - 'key' (string): Option name.
	 *                    - 'value' (mixed): Option value.
	 * @return array|false Returns array with 'key' and 'value' on success, false on failure.
	 * @since 1.0.0
	 */
	public function create( $data ) {
		if ( ! isset( $data['value'] ) || ! isset( $data['key'] ) ) {
			return false;
		}
		$data['value'] = $this->sanitize_value_for_options( $data['value'] );
		$existing_value = get_option( $data['key'] );
		if ( $existing_value === false ) {
			if ( add_option( $data['key'], $data['value'] ) ) {
				return [ 'key' => $data['key'], 'value' => get_option( $data['key'], null ) ];
			} else {
				return false;
			}
		}

		return [ 'key' => $data['key'], 'value' => $existing_value ];
	}

	/**
	 * Update an existing option or create it if it does not exist.
	 *
	 * Prevents race conditions by verifying the update was successful.
	 *
	 * @param array $where Associative array with key 'key' (string): Option name.
	 * @param array $data Associative array with key 'value' (mixed): New option value.
	 * @return array|false Returns array with updated 'key' and 'value' on success, false on failure.
	 * @since 1.0.0
	 */
	public function update( $where, $data ) {
		if ( ! isset( $data['value'] ) || ! isset( $where['key'] ) ) {
			return false;
		}
		$data['value'] = $this->sanitize_value_for_options( $data['value'] );
		$existing_value = get_option( $where['key'] );
		if ( $existing_value === false ) {
			return $this->create( [ 'key' => $where['key'], 'value' => $data['value'] ] );
		} elseif ( $existing_value === $data['value'] ) {
			return [ 'key' => $where['key'], 'value' => $existing_value ];
		}
		// Solution for race condition
		if ( update_option( $where['key'], $data['value'] ) || get_option( $where['key'] ) == $data['value'] ) {
			return [
				'key'   => $where['key'],
				'value' => get_option( $where['key'] ),
			];
		}

		return false;
	}

	/**
	 * Find an option by its key.
	 *
	 * Note: Always returns an array with keys 'key' and 'value'.
	 * If option does not exist, 'value' will be null.
	 *
	 * @param string $id Option name.
	 * @return array Associative array with keys 'key' and 'value'.
	 * @since 1.0.0
	 */
	public function find( $id ) {
		// TODO the fact that array is always returned instead of null on not-found case is doubtable
		return [ 'key' => $id, 'value' => get_option( $id, null ) ];
	}

	/**
	 * Retrieve options matching specified conditions.
	 *
	 * Supports:
	 * - option_nameLIKE for pattern matching keys via direct DB query
	 * - option_name as array of keys to get multiple options
	 * - option_name as string to get a single option
	 *
	 * @param array $conditions Associative array of conditions.
	 * @param mixed ...$args Additional arguments (not used).
	 * @return array|null List of matched options as ['key' => string, 'value' => mixed] or null if none found.
	 * @since 1.0.0
	 */
	public function where( $conditions, ...$args ) {
		$for_unique_default = md5( 'default' );
		if ( array_key_exists( 'option_nameLIKE', $conditions ) ) {
			$results = ( new WPDB() )->where( $conditions, 'options' );
			foreach ( $results as $result ) {
				$filtered_results[] = [ 'key' => $result['option_name'], 'value' => maybe_unserialize( $result['option_value'] ) ];
			}

			return $filtered_results ?? [];
		} elseif ( isset( $conditions['option_name'] ) && is_array( $conditions['option_name'] ) ) {
			$results = ( new WPDB() )->where( $conditions, 'options' );

			foreach ( $results as $result ) {
				$filtered_results[] = [ 
					'key' => $result['option_name'], 
					'value' => maybe_unserialize( $result['option_value'] ) 
				];
			}

			return $filtered_results ?? [];
		} else {
			$value = get_option( $conditions['option_name'], $for_unique_default );

			return $value !== $for_unique_default ? [ 'key' => $conditions['option_name'], 'value' => $value ] : null;
		}
	}

	/**
	 * Count matching options.
	 *
	 * @param array $conditions Conditions for filtering options.
	 * @param mixed ...$args Additional arguments (not used).
	 * @return void Count of matching options or null if not implemented.
	 * @since 1.0.0
	 */
	public function count( $conditions, ...$args ) {
	}

	/**
	 * Delete an option by key.
	 *
	 * @param array $where Associative array with 'key' => option name.
	 * @return bool True on success, false on failure.
	 * @since 1.0.0
	 */
	public function delete( $where ) {
		return delete_option( $where['key'] );
	}

	/**
	 * Check if an option exists.
	 *
	 * @param string $id Option name.
	 * @return bool True if option exists, false otherwise.
	 * @since 1.0.0
	 */
	public function exists( $id ) {
		$setting = get_option( $id, '__not_set__' );

		return $setting !== '__not_set__';
	}

	/**
	 * Sanitize a value for storage in the options table when it does not support emojis.
	 *
	 * Strips 4-byte UTF-8 (emojis) from strings; recursively sanitizes arrays.
	 *
	 * @param mixed $value Option value (string, array, scalar).
	 * @return mixed Sanitized value.
	 * @since 1.0.15
	 */
	protected function sanitize_value_for_options( $value ) {
		if ( WPDB::instance()->table_supports_emojis( 'options' ) ) {
			return $value;
		}
		if ( is_string( $value ) ) {
			return WPDB::strip_4byte_utf8( $value );
		}
		if ( is_array( $value ) ) {
			foreach ( $value as $k => $v ) {
				$value[ $k ] = $this->sanitize_value_for_options( $v );
			}
		}

		return $value;
	}
}