<?php
/**
 * Livada SEO — Module Loader
 *
 * Reads the saved settings once, merges them with defaults, then bootstraps
 * every enabled module.  Nothing in this file touches the database more than
 * once per request, keeping the frontend overhead at a single get_option() call.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_Loader {

	/**
	 * Merged settings cache (defaults ← saved options).
	 * Populated once during init(); never written back here.
	 *
	 * @var array<string,mixed>|null
	 */
	private static ?array $settings = null;

	// ── Public API ────────────────────────────────────────────────────────────

	/**
	 * Entry point: called on `plugins_loaded`.
	 * Idempotent — safe to call more than once (returns early after first run).
	 */
	public static function init(): void {
		if ( null !== self::$settings ) {
			return;
		}

		// 1. Load & merge settings (single DB read).
		self::$settings = self::load_settings();

		// 2. Run DB migrations if needed.
		self::maybe_migrate();

		// 3. Bootstrap modules.
		self::load_modules();
	}

	/**
	 * Returns a single setting value from the cached array.
	 *
	 * @param string $key     Setting key.
	 * @param mixed  $default Fallback value when key is absent.
	 * @return mixed
	 */
	public static function get_setting( string $key, $default = null ) {
		if ( null === self::$settings ) {
			self::init();
		}

		return self::$settings[ $key ] ?? $default;
	}

	/**
	 * Returns the full merged settings array (read-only copy).
	 *
	 * @return array<string,mixed>
	 */
	public static function get_settings(): array {
		if ( null === self::$settings ) {
			self::init();
		}

		return self::$settings;
	}

	// ── Private helpers ───────────────────────────────────────────────────────

	/**
	 * Reads `livada_seo_settings` from the DB and merges with defaults.
	 * Arrays inside are merged key-by-key so saved scalar values override
	 * defaults without wiping keys that are only in defaults.
	 *
	 * @return array<string,mixed>
	 */
	private static function load_settings(): array {
		$defaults = livada_seo_default_settings();
		$saved    = get_option( 'livada_seo_settings', [] );

		if ( ! is_array( $saved ) ) {
			$saved = [];
		}

		// Shallow merge: saved values win; missing keys fall back to defaults.
		return array_merge( $defaults, $saved );
	}

	/**
	 * Compares the stored `db_version` against the current plugin version and
	 * runs any pending upgrade routines.  Currently a no-op for 1.0.0 → 1.0.0,
	 * but the structure is here for future migrations.
	 */
	private static function maybe_migrate(): void {
		$stored_db_version = self::$settings['db_version'] ?? '0.0.0';

		if ( version_compare( $stored_db_version, '1.0.0', '<' ) ) {
			// Future: call self::migrate_to_100();
			self::update_db_version( '1.0.0' );
		}
	}

	/**
	 * Persists a new db_version to the settings option and updates the cache.
	 */
	private static function update_db_version( string $version ): void {
		self::$settings['db_version'] = $version;

		$saved = get_option( 'livada_seo_settings', [] );
		if ( ! is_array( $saved ) ) {
			$saved = [];
		}
		$saved['db_version'] = $version;
		update_option( 'livada_seo_settings', $saved, false );
	}

	/**
	 * Requires each module's class file and calls its static `init()`.
	 *
	 * Modules listed in `settings['modules']` are considered "enabled".
	 * Core infrastructure (REST API, import, Elementor integration, admin UI)
	 * is loaded unconditionally based on context.
	 */
	private static function load_modules(): void {
		$enabled = self::$settings['modules'] ?? [];

		// ── Feature modules (opt-in via settings) ────────────────────────────

		$feature_map = [
			'meta'        => [ 'class-meta.php',        'Livada_SEO_Meta'       ],
			'sitemap'     => [ 'class-sitemap.php',      'Livada_SEO_Sitemap'    ],
			'schema'      => [ 'class-schema.php',       'Livada_SEO_Schema'     ],
			'opengraph'   => [ 'class-opengraph.php',    'Livada_SEO_OpenGraph'  ],
			'redirects'   => [ 'class-redirects.php',    'Livada_SEO_Redirects'  ],
			'breadcrumbs' => [ 'class-breadcrumbs.php',  'Livada_SEO_Breadcrumbs'],
			'analysis'    => [ 'class-analysis.php',     'Livada_SEO_Analysis'   ],
			'robots'      => [ 'class-robots.php',       'Livada_SEO_Robots'     ],
			'indexnow'    => [ 'class-indexnow.php',     'Livada_SEO_IndexNow'   ],
			'toc'         => [ 'class-toc.php',          'Livada_SEO_TOC'        ],
			'image_seo'   => [ 'class-image-seo.php',    'Livada_SEO_Image_SEO'  ],
			'analytics'   => [ 'class-analytics.php',   'Livada_SEO_Analytics'  ],
			'geo'         => [ 'class-geo.php',          'Livada_SEO_GEO'        ],
			'aeo'         => [ 'class-aeo.php',          'Livada_SEO_AEO'        ],
			'sitemap_html'=> [ 'class-sitemap-html.php', 'Livada_SEO_Sitemap_HTML'],
			'site_audit'  => [ 'class-site-audit.php',   'Livada_SEO_Site_Audit'  ],
			'woocommerce' => [ 'class-woocommerce.php',  'Livada_SEO_WooCommerce' ],
			'og_image'    => [ 'class-og-image.php',     'Livada_SEO_OG_Image'    ],
		];

		foreach ( $feature_map as $key => [ $file, $class ] ) {
			if ( in_array( $key, $enabled, true ) ) {
				self::load_class( $file, $class );
			}
		}

		// ── Infrastructure (always loaded) ────────────────────────────────────

		// Import tool (admin-side, but we keep it available for WP-Cron too).
		self::load_class( 'class-import.php', 'Livada_SEO_Import' );
		self::load_class( 'class-import-yoast.php', 'Livada_SEO_Import_Yoast' );

		// AI suggestions (Anthropic). Loaded outside the admin block because
		// the AJAX endpoint must be reachable from the metabox; the AJAX hook
		// is only fired for authenticated `edit_posts` users anyway.
		self::load_class( 'class-ai.php', 'Livada_SEO_AI' );

		// Activity log — captures plugin events for the admin viewer.
		self::load_class( 'class-activity-log.php', 'Livada_SEO_Activity_Log' );

		// Licence manager — validates the licence key against livada.fr and gates
		// auto-updates behind a valid licence.
		self::load_class( 'class-license.php', 'Livada_SEO_License' );
		if ( class_exists( 'Livada_SEO_License' ) ) {
			Livada_SEO_License::init();
		}

		// Self-hosted updater — checks the remote manifest and surfaces updates
		// on the Extensions page like a wp.org plugin would.
		self::load_class( 'class-updater.php', 'Livada_SEO_Updater' );

		// GSC keywords CSV import (admin-side only).
		if ( is_admin() ) {
			self::load_class( 'class-gsc-import.php', 'Livada_SEO_GSC_Import' );
			self::load_class( 'class-link-graph.php', 'Livada_SEO_Link_Graph' );
			self::load_class( 'class-onboarding.php', 'Livada_SEO_Onboarding' );
		}

		// REST API endpoints.
		self::load_class( 'class-rest-api.php', 'Livada_SEO_REST_API' );

		// WP-CLI commands.
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			self::load_class( 'class-cli.php', 'Livada_SEO_CLI' );
		}

		// Admin screens & meta boxes.
		if ( is_admin() ) {
			self::load_class( 'admin/class-admin.php',         'Livada_SEO_Admin'         );
			self::load_class( 'admin/class-metabox.php',       'Livada_SEO_Metabox'       );
			self::load_class( 'admin/class-admin-filters.php', 'Livada_SEO_Admin_Filters' );
			self::load_class( 'class-bulk-editor.php',         'Livada_SEO_Bulk_Editor'   );
			self::load_class( 'class-link-suggestions.php',    'Livada_SEO_Link_Suggestions' );
			self::load_class( 'class-image-bulk.php',          'Livada_SEO_Image_Bulk'    );
			self::load_class( 'class-ai-visibility.php',       'Livada_SEO_AI_Visibility' );
		}

		// AI Bots Access Control — frontend (robots.txt enrichment) + admin AJAX.
		// Loaded unconditionally because the robots.txt filter runs on the public
		// front-end. The admin AJAX hooks are no-ops outside admin requests.
		self::load_class( 'class-ai-bots.php', 'Livada_SEO_AI_Bots' );

		// 404 monitor — frontend hook + admin UI.
		self::load_class( 'class-404-monitor.php', 'Livada_SEO_404_Monitor' );

		// Hreflang — frontend hook only.
		self::load_class( 'class-hreflang.php', 'Livada_SEO_Hreflang' );

		// Elementor Pro integration.
		self::load_class( 'elementor/class-elementor.php', 'Livada_SEO_Elementor' );
	}

	/**
	 * Requires a class file relative to `LIVADA_SEO_PATH . 'includes/'` and,
	 * if the class exists, calls its static `init()`.
	 *
	 * Silently skips files that have not yet been created (allows incremental
	 * development without fatal errors).
	 *
	 * @param string $relative_path File path relative to the `includes/` dir.
	 * @param string $class_name    Fully qualified class name to initialise.
	 */
	private static function load_class( string $relative_path, string $class_name ): void {
		$full_path = LIVADA_SEO_PATH . 'includes/' . $relative_path;

		if ( ! file_exists( $full_path ) ) {
			return;
		}

		require_once $full_path;

		if ( class_exists( $class_name ) && method_exists( $class_name, 'init' ) ) {
			$class_name::init();
		}
	}
}
