<?php
/**
 * Cached Repository
 *
 * Base repository class with caching capabilities
 *
 * @package NemtlyBooking
 * @since   PBOOKING_SINCE
 */

namespace NemtlyBooking\Core;

use NemtlyBooking\Interfaces\Repository_Interface;

/**
 * Cached Repository class
 *
 * @since PBOOKING_SINCE
 */
abstract class Cached_Repository implements Repository_Interface {

    /**
     * Cache group
     *
     * @var string
     */
    protected $cache_group;

    /**
     * Cache expiration
     *
     * @var int
     */
    protected $cache_expiration;

    /**
     * Constructor
     *
     * @since PBOOKING_SINCE
     *
     * @param string $cache_group     Cache group.
     * @param int    $cache_expiration Cache expiration in seconds.
     */
    public function __construct( $cache_group = '', $cache_expiration = null ) {
        $this->cache_group      = $cache_group;
        $this->cache_expiration = $cache_expiration ?? Cache_Manager::DEFAULT_EXPIRATION;
    }

    /**
     * Get cached data or execute callback
     *
     * @since PBOOKING_SINCE
     *
     * @param string   $key      Cache key.
     * @param callable $callback Callback to execute if not cached.
     *
     * @return mixed Cached or generated data.
     */
    protected function get_cached( $key, $callback ) {
        if ( ! Cache_Manager::is_available() ) {
            return call_user_func( $callback );
        }

        return Cache_Manager::remember(
            $key,
            $callback,
            $this->cache_expiration,
            $this->cache_group
        );
    }

    /**
     * Set cache data
     *
     * @since PBOOKING_SINCE
     *
     * @param string $key  Cache key.
     * @param mixed  $data Data to cache.
     *
     * @return bool True on success, false on failure.
     */
    protected function set_cache( $key, $data ) {
        if ( ! Cache_Manager::is_available() ) {
            return false;
        }

        return Cache_Manager::set( $key, $data, $this->cache_expiration, $this->cache_group );
    }

    /**
     * Delete cache data
     *
     * @since PBOOKING_SINCE
     *
     * @param string $key Cache key.
     *
     * @return bool True on success, false on failure.
     */
    protected function delete_cache( $key ) {
        if ( ! Cache_Manager::is_available() ) {
            return false;
        }

        return Cache_Manager::delete( $key, $this->cache_group );
    }

    /**
     * Clear all cache for this repository
     *
     * @since PBOOKING_SINCE
     *
     * @return bool True on success, false on failure.
     */
    protected function clear_cache() {
        if ( ! Cache_Manager::is_available() ) {
            return false;
        }

        return Cache_Manager::clear_group( $this->cache_group );
    }

    /**
     * Generate cache key from arguments
     *
     * @since PBOOKING_SINCE
     *
     * @param string $method Method name.
     * @param array  $args   Method arguments.
     *
     * @return string Cache key.
     */
    protected function generate_cache_key( $method, $args = [] ) {
        $key = $method;
        if ( ! empty( $args ) ) {
            $key .= '_' . md5( serialize( $args ) );
        }
        return $key;
    }

    /**
     * Find a record by ID with caching
     *
     * @since PBOOKING_SINCE
     *
     * @param int $id Record ID.
     *
     * @return mixed|null Record object or null if not found.
     */
    public function find( $id ) {
        $key = $this->generate_cache_key( 'find', [ $id ] );

        return $this->get_cached(
            $key,
            function () use ( $id ) {
				return $this->find_uncached( $id );
			}
        );
    }

    /**
     * Find all records with caching
     *
     * @since PBOOKING_SINCE
     *
     * @param array $args Query arguments.
     *
     * @return array Array of record objects.
     */
    public function find_all( $args = [] ) {
        $key = $this->generate_cache_key( 'find_all', $args );

        return $this->get_cached(
            $key,
            function () use ( $args ) {
				return $this->find_all_uncached( $args );
			}
        );
    }

    /**
     * Count records with caching
     *
     * @since PBOOKING_SINCE
     *
     * @param array $args Query arguments.
     *
     * @return int Record count.
     */
    public function count( $args = [] ) {
        $key = $this->generate_cache_key( 'count', $args );

        return $this->get_cached(
            $key,
            function () use ( $args ) {
				return $this->count_uncached( $args );
			}
        );
    }

    /**
     * Create a new record and invalidate cache
     *
     * @since PBOOKING_SINCE
     *
     * @param mixed $data Record data.
     *
     * @return mixed|false Created record object or false on failure.
     */
    public function create( $data ) {
        $result = $this->create_uncached( $data );

        if ( $result ) {
            $this->invalidate_after_create( $result );
        }

        return $result;
    }

    /**
     * Update a record and invalidate cache
     *
     * @since PBOOKING_SINCE
     *
     * @param int   $id   Record ID.
     * @param array $data Updated data.
     *
     * @return mixed|false Updated record object or false on failure.
     */
    public function update( $id, $data ) {
        $result = $this->update_uncached( $id, $data );

        if ( $result ) {
            $this->invalidate_after_update( $id, $result );
        }

        return $result;
    }

    /**
     * Delete a record and invalidate cache
     *
     * @since PBOOKING_SINCE
     *
     * @param int $id Record ID.
     *
     * @return bool True on success, false on failure.
     */
    public function delete( $id ) {
        $result = $this->delete_uncached( $id );

        if ( $result ) {
            $this->invalidate_after_delete( $id );
        }

        return $result;
    }

    /**
     * Find a record by ID without caching (to be implemented by subclasses)
     *
     * @since PBOOKING_SINCE
     *
     * @param int $id Record ID.
     *
     * @return mixed|null Record object or null if not found.
     */
    abstract protected function find_uncached( $id );

    /**
     * Find all records without caching (to be implemented by subclasses)
     *
     * @since PBOOKING_SINCE
     *
     * @param array $args Query arguments.
     *
     * @return array Array of record objects.
     */
    abstract protected function find_all_uncached( $args );

    /**
     * Count records without caching (to be implemented by subclasses)
     *
     * @since PBOOKING_SINCE
     *
     * @param array $args Query arguments.
     *
     * @return int Record count.
     */
    abstract protected function count_uncached( $args );

    /**
     * Create a new record without caching (to be implemented by subclasses)
     *
     * @since PBOOKING_SINCE
     *
     * @param mixed $data Record data.
     *
     * @return mixed|false Created record object or false on failure.
     */
    abstract protected function create_uncached( $data );

    /**
     * Update a record without caching (to be implemented by subclasses)
     *
     * @since PBOOKING_SINCE
     *
     * @param int   $id   Record ID.
     * @param array $data Updated data.
     *
     * @return mixed|false Updated record object or false on failure.
     */
    abstract protected function update_uncached( $id, $data );

    /**
     * Delete a record without caching (to be implemented by subclasses)
     *
     * @since PBOOKING_SINCE
     *
     * @param int $id Record ID.
     *
     * @return bool True on success, false on failure.
     */
    abstract protected function delete_uncached( $id );

    /**
     * Invalidate cache after create operation
     *
     * @since PBOOKING_SINCE
     *
     * @param mixed $result Created record.
     */
    protected function invalidate_after_create( $result ) {
        $this->clear_cache();
    }

    /**
     * Invalidate cache after update operation
     *
     * @since PBOOKING_SINCE
     *
     * @param int   $id     Record ID.
     * @param mixed $result Updated record.
     */
    protected function invalidate_after_update( $id, $result ) {
        $this->delete_cache( $this->generate_cache_key( 'find', [ $id ] ) );
        $this->clear_cache();
    }

    /**
     * Invalidate cache after delete operation
     *
     * @since PBOOKING_SINCE
     *
     * @param int $id Record ID.
     */
    protected function invalidate_after_delete( $id ) {
        $this->delete_cache( $this->generate_cache_key( 'find', [ $id ] ) );
        $this->clear_cache();
    }
}
