<?php
/**
 * Plugin container and bootstrap.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango;

/*
 * Above the imports rather than below them, which is where every other file in
 * this plugin puts it.
 *
 * Plugin Check looks for this guard in the first fifty lines of a file. This one
 * carries thirty-five `use` statements, which had pushed it to line fifty-one —
 * so the check passed by a single line, and the next import to be added broke
 * it. Up here it cannot drift out of range however long the list grows.
 */
defined( 'ABSPATH' ) || exit;

use TextTango\Admin\Ajax;
use TextTango\Admin\AudiencePage;
use TextTango\Admin\CampaignsPage;
use TextTango\Admin\DashboardWidget;
use TextTango\Admin\FirstRun;
use TextTango\Admin\Menu;
use TextTango\Admin\SettingsPage;
use TextTango\Admin\Wizard;
use TextTango\Analytics\Stats;
use TextTango\Audit\Recorder;
use TextTango\Forms\Forms;
use TextTango\Consent\AccountPreference;
use TextTango\Consent\CheckoutConsent;
use TextTango\Consent\DoubleOptIn;
use TextTango\Consent\SuppressionListener;
use TextTango\Contacts\ContactReconciler;
use TextTango\Contacts\ContactSync;
use TextTango\Marketing\PersonalCampaign;
use TextTango\Marketing\AbandonedCart;
use TextTango\Marketing\Campaign;
use TextTango\Health\SiteHealth;
use TextTango\Install\Activator;
use TextTango\Privacy\PersonalData;
use TextTango\Privacy\Retention;
use TextTango\Otp\CheckoutGate;
use TextTango\Otp\LoginController;
use TextTango\Otp\LoginForm;
use TextTango\Otp\PasswordReset;
use TextTango\Otp\PhoneVerification;
use TextTango\Otp\ProfileFields;
use TextTango\Otp\TwoFactor;
use TextTango\Sending\BalanceWatch;
use TextTango\Sending\EmailFallback;
use TextTango\Sending\HealthAlert;
use TextTango\Sending\Queue;
use TextTango\Webhook\Reconciler;
use TextTango\Webhook\Route;
use TextTango\WooCommerce\Accounts;
use TextTango\WooCommerce\Notifications;
use TextTango\WooCommerce\Stock;


/**
 * Wires the plugin's modules together on `plugins_loaded`.
 */
final class Plugin {

	/**
	 * Text domain, matching the plugin slug.
	 *
	 * Translations are loaded automatically by WordPress since 4.6 for plugins
	 * whose text domain matches their slug, so no loader call is needed.
	 */
	public const TEXT_DOMAIN = 'texttango';

	/**
	 * Singleton instance.
	 */
	private static ?Plugin $instance = null;

	/**
	 * Boot the plugin, once.
	 */
	public static function instance(): Plugin {
		if ( null === self::$instance ) {
			self::$instance = new self();
			self::$instance->boot();
		}

		return self::$instance;
	}

	/**
	 * Not constructed directly.
	 */
	private function __construct() {}

	/**
	 * Register hooks for every module.
	 */
	private function boot(): void {
		add_action( 'init', array( Activator::class, 'maybe_upgrade' ) );

		// Registered unconditionally: the AJAX endpoints, the shortcode, and the
		// login-screen hooks all have to exist for logged-out visitors too.
		( new LoginController() )->register();
		( new LoginForm() )->register();
		( new TwoFactor() )->register();
		( new PhoneVerification() )->register();
		// Guest checkout has to reach it too, like LoginController above.
		( new CheckoutGate() )->register();

		// The reset link is requested from the login screen, so this registers on
		// the front end as well as in admin.
		( new PasswordReset() )->register();

		// The queue worker must exist wherever Action Scheduler runs, which
		// includes cron and WP-CLI requests that never touch admin.
		( new Queue() )->register();
		( new PersonalCampaign() )->register();
		( new BalanceWatch() )->register();
		( new EmailFallback() )->register();
		( new HealthAlert() )->register();
		( new Notifications() )->register();
		( new Accounts() )->register();
		( new Stock() )->register();

		// The receipt endpoint is called by TextTango, not by a logged-in user, so
		// it registers everywhere; the reconciler runs from cron.
		( new Route() )->register();
		( new Reconciler() )->register();

		// Consent gates sending, so it registers everywhere a send can start.
		( new SuppressionListener() )->register();
		( new CheckoutConsent() )->register();
		( new AccountPreference() )->register();

		// The confirmation link is opened by a logged-out phone, so its handler
		// registers on the front end whether or not anyone is signed in.
		( new DoubleOptIn() )->register();

		( new ContactSync() )->register();
		( new ContactReconciler() )->register();

		// Campaigns are polled rather than pushed — the plugin never claims the
		// token's campaign callback URL, so nothing tells it when one finishes.
		( new Campaign() )->register();

		// Cart changes happen on the front end, so this registers everywhere
		// rather than only in admin.
		( new AbandonedCart() )->register();

		// The audit trail listens for events fired elsewhere, including on cron and
		// WP-CLI requests that never touch admin.
		( new Forms() )->register();
		( new Recorder() )->register();

		/*
		 * Anything that changes what the analytics count drops their cache.
		 *
		 * Nothing did this before, so the figures were only ever invalidated by
		 * the nightly retention sweep — meaning a campaign or a message just sent
		 * was missing from both screens for up to five minutes, which reads as
		 * the send having failed.
		 *
		 * Campaign writes flush from CampaignLog itself, since those go through
		 * one pair of methods. Messages have no such choke point, so these are the
		 * hooks the send path already fires. Stats::flush() is idempotent within a
		 * request, so a campaign writing a row per recipient still flushes once.
		 */
		foreach ( array( 'texttango_after_send', 'texttango_sandbox_send', 'texttango_webhook_received' ) as $texttango_changed ) {
			add_action( $texttango_changed, array( Stats::class, 'flush' ) );
		}

		// Privacy tooling: WordPress's own export/erase flows, and log retention.
		( new PersonalData() )->register();
		( new Retention() )->register();

		if ( is_admin() ) {
			// Site Health runs its async tests through admin-ajax, so this has to
			// exist for those requests too — not only when rendering the screen.
			( new SiteHealth() )->register();

			( new Menu() )->register();
			( new DashboardWidget() )->register();

			// Shows across the whole admin, not only the plugin's own screens —
			// somebody who has just activated has no reason to look here yet.
			( new FirstRun() )->register();
			( new SettingsPage() )->register();
			( new Ajax() )->register();
			( new Wizard() )->register();
			( new CampaignsPage() )->register();
			( new AudiencePage() )->register();
			( new ProfileFields() )->register();
		}
	}
}
