<?php
/**
 * Plugin
 *
 * Central service container and bootstrap.
 * All classes are wired here via constructor injection.
 * No class instantiates another plugin class on its own.
 *
 * @package Relatedly\Core
 * @since   2.0.0
 */

namespace Relatedly\Core;

use Relatedly\Admin\Menu;
use Relatedly\Admin\Assets as AdminAssets;
use Relatedly\Admin\Dashboard\Widget;
use Relatedly\Admin\Email\ReportSender;
use Relatedly\Frontend\ContentFilter;
use Relatedly\Frontend\Assets as FrontendAssets;
use Relatedly\Premium\AdvancedFeatures;
use Relatedly\Premium\PremiumThemes;
use Relatedly\RSS\RSSHandler;
use Relatedly\Theming\ThemeRegistry;
use Relatedly\Tracking\AjaxTracker;
use Relatedly\Tracking\Cleanup;
use Relatedly\Theming\Floating\Sidebar as FloatingSidebar;

defined( 'ABSPATH' ) || exit;

/**
 * Wires plugin services and registers their hooks.
 *
 * @since 2.0.0
 */
final class Plugin {

	/**
	 * Singleton plugin container instance.
	 *
	 * @since 2.0.0
	 * @var Plugin|null
	 */
	private static ?Plugin $instance = null;

	/**
	 * Service instances keyed by service name.
	 *
	 * @since 2.0.0
	 * @var array<string, object>
	 */
	private array $services = array();

	/**
	 * Absolute path to relatedly.php.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	private string $plugin_file;

	/**
	 * Store the main plugin file path.
	 *
	 * @since 2.0.0
	 * @param string $plugin_file Absolute path to relatedly.php.
	 */
	private function __construct( string $plugin_file ) {
		$this->plugin_file = $plugin_file;
	}

	/**
	 * Boot the plugin. Called once on plugins_loaded.
	 *
	 * @since 2.0.0
	 * @param string $plugin_file Absolute path to relatedly.php.
	 * @return Plugin
	 */
	public static function boot( string $plugin_file ): Plugin {
		if ( null === self::$instance ) {
			self::$instance = new self( $plugin_file );
			self::$instance->init();
		}
		return self::$instance;
	}

	/**
	 * Return the singleton after boot.
	 *
	 * @since 2.0.0
	 */
	public static function instance(): ?Plugin {
		return self::$instance;
	}

	/**
	 * Retrieve a registered service by key.
	 *
	 * @since 2.0.0
	 * @param string $key Service key.
	 * @return object|null
	 */
	public function get( string $key ): ?object {
		return $this->services[ $key ] ?? null;
	}

	// -------------------------------------------------------------------------
	// Private bootstrap
	// -------------------------------------------------------------------------

	/**
	 * Initialize migrations, services, and hooks.
	 *
	 * @since 2.0.0
	 */
	private function init(): void {
		// Run DB migrations before anything else.
		Installer::maybe_upgrade();

		$this->register_services();
		$this->register_hooks();
	}

	/**
	 * Create and store all service instances.
	 *
	 * @since 2.0.0
	 */
	private function register_services(): void {
		$options        = new Options();
		$cache          = new Cache();
		$query          = new QueryBuilder( $options, $cache );
		$theme_registry = new ThemeRegistry( $options );
		$debugger       = new Debugger( $options );

		$this->services['options']        = $options;
		$this->services['cache']          = $cache;
		$this->services['query']          = $query;
		$this->services['theme_registry'] = $theme_registry;
		$this->services['debugger']       = $debugger;

		// Premium layer — registered here so it can hook into the theme registry and other services.
		// These classes are tagged @fs_premium_only; in the free package this block is stripped.
		if ( \function_exists( 'relatedly_fs' ) && \relatedly_fs()->is__premium_only() ) {
			$this->services['premium_themes']   = new PremiumThemes();
			$this->services['premium_features'] = new AdvancedFeatures();
			$this->services['floating_sidebar'] = new FloatingSidebar( $options, $query );
		}

		// Frontend.
		$this->services['content_filter']  = new ContentFilter( $options, $query, $theme_registry );
		$this->services['frontend_assets'] = new FrontendAssets( $options );

		// Cron intervals + scheduled email reports.
		$this->services['cron_schedules'] = new CronSchedules();
		$this->services['report_sender']  = new ReportSender( $options, $cache );

		// Tracking.
		$this->services['ajax_tracker'] = new AjaxTracker( $options );
		$this->services['cleanup']      = new Cleanup( $options );

		// RSS.
		$this->services['rss_handler'] = new RSSHandler( $options, $query, $debugger );

		// Admin — only in admin context.
		if ( is_admin() ) {
			$this->services['admin_menu']   = new Menu( $options, $cache, $theme_registry );
			$this->services['admin_assets'] = new AdminAssets( $options );
			$this->services['dashboard']    = new Widget( $options, $cache );
		}
	}

	/**
	 * Register hooks exposed by services and cache invalidation.
	 *
	 * @since 2.0.0
	 */
	private function register_hooks(): void {
		foreach ( $this->services as $service ) {
			if ( method_exists( $service, 'init_hooks' ) ) {
				$service->init_hooks();
			}
		}

		// Invalidate cached pools when a post is saved.
		add_action(
			'save_post',
			function ( int $post_id ) {
				/**
				 * Cache service.
				 *
				 * @var Cache $cache
				 */
				$cache = $this->services['cache'];
				$cache->flush_post( $post_id );
			}
		);
	}
}
