<?php

namespace Limb_Chatbot\Includes\Data_Objects;

use Exception;
use Limb_Chatbot\Includes\Database_Strategies\WPDB;
use Limb_Chatbot\Includes\Services\Collection;
use Limb_Chatbot\Includes\Services\Data_Object_Collection;

/**
 * Class WPDB_Data_Object
 *
 * Base data object for models using the WPDB database strategy.
 *
 * @since 1.0.0
 */
class WPDB_Data_Object extends Data_Object {

	/**
	 * Primary ID of the object.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $id = null;

	/**
	 * Find objects matching the given conditions.
	 *
	 * @param array $where
	 * @param mixed ...$args
	 * @return Data_Object_Collection
	 * @since 1.0.0
	 */
	public static function where( $where, ...$args ): Data_Object_Collection {
		array_unshift( $args, static::TABLE_NAME );
		$results = static::get_db_strategy()->where( static::filter_where( $where ), ...$args );
		foreach ( $results as $result ) {
			$item      = static::make( $result );
			$objects[] = $item;
		}

		return new Data_Object_Collection( $objects ?? [] );
	}

	/**
	 * Filter the $where array to only include fillable fields.
	 *
	 * @param array $where
	 * @return array
	 * @since 1.0.0
	 */
	protected static function filter_where( $where ) {
		$id          = $where['id'] ?? null;
		$json_params = $where['json_params'] ?? null;
		$where       = array_filter( $where, function ( $value, $key ) {
			return static::is_fillable( $key );
		}, ARRAY_FILTER_USE_BOTH );
		if ( isset( $id ) ) {
			$where['id'] = $id;
		}
		if ( isset( $json_params ) ) {
			$where['json_params'] = $json_params;
		}

		return $where;
	}

	/**
	 * Check if a key is fillable.
	 *
	 * @param string $key
	 * @return bool
	 * @since 1.0.0
	 */
	protected static function is_fillable( $key ): bool {
		$key = preg_replace( '/(NOT IN|>=|<=|>|<|!=|=|LIKE|IS NOT|IS)$/', '', $key );
		$key = trim( $key );

		return defined( 'static::FILLABLE' ) && in_array( $key, static::FILLABLE );
	}

	/**
	 * Get the WPDB strategy instance.
	 *
	 * @return WPDB
	 * @since 1.0.0
	 */
	static function get_db_strategy(): WPDB {
		return WPDB::instance();
	}

	/**
	 * Delete records matching the given where clause.
	 *
	 * @param array $where
	 * @return bool
	 * @since 1.0.0
	 */
	public static function delete( $where ): bool {
		return static::get_db_strategy()->delete( $where, static::TABLE_NAME );
	}

	/**
	 * Count matching records.
	 *
	 * @param array $where
	 * @param mixed ...$args
	 * @return int|null
	 * @since 1.0.0
	 */
	public static function count( $where = [], ...$args ): ?int {
		return static::get_db_strategy()->count( static::filter_where( $where ), static::TABLE_NAME, ...$args );
	}

	/**
	 * Find a single object by ID.
	 *
	 * @param int $id
	 * @return static|null
	 * @since 1.0.0
	 */
	public static function find( $id ) {
		if ( empty( $id ) ) {
			return null;
		}
		$res = static::get_db_strategy()->find( $id, static::TABLE_NAME );

		return ! empty( $res ) ? static::make( $res ) : null;
	}

	/**
	 * Get the ID of the object.
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Set the ID of the object.
	 *
	 * @param null|int $id
	 * @return void
	 * @since 1.0.0
	 */
	public function set_id( $id ) {
		$this->id = $id;
	}

	/**
	 * Save the object (create or update).
	 *
	 * @return WPDB_Data_Object
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function save() {
		$for_db = $data = $this->to_internal_array();
		array_walk( $for_db, function ( &$value, $key ) {
			if ( in_array( $key, static::FILLABLE ) && ( is_array( $value ) || is_object( $value ) || $value instanceof Collection ) ) {
				$value = json_encode( $value );
			}
		} );
		if ( empty( $this->id ) ) {
			$db_data = static::get_db_strategy()->create( $for_db, static::TABLE_NAME, static::FILLABLE );
		} else {
			$db_data = static::get_db_strategy()->update( [ 'id' => $this->id ], $for_db, static::TABLE_NAME, static::FILLABLE );
		}
		unset( $for_db );
		foreach ( $db_data as $key => $value ) {
			if ( ! array_key_exists( $key, $data ) || $data[ $key ] === null ) {
				$data[ $key ] = $value;
			}
		}
		$this->set_data( $data );
		$this->hydrate();

		return $this;
	}

	/**
	 * Create a new object.
	 *
	 * @param  array  $data
	 *
	 * @return static
	 * @throws Exception
	 * @since 1.0.0
	 */
	public static function create( $data ) {
		foreach ( $data as $key => $value ) {
			if ( is_object( $key ) || is_array( $value ) ) {
				$data[ $key ] = json_encode( $value );
			}
		}
		$res = self::get_db_strategy()->create( $data, static::TABLE_NAME, static::FILLABLE );

		return static::make( $res );
	}

	/**
	 * Update an object based on the given criteria.
	 *
	 * @param array $where
	 * @param array $data
	 * @return static
	 * @since 1.0.0
	 */
	public static function update( $where, $data ) {
		foreach ( $data as $key => $value ) {
			if ( is_array( $key ) || is_array( $value ) ) {
				$data[ $key ] = json_encode( $value );
			}
		}
		$res = static::get_db_strategy()->update( $where, $data, static::TABLE_NAME, static::FILLABLE );

		return static::make( $res );
	}

	public function touch(): void {
		if ( property_exists( $this, 'updated_at' ) ) {
			$this->updated_at = current_time( 'mysql' );
			$this->save();
		}
	}
}