<?php
/**
 * Contentosapp Studio — exception classes.
 *
 * Loaded unconditionally (before the autoloader) so any adapter or class
 * can catch these exceptions without a load-order dependency.
 *
 * @package Contentosapp_Studio
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Thrown by Costudio_Gemini_Adapter when Google Search Grounding returns an
 * empty response (e.g. free-tier / billing disabled on the Cloud project).
 * Caught by generateWithSearch() to trigger the no-search fallback.
 */
class Costudio_Grounding_Exception extends Exception {}

/**
 * Thrown by Costudio_ContentOS_Adapter when the managed service refuses a
 * request because the user's plan does not allow the feature or the monthly
 * quota is exhausted. Carries structured service context so the runner can
 * persist it and the UI can render a managed-service action instead of a generic error.
 */
class Costudio_Upgrade_Required_Exception extends Exception {

	private string $error_code;
	private string $upgrade_url;

	public function __construct( string $error_code, string $message, string $upgrade_url = '' ) {
		parent::__construct( $message );
		$this->error_code  = $error_code;
		$this->upgrade_url = $upgrade_url ?: 'https://contentosapp.com/#pricing';
	}

	public function get_error_code(): string {
		return $this->error_code;
	}

	public function get_upgrade_url(): string {
		return $this->upgrade_url;
	}
}

/**
 * Thrown by Costudio_Image_Generator when the managed service responds that
 * image generation is not available on the current plan (HTTP 200, generated:false).
 * Caught by the runner/pipeline to keep the [IMG:] placeholder in the article
 * instead of failing the production.
 */
class Costudio_Image_Plan_Limited_Exception extends RuntimeException {

	private string $reason;

	public function __construct( string $reason = 'plan_limit' ) {
		parent::__construct( 'Managed image generation is not available on this plan. The [IMG:] placeholder will be kept for manual insertion.' );
		$this->reason = $reason;
	}

	public function get_reason(): string {
		return $this->reason;
	}
}

/**
 * Raised when the managed image provider rejects generation for a deterministic
 * account-level reason (currently exhausted or unavailable provider billing).
 * The publisher treats this as a production-scoped circuit breaker: the article
 * is still published, but no further managed image calls are attempted.
 */
class Costudio_Image_Provider_Unavailable_Exception extends RuntimeException {

	private string $reason;

	public function __construct( string $message, string $reason = 'provider_billing' ) {
		parent::__construct( $message );
		$this->reason = $reason;
	}

	public function get_reason(): string {
		return $this->reason;
	}
}

/**
 * Thrown internally by Costudio_WP_Publisher during CHUNKED publishing to defer
 * an image to a later background pass: only ONE new image is generated per
 * publish pass, so the publish step re-dispatches itself (one HTTP request per
 * image) and never accumulates several slow generations in a single request
 * that the web server would kill. Caught inside publish() — never escapes it.
 */
class Costudio_Image_Deferred_Exception extends RuntimeException {}

/**
 * Thrown by Costudio_ContentOS_Adapter when a remote AI generation job is
 * still processing after one short polling chunk (~45s). Mirrors the pattern
 * above (Costudio_Image_Deferred_Exception) for the AI-generation polling
 * loop: instead of blocking a single HTTP request for many minutes while
 * sleeping/polling the remote job — which gets killed by many hosting
 * environments' own PHP execution/worker timeouts well before the job
 * finishes, and which also stretches out the 5xx-retry backoff long enough to
 * hit that same ceiling — the adapter persists the remote job state (job_id,
 * poll token, retry/backoff counters) in a transient and yields control back
 * to the pipeline. Costudio_Runner catches this specifically (never the
 * generic Throwable handler) and re-dispatches the SAME agent step in a
 * fresh, short-lived background request; the next dispatch resumes polling
 * the same remote job instead of submitting a new one. Deliberately bypasses
 * the manual-approval gate and the "advance to next agent" logic, since
 * nothing has actually finished yet.
 */
class Costudio_Generation_Deferred_Exception extends RuntimeException {}
