<?php

namespace SugarCalendarBookings\Support\Database;

use DateTimeInterface;
use Exception;
use SugarCalendarBookings\DeleteManager;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Database\Relations\BelongsToManyRelation;
use SugarCalendarBookings\Support\Database\Relations\BelongsToRelation;
use SugarCalendarBookings\Support\Database\Relations\HasManyRelation;
use SugarCalendarBookings\Support\Database\Relations\HasOneRelation;
use SugarCalendarBookings\Support\Database\Relations\MorphManyRelation;
use SugarCalendarBookings\Support\Helpers\Date;
use SugarCalendarBookings\Support\Helpers\DB;

/**
 * Abstract Model Class.
 *
 * Base class for all models in the Sugar Calendar Bookings plugin.
 * Provides common functionality for database operations and attribute management.
 *
 * @since 1.0.0
 */
abstract class AbstractModel {

	/**
	 * Meta data trait.
	 *
	 * Any model can have meta data associated with it.
	 *
	 * @since 1.0.0
	 */
	use HasMeta;

	/**
	 * The database table name without prefix.
	 *
	 * This should be set in the subclass.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected static $table_name = '';

	/**
	 * The model type identifier.
	 *
	 * This should be set in the subclass.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	protected static $type = '';

	/**
	 * Column formats for the model.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, string>
	 */
	protected static $column_formats = [];

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

	/**
	 * The attributes and meta that should be cast.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, string>
	 */
	protected static $casts = [];

	/**
	 * The single instance model cache.
	 *
	 * @since 1.0.0
	 *
	 * @var array<int, AbstractModel>
	 */
	private static $cache = [];

	/**
	 * The model's attributes.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, mixed>
	 */
	protected $attributes = [];

	/**
	 * The model's original attributes.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, mixed>
	 */
	protected $original = [];

	/**
	 * The attributes that have been changed.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, mixed>
	 */
	protected $changes = [];

	/**
	 * The model's previous attributes (before the last update).
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, mixed>
	 */
	protected $previous = [];

	/**
	 * The model's loaded relationships.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, AbstractModel>
	 */
	protected $relations = [];

	/**
	 * Whether lazy loading of relationships is allowed for this model instance.
	 *
	 * Only allowed when the model is loaded individually.
	 *
	 * @since 1.0.0
	 *
	 * @var bool
	 */
	public $allow_lazy_loading = true;

	/**
	 * Pivot data for many-to-many relationships.
	 *
	 * Contains additional data from the pivot table when using belongs_to_many relationships.
	 *
	 * @since 1.0.0
	 *
	 * @var mixed|null
	 */
	public $pivot = null;

	/**
	 * Column defaults for the model.
	 *
	 * This should be set in the subclass.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed>
	 */
	protected static function column_defaults() {

		return [];
	}

	/**
	 * Create a new model instance.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $data Model data. Can contain attributes and meta data.
	 */
	public function __construct( $data = [] ) {

		$this->fill( $data );
	}

	/**
	 * Fill the model attributes and meta data.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $data Model data.
	 *
	 * @return self
	 */
	protected function fill( $data ) {

		$attributes = array_intersect_key( $data, array_flip( $this->get_columns() ) );
		$meta       = array_intersect_key( $data, array_flip( $this->get_meta_keys() ) );

		foreach ( $attributes as $key => $value ) {
			$this->set_attribute( $key, $value );
		}

		foreach ( $meta as $key => $value ) {
			$this->set_meta( $key, $value );
		}

		return $this;
	}

	/**
	 * Dynamically retrieve attributes on the model.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Attribute key.
	 *
	 * @return mixed|null
	 *
	 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing
	 */
	public function __get( $key ) {

		$value = null;

		if ( $this->is_attribute( $key ) ) {
			$value = $this->get_attribute( $key );
			$value = $this->cast_value( $key, $value, 'get' );
		} elseif ( $this->is_meta( $key ) ) {
			$value = $this->get_meta( $key );
			$value = $this->cast_value( $key, $value, 'get' );
		} elseif ( $this->is_relation( $key ) ) {
			$value = $this->get_relation_value( $key );
		} elseif ( defined( 'SCBOOKINGS_DEV_MODE' ) && SCBOOKINGS_DEV_MODE ) {
			throw new Exception( 'Accessing undefined property: ' . esc_html( static::class ) . '::$' . esc_html( $key ) );
		}

		return $value;
	}

	/**
	 * Dynamically set attributes, meta, or relationship properties on the model.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key   Attribute, meta, or relation key.
	 * @param mixed  $value Value to set.
	 *
	 * @return void
	 *
	 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing
	 */
	public function __set( $key, $value ) {

		if ( $key === 'pivot' ) {
			$this->pivot = $value;
		} elseif ( $this->is_attribute( $key ) ) {
			$this->set_attribute( $key, $value );
		} elseif ( $this->is_meta( $key ) ) {
			$this->set_meta( $key, $value );
		} elseif ( $this->is_relation( $key ) ) {
			$this->relations[ $key ] = $value;
		} elseif ( defined( 'SCBOOKINGS_DEV_MODE' ) && SCBOOKINGS_DEV_MODE ) {
			throw new Exception( 'Setting undefined property: ' . esc_html( static::class ) . '::$' . esc_html( $key ) );
		}
	}

	/**
	 * Determine if model has a provided property.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Attribute, meta, or relation key.
	 *
	 * @return bool
	 */
	public function __isset( $key ) {

		return $this->is_attribute( $key ) || $this->is_meta( $key ) || $this->is_relation( $key );
	}

	/**
	 * Set raw attributes on the model without marking them as changed.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $attributes Model attributes.
	 *
	 * @return self
	 */
	public function set_raw_attributes( $attributes ) {

		$attributes = array_intersect_key( $attributes, array_flip( $this->get_columns() ) );

		$this->attributes = $attributes;
		$this->original   = $attributes;
		$this->previous   = $attributes;
		$this->changes    = [];

		return $this;
	}

	/**
	 * Get the model's ID.
	 *
	 * @since 1.0.0
	 *
	 * @return int The model's ID.
	 */
	public function get_id() {

		return $this->id;
	}

	/**
	 * Set a given attribute on the model.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key   Attribute key.
	 * @param mixed  $value Attribute value.
	 *
	 * @return self
	 */
	public function set_attribute( $key, $value ) {

		if ( $this->is_attribute( $key ) ) {
			$value                    = $this->cast_value( $key, $value, 'set' );
			$this->attributes[ $key ] = $value;
			$this->changes[ $key ]    = $value;
		}

		return $this;
	}

	/**
	 * Get an attribute from the model (raw - not casted).
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Attribute key.
	 *
	 * @return mixed|null The attribute value or null if not set.
	 */
	public function get_attribute( $key ) {

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

	/**
	 * Get all of the current attributes on the model (raw - not casted).
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed> The model's attributes.
	 */
	public function get_attributes() {

		return $this->attributes;
	}

	/**
	 * Get the attributes that have been changed (raw - not casted).
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed> The changed attributes.
	 */
	public function get_changes() {

		return $this->changes;
	}

	/**
	 * Get the previous value(s) of attribute(s) (casted).
	 *
	 * @since 1.0.0
	 *
	 * @param string|null $key Optional attribute key. If null, returns all previous attributes.
	 *
	 * @return mixed|array<string, mixed> The previous attribute value or all previous attributes.
	 */
	public function get_previous( $key = null ) {

		if ( $key === null ) {
			return $this->previous;
		}

		return $this->cast_value( $key, $this->previous[ $key ] ?? null, 'get' );
	}

	/**
	 * Determine if a specific attribute has changed.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Attribute key.
	 *
	 * @return bool True if the attribute has changed, false otherwise.
	 */
	public function has_changed( $key ) {

		// Compare raw values (that's why "set" cast direction is used).
		$previous_value = $this->cast_value( $key, $this->previous[ $key ] ?? null, 'set' );
		$current_value  = $this->cast_value( $key, $this->attributes[ $key ] ?? null, 'set' );

		return $current_value !== $previous_value;
	}

	/**
	 * Determine if the model has been modified.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function is_dirty() {

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

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

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

	/**
	 * Save the model to the database.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function save() {

		if ( empty( $this->get_id() ) ) {
			$is_inserted = $this->insert();

			if ( $is_inserted ) {
				$meta_saved = $this->save_meta();

				// Prevent partially saved models.
				if ( ! $meta_saved ) {
					$this->delete( false );

					return false;
				}
			}

			return $is_inserted;
		} elseif ( $this->is_dirty() ) {
			return $this->update() && $this->save_meta();
		}

		return true;
	}

	/**
	 * Insert a new record into the database.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	protected function insert() {

		if ( ! DB::table_exists( static::get_table_name() ) ) {
			return false;
		}

		global $wpdb;

		/**
		 * Fires before inserting a record into the database.
		 *
		 * @since 1.0.0
		 *
		 * @param AbstractModel $model Model instance.
		 */
		do_action( 'sugar_calendar_bookings_model_pre_insert', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		// Set default values.
		$attributes = wp_parse_args( $this->attributes, static::get_column_defaults() );

		if ( in_array( 'author_id', $this->get_columns(), true ) && empty( $attributes['author_id'] ) ) {
			$attributes['author_id'] = get_current_user_id();
		}

		if ( in_array( 'created_at', $this->get_columns(), true ) ) {
			$attributes['created_at'] = Date::create()->format( Date::DB_DATETIME_FORMAT );
		}

		if ( in_array( 'updated_at', $this->get_columns(), true ) ) {
			$attributes['updated_at'] = Date::create()->format( Date::DB_DATETIME_FORMAT );
		}

		// Initialise column format array.
		$column_formats = $this->get_column_formats();

		// Whitelist columns.
		$attributes = array_intersect_key( $attributes, $column_formats );
		$attributes = array_filter( $attributes, fn( $value ) => $value !== null );

		// Reorder $column_formats to match the order of columns given in $data.
		$column_formats = array_merge( array_flip( array_keys( $attributes ) ), $column_formats );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		if ( $wpdb->insert( static::get_table_name(), $attributes, $column_formats ) === false ) {
			return false;
		}

		$this->id = $wpdb->insert_id;

		/**
		 * Fires after inserting a record into the database.
		 *
		 * @since 1.0.0
		 *
		 * @param AbstractModel $model Model instance.
		 */
		do_action( 'sugar_calendar_bookings_model_post_insert', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		return true;
	}

	/**
	 * Update an existing record in the database.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	protected function update() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		global $wpdb;

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

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

		if ( ! DB::table_exists( static::get_table_name() ) ) {
			return false;
		}

		/**
		 * Fires before updating a record in the database.
		 *
		 * @since 1.0.0
		 *
		 * @param AbstractModel $model Model instance.
		 */
		do_action( 'sugar_calendar_bookings_model_pre_update', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		$attributes = $this->get_changes();

		// Update the updated_at column.
		if ( in_array( 'updated_at', $this->get_columns(), true ) ) {
			$attributes['updated_at'] = current_time( 'mysql', true );
		}

		// Initialise column format array.
		$column_formats = $this->get_column_formats();

		// Whitelist attributes.
		$attributes = array_intersect_key( $attributes, $column_formats );

		// Build SET clause, handling NULL values explicitly since $wpdb->prepare()
		// converts null to '' (%s) or 0 (%d) which breaks nullable columns.
		$set_parts = [];
		$values    = [];

		foreach ( $attributes as $column => $value ) {
			if ( $value === null ) {
				$set_parts[] = "`{$column}` = NULL";
			} else {
				$set_parts[] = "`{$column}` = {$column_formats[ $column ]}";
				$values[]    = $value;
			}
		}

		if ( empty( $set_parts ) ) {
			return true;
		}

		$table = static::get_table_name();
		$set   = implode( ', ', $set_parts );

		$values[] = $this->get_id();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		if ( $wpdb->query( $wpdb->prepare( "UPDATE `{$table}` SET {$set} WHERE `id` = %d", $values ) ) === false ) {
			return false;
		}

		// Store previous values before syncing original.
		$this->previous = $this->original;
		$this->original = $this->attributes;
		$this->changes  = [];

		/**
		 * Fires after a record has been updated in the database.
		 *
		 * @since 1.0.0
		 *
		 * @param AbstractModel $model Model instance.
		 */
		do_action( 'sugar_calendar_bookings_model_post_update', $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName

		return true;
	}

	/**
	 * Delete a record from the database.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $clean_up Whether to clean up related data.
	 *
	 * @return bool True on success, false on failure.
	 */
	public function delete( $clean_up = true ) {

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

		$delete_manager = Plugin::get( DeleteManager::class );

		if ( $clean_up && $delete_manager->is_supported_type( static::get_type() ) ) {
			$delete_manager->delete( [ $this->get_id() ], static::get_type() );
		} else {
			$this->query()->where( 'id', '=', $this->get_id(), '%d' )->delete();

			if ( $clean_up ) {
				$this->delete_all_meta();
			}
		}

		return true;
	}

	/**
	 * Retrieve a model instance by its ID.
	 *
	 * @since 1.0.0
	 *
	 * @param int $id Row ID.
	 *
	 * @return static|null The model instance or null if not found.
	 */
	public static function get( $id ) {

		if ( isset( self::$cache[ static::class ][ $id ] ) ) {
			return self::$cache[ static::class ][ $id ];
		}

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

		$model = self::query()->where( 'id', '=', $id, '%d' )->first();

		self::$cache[ static::class ][ $id ] = $model;

		return $model;
	}

	/**
	 * Create a new model instance and save it to the database.
	 *
	 * @since 1.0.0
	 *
	 * @param array $attributes Attributes to set on the model.
	 *
	 * @return static The created model instance.
	 */
	public static function create( $attributes ) {

		$model = new static( $attributes );

		$model->save();

		return $model;
	}

	/**
	 * Get the full table name with prefix.
	 *
	 * @since 1.0.0
	 *
	 * @return string The full table name.
	 */
	public static function get_table_name() {

		global $wpdb;

		return $wpdb->prefix . 'scbookings_' . static::get_raw_table_name();
	}

	/**
	 * Get the table name without prefix.
	 *
	 * @since 1.0.0
	 *
	 * @return string The raw table name.
	 */
	public static function get_raw_table_name() {

		return static::$table_name;
	}

	/**
	 * Get the model type.
	 *
	 * @since 1.0.0
	 *
	 * @return string The model type.
	 */
	public static function get_type() {

		return static::$type;
	}

	/**
	 * Retrieve column formats.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, string> List of columns and their formats.
	 */
	public function get_column_formats() {

		return static::$column_formats;
	}

	/**
	 * Retrieve column defaults.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed> All defined column defaults.
	 */
	public static function get_column_defaults() {

		$class = static::class;

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

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

	/**
	 * Retrieve the list of columns for the database table.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string> List of columns.
	 */
	public function get_columns() {

		return array_keys( $this->get_column_formats() );
	}

	/**
	 * Begin a fluent query against the model's table using ModelQueryBuilder.
	 *
	 * @since 1.0.0
	 *
	 * @return ModelQueryBuilder<static>
	 */
	public static function query() {

		$qb = new ModelQueryBuilder();

		$qb->set_model( static::class );

		return $qb;
	}

	/**
	 * Hydrate an array of rows into model instances.
	 *
	 * @since 1.0.0
	 *
	 * @param array $rows Array of associative arrays representing rows.
	 *
	 * @return array<AbstractModel> Array of model instances.
	 */
	public static function hydrate( $rows ) {

		$allow_lazy_loading = count( $rows ) > 1 ? false : true;

		return array_map(
			function ( $row ) use ( $allow_lazy_loading ) {

				$model = new static();

				$model->set_raw_attributes( $row );

				$model->allow_lazy_loading = $allow_lazy_loading;

				return $model;
			},
			$rows
		);
	}

	/**
	 * Define a has_one (one-to-one) relationship.
	 *
	 * @since 1.0.0
	 *
	 * @param class-string<AbstractModel> $related Related model class.
	 *
	 * @return HasOneRelation Relationship definition object.
	 */
	protected function has_one( $related ) {

		return new HasOneRelation(
			$this,
			$related,
			self::get_type() . '_id',
			'id'
		);
	}

	/**
	 * Define a has_many (one-to-many) relationship.
	 *
	 * @since 1.0.0
	 *
	 * @param class-string<AbstractModel> $related     Related model class.
	 * @param string|null                 $foreign_key Optional foreign key.
	 *
	 * @return HasManyRelation Relationship definition object.
	 */
	protected function has_many( $related, $foreign_key = null ) {

		return new HasManyRelation(
			$this,
			$related,
			$foreign_key ?? self::get_type() . '_id',
			'id'
		);
	}

	/**
	 * Define a belongs_to (one-to-one) relationship.
	 *
	 * @since 1.0.0
	 *
	 * @param class-string<AbstractModel> $related Related model class.
	 *
	 * @return BelongsToRelation Relationship definition object.
	 */
	protected function belongs_to( $related ) {

		return new BelongsToRelation(
			$this,
			$related,
			$related::get_type() . '_id',
			'id'
		);
	}

	/**
	 * Define a belongs_to_many (many-to-many) relationship.
	 *
	 * @since 1.0.0
	 *
	 * @param class-string<AbstractModel> $related           Related model class.
	 * @param string|null                 $pivot_table       Optional pivot table name.
	 * @param string|null                 $foreign_pivot_key Optional foreign pivot key.
	 * @param string|null                 $related_pivot_key Optional related pivot key.
	 * @param array                       $pivot_defaults    Optional pivot defaults.
	 *
	 * @return BelongsToManyRelation Relationship definition object.
	 */
	protected function belongs_to_many( $related, $pivot_table = null, $foreign_pivot_key = null, $related_pivot_key = null, $pivot_defaults = [] ) {

		$parent_type  = self::get_type();
		$related_type = $related::get_type();

		if ( $pivot_table === null ) {
			$tables = [ $parent_type, $related_type ];

			sort( $tables, SORT_STRING );

			$pivot_table = implode( '_', $tables );
		}

		return new BelongsToManyRelation(
			$this,
			$related,
			$pivot_table,
			$foreign_pivot_key ?? $parent_type . '_id',
			$related_pivot_key ?? $related_type . '_id',
			'id',
			'id',
			$pivot_defaults
		);
	}

	/**
	 * Define a morph_many (polymorphic one-to-many) relationship.
	 *
	 * @since 1.0.0
	 *
	 * @param class-string<AbstractModel> $related Related model class.
	 *
	 * @return MorphManyRelation Relationship definition object.
	 */
	protected function morph_many( $related ) {

		return new MorphManyRelation(
			$this,
			$related,
			'object_type',
			'object_id',
			'id',
			self::get_type()
		);
	}

	/**
	 * Determine if the given key is a defined relationship on the model.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Relationship name.
	 *
	 * @return bool True if the key is a relationship method, false otherwise.
	 */
	protected function is_relation( $key ) {

		return method_exists( $this, $key );
	}

	/**
	 * Determine if the given relationship has already been loaded.
	 *
	 * @since 1.0.0
	 *
	 * @param string $relation Relationship name.
	 *
	 * @return bool True if the relationship is loaded, false otherwise.
	 */
	protected function relation_loaded( $relation ) {

		return array_key_exists( $relation, $this->relations );
	}

	/**
	 * Get the value of relationship.
	 *
	 * @since 1.0.0
	 *
	 * @param string $relation Relationship name.
	 *
	 * @return mixed|null The value of the relationship, or null if not loaded.
	 */
	protected function get_relation_value( $relation ) {

		if ( ! $this->relation_loaded( $relation ) ) {
			$this->relations[ $relation ] = $this->{$relation}()->get();
		}

		return $this->relations[ $relation ] ?? null;
	}

	/**
	 * Get the cast type for a given key.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Attribute or meta key.
	 *
	 * @return string|null The cast type or null if not set.
	 */
	public function get_cast_type( $key ) {

		return static::$casts[ $key ] ?? null;
	}

	/**
	 * Cast a value for an attribute or meta key, for get or set direction.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key       Attribute or meta key.
	 * @param mixed  $value     Value to cast.
	 * @param string $direction Cast direction ('get' or 'set').
	 *
	 * @return mixed The casted value.
	 */
	protected function cast_value( $key, $value, $direction = 'get' ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded, Generic.Metrics.NestingLevel.MaxExceeded

		if ( is_null( $value ) ) {
			return null;
		}

		$type = $this->get_cast_type( $key );

		if ( is_null( $type ) ) {
			return $value;
		}

		// phpcs:disable WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement, WPForms.Formatting.Switch.AddEmptyLineBefore
		switch ( $type ) {
			case 'integer':
				return (int) $value;

			case 'float':
			case 'double':
				return (float) $value;

			case 'boolean':
				return (bool) $value;

			case 'string':
				return (string) $value;

			case 'array':
				if ( $direction === 'get' ) {
					if ( is_string( $value ) ) {
						$decoded = json_decode( $value, true );

						if ( json_last_error() === JSON_ERROR_NONE ) {
							return $decoded;
						}
					}

					return (array) $value;
				} else { // set.
					if ( is_array( $value ) ) {
						return wp_json_encode( $value );
					}

					return (string) $value;
				}

			case 'datetime':
				if ( $direction === 'get' ) {
					return Date::create( $value );
				} else { // set.
					if ( ! $value instanceof DateTimeInterface ) {
						$value = Date::create( $value );
					}

					return $value ? $value->format( 'Y-m-d H:i:s' ) : null;
				}

			case 'date':
				if ( $direction === 'get' ) {
					$dt = Date::create( $value );

					return $dt ? $dt->setTime( 0, 0, 0 ) : null;
				} else { // set.
					if ( ! $value instanceof DateTimeInterface ) {
						$value = Date::create( $value );
					}

					return $value ? $value->format( 'Y-m-d' ) : null;
				}

			case 'timestamp':
				if ( $direction === 'get' ) {
					$dt = Date::create( $value );

					return $dt ? $dt->getTimestamp() : null;
				} else { // set.
					if ( ! $value instanceof DateTimeInterface ) {
						$value = Date::create( $value );
					}

					return $value ? $value->getTimestamp() : null;
				}

			default:
				return $value;
		}
		// phpcs:enable WPForms.Formatting.EmptyLineBeforeReturn.AddEmptyLineBeforeReturnStatement, WPForms.Formatting.Switch.AddEmptyLineBefore
	}

	/**
	 * Add or override multiple casts for the model.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $casts Array of casts to add or override.
	 */
	public static function add_casts( array $casts ) {

		static::$casts = array_merge( static::$casts, $casts );
	}
}
