<?php
/**
 * Plugin — main bootstrap for CartCue for WooCommerce (Free).
 *
 * Registers triggers, actions, and rules for the WordPress.org plugin.
 *
 * @package CartCue
 */

namespace CartCue;

defined( 'ABSPATH' ) || exit;

class Plugin {

	private static ?Plugin $instance = null;

	public Logger         $logger;
	public Variables      $variables;
	public Mailer         $mailer;
	public WorkflowRunner $runner;
	public PastMatchesBackfill $backfill;

	/** @var array<string, Triggers\AbstractTrigger> */
	private array $triggers = [];
	/** @var array<string, Actions\AbstractAction> */
	private array $actions  = [];
	/** @var array<string, Rules\AbstractRule> */
	private array $rules    = [];

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

	private function __construct() {}

	private function init(): void {
		$this->logger    = new Logger();
		$this->variables = new Variables();
		$this->mailer    = new Mailer();
		$this->runner    = new WorkflowRunner( $this );
		$this->backfill  = new PastMatchesBackfill( $this );

		// ---- Free Triggers --------------------------------------------------
		$this->register_trigger( new Triggers\OrderPaidTrigger( $this ) );
		$this->register_trigger( new Triggers\CartAbandonedTrigger( $this ) );
		$this->register_trigger( new Triggers\CheckoutStartedTrigger( $this ) );
		$this->register_trigger( new Triggers\CustomerWinBackTrigger( $this ) );
		$this->register_trigger( new Triggers\CustomerCreatedTrigger( $this ) );
		$this->register_trigger( new Triggers\CustomerNeverPurchasedTrigger( $this ) );
		$this->register_trigger( new Triggers\PaymentFailedTrigger( $this ) );
		$this->register_trigger( new Triggers\OrderStatusChangedTrigger( $this ) );
		$this->register_trigger( new Triggers\OrderRefundedTrigger( $this ) );
		$this->register_trigger( new Triggers\OrderCancelledTrigger( $this ) );

		$this->register_trigger( new Triggers\PurchasedProductTrigger( $this ) );
		$this->register_trigger( new Triggers\PurchasedCategoryTrigger( $this ) );
		$this->register_trigger( new Triggers\PurchasedTagTrigger( $this ) );
		$this->register_trigger( new Triggers\OrderNoteAddedTrigger( $this ) );

		// Pro trigger teasers (editor only — never fire on free).
		$this->register_trigger( new Triggers\CustomerBirthdayGhostTrigger( $this ) );
		$this->register_trigger( new Triggers\NewReviewGhostTrigger( $this ) );
		$this->register_trigger( new Triggers\ManualCampaignGhostTrigger( $this ) );
		$this->register_trigger( new Triggers\ProductStockGhostTrigger( $this ) );
		$this->register_trigger( new Triggers\SubscriptionGhostTrigger( $this ) );
		$this->register_trigger( new Triggers\InboundWebhookGhostTrigger( $this ) );

		// ---- Free Actions ---------------------------------------------------
		$this->register_action( new Actions\SendEmailAction( $this ) );
		$this->register_action( new Actions\AddOrderNoteAction( $this ) );
		$this->register_action( new Actions\ChangeOrderStatusAction( $this ) );
		$this->register_action( new Actions\CreateCouponAction( $this ) );

		// ---- Ghost Actions (Pro teasers — never run, shown locked in picker) ----
		$this->register_action( new Actions\SendSmsGhostAction( $this ) );
		$this->register_action( new Actions\SendWhatsAppGhostAction( $this ) );
		$this->register_action( new Actions\SlackNotificationGhostAction( $this ) );
		$this->register_action( new Actions\WebhookGhostAction( $this ) );
		$this->register_action( new Actions\AddCustomerTagGhostAction( $this ) );

		// ---- Free Rules -----------------------------------------------------
		$this->register_rule( new Rules\OrderTotalRule() );
		$this->register_rule( new Rules\IsFirstOrderRule() );
		$this->register_rule( new Rules\OrderCountRule() );
		$this->register_rule( new Rules\CustomerRoleRule() );
		$this->register_rule( new Rules\CustomerRegisteredRule() );
		$this->register_rule( new Rules\OrderGuestRule() );
		$this->register_rule( new Rules\OrderContainsProductRule() );
		$this->register_rule( new Rules\OrderContainsCategoryRule() );
		$this->register_rule( new Rules\EmailOpenedRule() );
		$this->register_rule( new Rules\EmailClickedRule() );
		$this->register_rule( new Rules\CustomerEmailEngagementRule() );
		$this->register_rule( new Rules\OrderStatusRule() );
		$this->register_rule( new Rules\OrderPaymentMethodRule() );
		if ( StoreMultiCurrency::is_enabled() ) {
			$this->register_rule( new Rules\OrderCurrencyRule() );
		}
		$this->register_rule( new Rules\CustomerTotalSpentRule() );
		$this->register_rule( new Rules\CustomerLastOrderDaysRule() );

		// ---- System services ------------------------------------------------
		MarketingConsent::init();
		EnvironmentGuard::init();
		CheckoutConsent::init();
		CartRecovery::init();
		MailDelivery::init();
		YayMailBridge::init();
		EmailTracker::init();      // Open/click tracking is available in free.
		ConversionTracker::init(); // Revenue attribution for reports.
		Unsubscribe::init();
		Onboarding\FirstRunWizard::init();

		// Action Scheduler callbacks.
		add_action( 'cartcue_run_workflow',            [ $this->runner, 'run_queued' ], 10, 2 );
		add_action( 'cartcue_sequence_step',           [ $this->runner, 'run_sequence_step' ], 10, 3 );
		add_action( 'cartcue_process_abandoned_carts', [ $this, 'process_abandoned_carts' ] );
		add_action( 'cartcue_process_checkout_started', [ $this, 'process_checkout_started' ] );
		add_action( 'cartcue_process_win_back',        [ $this, 'process_win_back' ] );
		add_action( 'cartcue_process_never_purchased', [ $this, 'process_never_purchased' ] );
		add_action( 'cartcue_prune_logs',              [ $this->logger, 'prune' ] );
		add_action( 'cartcue_prune_carts',             [ $this, 'prune_stale_carts' ] );
		add_action( 'cartcue_prune_play_runs',         [ $this->runner, 'prune_play_runs' ] );

		if ( $this->is_pro() ) {
			add_action( 'cartcue_inbound_dispatch', [ $this, 'process_inbound_dispatch' ], 10, 2 );
		}

		// AJAX handlers.
		add_action( 'wp_ajax_cartcue_test_play', [ $this, 'handle_test_play_ajax' ] );
		add_action( 'admin_post_cartcue_cancel_queue_item', [ $this, 'handle_cancel_queue_item' ] );

		if ( is_admin() ) {
			new Admin\Admin( $this );
		}

		add_action( 'rest_api_init', [ RestController::class, 'register_routes' ] );
		CLI::register();

		$this->maybe_upgrade();

		add_action( 'admin_init', [ Migrations::class, 'repair_corrupted_branch_audience_rules' ] );
	}

	// -------------------------------------------------------------------------
	// Registration helpers
	// -------------------------------------------------------------------------

	public function register_trigger( Triggers\AbstractTrigger $t ): void {
		$this->triggers[ $t->get_slug() ] = $t;
		$t->register_hooks();
	}

	public function register_action( Actions\AbstractAction $a ): void {
		$this->actions[ $a->get_slug() ] = $a;
	}

	public function register_rule( Rules\AbstractRule $r ): void {
		$this->rules[ $r->get_slug() ] = $r;
	}

	/** @var array<string,string> Legacy rule slug aliases (cross-edition imports). */
	private const RULE_SLUG_ALIASES = [
		'order_category' => 'order_contains_category',
	];

	public function get_triggers(): array { return $this->triggers; }
	public function get_trigger( string $slug ): ?Triggers\AbstractTrigger { return $this->triggers[$slug] ?? null; }
	public function get_actions(): array  { return $this->actions; }
	public function get_action( string $slug ): ?Actions\AbstractAction  { return $this->actions[$slug] ?? null; }
	public function get_rules(): array    { return $this->rules; }
	public function get_rule( string $slug ): ?Rules\AbstractRule {
		if ( isset( $this->rules[ $slug ] ) ) {
			return $this->rules[ $slug ];
		}
		$canonical = self::RULE_SLUG_ALIASES[ $slug ] ?? null;
		if ( $canonical && isset( $this->rules[ $canonical ] ) ) {
			return $this->rules[ $canonical ];
		}
		return null;
	}

	/** Free plugin — Pro features show upgrade prompts in the editor. */
	public function is_pro(): bool {
		return false;
	}

	/** Max timed follow-up steps per play on free (0 = unlimited on Pro). */
	public function max_sequence_steps(): int {
		return 2;
	}

	/** If/Else branching in follow-up journeys is Pro-only. */
	public function can_use_branching(): bool {
		return false;
	}

	/** Max concurrently active plays on free (0 = unlimited on Pro). */
	public function max_active_plays(): int {
		return $this->is_pro() ? 0 : 5;
	}

	/**
	 * Count plays with status active, optionally excluding one ID (e.g. play being re-saved).
	 */
	public function active_play_count( int $exclude_id = 0 ): int {
		if ( $exclude_id <= 0 ) {
			$cached = get_transient( 'cartcue_active_play_count' );
			if ( false !== $cached ) {
				return (int) $cached;
			}
		}

		global $wpdb;
		if ( $exclude_id > 0 ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$count = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM %i WHERE status = 'active' AND id != %d",
					DB::workflows(),
					$exclude_id
				)
			);
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$count = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT COUNT(*) FROM %i WHERE status = 'active'",
					DB::workflows()
				)
			);
			set_transient( 'cartcue_active_play_count', $count, 60 );
		}

		return $count;
	}

	/** Whether another play can be set to active under the free plan cap. */
	public function can_activate_more_plays( int $exclude_play_id = 0 ): bool {
		$max = $this->max_active_plays();
		if ( $max <= 0 ) {
			return true;
		}
		return $this->active_play_count( $exclude_play_id ) < $max;
	}

	/**
	 * Active play IDs permitted to execute on free (oldest by ID, up to the cap).
	 *
	 * @return int[]
	 */
	public function allowed_active_play_ids(): array {
		$max = $this->max_active_plays();
		global $wpdb;

		if ( $max <= 0 ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$ids = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT id FROM %i WHERE status = 'active' ORDER BY id ASC",
					DB::workflows()
				)
			);
			return array_map( 'intval', $ids ?: [] );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM %i WHERE status = 'active' ORDER BY id ASC LIMIT %d",
				DB::workflows(),
				$max
			)
		);
		return array_map( 'intval', $ids ?: [] );
	}

	/** Whether an active play may run automations under the free plan cap. */
	public function is_play_allowed_to_run( int $workflow_id ): bool {
		$max = $this->max_active_plays();
		if ( $max <= 0 ) {
			return true;
		}
		return in_array( $workflow_id, $this->allowed_active_play_ids(), true );
	}

	/**
	 * Pause active plays over the free cap (oldest kept). Safe to run on install/upgrade.
	 */
	public static function enforce_active_play_cap(): void {
		if ( defined( 'CARTCUE_PRO_VERSION' ) ) {
			return;
		}

		$max = self::instance()->max_active_plays();
		if ( $max <= 0 ) {
			return;
		}

		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM %i WHERE status = 'active' ORDER BY id ASC",
				DB::workflows()
			)
		);
		$ids = array_map( 'intval', $ids ?: [] );
		if ( count( $ids ) <= $max ) {
			return;
		}

		$now = current_time( 'mysql' );
		foreach ( array_slice( $ids, $max ) as $id ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->update(
				DB::workflows(),
				[
					'status'     => 'paused',
					'updated_at' => $now,
				],
				[ 'id' => $id ],
				[ '%s', '%s' ],
				[ '%d' ]
			);
		}

		self::bust_play_count_cache();
	}

	/**
	 * Upgrade URL with optional campaign parameter for analytics.
	 *
	 * @param string $campaign UTM-style campaign slug.
	 */
	public static function upgrade_url( string $campaign = '' ): string {
		$url = 'https://www.userelements.com/cartcue-woocommerce-automation/';
		if ( '' !== $campaign ) {
			$url = add_query_arg(
				[
					'utm_source'   => 'plugin',
					'utm_medium'   => 'admin',
					'utm_campaign' => sanitize_key( $campaign ),
				],
				$url
			);
		}
		return $url;
	}

	public function handle_cancel_queue_item(): void {
		check_admin_referer( 'cartcue_cancel_queue' );
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_die();
		}
		$action_id = isset( $_REQUEST['action_id'] ) ? absint( wp_unslash( $_REQUEST['action_id'] ) ) : 0;
		if ( $action_id && class_exists( \ActionScheduler_Store::class ) ) {
			$store  = \ActionScheduler_Store::instance();
			$action = $store->fetch_action( $action_id );
			if ( $action && 'cartcue' === $action->get_group() ) {
				$store->cancel_action( $action_id );
			}
		}
		wp_safe_redirect( add_query_arg( [ 'page' => 'cartcue-queue' ], admin_url( 'admin.php' ) ) );
		exit;
	}

	// -------------------------------------------------------------------------
	// Scheduled callbacks
	// -------------------------------------------------------------------------

	public function process_abandoned_carts(): void {
		$t = $this->get_trigger( 'cart_abandoned' );
		if ( $t instanceof Triggers\CartAbandonedTrigger ) { $t->process_abandoned_carts(); }
	}

	public function process_checkout_started(): void {
		$t = $this->get_trigger( 'checkout_started' );
		if ( $t instanceof Triggers\CheckoutStartedTrigger ) {
			$t->process_checkout_started();
		}
	}

	public function process_win_back(): void {
		$t = $this->get_trigger( 'customer_win_back' );
		if ( $t instanceof Triggers\CustomerWinBackTrigger ) { $t->process_win_back_customers(); }
	}

	public function process_never_purchased(): void {
		$t = $this->get_trigger( 'customer_never_purchased' );
		if ( $t instanceof Triggers\CustomerNeverPurchasedTrigger ) {
			$t->process_eligible_customers();
		}
	}

	public function prune_stale_carts(): void {
		$t = $this->get_trigger( 'cart_abandoned' );
		if ( $t instanceof Triggers\CartAbandonedTrigger ) {
			$t->prune_stale_carts();
		}
	}

	// -------------------------------------------------------------------------
	// Test Play AJAX (free — sends to admin email, prefixes [TEST])
	// -------------------------------------------------------------------------

	public function handle_test_play_ajax(): void {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			wp_send_json_error( __( 'Permission denied.', 'cartcue-sales-automation' ) );
		}
		check_ajax_referer( 'cartcue_test_play', 'nonce' );

		$play_id  = isset( $_POST['play_id'] ) ? absint( wp_unslash( $_POST['play_id'] ) ) : 0;
		$workflow = null;

		if ( $play_id ) {
			$workflow = $this->runner->get_workflow( $play_id );
			if ( ! $workflow ) {
				wp_send_json_error( __( 'Play not found.', 'cartcue-sales-automation' ) );
			}
		} else {
			$trigger_slug = sanitize_key( wp_unslash( $_POST['trigger_slug'] ?? '' ) );
			$actions_raw  = isset( $_POST['actions'] ) ? wp_unslash( $_POST['actions'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded below.
			$actions      = is_string( $actions_raw ) ? json_decode( $actions_raw, true ) : $actions_raw;
			if ( ! $trigger_slug || ! is_array( $actions ) || empty( $actions ) ) {
				wp_send_json_error( __( 'Choose a trigger and add at least one action before testing.', 'cartcue-sales-automation' ) );
			}
			$workflow = [
				'trigger_slug' => $trigger_slug,
				'actions'      => $actions,
			];
		}

		$rate_limit_key = 'cartcue_test_play_' . get_current_user_id() . '_' . substr( md5( $play_id . wp_json_encode( $workflow ) ), 0, 10 );
		if ( get_transient( $rate_limit_key ) ) {
			wp_send_json_error( __( 'Please wait before testing this play again.', 'cartcue-sales-automation' ) );
		}
		set_transient( $rate_limit_key, 1, MINUTE_IN_SECONDS );

		$trigger = $this->get_trigger( $workflow['trigger_slug'] ?? '' );
		if ( ! $trigger ) {
			wp_send_json_error( __( 'Trigger not found.', 'cartcue-sales-automation' ) );
		}

		$data                = $trigger->get_test_data();
		$data['workflow_id'] = $play_id;
		$test_to             = isset( $_POST['test_to'] ) ? sanitize_email( wp_unslash( $_POST['test_to'] ) ) : '';
		if ( $test_to && is_email( $test_to ) ) {
			$data['customer_email'] = $test_to;
		}
		$data['_is_test'] = true;
		$ctx              = new Context( $data );

		foreach ( $workflow['actions'] ?? [] as $action_config ) {
			$slug   = $action_config['type'] ?? '';
			$action = $this->get_action( $slug );
			if ( ! $action ) {
				continue;
			}
			try {
				$action->run( $action_config['settings'] ?? [], $ctx );
			} catch ( \Exception $e ) {
				// Continue with remaining actions during test runs.
			}
		}

		wp_send_json_success(
			sprintf(
				/* translators: %s: recipient email address */
				__( 'Test play sent to %s. Check your inbox.', 'cartcue-sales-automation' ),
				esc_html( $data['customer_email'] ?? get_option( 'admin_email' ) )
			)
		);
	}

	public function process_inbound_dispatch( int $workflow_id, array $data ): void {
		if ( ! EnvironmentGuard::should_run_workflows( $data ) ) {
			return;
		}
		$this->runner->trigger( $workflow_id, $data );
	}

	public function play_count(): int {
		$cached = get_transient( 'cartcue_play_count' );
		if ( false !== $cached ) {
			return (int) $cached;
		}

		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', DB::workflows() ) );
		set_transient( 'cartcue_play_count', $count, 60 );

		return $count;
	}

	/** Invalidate cached play count after CRUD on workflows. */
	public static function bust_play_count_cache(): void {
		delete_transient( 'cartcue_play_count' );
		delete_transient( 'cartcue_active_play_count' );
	}

	// -------------------------------------------------------------------------
	// Schema upgrade
	// -------------------------------------------------------------------------

	private function maybe_upgrade(): void {
		Installer::upgrade_schema_columns();
		Installer::upgrade_email_tracking_columns();

		$installed = get_option( 'cartcue_db_version', '0.0.0' );
		if ( version_compare( $installed, CARTCUE_VERSION, '>=' ) ) {
			return;
		}

		if ( ! is_admin() && ! ( defined( 'WP_CLI' ) && WP_CLI ) ) {
			return;
		}
		Installer::activate();
	}
}
