<?php
/**
 * Repository service.
 *
 * @author    Code Atlantic
 * @package   PopupMaker
 * @copyright (c) 2024, Code Atlantic LLC.
 */

namespace PopupMaker\Base\Service;

use PopupMaker\Base\Service;
use PopupMaker\Base\Model\Post;

defined( 'ABSPATH' ) || exit;

/**
 * Repository service for managing Post-based entities.
 *
 * @since 1.21.0
 * @template TPost of Post
 * @template-extends Service<\PopupMaker\Plugin\Core>
 */
abstract class Repository extends Service {

	/**
	 * Post type key for registration.
	 *
	 * @var non-empty-string
	 */
	protected $post_type_key;

	/**
	 * Registered WordPress post type name.
	 *
	 * @var non-empty-string
	 */
	protected $post_type;

	/**
	 * Cache of instantiated items indexed by post ID.
	 *
	 * @var array<int|string, TPost>
	 */
	protected $items_by_id = [];

	/**
	 * Get the cache key for an item ID.
	 *
	 * @param int|numeric-string $item_id Item ID.
	 *
	 * @return int|string
	 */
	protected function get_item_cache_key( $item_id ) {
		return (int) $item_id;
	}

	/**
	 * Initialize the service.
	 *
	 * @param \PopupMaker\Plugin\Core $container Plugin container.
	 */
	public function __construct( $container ) {
		parent::__construct( $container );
		$this->post_type = $container->get_controller( 'PostTypes' )->get_type_key( $this->post_type_key );
	}

	/**
	 * Instantiate model from post.
	 *
	 * @param \WP_Post $post Post object.
	 *
	 * @return TPost|null
	 */
	abstract public function instantiate_model_from_post( $post );

	/**
	 * Cache an item in internal storage.
	 *
	 * @param TPost $item Item to cache by ID for fast retrieval.
	 * @return void
	 */
	protected function cache_item( $item ) {
		$this->items_by_id[ $this->get_item_cache_key( $item->ID ) ] = $item;
	}

	/**
	 * Get a list of all queried items.
	 *
	 * @param array<string, mixed> $args {
	 *     Optional. WP_Query arguments for filtering posts.
	 *
	 *     @type string|string[] $post_type      Post type to query.
	 *     @type int             $posts_per_page Number of posts to retrieve.
	 *     @type string|string[] $post_status    Post status to query.
	 *     @type string          $meta_key       Meta key to query.
	 *     @type mixed           $meta_value     Meta value to query.
	 * }
	 * @return TPost[] Array of instantiated model objects matching the query.
	 */
	public function query( $args = [] ) {
		/** @var TPost[] $items */
		$items = [];

		foreach ( $this->query_posts( $args ) as $post ) {
			$item = $this->instantiate_model_from_post( $post );

			if ( ! $item ) {
				continue;
			}

			// Cache the item.
			$this->cache_item( $item );

			$items[] = $item;
		}

		return $items;
	}

	/**
	 * Query repository records as WordPress post objects without model hydration.
	 *
	 * @param array<string,mixed> $args WP_Query arguments.
	 * @return \WP_Post[] Matching post objects.
	 */
	public function query_posts( $args = [] ) {
		$args = is_array( $args ) ? $args : [];
		unset( $args['post_type'], $args['fields'] );

		$query_args = wp_parse_args(
			$args,
			[
				'posts_per_page' => -1,
			]
		);

		$query_args['post_type'] = $this->post_type;
		$query_args['fields']    = 'all';

		$query = new \WP_Query( $query_args );

		return array_values(
			array_filter(
				$query->posts,
				static function ( $post ) {
					return $post instanceof \WP_Post;
				}
			)
		);
	}

	/**
	 * Query repository record IDs without post-object or model hydration.
	 *
	 * @param array<string,mixed> $args WP_Query arguments.
	 * @return int[] Matching post IDs.
	 */
	public function query_ids( $args = [] ) {
		$args = is_array( $args ) ? $args : [];
		unset( $args['post_type'], $args['fields'] );

		$query_args = wp_parse_args(
			$args,
			[
				'posts_per_page'         => -1,
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			]
		);

		$query_args['post_type'] = $this->post_type;
		$query_args['fields']    = 'ids';

		$query = new \WP_Query( $query_args );

		return wp_parse_id_list( $query->posts );
	}

	/**
	 * Get item by ID.
	 *
	 * @param int|numeric-string $item_id Item ID to retrieve.
	 * @return TPost|null Model instance if found, null otherwise.
	 */
	public function get_by_id( $item_id = 0 ) {
		// Convert to integer for consistent handling.
		$item_id   = (int) $item_id;
		$cache_key = $this->get_item_cache_key( $item_id );

		// If item is cached, get the object.
		if ( isset( $this->items_by_id[ $cache_key ] ) ) {
			return $this->items_by_id[ $cache_key ];
		}

		// Query for a post by ID.
		if ( $item_id > 0 ) {
			$post = get_post( $item_id );

			if ( $post && $post->post_type === $this->post_type ) {
				$item = $this->instantiate_model_from_post( $post );

				if ( $item ) {
					$this->cache_item( $item );
				}

				return $item;
			}
		}

		return null;
	}

	/**
	 * Get item by custom field or column.
	 *
	 * @param non-empty-string $field Field name (post column like 'post_name' or meta key).
	 * @param string|int|float $value Field value to search for.
	 * @param 'column'|'meta'  $type Search type: 'column' for post table columns or 'meta' for post meta fields.
	 * @return TPost|null Model instance if found, null otherwise.
	 */
	public function get_by_field( $field, $value, $type = 'column' ) {
		if ( empty( $field ) || ( empty( $value ) && 0 !== $value && '0' !== $value ) ) {
			return null;
		}

		$query_args = [
			'post_type'      => $this->post_type,
			'posts_per_page' => 1,
			'post_status'    => [ 'publish', 'private', 'draft' ],
		];

		if ( 'meta' === $type ) {
			$query_args['meta_key']   = $field; // phpcs:ignore WordPress.DB.SlowDBQuery
			$query_args['meta_value'] = $value; // phpcs:ignore WordPress.DB.SlowDBQuery
		} else {
			// For post columns like post_name, post_title, etc.
			$query_args[ $field ] = $value;
		}

		$query = new \WP_Query( $query_args );

		if ( $query->have_posts() ) {
			$post = $query->posts[0];
			if ( $post instanceof \WP_Post ) {
				$item = $this->instantiate_model_from_post( $post );
				if ( $item ) {
					$this->cache_item( $item );
				}
				return $item;
			}
		}

		return null;
	}
}
