<?php
/**
 * Plugin settings page with tabbed UI.
 *
 * Registers a settings page under Listings > Settings with three tabs:
 * - General: archive slug, listings per page, units, currency.
 * - Custom Fields: create and manage custom meta fields for vehicles.
 * - Taxonomies (Pro): create and manage user-defined taxonomies.
 *
 * @package ThemeREX\AutoDealer
 */

declare(strict_types=1);

namespace ThemeREX\AutoDealer\Admin;

defined( 'ABSPATH' ) || exit;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\AppearanceTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\CompareFavoritesTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\FieldsTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\FormsTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\GeneralTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\LayoutsTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\MaintenanceTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\MapsTab;
use ThemeREX\AutoDealer\Admin\Settings\Tabs\TaxonomiesTab;

class AdminSettings {

	/**
	 * Tab companions extracted per QUALITY-IMPROVEMENT-PLAN.md §T2-2.
	 *
	 * Each tab owns its `render()` + section/field registration + (optional)
	 * save handler. AdminSettings orchestrates: registers them on `register()`,
	 * dispatches to `render()` in renderPage(), and forwards sanitize routing.
	 *
	 * @var array<string, \ThemeREX\AutoDealer\Admin\Settings\AbstractSettingsTab>
	 */
	private array $tabs = array();

	/**
	 * Option key for all plugin settings.
	 */
	public const OPTION_KEY = 'trx_ad_settings';

	/**
	 * Option key for custom meta field definitions.
	 */
	public const FIELDS_OPTION_KEY = 'trx_ad_custom_fields';

	/**
	 * Settings page slug.
	 */
	public const PAGE_SLUG = 'trx-auto-dealer-settings';

	/**
	 * Default settings.
	 *
	 * @var array<string,mixed>
	 */
	private const DEFAULTS = array(
		'archive_slug'                   => 'inventory',
		'listings_per_page'              => 12,
		'distance_unit'                  => 'km',
		'currency_symbol'                => '$',
		'currency_code'                  => 'USD',
		'default_sort'                   => 'newest',
		'single_layout'                  => 'default',
		'single_elementor_template'      => '',
		'dealer_can_publish'             => 0,
		'dealer_profile_requires_review' => 0,
		'allow_svg_uploads'              => 0,
		'map_provider'                   => 'none',
		'google_maps_api_key'            => '',
		'map_default_zoom'               => 12,
		'map_cluster'                    => 1,
		// Appearance customization — mirrors :root custom properties in frontend.css.
		// Empty string = "use stylesheet default" so we only emit non-empty overrides.
		'color_primary'                  => '',
		'color_primary_hover'            => '',
		'color_success'                  => '',
		'color_warning'                  => '',
		'color_danger'                   => '',
		'radius'                         => 8,
		'radius_explicit'                => 0,
		'use_theme_fonts'                => 0,
		'show_price'                     => 1,
		'show_featured_badge'            => 1,
		'enable_compare'                 => 1,
		'enable_favorites'               => 1,
	);

	/**
	 * Allowed map providers.
	 *
	 * @var string[]
	 */
	private const MAP_PROVIDERS = array( 'none', 'leaflet', 'google' );

	/**
	 * Allowed default-sort tokens. Matches the archive sort dropdown.
	 *
	 * @var string[]
	 */
	public const SORT_OPTIONS = array( 'newest', 'price_asc', 'price_desc', 'year_desc' );

	/**
	 * Register hooks.
	 */
	public function register(): void {
		$this->tabs = array(
			'general'           => new GeneralTab(),
			'appearance'        => new AppearanceTab(),
			'fields'            => new FieldsTab(),
			'forms'             => new FormsTab(),
			'layouts'           => new LayoutsTab(),
			'compare_favorites' => new CompareFavoritesTab(),
			'maps'              => new MapsTab(),
			'taxonomies'        => new TaxonomiesTab(),
			'maintenance'       => new MaintenanceTab(),
		);

		add_action( 'admin_menu', array( $this, 'addSettingsPage' ) );
		add_action( 'admin_init', array( $this, 'registerSettings' ) );
		add_action( 'admin_post_trx_ad_resync_specs', array( $this, 'handleResyncSpecs' ) );
		add_action( 'admin_post_trx_ad_flush_cache', array( $this, 'handleFlushCache' ) );
		add_action( 'admin_post_trx_ad_uninstall_pref', array( $this, 'handleUninstallPref' ) );
	}

	/**
	 * Manual re-sync of every listing's specs row + term counts + aggregation
	 * cache. Fixes stale/missing specs rows that cause cards or the compare
	 * page to render with empty values even when the admin edit screen shows
	 * the data. (Symptom example: a single listing on /compare-vehicles/ with
	 * blank year/make/model cells when its meta box has them populated.)
	 */
	public function handleResyncSpecs(): void {
		check_admin_referer( 'trx_ad_resync_specs' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'trx-auto-dealer' ), 403 );
		}

		$report = \ThemeREX\AutoDealer\Core\Activator::resyncAllListingSpecsWithReport();
		( new \ThemeREX\AutoDealer\Cache\AggregationCache() )->flush();

		set_transient( 'trx_ad_resync_report', $report, MINUTE_IN_SECONDS * 10 );

		wp_safe_redirect(
			add_query_arg(
				array(
					'post_type'         => 'trx_ad_listing',
					'page'              => self::PAGE_SLUG,
					'tab'               => 'maintenance',
					'trx_ad_resync_msg' => 'done',
				),
				admin_url( 'edit.php' )
			)
		);
		exit;
	}

	/**
	 * Manual flush of the aggregation cache (filter facets + counts). Cheap and
	 * always safe — the cache rebuilds on the next archive view.
	 */
	public function handleFlushCache(): void {
		check_admin_referer( 'trx_ad_flush_cache' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'trx-auto-dealer' ), 403 );
		}

		( new \ThemeREX\AutoDealer\Cache\AggregationCache() )->flush();

		wp_safe_redirect(
			add_query_arg(
				array(
					'post_type'        => 'trx_ad_listing',
					'page'             => self::PAGE_SLUG,
					'tab'              => 'maintenance',
					'trx_ad_flush_msg' => 'done',
				),
				admin_url( 'edit.php' )
			)
		);
		exit;
	}

	/**
	 * Save the "Delete all data on uninstall" opt-in. Stored as a standalone
	 * option (not inside trx_ad_settings) because uninstall.php reads it
	 * directly before wiping the options table.
	 */
	public function handleUninstallPref(): void {
		check_admin_referer( 'trx_ad_uninstall_pref' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'trx-auto-dealer' ), 403 );
		}

		update_option( 'trx_ad_delete_data_on_uninstall', ! empty( $_POST['trx_ad_delete_data_on_uninstall'] ), false );

		wp_safe_redirect(
			add_query_arg(
				array(
					'post_type'            => 'trx_ad_listing',
					'page'                 => self::PAGE_SLUG,
					'tab'                  => 'maintenance',
					'trx_ad_uninstall_msg' => 'done',
				),
				admin_url( 'edit.php' )
			)
		);
		exit;
	}

	/**
	 * Add the settings page as the last submenu under the Listings CPT menu.
	 */
	public function addSettingsPage(): void {
		add_submenu_page(
			'edit.php?post_type=trx_ad_listing',
			__( 'Auto Dealer Settings', 'trx-auto-dealer' ),
			__( 'Settings', 'trx-auto-dealer' ),
			'manage_options',
			self::PAGE_SLUG,
			array( $this, 'renderPage' ),
			100
		);
	}

	/**
	 * Delegate Settings API + custom POST handler registration to each tab.
	 * Lifecycle: hooks on `admin_init`; each tab decides whether it owns a
	 * Settings API section, a custom POST endpoint, or both.
	 */
	public function registerSettings(): void {
		foreach ( $this->tabs as $tab ) {
			$tab->register();
		}
	}

	// ------------------------------------------------------------------
	// General settings helpers
	// ------------------------------------------------------------------

	public static function getSettings(): array {
		$settings = get_option( self::OPTION_KEY, array() );
		if ( ! is_array( $settings ) ) {
			$settings = array();
		}

		// Radius semantics changed in 1.0.14 (tester feedback #11): 0 used to be
		// the "use stylesheet default (8px)" sentinel, so square corners were
		// impossible. Now 0 means an actual 0px and the default is 8. Options
		// saved before the change carry radius=0 WITHOUT the radius_explicit
		// marker — translate them to 8 so existing sites keep their look.
		if ( isset( $settings['radius'] ) && 0 === (int) $settings['radius'] && empty( $settings['radius_explicit'] ) ) {
			$settings['radius'] = 8;
		}

		return array_merge( self::DEFAULTS, $settings );
	}

	public static function get( string $key, mixed $default = null ): mixed {
		$settings = self::getSettings();
		return $settings[ $key ] ?? $default ?? ( self::DEFAULTS[ $key ] ?? null );
	}

	// ------------------------------------------------------------------
	// Custom fields helpers
	// ------------------------------------------------------------------

	/**
	 * Get all custom field definitions.
	 *
	 * @return array<int, array{id: string, label: string, key: string, type: string, options: string, description: string, group: string}>
	 */
	public static function getCustomFields(): array {
		$fields = get_option( self::FIELDS_OPTION_KEY, array() );
		return is_array( $fields ) ? $fields : array();
	}

	// ------------------------------------------------------------------
	// Sanitize General settings
	// ------------------------------------------------------------------

	public static function sanitize( mixed $input ): array {
		if ( ! is_array( $input ) ) {
			return self::DEFAULTS;
		}

		// Settings API submits one tab at a time. Start from the saved record
		// merged over defaults so unsubmitted keys retain their stored value;
		// only sanitize fields actually present in $input.
		$old   = get_option( self::OPTION_KEY, array() );
		$clean = array_merge( self::DEFAULTS, is_array( $old ) ? $old : array() );

		if ( array_key_exists( 'archive_slug', $input ) ) {
			$slug                  = sanitize_title( (string) $input['archive_slug'] );
			$clean['archive_slug'] = '' !== $slug ? $slug : self::DEFAULTS['archive_slug'];
			update_option( 'trx_ad_archive_slug', $clean['archive_slug'] );
		}

		if ( array_key_exists( 'listings_per_page', $input ) ) {
			$clean['listings_per_page'] = min( 100, max( 1, (int) $input['listings_per_page'] ) );
		}

		if ( array_key_exists( 'distance_unit', $input ) ) {
			$unit                   = sanitize_key( (string) $input['distance_unit'] );
			$clean['distance_unit'] = in_array( $unit, array( 'km', 'miles' ), true ) ? $unit : 'km';
		}

		if ( array_key_exists( 'currency_symbol', $input ) ) {
			$currency                 = sanitize_text_field( (string) $input['currency_symbol'] );
			$clean['currency_symbol'] = mb_substr( $currency, 0, 5 );
			if ( '' === $clean['currency_symbol'] ) {
				$clean['currency_symbol'] = self::DEFAULTS['currency_symbol'];
			}
		}

		if ( array_key_exists( 'currency_code', $input ) ) {
			$code                   = (string) preg_replace( '/[^A-Za-z]/', '', (string) $input['currency_code'] );
			$code                   = strtoupper( $code );
			$clean['currency_code'] = '' !== $code ? mb_substr( $code, 0, 3 ) : self::DEFAULTS['currency_code'];
		}

		if ( array_key_exists( 'default_sort', $input ) ) {
			$sort                  = sanitize_key( (string) $input['default_sort'] );
			$clean['default_sort'] = in_array( $sort, self::SORT_OPTIONS, true ) ? $sort : self::DEFAULTS['default_sort'];
		}

		if ( array_key_exists( 'single_layout', $input ) ) {
			$layout                 = sanitize_key( (string) $input['single_layout'] );
			$clean['single_layout'] = in_array( $layout, array( 'default', 'elementor', 'gutenberg' ), true ) ? $layout : 'default';
		}
		if ( array_key_exists( 'single_gutenberg_template', $input ) ) {
			$clean['single_gutenberg_template'] = absint( $input['single_gutenberg_template'] );
		}

		if ( array_key_exists( 'single_elementor_template', $input ) ) {
			$clean['single_elementor_template'] = absint( $input['single_elementor_template'] );
		}

		// `dealer_can_publish` + `dealer_profile_requires_review` are
		// checkboxes — absent in $input means "unchecked", but only on the
		// General tab post. We detect that by the presence of any other
		// always-posted General field.
		if ( array_key_exists( 'archive_slug', $input ) ) {
			$clean['dealer_can_publish']             = ! empty( $input['dealer_can_publish'] ) ? 1 : 0;
			$clean['dealer_profile_requires_review'] = ! empty( $input['dealer_profile_requires_review'] ) ? 1 : 0;
			$clean['allow_svg_uploads']              = ! empty( $input['allow_svg_uploads'] ) ? 1 : 0;
		}

		// ----- Appearance tab. Hidden marker `trx_ad_appearance_tab` so a
		// posted Appearance form can write empty color strings (= reset to
		// stylesheet default) instead of being treated as "no change".
		$appearanceSubmitted = ! empty( $input['trx_ad_appearance_tab'] );

		if ( $appearanceSubmitted ) {
			foreach ( array( 'color_primary', 'color_primary_hover', 'color_success', 'color_warning', 'color_danger' ) as $colorKey ) {
				$raw = isset( $input[ $colorKey ] ) ? trim( (string) $input[ $colorKey ] ) : '';
				if ( '' === $raw ) {
					$clean[ $colorKey ] = '';
				} else {
					$sanitized          = sanitize_hex_color( $raw );
					$clean[ $colorKey ] = is_string( $sanitized ) ? $sanitized : '';
				}
			}

			$radius          = isset( $input['radius'] ) ? (int) $input['radius'] : 8;
			$clean['radius'] = max( 0, min( 32, $radius ) );
			// Marks the value as user-chosen so 0 means real square corners,
			// not the pre-1.0.14 "use stylesheet default" sentinel.
			$clean['radius_explicit'] = 1;

			// Checkboxes — absent = unchecked.
			$clean['show_price']          = ! empty( $input['show_price'] ) ? 1 : 0;
			$clean['show_featured_badge'] = ! empty( $input['show_featured_badge'] ) ? 1 : 0;
			$clean['use_theme_fonts']     = ! empty( $input['use_theme_fonts'] ) ? 1 : 0;
		}

		// ----- Compare/Favorites tab — hidden marker so unchecked toggles read as 0.
		if ( ! empty( $input['trx_ad_compare_favorites_tab'] ) ) {
			$clean['enable_compare']   = ! empty( $input['enable_compare'] ) ? 1 : 0;
			$clean['enable_favorites'] = ! empty( $input['enable_favorites'] ) ? 1 : 0;
		}

		// ----- Maps tab (Pro per ADR-11). The Maps form posts a hidden
		// `trx_ad_maps_tab` marker so unchecked checkboxes (cluster) read as
		// 0 instead of "absent → keep old" — checkboxes don't post when off.
		$mapsSubmitted = ! empty( $input['trx_ad_maps_tab'] );

		if ( array_key_exists( 'map_provider', $input ) ) {
			$provider              = sanitize_key( (string) $input['map_provider'] );
			$clean['map_provider'] = in_array( $provider, self::MAP_PROVIDERS, true ) ? $provider : 'none';
		}

		if ( array_key_exists( 'google_maps_api_key', $input ) ) {
			$clean['google_maps_api_key'] = sanitize_text_field( (string) $input['google_maps_api_key'] );
		}

		if ( array_key_exists( 'map_default_zoom', $input ) ) {
			$clean['map_default_zoom'] = min( 20, max( 1, (int) $input['map_default_zoom'] ) );
		}

		if ( $mapsSubmitted ) {
			$clean['map_cluster'] = ! empty( $input['map_cluster'] ) ? 1 : 0;
		}

		if ( ( $old['archive_slug'] ?? self::DEFAULTS['archive_slug'] ) !== $clean['archive_slug'] ) {
			flush_rewrite_rules();
		}

		return $clean;
	}


	// ------------------------------------------------------------------
	// Render the settings page with tabs
	// ------------------------------------------------------------------

	public function renderPage(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$activeTab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		// Pro-badge styling lives in assets/css/admin.css (enqueued for this
		// screen by AdminAssets) — no inline <style> per wp.org guidelines.

		// Built-in tabs in display order. Pro and third-party extensions can
		// add more via the `trx_ad_settings_tabs` filter — slug => label.
		// Slugs prefixed with "pro:" get a Pro badge appended to their label;
		// built-in tabs must NOT use it — everything shipped here is fully
		// functional, and badging working features as Pro is a wp.org
		// Guideline 5 (trialware) violation (2026-07 review finding).
		$tabs = array(
			'general'           => __( 'General', 'trx-auto-dealer' ),
			'appearance'        => __( 'Appearance', 'trx-auto-dealer' ),
			'fields'            => __( 'Custom Fields', 'trx-auto-dealer' ),
			'forms'             => __( 'Forms', 'trx-auto-dealer' ),
			'layouts'           => __( 'Single Listing', 'trx-auto-dealer' ),
			'compare_favorites' => __( 'Compare / Favorites', 'trx-auto-dealer' ),
			'maps'              => __( 'Maps', 'trx-auto-dealer' ),
			'taxonomies'        => __( 'Taxonomies', 'trx-auto-dealer' ),
			'maintenance'       => __( 'Maintenance', 'trx-auto-dealer' ),
		);
		/**
		 * Filter the list of settings tabs.
		 *
		 * @param array<string,string> $tabs slug => label.
		 */
		$tabs = (array) apply_filters( 'trx_ad_settings_tabs', $tabs );
		?>
		<div class="wrap">
			<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>

			<?php
			// The Settings-API tabs (General / Appearance / Compare-Favorites /
			// Maps) post to options.php, which already stashes a "Settings
			// saved." message in the `settings_errors` transient — but that
			// transient is never RENDERED on a custom (non-options-general)
			// admin page, so a save looked like it did nothing (UAT #3).
			// Rendering it here surfaces the standard confirmation (and any
			// sanitize-callback errors). The custom-POST tabs (Forms / Custom
			// Fields) print their own notice and don't touch this transient.
			settings_errors();
			?>

			<nav class="nav-tab-wrapper">
				<?php
				foreach ( $tabs as $slugRaw => $label ) {
					$is_pro = 0 === strpos( (string) $slugRaw, 'pro:' );
					$slug   = $is_pro ? substr( (string) $slugRaw, 4 ) : (string) $slugRaw;
					printf(
						'<a href="%s" class="nav-tab %s">%s%s</a>',
						esc_url( $this->tabUrl( $slug ) ),
						$activeTab === $slug ? 'nav-tab-active' : '',
						esc_html( (string) $label ),
						$is_pro ? '<span class="trx-ad-pro-badge">' . esc_html__( 'Pro', 'trx-auto-dealer' ) . '</span>' : ''
					);
				}
				?>
			</nav>

			<?php
			if ( isset( $this->tabs[ $activeTab ] ) ) {
				$this->tabs[ $activeTab ]->render();
			} elseif ( has_action( 'trx_ad_settings_tab_' . $activeTab ) ) {
				/**
				 * Fires when a tab added via `trx_ad_settings_tabs` is active.
				 * The Pro plugin uses this to render the License tab body
				 * without needing to extend AdminSettings.
				 */
				do_action( 'trx_ad_settings_tab_' . $activeTab );
			} else {
				// Unknown tab slug — fall back to General.
				$this->tabs['general']->render();
			}
			?>
		</div>
		<?php
	}

	private function tabUrl( string $tab ): string {
		return add_query_arg(
			array(
				'post_type' => 'trx_ad_listing',
				'page'      => self::PAGE_SLUG,
				'tab'       => $tab,
			),
			admin_url( 'edit.php' )
		);
	}
}
