<?php

namespace WPDRMS\ASP\AI;

use WPDRMS\ASP\AI\ORM\AIInteraction;

if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

/**
 * Template Method base for cached one-shot AI feature services.
 *
 * Subclasses implement the six domain-specific hooks; this class owns the
 * cache-check → collect → validate → prompt → AI-call → store → respond lifecycle.
 * Adding a new cached AI feature = extend this class and implement the abstract methods.
 */
abstract class AbstractCachedAIFeatureService {

	/**
	 * Feature slug used in the interaction log and as a cache key segment.
	 */
	abstract protected function getFeatureSlug(): string;

	/**
	 * Agent ID sent to the gateway — may differ from the feature slug.
	 */
	abstract protected function getAgentId(): string;

	/**
	 * Cache key suffix derived from the call arguments.
	 *
	 * @param array<string, mixed> $args
	 */
	abstract protected function buildCacheKey( array $args ): string;

	/**
	 * Collect all domain data needed for this feature (DB queries, WP API calls, etc.).
	 *
	 * @param array<string, mixed> $args Same args passed to runCached().
	 * @return array<string, mixed>
	 */
	abstract protected function collectData( array $args ): array;

	/**
	 * Build the prompt string from the collected data.
	 *
	 * @param array<string, mixed> $data Return value of collectData().
	 */
	abstract protected function buildPrompt( array $data ): string;

	/**
	 * Shape the payload persisted in the transient cache.
	 * Must contain every field needed to reconstruct the response on a subsequent cache hit.
	 *
	 * @param array<string, mixed> $data      Return value of collectData().
	 * @param string               $ai_result Raw AI text (markdown fences already stripped).
	 * @return array<string, mixed>
	 */
	abstract protected function buildPayload( array $data, string $ai_result ): array;

	/**
	 * Transient TTL in seconds.
	 */
	abstract protected function getTtl(): int;

	/**
	 * Optional data guard — throw AIInsufficientDataException when the collected
	 * data is unsuitable for an AI call. Default is a no-op.
	 *
	 * @param array<string, mixed> $data Return value of collectData().
	 * @throws AIInsufficientDataException
	 */
	protected function validateData( array $data ): void {} // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found

	/**
	 * Template Method — runs the full cached lifecycle:
	 * cache check → collect → validate → prompt → AI call → store → respond.
	 *
	 * The cache key includes the asp_ai_cache_gen option so that purgeCache() (which
	 * increments that option) busts these transients even on a persistent object cache.
	 *
	 * @param array<string, mixed> $args  Forwarded to collectData() and buildCacheKey().
	 * @param bool                 $force Skip the read cache when true.
	 * @return array<string, mixed>  Always includes 'cached' (bool) and 'tokens_used' (int).
	 * @throws AIInsufficientDataException  Propagated from validateData().
	 * @throws \RuntimeException            Propagated from AIService::generate().
	 */
	final protected function runCached( array $args, bool $force ): array {
		$gen       = (int) get_option( 'asp_ai_cache_gen', 1 );
		$cache_key = 'asp_ai_' . $gen . '_' . $this->getFeatureSlug() . '_' . $this->buildCacheKey( $args );
		$cached    = $force ? false : get_transient( $cache_key );

		if ( is_array( $cached ) ) {
			$row                 = new AIInteraction();
			$row->feature        = $this->getFeatureSlug();
			$row->prompt_summary = $cache_key;
			$row->tokens_used    = 0;
			$row->cached         = 1;
			$row->save();

			return array_merge(
				$cached,
				array(
					'cached'      => true,
					'tokens_used' => 0,
				) 
			);
		}

		$data = $this->collectData( $args );
		$this->validateData( $data );

		$prompt = $this->buildPrompt( $data );
		// This service owns the result cache (the feature-level transient + interaction log below),
		// so disable AIService's own caching layer to avoid a redundant second transient and a
		// misleading "cached" log row on a feature-level miss.
		$ai = AIService::instance()->generate( $this->getFeatureSlug(), $this->getAgentId(), $prompt, $this->getTtl(), false );

		$raw = AIService::extractJsonPayload( (string) ( $ai['result'] ?? '' ) );

		$payload = $this->buildPayload( $data, $raw );
		set_transient( $cache_key, $payload, $this->getTtl() );

		return array_merge(
			$payload,
			array(
				'cached'      => false,
				'tokens_used' => (int) ( $ai['tokens_used'] ?? 0 ),
			) 
		);
	}
}
