<?php
/**
 * DataStore class.
 *
 * @package DokanKits\Core\Data
 * @since   3.0.0
 */

namespace DokanKits\Core\Data;

use WC_Data;
use WC_Data_Store_WP;
use WC_Object_Data_Store_Interface;

/**
 * Data Store Base Class
 *
 * Handles storage and retrieval of data
 *
 * @since   3.0.0
 * @package DokanKits\Core\Data\Stores
 */
abstract class DataStore extends WC_Data_Store_WP implements WC_Object_Data_Store_Interface {
	/**
	 * Database version setting key
	 *
	 * @var string
	 */
	protected const DB_VERSION_KEY = 'db_version';

	/**
	 * Table name for the data
	 *
	 * @var string
	 */
	protected string $table_name;

	/**
	 * Meta type for data
	 *
	 * @var string
	 */
	protected $meta_type = 'data';

	/**
	 * Object type this store handles
	 *
	 * @var string
	 */
	protected $object_type = 'data';

	/**
	 * Data stored in meta keys, but not considered "meta" for an object.
	 *
	 * @var array
	 */
	protected $internal_meta_keys = array();


	/**
	 * Constructor
	 */
	public function __construct() {
		global $wpdb;
		$this->table_name               = $wpdb->prefix . 'dokan_kits_' . $this->meta_type;
		$this->object_id_field_for_meta = $this->meta_type . '_id';
	}

	/**
	 * Method to create a new record in the database
	 *
	 * @param WC_Data $data Data object.
	 *
	 * @return void
	 */
	abstract public function create( &$data ): void;

	/**
	 * Method to read data from the database
	 *
	 * @param WC_Data $data Data object.
	 *
	 * @return void
	 */
	abstract public function read( &$data ): void;

	/**
	 * Method to update data in the database
	 *
	 * @param WC_Data $data Data object.
	 *
	 * @return void
	 */
	abstract public function update( &$data ): void;

	/**
	 * Method to delete data from the database
	 *
	 * @param WC_Data $data Data object.
	 * @param array   $args Array of args to pass to the delete method.
	 *
	 * @return bool
	 */
	abstract public function delete( &$data, $args = array() ): bool;

	/**
	 * Update database version
	 *
	 * @return void
	 */
	public function update_db_version(): void {
		/**
		 * Action that fires after updating the database version
		 *
		 * @since 3.0.0
		 *
		 * @param string    $version The new database version.
		 * @param DataStore $this    The current data store instance.
		 */
		do_action( 'dokan_kits_datastore_update_db_version', DOKAN_KITS_VERSION, $this );
	}

	/**
	 * Prepare value for database storage
	 *
	 * @param mixed $value Value to prepare.
	 *
	 * @return string
	 */
	protected function prepare_value_for_db( $value ): string {
		/**
		 * Filter the value before storing it in the database.
		 *
		 * @since 3.0.0
		 *
		 * @param mixed     $value      The value being prepared.
		 * @param DataStore $data_store The current data store instance.
		 */
		$value = apply_filters( 'dokan_kits_prepare_value_for_db', $value, $this );

		// Handle arrays and objects by serializing them.
		if ( is_array( $value ) || is_object( $value ) ) {
			return maybe_serialize( $value );
		}

		return (string) $value;
	}

	/**
	 * Prepare value from database for use
	 *
	 * @param string|array|mixed $value Value from database.
	 *
	 * @return mixed
	 */
	protected function prepare_value_from_db( $value ) {
		$prepared_value = maybe_unserialize( $value );

		/**
		 * Filter the value after retrieving it from the database.
		 *
		 * @since 3.0.0
		 *
		 * @param mixed     $prepared_value The prepared value.
		 * @param string    $raw_value      The raw value from the database.
		 * @param DataStore $data_store     The current data store instance.
		 */
		return apply_filters( 'dokan_kits_prepare_value_from_db', $prepared_value, $value, $this );
	}

	/**
	 * Check if a string is JSON
	 *
	 * @param mixed $string_data String to check.
	 *
	 * @return bool
	 */
	protected function is_json( $string_data ): bool {
		if ( empty( $string_data ) || ! is_string( $string_data ) ) {
			return false;
		}

		$first_char = $string_data[0];
		$last_char  = substr( $string_data, -1 );

		// Quick check for JSON-like structure.
		if ( ( '{' === $first_char && '}' === $last_char ) ||
			( '[' === $first_char && ']' === $last_char ) ) {
			try {
				json_decode( $string_data, false, 512, JSON_THROW_ON_ERROR );
				return true;
			} catch ( \JsonException $e ) {
				return false;
			}
		}

		return false;
	}

	/**
	 * Clear caches for an object
	 *
	 * @param WC_Data $object Object to clear caches for.
	 *
	 * @return void
	 */
	public function clear_caches( &$object ): void {
		$id = $object->get_id();
		if ( $id ) {
			/**
			 * Action that fires after clearing caches for an object.
			 *
			 * @since 3.0.0
			 *
			 * @param int       $id     The object ID.
			 * @param WC_Data   $object The object.
			 * @param DataStore $this   The current data store instance.
			 */
			do_action( 'dokan_kits_clear_caches', $id, $object, $this );
		}
	}

	/**
	 * Helper method to process meta data
	 *
	 * @param array $raw_meta_data Raw meta data from DB.
	 *
	 * @return array
	 */
	protected function process_meta_data( array $raw_meta_data ): array {
		$meta_data = array();

		foreach ( $raw_meta_data as $meta ) {
			$meta_data[] = array(
				'id'    => $meta->meta_id,
				'key'   => $meta->meta_key,
				'value' => maybe_unserialize( $meta->meta_value ),
			);
		}

		/**
		 * Filter the processed meta data.
		 *
		 * @since 3.0.0
		 *
		 * @param array     $meta_data     The processed meta data.
		 * @param array     $raw_meta_data The raw meta data.
		 * @param DataStore $data_store    The current data store instance.
		 */
		return apply_filters( 'dokan_kits_process_meta_data', $meta_data, $raw_meta_data, $this );
	}

	/**
	 * Convert string date to timestamp
	 *
	 * @param string|int|null $time_string Time string.
	 *
	 * @return int
	 */
	protected function string_to_timestamp( $time_string ): int {
		if ( empty( $time_string ) ) {
			return 0;
		}

		if ( is_numeric( $time_string ) ) {
			return (int) $time_string;
		}

		return wc_string_to_timestamp( $time_string );
	}

	/**
	 * Create database tables
	 *
	 * @return void
	 */
	public function create_tables(): void {
		global $wpdb;

		$wpdb->hide_errors();

		$collate = '';

		if ( $wpdb->has_cap( 'collation' ) ) {
			$collate = $wpdb->get_charset_collate();
		}

		$tables = $this->get_table_schema( $collate );

		require_once ABSPATH . 'wp-admin/includes/upgrade.php'; // @phpstan-ignore-line

		/**
		 * Action before creating tables
		 *
		 * @since 3.0.0
		 *
		 * @param string    $tables Table schema SQL.
		 * @param DataStore $this   The current data store instance.
		 */
		do_action( 'dokan_kits_before_create_tables', $tables, $this );

		// Execute dbDelta.
		dbDelta( $tables );

		/**
		 * Action after creating tables
		 *
		 * @since 3.0.0
		 *
		 * @param string    $tables Table schema SQL.
		 * @param DataStore $this   The current data store instance.
		 */
		do_action( 'dokan_kits_after_create_tables', $tables, $this );

		// Verify tables exist and log results.
		$this->verify_tables();
	}

	/**
	 * Get table schema for creating tables
	 *
	 * @param string $collate Collation to use.
	 *
	 * @return string SQL for table creation.
	 */
	abstract protected function get_table_schema( string $collate ): string;

	/**
	 * Verify tables were created properly
	 *
	 * @return void
	 */
	public function verify_tables(): void {
		global $wpdb;

		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			$table_exists = $wpdb->get_var(
				$wpdb->prepare(
					'SHOW TABLES LIKE %s',
					$this->table_name
				)
			);

			$meta_table_name   = $this->table_name . 'meta';
			$meta_table_exists = $wpdb->get_var(
				$wpdb->prepare(
					'SHOW TABLES LIKE %s',
					$meta_table_name
				)
			);

			if ( function_exists( 'dokan_kits_logger' ) ) {
				dokan_kits_logger()->debug(
					'Dokan_Kits ' . ucfirst( $this->meta_type ) . ' Tables - Main table: ' . ( $table_exists ? 'Exists' : 'Missing' ) .
					', Meta table: ' . ( $meta_table_exists ? 'Exists' : 'Missing' )
				);
			}

			/**
			 * Action after verifying tables
			 *
			 * @since 3.0.0
			 *
			 * @param bool      $table_exists      Whether the main table exists.
			 * @param bool      $meta_table_exists Whether the meta table exists.
			 * @param DataStore $this              The current data store instance.
			 */
			do_action( 'dokan_kits_verify_tables', $table_exists, $meta_table_exists, $this );
		}
	}

	/**
	 * Get required columns for table verification
	 *
	 * @return array Array of required columns.
	 */
	abstract public function get_required_columns(): array;

	/**
	 * Get table name
	 *
	 * @return string Table name.
	 */
	public function get_table_name(): string {
		return $this->table_name;
	}

	/**
	 * Get meta type
	 *
	 * @return string Meta type.
	 */
	public function get_meta_type(): string {
		return $this->meta_type;
	}

	/**
	 * Get object type
	 *
	 * @return string Object type.
	 */
	public function get_object_type(): string {
		return $this->object_type;
	}


	/**
	 * Table structure is slightly different between meta types, this function will return what we need to know.
	 *
	 * @since  3.0.0
	 * @return array Array elements: table, object_id_field, meta_id_field.
	 */
	protected function get_db_info() {
		global $wpdb;

		return array(
			'table'           => "{$wpdb->prefix}dokan_kits_{$this->object_type}meta",
			'object_id_field' => "{$this->object_type}_id",
			'meta_id_field'   => 'meta_id',
		);
	}
}
