<?php

namespace SugarCalendarBookings\Support\Database;

use SugarCalendarBookings\Support\Database\Relations\HasManyRelation;

/**
 * Trait for handling meta table functionality.
 *
 * @since 1.0.0
 */
trait HasMeta {

	/**
	 * Meta keys for the model.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, mixed>
	 */
	protected static $meta_keys = [];

	/**
	 * Meta defaults cache for each model class.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, array<string, mixed>>
	 */
	private static $meta_defaults_cache = [];

	/**
	 * Whether meta data has been loaded.
	 *
	 * @since 1.0.0
	 *
	 * @var bool
	 */
	protected $meta_loaded = false;

	/**
	 * The meta data for the model.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $meta = [];

	/**
	 * The meta data that has been changed.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	protected $meta_changes = [];

	/**
	 * Meta defaults for the model.
	 *
	 * This should be overridden in the model to provide default meta values.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed>
	 */
	protected static function meta_defaults() {

		return [];
	}

	/**
	 * Get a meta value.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Meta key.
	 *
	 * @return mixed
	 */
	public function get_meta( $key ) {

		if ( ! $this->meta_loaded ) {
			$this->load_meta();
		}

		return $this->meta[ $key ] ?? static::get_meta_defaults()[ $key ] ?? null;
	}

	/**
	 * Set a meta value.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key   Meta key.
	 * @param mixed  $value Meta value.
	 *
	 * @return self
	 */
	public function set_meta( $key, $value ) {

		if ( $this->is_meta( $key ) ) {
			// Meta values are always strings.
			if ( $value !== null ) {
				$value = (string) $this->cast_value( $key, $value, 'set' );
			}

			$this->meta[ $key ]         = $value;
			$this->meta_changes[ $key ] = $value;
		}

		return $this;
	}

	/**
	 * Delete a meta value.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Meta key.
	 *
	 * @return self
	 */
	public function delete_meta( $key ) {

		unset( $this->meta[ $key ] );
		$this->meta_changes[ $key ] = null;

		return $this;
	}

	/**
	 * Get all meta data.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_all_meta() {

		return $this->meta;
	}

	/**
	 * Check if meta exists.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Meta key.
	 *
	 * @return bool
	 */
	public function has_meta( $key ) {

		return isset( $this->meta[ $key ] );
	}

	/**
	 * Get meta changes.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_meta_changes() {

		return $this->meta_changes;
	}

	/**
	 * Check if meta has been modified.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function is_meta_dirty() {

		return ! empty( $this->meta_changes );
	}

	/**
	 * Determine if the given key is an meta of the model.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Meta key.
	 *
	 * @return bool
	 */
	public function is_meta( $key ) {

		return in_array( $key, $this->get_meta_keys(), true );
	}

	/**
	 * Set raw meta values.
	 *
	 * @since 1.0.0
	 *
	 * @param array $values Meta values.
	 *
	 * @return self
	 */
	public function set_raw_meta( $values ) {

		$values = array_intersect_key( $values, $this->get_meta_keys() );

		$this->meta         = $values;
		$this->meta_changes = [];
		$this->meta_loaded  = true;

		return $this;
	}

	/**
	 * Save meta data to the database.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	protected function save_meta() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		if ( empty( $this->get_id() ) ) {
			return false;
		}

		if ( empty( $this->get_meta_changes() ) ) {
			return true;
		}

		$to_delete = [];
		$to_update = [];

		foreach ( $this->get_meta_changes() as $meta_key => $meta_value ) {
			if ( $meta_value === null ) {
				$to_delete[] = $meta_key;
			} else {
				$to_update[ $meta_key ] = $meta_value;
			}
		}

		if ( ! empty( $to_delete ) ) {
			( new QueryBuilder() )
				->table( 'meta' )
				->where( 'object_type', '=', static::$type, '%s' )
				->where( 'object_id', '=', $this->get_id(), '%d' )
				->where( 'meta_key', 'IN', $to_delete, '%s' )
				->delete();
		}

		// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
		if ( ! empty( $to_update ) ) {
			$rows = [];

			foreach ( $to_update as $meta_key => $meta_value ) {
				if ( is_array( $meta_value ) || is_object( $meta_value ) ) {
					$meta_value = wp_json_encode( $meta_value );
				}

				$rows[] = [
					'object_type' => static::$type,
					'object_id'   => $this->get_id(),
					'meta_key'    => $meta_key,
					'meta_value'  => $meta_value,
				];
			}

			$placeholders = [
				'object_type' => '%s',
				'object_id'   => '%d',
				'meta_key'    => '%s',
				'meta_value'  => '%s',
			];

			$result = ( new QueryBuilder() )
				->table( 'meta' )
				->upsert( $rows, [ 'meta_value' ], $placeholders );

			if ( $result === false ) {
				return false;
			}
		}
		// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value

		$this->meta_changes = [];

		return true;
	}

	/**
	 * Delete all meta data for the model.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function delete_all_meta() {

		( new QueryBuilder() )
			->table( 'meta' )
			->where( 'object_type', '=', static::$type, '%s' )
			->where( 'object_id', '=', $this->get_id(), '%d' )
			->delete();
	}

	/**
	 * Load meta data from the database.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function load_meta() {

		if ( empty( $this->get_id() ) ) {
			return;
		}

		$results    = $this->get_relation_value( 'meta' ) ?? [];
		$this->meta = [];

		foreach ( $results as $meta ) {
			$this->meta[ $meta->meta_key ] = $this->maybe_json_decode( $meta->meta_value );
		}

		$this->meta_loaded = true;
	}

	/**
	 * Get the list of meta keys.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_meta_keys() {

		return static::$meta_keys;
	}

	/**
	 * Get the default meta values.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public static function get_meta_defaults() {

		$class = static::class;

		if ( ! isset( self::$meta_defaults_cache[ $class ] ) ) {
			self::$meta_defaults_cache[ $class ] = static::meta_defaults();
		}

		return self::$meta_defaults_cache[ $class ];
	}

	/**
	 * Register a meta field.
	 *
	 * @since 1.0.0
	 *
	 * @param string $meta_key      Meta key.
	 * @param mixed  $default_value Default value.
	 *
	 * @return void
	 */
	public static function register_meta_field( $meta_key, $default_value = null ) {

		if ( ! in_array( $meta_key, static::$meta_keys, true ) ) {
			static::$meta_keys[] = $meta_key;
		}

		$meta_defaults = static::get_meta_defaults();

		if ( $default_value !== null && ! isset( $meta_defaults[ $meta_key ] ) ) {
			self::$meta_defaults_cache[ static::class ][ $meta_key ] = $default_value;
		}
	}

	/**
	 * Maybe JSON decode a value.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Value to decode.
	 *
	 * @return mixed
	 */
	private function maybe_json_decode( $value ) {

		if ( ! is_string( $value ) ) {
			return $value;
		}

		$first_char = trim( $value )[0] ?? '';

		if ( $first_char !== '{' && $first_char !== '[' ) {
			return $value; // Not JSON.
		}

		$decoded = json_decode( $value, true );

		return json_last_error() === JSON_ERROR_NONE ? $decoded : $value;
	}

	/**
	 * Get the meta relationship.
	 *
	 * @since 1.0.0
	 *
	 * @return HasManyRelation
	 */
	public function meta() {

		return $this->has_many( MetaModel::class, 'object_id' )
			->where( 'object_type', '=', static::get_type(), '%s' );
	}
}
