<?php
/**
 * Costudio_Admin_Ajax_Misc — miscellaneous AJAX endpoints.
 *
 * Hosts the smaller, loosely related handlers that do not belong to a
 * dedicated controller (yet): agent-prompt save/reset, local-site connect,
 * dashboard welcome dismissal, Advanced tab toggles.
 *
 * Extracted from Costudio_Admin as part of Task 2 (PR 7 — Misc AJAX split).
 *
 * @package Contentosapp_Studio
 * @since   3.2.0
 */

defined( 'ABSPATH' ) || exit;

class Costudio_Admin_Ajax_Misc extends Costudio_Admin_Ajax_Base {

	/** @var self|null */
	private static ?self $instance = null;

	private function __construct() {}

	public static function get_instance(): self {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function register_hooks(): void {
		$this->register_ajax_actions(
			array(
				'costudio_toggle_uninstall_data',
				'costudio_toggle_advanced_option',
				'costudio_connect_local_site',
				'costudio_dismiss_welcome',
				'costudio_dismiss_byok_warning',
				'costudio_save_np_mode',
				'costudio_addon_history',
				'costudio_addon_packages',
				'costudio_addon_checkout',
				'costudio_addon_resync_quota',
				'costudio_license_portal',
				'costudio_save_agent_single',
				'costudio_get_prompt_history',
				'costudio_restore_agent_version',
				'costudio_restore_all_defaults',
			)
		);
	}

	/**
	 * AJAX: Fetch the user's extra-quotas purchase history (lazy-loaded by
	 * the License page when the "View history" section is expanded).
	 *
	 * Always proxied through Costudio_Standalone_License::fetch_addon_history()
	 * so the dual-path strategy (local / admin-ajax) and 5-min cache are
	 * applied uniformly.
	 */
	public function ajax_costudio_addon_history(): void {
		$in    = $this->read_request( array( 'force' => 'bool' ) );
		$force = $in['force'];

		try {
			$purchases = Costudio_Standalone_License::fetch_addon_history( (bool) $force );
		} catch ( \Throwable $e ) {
			wp_send_json_error( $e->getMessage() );
		}

		wp_send_json_success(
			array(
				'purchases' => $purchases,
				'count'     => count( $purchases ),
			)
		);
	}

	/**
	 * AJAX: List addon packages available for purchase (Sprint B.3).
	 *
	 * Proxies through Costudio_Standalone_License::fetch_addon_packages()
	 * so the dual-path strategy and 1-hour cache are uniform across callers.
	 *
	 * Empty packages array → JS shows the unavailable state (config issue
	 * server-side: addons disabled, Stripe not configured, plan below min).
	 */
	public function ajax_costudio_addon_packages(): void {
		$in    = $this->read_request( array( 'force' => 'bool' ) );
		$force = $in['force'];

		try {
			$payload = Costudio_Standalone_License::fetch_addon_packages( (bool) $force );
		} catch ( \Throwable $e ) {
			wp_send_json_error( $e->getMessage() );
		}

		// Normalize empty result to a stable shape so the JS doesn't have
		// to null-check before reading currency/packages.
		if ( empty( $payload ) ) {
			$payload = array( 'currency' => 'usd', 'packages' => array() );
		}

		wp_send_json_success( $payload );
	}

	/**
	 * AJAX: Create a Stripe Checkout Session for one of the addon packages
	 * (Sprint B.3).
	 *
	 * The JS sends { package, return_url_success, return_url_cancel }; we
	 * sanitize, hand off to Costudio_Standalone_License::create_addon_checkout()
	 * and return either { checkout_url } (so the JS can window.location to it)
	 * or a structured error.
	 *
	 * Server-side errors are surfaced with their original code (e.g.
	 * 'rate_limited', 'invalid_package') so the JS can pick the right
	 * user-facing message instead of one generic "something went wrong".
	 */
	public function ajax_costudio_addon_checkout(): void {
		$in = $this->read_request(
			array(
				'package'            => 'key',
				'return_url_success' => 'url',
				'return_url_cancel'  => 'url',
			)
		);

		$package    = $in['package'];
		$url_ok     = $in['return_url_success'];
		$url_cancel = $in['return_url_cancel'];

		if ( $package === '' || $url_ok === '' || $url_cancel === '' ) {
			wp_send_json_error( array(
				'code'    => 'missing_params',
				'message' => __( 'Missing required parameters.', 'contentosapp-studio' ),
			), 400 );
		}

		try {
			$result = Costudio_Standalone_License::create_addon_checkout( $package, $url_ok, $url_cancel );
		} catch ( \Throwable $e ) {
			wp_send_json_error( array(
				'code'    => 'checkout_exception',
				'message' => $e->getMessage(),
			), 500 );
		}

		if ( is_wp_error( $result ) ) {
			$status = (int) ( $result->get_error_data()['status'] ?? 500 );
			wp_send_json_error( array(
				'code'    => $result->get_error_code(),
				'message' => $result->get_error_message(),
			), $status );
		}

		wp_send_json_success( array(
			'session_id'   => $result['session_id']   ?? '',
			'checkout_url' => $result['checkout_url'] ?? '',
			'expires_at'   => $result['expires_at']   ?? 0,
		) );
	}

	/**
	 * AJAX: Force a fresh server-side sync of usage + addon balance (Sprint B.3.4).
	 *
	 * Called by the modal JS controller right after the user returns from a
	 * successful Stripe checkout. Without this, the local transient cache
	 * (1h TTL on costudio_addon_balance) would mask the just-credited
	 * balance until expiry — making the UI feel disconnected from the
	 * purchase the user just completed.
	 *
	 * Idempotent: callable arbitrarily often. Returns the resync payload
	 * but the caller typically ignores it and just relies on the side
	 * effect of the transient being overwritten with fresh server data.
	 */
	public function ajax_costudio_addon_resync_quota(): void {
		$this->read_request( array() );

		try {
			$result = Costudio_Standalone_License::sync_remote_usage( true );
		} catch ( \Throwable $e ) {
			wp_send_json_error( $e->getMessage() );
		}

		wp_send_json_success( $result ?: array() );
	}

	/**
	 * AJAX: Generate a Stripe Customer Portal URL for the current license.
	 *
	 * The portal is Stripe-hosted and lets the subscriber cancel their
	 * subscription, update payment method, or download invoices without
	 * needing to contact support.
	 *
	 * Returns { portal_url } on success so the JS can window.location to it.
	 */
	public function ajax_costudio_license_portal(): void {
		$in         = $this->read_request( array( 'return_url' => 'url' ) );
		$return_url = $in['return_url'];
		if ( '' === $return_url ) {
			$return_url = admin_url( 'admin.php?page=costudio-license' );
		}

		try {
			$result = Costudio_Standalone_License::get_subscription_portal_url( $return_url );
		} catch ( \Throwable $e ) {
			wp_send_json_error( array(
				'code'    => 'portal_exception',
				'message' => $e->getMessage(),
			), 500 );
		}

		if ( is_wp_error( $result ) ) {
			$status = (int) ( $result->get_error_data()['status'] ?? 500 );
			wp_send_json_error( array(
				'code'    => $result->get_error_code(),
				'message' => $result->get_error_message(),
			), $status );
		}

		wp_send_json_success( array(
			'portal_url' => $result['portal_url'] ?? '',
		) );
	}

	/**
	 * AJAX: Save the user's preference for New Production mode (Simple vs Advanced).
	 */
	public function ajax_costudio_save_np_mode(): void {
		$in   = $this->read_request( array( 'mode' => 'key' ) );
		$mode = $in['mode'] ?: 'advanced';
		if ( ! in_array( $mode, [ 'simple', 'advanced' ], true ) ) {
			wp_send_json_error( __( 'Invalid mode.', 'contentosapp-studio' ) );
		}

		update_user_meta( get_current_user_id(), 'costudio_np_mode', $mode );
		wp_send_json_success();
	}

	/**
	 * AJAX: Toggle the "delete all data on uninstall" option.
	 */
	public function ajax_costudio_toggle_uninstall_data(): void {
		$in      = $this->read_request( array( 'enabled' => 'bool' ) );
		$enabled = $in['enabled'];
		update_option( 'costudio_uninstall_delete_data', $enabled, false );

		wp_send_json_success();
	}

	/**
	 * AJAX: Toggle a whitelisted boolean option from the Advanced tab.
	 */
	public function ajax_costudio_toggle_advanced_option(): void {
		$in = $this->read_request(
			array(
				'option'  => 'key',
				'enabled' => 'bool',
			)
		);

		$allowed = array( 'costudio_auto_retry_agents', 'costudio_enable_web_search', 'costudio_housekeeping_optimize_tables' );
		$option  = $in['option'];

		if ( ! in_array( $option, $allowed, true ) ) {
			wp_send_json_error( __( 'Invalid option.', 'contentosapp-studio' ) );
		}

		$enabled = $in['enabled'];
		update_option( $option, $enabled, false );

		wp_send_json_success();
	}

	/**
	 * AJAX: Register the current WordPress install as a Contentosapp Studio destination site.
	 */
	public function ajax_costudio_connect_local_site(): void {
		$this->read_request( array() );

		$sites_repo = costudio_sites();
		if ( ! $sites_repo ) {
			wp_send_json_error( __( 'Sites system unavailable.', 'contentosapp-studio' ) );
		}

		$sites      = $sites_repo ? $sites_repo->get_all() : array();

		// Fetch local categories
		$local_categories = array();
		$terms            = get_terms(
			array(
				'taxonomy'   => 'category',
				'hide_empty' => false,
			)
		);

		if ( ! is_wp_error( $terms ) && is_array( $terms ) ) {
			foreach ( $terms as $term ) {
				if ( 'Uncategorized' !== $term->name && 'Sem categoria' !== $term->name ) {
					$local_categories[] = html_entity_decode( $term->name, ENT_QUOTES, 'UTF-8' );
				}
			}
			$local_categories = array_values( array_unique( $local_categories ) );
		}

		// Try to determine a representative color
		$color = '#3b82f6';
		if ( function_exists( 'wp_get_global_settings' ) ) {
			$palette = wp_get_global_settings( array( 'color', 'palette', 'theme' ) );
			if ( is_array( $palette ) && ! empty( $palette ) ) {
				$color = $palette[0]['color'] ?? $color;
			}
		} else {
			// Fallback: assign a distinct color based on site name hash to make it look nice
			$colors = array( '#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#ec4899', '#ef4444' );
			$hash   = abs( crc32( get_bloginfo( 'name' ) ) );
			$color  = $colors[ $hash % count( $colors ) ];
		}

		$local_entry = array(
			'key'                    => Costudio_WP_Publisher::LOCAL_SITE_KEY,
			'name'                   => get_bloginfo( 'name' ) ?: __( 'This site', 'contentosapp-studio' ),
			'label'                  => get_bloginfo( 'name' ) ?: __( 'This site', 'contentosapp-studio' ),
			'url'                    => rtrim( home_url(), '/' ),
			'user'                   => '',
			'password'               => '',
			'color'                  => $color,
			'categories'             => $local_categories,
			'active'                 => true,
			'is_local'               => true,
			'last_connection_status' => 'success',
			'last_connection_test'   => time(),
		);

		$sites[ Costudio_WP_Publisher::LOCAL_SITE_KEY ] = $local_entry;

		// Persist directly — bypasses connection test and limit logic in Standalone_Sites::save().
		$option_key = 'costudio_sites';
		update_option( $option_key, $sites, false );

		wp_send_json_success(
			array(
				'site_key'  => Costudio_WP_Publisher::LOCAL_SITE_KEY,
				'site_name' => $local_entry['name'],
				'site_url'  => $local_entry['url'],
			)
		);
	}

	/**
	 * AJAX: Dismiss the dashboard welcome banner.
	 */
	public function ajax_costudio_dismiss_welcome(): void {
		$this->read_request( array() );

		update_option( 'costudio_welcome_dismissed', 1, false );
		wp_send_json_success();
	}

	/**
	 * AJAX: One-time BYOK cost advisory dismissal.
	 *
	 * Persists the user's acknowledgement that AI costs run on their own
	 * provider account. After this is set, the warning modal in
	 * page-nova-producao.php never shows again.
	 */
	public function ajax_costudio_dismiss_byok_warning(): void {
		$this->read_request( array() );

		update_option( 'costudio_byok_warning_dismissed', 1, false );
		wp_send_json_success();
	}

	/**
	 * AJAX: Save a single agent's settings and push the previous value to its history.
	 */
	public function ajax_costudio_save_agent_single(): void {
		$in = $this->read_request(
			array(
				'agent'       => 'int',
				'niche'       => 'text',
				'extra'       => 'textarea',
				'temperature' => 'float_nullable',
			)
		);

		$agent = $in['agent'];
		if ( $agent < 1 || $agent > 7 ) {
			wp_send_json_error( __( 'Invalid agent number.', 'contentosapp-studio' ) );
		}

		$niche    = $in['niche'];
		$extra    = Costudio_Prompt_Sanitizer::sanitize( $in['extra'], 20000 );
		$temp     = null;
		$temp_raw = $in['temperature'];
		if ( null !== $temp_raw ) {
			$temp = max( 0.0, min( 2.0, (float) $temp_raw ) );
		}

		$niche_key   = sprintf( 'costudio_agent_%02d_niche', $agent );
		$extra_key   = sprintf( 'costudio_agent_%02d_extra', $agent );
		$temp_key    = sprintf( 'costudio_agent_%02d_temperature', $agent );
		$history_key = sprintf( 'costudio_agent_%02d_history', $agent );

		// Snapshot the CURRENT adjustments into history before overwriting — only
		// when there is something to remember (avoids noisy empty entries on the
		// very first save).
		$current_niche = (string) costudio_get_option( $niche_key, '' );
		$current_extra = (string) costudio_get_option( $extra_key, '' );
		$current_temp  = (string) costudio_get_option( $temp_key, '' );

		if ( '' !== $current_niche || '' !== $current_extra || '' !== $current_temp ) {
			$history = json_decode( (string) costudio_get_option( $history_key, '[]' ), true );
			if ( ! is_array( $history ) ) {
				$history = array();
			}
			array_unshift(
				$history,
				array(
					'timestamp'   => time(),
					'niche'       => $current_niche,
					'extra'       => $current_extra,
					'temperature' => '' !== $current_temp ? (float) $current_temp : null,
				)
			);
			$history = array_slice( $history, 0, 5 );
			costudio_update_option( $history_key, wp_json_encode( $history ), false );
		}

		// Persist the new adjustments. The canonical prompt (costudio_agent_NN) is
		// never written here — it is a read-only cache owned by Costudio_Agent_Seeder.
		costudio_update_option( $niche_key, $niche );
		costudio_update_option( $extra_key, $extra );
		if ( null !== $temp ) {
			costudio_update_option( $temp_key, (string) $temp );
		}

		// Global flag consumed by the ContentOS Auto adapter: whether any agent
		// carries adjustments (turns the "send adjustments" path on).
		if ( '' !== $niche || '' !== $extra || ( null !== $temp && 0.7 !== $temp ) ) {
			costudio_update_option( 'costudio_prompts_customized', '1', false );
		}

		wp_send_json_success( array( 'message' => __( 'Agent saved.', 'contentosapp-studio' ) ) );
	}

	/**
	 * AJAX: Return the version history for a given agent.
	 */
	public function ajax_costudio_get_prompt_history(): void {
		$in = $this->read_request( array( 'agent' => 'int' ) );

		$agent = $in['agent'];
		if ( $agent < 1 || $agent > 7 ) {
			wp_send_json_error( __( 'Invalid agent number.', 'contentosapp-studio' ) );
		}

		$history_key = sprintf( 'costudio_agent_%02d_history', $agent );
		$history     = json_decode( (string) costudio_get_option( $history_key, '[]' ), true );

		if ( ! is_array( $history ) ) {
			$history = array();
		}

		wp_send_json_success( array( 'history' => $history ) );
	}

	/**
	 * AJAX: Restore a specific version from an agent's history.
	 */
	public function ajax_costudio_restore_agent_version(): void {
		$in = $this->read_request(
			array(
				'agent'   => 'int',
				'version' => 'text',
			)
		);

		$agent       = $in['agent'];
		$version_raw = $in['version'];
		$version     = is_numeric( $version_raw ) ? (int) $version_raw : -1;

		if ( $agent < 1 || $agent > 7 ) {
			wp_send_json_error( __( 'Invalid agent number.', 'contentosapp-studio' ) );
		}

		$history_key = sprintf( 'costudio_agent_%02d_history', $agent );
		$history     = json_decode( (string) costudio_get_option( $history_key, '[]' ), true );

		if ( ! is_array( $history ) || ! isset( $history[ $version ] ) ) {
			wp_send_json_error( __( 'Version not found.', 'contentosapp-studio' ) );
		}

		$entry = $history[ $version ];

		$niche_key = sprintf( 'costudio_agent_%02d_niche', $agent );
		$extra_key = sprintf( 'costudio_agent_%02d_extra', $agent );
		$temp_key  = sprintf( 'costudio_agent_%02d_temperature', $agent );

		// Snapshot current adjustments into history before restoring.
		$current_niche = (string) costudio_get_option( $niche_key, '' );
		$current_extra = (string) costudio_get_option( $extra_key, '' );
		$current_temp  = (string) costudio_get_option( $temp_key, '' );

		if ( '' !== $current_niche || '' !== $current_extra || '' !== $current_temp ) {
			array_unshift(
				$history,
				array(
					'timestamp'   => time(),
					'niche'       => $current_niche,
					'extra'       => $current_extra,
					'temperature' => '' !== $current_temp ? (float) $current_temp : null,
				)
			);
			// The entry we're restoring is now shifted by 1; drop it to avoid duplicates.
			unset( $history[ $version + 1 ] );
			$history = array_values( array_slice( $history, 0, 5 ) );
			costudio_update_option( $history_key, wp_json_encode( $history ), false );
		}

		// Restore the adjustments from the chosen version.
		costudio_update_option( $niche_key, sanitize_text_field( $entry['niche'] ?? '' ) );
		costudio_update_option( $extra_key, Costudio_Prompt_Sanitizer::sanitize( $entry['extra'] ?? '', 20000 ) );
		if ( isset( $entry['temperature'] ) && null !== $entry['temperature'] ) {
			costudio_update_option( $temp_key, (string) (float) $entry['temperature'] );
		} else {
			costudio_update_option( $temp_key, '' );
		}

		wp_send_json_success(
			array(
				'message'     => __( 'Version restored.', 'contentosapp-studio' ),
				'niche'       => $entry['niche'] ?? '',
				'extra'       => $entry['extra'] ?? '',
				'temperature' => $entry['temperature'] ?? null,
			)
		);
	}

	/**
	 * AJAX: Re-sync the canonical prompts from the server and clear every agent's
	 * Adjustments (niche / extra / temperature) back to defaults.
	 */
	public function ajax_costudio_restore_all_defaults(): void {
		$in = $this->read_request( array( 'lang' => 'text' ) );

		// Re-fetch canonical prompts from the vault.
		$lang = $in['lang'];
		if ( empty( $lang ) ) {
			$lang = class_exists( 'Costudio_Admin' )
				? Costudio_Admin::get_default_content_language()
				: 'pt_BR';
		}

		$sync_confirmed = false;
		if ( class_exists( 'Costudio_Agent_Seeder' ) ) {
			$sync_confirmed = Costudio_Agent_Seeder::force_sync_from_vault( $lang );
		}

		// Reset every agent's Adjustments (niche / extra / temperature) and the
		// global "has adjustments" flag.
		for ( $i = 1; $i <= 7; $i++ ) {
			costudio_update_option( sprintf( 'costudio_agent_%02d_niche', $i ), '', false );
			costudio_update_option( sprintf( 'costudio_agent_%02d_extra', $i ), '', false );
			costudio_update_option( sprintf( 'costudio_agent_%02d_temperature', $i ), '', false );
		}
		costudio_update_option( 'costudio_prompts_customized', '', false );

		// force_sync_from_vault() returning false means the fetch failed or the
		// runtime manifest didn't match the canonical one after writing — the
		// Adjustments were still reset, but the prompt body itself may still be
		// stale. Gate 0-B (2026-08-20): surface that instead of always saying
		// "success" regardless of what the sync actually did.
		wp_send_json_success(
			array(
				'message'          => $sync_confirmed
					? __( 'All adjustments reset.', 'contentosapp-studio' )
					: __( 'Adjustments reset, but the prompt re-sync could not be confirmed — check Technical Logs.', 'contentosapp-studio' ),
				'provenance_warning' => ! $sync_confirmed,
			)
		);
	}
}
