<?php
/**
 * Plugin Name:       affilun
 * Plugin URI:        https://affilun.st/
 * Description:       AIで、アフィリエイトリンクの設置をもっとかんたんにする WordPress プラグイン。
 * Version:           0.9.3
 * Requires at least: 6.6
 * Requires PHP:      8.1
 * Author:            Rio Fujita
 * Author URI:        https://rio.st/
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       affilun
 * Domain Path:       /languages
 *
 * @package Affilun
 */

declare(strict_types=1);

namespace Affilun;

use Affilun\Application\Cache\CacheServiceProvider;
use Affilun\Application\Render\RenderServiceProvider;
use Affilun\Bootstrap\AdminCapabilityActivator;
use Affilun\Bootstrap\AmazonDiscountServiceProvider;
use Affilun\Bootstrap\LinkHealthCronServiceProvider;
use Affilun\Bootstrap\Plugin;
use Affilun\Bootstrap\SyncCronServiceProvider;
use Affilun\Infrastructure\Persistence\PersistenceServiceProvider;
use Affilun\Presentation\Admin\AffiliateRouting\AffiliateRoutingAdminServiceProvider;
use Affilun\Presentation\Admin\AdminServiceProvider;
use Affilun\Presentation\Admin\ProductEdit\ProductEditAdminServiceProvider;
use Affilun\Presentation\Block\BlockServiceProvider;
use Affilun\Presentation\Rest\AuditServiceProvider;
use Affilun\Presentation\Rest\ClicksServiceProvider;
use Affilun\Presentation\Rest\DiagnosticsServiceProvider;
use Affilun\Presentation\Rest\EditorServiceProvider;
use Affilun\Presentation\Rest\LogsServiceProvider;
use Affilun\Presentation\Rest\ProductsServiceProvider;
use Affilun\Presentation\Rest\ProviderServiceProvider;
use Affilun\Presentation\Rest\RestServiceProvider;
use Affilun\Presentation\Rest\SettingsServiceProvider;
use Affilun\Presentation\Rest\SyncJobsServiceProvider;
use Affilun\Presentation\Rest\SyncServiceProvider;
use Affilun\Presentation\Shortcode\ShortcodeServiceProvider;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Plugin constants.
 *
 * AFFILUN_ prefix is used across the codebase to avoid collisions with other
 * plugins or themes, as required by the plugin design specification
 * (§22 共存・破壊防止戦略).
 */
if ( ! defined( 'AFFILUN_VERSION' ) ) {
	define( 'AFFILUN_VERSION', '0.9.3' );
}

if ( ! defined( 'AFFILUN_PLUGIN_FILE' ) ) {
	define( 'AFFILUN_PLUGIN_FILE', __FILE__ );
}

if ( ! defined( 'AFFILUN_PLUGIN_DIR' ) ) {
	define( 'AFFILUN_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
}

if ( ! defined( 'AFFILUN_PLUGIN_URL' ) ) {
	define( 'AFFILUN_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
}

if ( ! defined( 'AFFILUN_PLUGIN_BASENAME' ) ) {
	define( 'AFFILUN_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
}

if ( ! defined( 'AFFILUN_MIN_PHP_VERSION' ) ) {
	define( 'AFFILUN_MIN_PHP_VERSION', '8.1' );
}

if ( ! defined( 'AFFILUN_MIN_WP_VERSION' ) ) {
	define( 'AFFILUN_MIN_WP_VERSION', '6.6' );
}

/*
 * Composer autoloader.
 *
 * Required for PSR-4 classes under the Affilun\ namespace,
 * including the Bootstrap\Plugin class wired up below. If the vendor
 * directory is missing we fail fast with an admin notice rather than
 * letting the Plugin class fatal on an undefined symbol.
 */
$affilun_autoload = AFFILUN_PLUGIN_DIR . 'vendor/autoload.php';
if ( is_readable( $affilun_autoload ) ) {
	require_once $affilun_autoload;
} else {
	add_action(
		'admin_notices',
		static function (): void {
			if ( ! current_user_can( 'activate_plugins' ) ) {
				return;
			}
			echo '<div class="notice notice-error"><p>';
			// phpcs:disable Generic.Files.LineLength.TooLong -- Single literal required by WordPress.WP.I18n.NonSingularStringLiteralText.
			echo esc_html__(
				'affilun: Composer dependencies are not installed. Run `composer install` in the plugin directory.',
				'affilun'
			);
			// phpcs:enable Generic.Files.LineLength.TooLong
			echo '</p></div>';
		}
	);
	return;
}

/**
 * Plugin bootstrap singleton accessor.
 *
 * Returns the single Plugin instance owned by this plugin file. Creating
 * the instance lazily keeps the entry file cheap to load in contexts
 * that only need the plugin constants (for example uninstall.php or
 * PHPStan's bootstrap).
 *
 * The instance is cached in a function-static variable rather than a
 * global so that test code can observe and assert on a single canonical
 * object without having to reach into $GLOBALS.
 */
function affilun_plugin(): Plugin {
	static $instance = null;

	if ( $instance === null ) {
		$instance = new Plugin( AFFILUN_PLUGIN_FILE );
	}

	return $instance;
}

/*
 * Activation / deactivation hooks.
 *
 * Delegated to instance methods on the Plugin class so that activation
 * logic can evolve without touching this entry file. The callbacks
 * resolve the singleton at call time rather than capturing it in the
 * closure, so activation works correctly even when WordPress loads this
 * file before the autoloader has registered further classes.
 */
register_activation_hook(
	__FILE__,
	static function (): void {
		affilun_plugin()->onActivate();
	}
);

register_deactivation_hook(
	__FILE__,
	static function (): void {
		// Phase 2 KI-003 / KI-004 / KI-006 follow-up: clear the
		// SyncCronServiceProvider's scheduled cron events directly here
		// because plugins_loaded (and therefore
		// SyncCronServiceProvider::boot()) does not fire during the
		// deactivation request, so the provider instance returned by
		// affilun_plugin()->registry() may not be hydrated. Hook names are
		// duplicated as literals to avoid loading the provider class
		// on a request that is about to deactivate the plugin; they
		// must stay in sync with
		// SyncCronServiceProvider::HOOK_DISPATCH_JOBS /
		// HOOK_PERIODIC_SYNC / HOOK_AUDIT_RETENTION_PURGE and
		// LinkHealthCronServiceProvider::HOOK_LINK_HEALTH_BATCH.
		if ( function_exists( 'wp_clear_scheduled_hook' ) ) {
			wp_clear_scheduled_hook( 'affilun_cron_dispatch_jobs' );
			wp_clear_scheduled_hook( 'affilun_cron_periodic_sync' );
			wp_clear_scheduled_hook( 'affilun_cron_audit_retention_purge' );
			wp_clear_scheduled_hook( 'affilun_cron_link_health_batch' );
			wp_clear_scheduled_hook( 'affilun_cron_amazon_discount_sync' );
		}

		affilun_plugin()->onDeactivate();
	}
);

/*
 * Kick off the bootstrap pipeline.
 *
 * Plugin::boot() schedules the real work (environment check + service
 * provider registration) on `plugins_loaded`, and fires the `affilun_loaded`
 * action once every provider has booted. Integrators should hook
 * `affilun_loaded` rather than `plugins_loaded` directly so they only run
 * when the environment check has passed.
 *
 * The PersistenceServiceProvider binds the Phase 2 persistence
 * collaborators (option store, schema version repository, table name
 * resolver, schema installer, migration runner) and subscribes the
 * migration runner to the `affilun_activate` action fired by
 * Plugin::onActivate(). The global $wpdb is forwarded so the resolver
 * and charset/collation clause pick up the host site's values; on the
 * activation request WordPress has already populated $wpdb by the
 * time the plugin main file loads.
 */
global $wpdb;
affilun_plugin()
	->withProviders(
		array(
			new PersistenceServiceProvider( isset( $wpdb ) && is_object( $wpdb ) ? $wpdb : null ),
			new CacheServiceProvider(
				null,
				null,
				null,
				null,
				null,
				null,
				isset( $wpdb ) && is_object( $wpdb ) ? $wpdb : null
			),
			new RenderServiceProvider( AFFILUN_PLUGIN_DIR ),
			new RestServiceProvider(),
			new ProviderServiceProvider(),
			new AmazonDiscountServiceProvider(),
			new ProductsServiceProvider(),
			new BlockServiceProvider( AFFILUN_PLUGIN_DIR ),
			new EditorServiceProvider(),
			new SyncServiceProvider(),
			new SyncJobsServiceProvider(),
			new LogsServiceProvider(),
			new AuditServiceProvider(),
			new DiagnosticsServiceProvider(),
			new SettingsServiceProvider(),
			new ClicksServiceProvider(),
			new AdminServiceProvider(),
			new AffiliateRoutingAdminServiceProvider(),
			new ProductEditAdminServiceProvider(),
			new ShortcodeServiceProvider(),
			new SyncCronServiceProvider(),
			new LinkHealthCronServiceProvider(),
			new AdminCapabilityActivator(),
		)
	)
	->boot();
