<?php
/**
 * Livada SEO — Admin UI
 *
 * Registers all admin menus, enqueues assets, handles settings saves,
 * and detects conflicting SEO plugins.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_Admin {

	/**
	 * All sub-menu pages: slug => [ title, view file, save section key ].
	 *
	 * @var array<string, array{string, string, string}>
	 */
	private static array $pages = [
		'livada-seo'                   => [ 'Dashboard',      'dashboard',           ''           ],
		'livada-seo-titles'            => [ 'Titles & Metas',  'settings-titles',     'titles'     ],
		'livada-seo-schema'            => [ 'Schema',          'settings-schema',     'schema'     ],
		'livada-seo-locations'         => [ 'Locations',       'settings-locations',  'locations'  ],
		'livada-seo-social'            => [ 'Social',          'settings-social',     'social'     ],
		'livada-seo-sitemap'           => [ 'Sitemap',         'settings-sitemap',    'sitemap'    ],
		'livada-seo-redirects'         => [ 'Redirections',    'settings-redirects',  'redirects'  ],
		'livada-seo-robots'            => [ 'Robots.txt',      'settings-robots',     'robots'     ],
		'livada-seo-tools'             => [ 'Tools',           'settings-tools',      'tools'      ],
		'livada-seo-general'           => [ 'General',         'settings-general',    'general'    ],
		'livada-seo-audit'             => [ 'Site Audit',      'audit',               ''           ],
		'livada-seo-ai-visibility'     => [ 'AI Visibility',   'ai-visibility',       ''           ],
		'livada-seo-ai-bots'           => [ 'AI Bots Access',  'settings-ai-bots',    'ai_bots'    ],
		'livada-seo-license'           => [ 'Licence',         'settings-license',    'license'    ],
	];

	// ── Bootstrap ──────────────────────────────────────────────────────────────

	public static function init(): void {
		add_action( 'admin_menu',            [ self::class, 'register_menus'        ] );
		add_action( 'admin_enqueue_scripts', [ self::class, 'enqueue_assets'        ] );
		add_action( 'admin_init',            [ self::class, 'detect_seo_conflicts'  ] );
		add_action( 'admin_notices',         [ self::class, 'render_quick_wins'     ] );

		// Register a save handler for every section.
		$sections = [ 'general', 'titles', 'schema', 'social', 'sitemap', 'redirects', 'robots', 'tools', 'license', 'ai_bots' ];
		foreach ( $sections as $section ) {
			add_action( 'admin_post_livada_seo_save_' . $section, [ self::class, 'handle_save' ] );
		}

		// Locations is stored in its own option, not in livada_seo_settings, so
		// it has its own dedicated handler.
		add_action( 'admin_post_livada_seo_save_locations', [ self::class, 'save_locations' ] );

		// AJAX handlers. Note: livada_seo_import_seopress and livada_seo_import_yoast
		// are registered by their respective Import classes, not here.
		add_action( 'wp_ajax_livada_seo_reset_settings',     [ self::class, 'ajax_reset_settings'     ] );
		add_action( 'wp_ajax_livada_seo_regenerate_indexnow',[ self::class, 'ajax_regenerate_indexnow'] );
		add_action( 'wp_ajax_livada_seo_add_redirect',       [ self::class, 'ajax_add_redirect'       ] );
		add_action( 'wp_ajax_livada_seo_update_redirect',    [ self::class, 'ajax_update_redirect'    ] );
		add_action( 'wp_ajax_livada_seo_delete_redirect',    [ self::class, 'ajax_delete_redirect'    ] );

		// Site Audit CSV export.
		add_action( 'admin_post_livada_seo_audit_export_csv', [ self::class, 'export_audit_csv' ] );

		// Full per-post SEO data CSV export.
		add_action( 'admin_post_livada_seo_export_seo_csv',   [ self::class, 'export_seo_csv'   ] );

		// Settings JSON export / import.
		add_action( 'admin_post_livada_seo_export_settings',  [ self::class, 'export_settings'  ] );
		add_action( 'admin_post_livada_seo_import_settings',  [ self::class, 'import_settings'  ] );
	}

	// ── Settings JSON export / import ──────────────────────────────────────────

	/**
	 * Streams the plugin's full configuration as a JSON file:
	 *   - livada_seo_settings option (everything in General/Schema/Sitemap/etc.)
	 *   - livada_seo_locations option (multi-location)
	 *
	 * Sensitive values (`anthropic_api_key`) are stripped from the export so
	 * a leaked JSON doesn't reveal the API key.
	 */
	public static function export_settings(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'livada_seo_export_settings' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'livada-seo' ) );
		}

		$settings = (array) get_option( 'livada_seo_settings', [] );
		// Strip sensitive keys.
		unset( $settings['anthropic_api_key'] );
		// Licence key is stored in its own option (livada_seo_license_key), not
		// in livada_seo_settings, so nothing extra to strip here.

		$payload = [
			'plugin'    => 'livada-seo',
			'version'   => defined( 'LIVADA_SEO_VERSION' ) ? LIVADA_SEO_VERSION : '',
			'exported'  => gmdate( 'c' ),
			'home_url'  => home_url( '/' ),
			'settings'  => $settings,
			'locations' => (array) get_option( 'livada_seo_locations', [] ),
		];

		nocache_headers();
		header( 'Content-Type: application/json; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="livada-seo-settings-' . gmdate( 'Y-m-d' ) . '.json"' );

		echo wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		exit;
	}

	/**
	 * Imports a previously exported settings JSON file. Merges over the
	 * existing settings (does NOT clear other keys — additive). The Anthropic
	 * API key is preserved; any encrypted key in the export (which shouldn't
	 * happen because export strips it, but defensive) is dropped.
	 */
	public static function import_settings(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		check_admin_referer( 'livada_seo_import_settings' );

		if ( empty( $_FILES['livada_seo_import']['tmp_name'] ) || ! is_uploaded_file( $_FILES['livada_seo_import']['tmp_name'] ) ) { // phpcs:ignore
			wp_safe_redirect( admin_url( 'admin.php?page=livada-seo-tools&livada_import=missing' ) );
			exit;
		}

		$raw = file_get_contents( $_FILES['livada_seo_import']['tmp_name'] ); // phpcs:ignore
		$payload = json_decode( (string) $raw, true );

		if ( ! is_array( $payload ) || empty( $payload['plugin'] ) || 'livada-seo' !== $payload['plugin'] ) {
			wp_safe_redirect( admin_url( 'admin.php?page=livada-seo-tools&livada_import=invalid' ) );
			exit;
		}

		// Settings: merge over existing, preserve current Anthropic key.
		if ( isset( $payload['settings'] ) && is_array( $payload['settings'] ) ) {
			$incoming = $payload['settings'];
			unset( $incoming['anthropic_api_key'] );

			$existing = get_option( 'livada_seo_settings', [] );
			if ( ! is_array( $existing ) ) {
				$existing = [];
			}

			$merged = array_merge( $existing, $incoming );

			// Restore the existing Anthropic key (export strips it but a manual
			// edit to the JSON could try to inject one — we never accept it).
			if ( isset( $existing['anthropic_api_key'] ) ) {
				$merged['anthropic_api_key'] = $existing['anthropic_api_key'];
			}

			update_option( 'livada_seo_settings', $merged, false );
		}

		// Locations: full replace (the array is self-contained).
		if ( isset( $payload['locations'] ) && is_array( $payload['locations'] ) ) {
			update_option( 'livada_seo_locations', $payload['locations'], false );
		}

		wp_safe_redirect( admin_url( 'admin.php?page=livada-seo-tools&livada_import=ok' ) );
		exit;
	}

	// ── Multi-location save ────────────────────────────────────────────────────

	/**
	 * Persists the `livada_seo_locations` option from the locations form.
	 *
	 * Each row arrives as `livada_seo_locations[N][field]`. We read the raw
	 * comma/newline-separated text fields (amenities_csv, same_as_csv,
	 * services_csv) and parse them into the structured arrays the schema
	 * builder expects.
	 */
	public static function save_locations(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		$nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; // phpcs:ignore
		if ( ! wp_verify_nonce( $nonce, 'livada_seo_save_locations' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'livada-seo' ) );
		}

		$raw = isset( $_POST['livada_seo_locations'] ) && is_array( $_POST['livada_seo_locations'] )
			? wp_unslash( $_POST['livada_seo_locations'] ) // phpcs:ignore
			: [];

		$allowed_types = [
			'LocalBusiness', 'Organization',
			'Campground', 'LodgingBusiness', 'Hotel', 'BedAndBreakfast', 'Resort', 'Hostel', 'Motel',
			'Restaurant', 'Store', 'TouristAttraction',
		];

		$clean = [];
		foreach ( $raw as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$name = sanitize_text_field( $row['name'] ?? '' );
			if ( '' === $name ) {
				// Drop rows without a name; the form template starts with one
				// hidden empty row that should not be persisted.
				continue;
			}

			$type = sanitize_text_field( $row['type'] ?? 'LocalBusiness' );
			if ( ! in_array( $type, $allowed_types, true ) ) {
				$type = 'LocalBusiness';
			}

			$pets = '';
			if ( isset( $row['pets_allowed'] ) && '' !== $row['pets_allowed'] ) {
				$pets = filter_var( $row['pets_allowed'], FILTER_VALIDATE_BOOLEAN ) ? '1' : '0';
			}

			// Country code: 2 uppercase letters or empty.
			$country = strtoupper( substr( preg_replace( '/[^A-Za-z]/', '', (string) ( $row['country'] ?? 'FR' ) ), 0, 2 ) );

			// Parse amenities from the comma-separated textarea.
			$amenities = [];
			$am_csv    = (string) ( $row['amenities_csv'] ?? '' );
			if ( '' !== $am_csv ) {
				foreach ( preg_split( '/[,\n]/', $am_csv ) as $item ) {
					$item = trim( $item );
					if ( '' === $item ) {
						continue;
					}
					$value = '1';
					if ( false !== strpos( $item, '|' ) ) {
						[ $n, $v ] = array_map( 'trim', explode( '|', $item, 2 ) );
						$item  = $n;
						$value = ( '0' === $v || 'no' === strtolower( $v ) || 'false' === strtolower( $v ) ) ? '0' : '1';
					}
					$amenities[] = [
						'name'  => sanitize_text_field( $item ),
						'value' => $value,
					];
				}
			}

			// sameAs URLs from the multi-line textarea.
			$same_as = [];
			$sa_csv  = (string) ( $row['same_as_csv'] ?? '' );
			if ( '' !== $sa_csv ) {
				foreach ( preg_split( '/[\r\n]+/', $sa_csv ) as $url ) {
					$url = esc_url_raw( trim( $url ) );
					if ( '' !== $url ) {
						$same_as[] = $url;
					}
				}
			}

			// Services as "name | description" lines.
			$services = [];
			$sv_csv   = (string) ( $row['services_csv'] ?? '' );
			if ( '' !== $sv_csv ) {
				foreach ( preg_split( '/[\r\n]+/', $sv_csv ) as $line ) {
					$line = trim( $line );
					if ( '' === $line ) {
						continue;
					}
					$svc_name = $line;
					$svc_desc = '';
					if ( false !== strpos( $line, '|' ) ) {
						[ $svc_name, $svc_desc ] = array_map( 'trim', explode( '|', $line, 2 ) );
					}
					if ( '' === $svc_name ) {
						continue;
					}
					$services[] = [
						'name'        => sanitize_text_field( $svc_name ),
						'description' => sanitize_textarea_field( $svc_desc ),
					];
				}
			}

			$clean[] = [
				'name'             => $name,
				'type'             => $type,
				'address'          => sanitize_text_field( $row['address']     ?? '' ),
				'city'             => sanitize_text_field( $row['city']        ?? '' ),
				'postal'           => sanitize_text_field( $row['postal']      ?? '' ),
				'country'          => '' !== $country ? $country : 'FR',
				'phone'            => sanitize_text_field( $row['phone']       ?? '' ),
				'email'            => sanitize_email(    $row['email']         ?? '' ),
				'url'              => esc_url_raw(       $row['url']           ?? '' ),
				'logo'             => esc_url_raw(       $row['logo']          ?? '' ),
				'geo_lat'          => sanitize_text_field( $row['geo_lat']     ?? '' ),
				'geo_lng'          => sanitize_text_field( $row['geo_lng']     ?? '' ),
				'price_range'      => sanitize_text_field( $row['price_range'] ?? '' ),
				'star_rating'      => sanitize_text_field( $row['star_rating'] ?? '' ),
				'number_of_rooms'  => sanitize_text_field( $row['number_of_rooms'] ?? '' ),
				'pets_allowed'     => $pets,
				'checkin_time'     => sanitize_text_field( $row['checkin_time']    ?? '' ),
				'checkout_time'    => sanitize_text_field( $row['checkout_time']   ?? '' ),
				'rating_value'     => sanitize_text_field( $row['rating_value']    ?? '' ),
				'review_count'     => sanitize_text_field( $row['review_count']    ?? '' ),
				'amenities'        => $amenities,
				'same_as'          => $same_as,
				'services'         => $services,
				'opening_hours'    => [], // Hours stay in the global Schema settings page for now.
			];
		}

		update_option( 'livada_seo_locations', $clean, false );

		wp_safe_redirect( admin_url( 'admin.php?page=livada-seo-locations&updated=true' ) );
		exit;
	}

	// ── Full per-post SEO data CSV export ──────────────────────────────────────

	/**
	 * Streams a CSV with one row per published post / page across the public
	 * post types listed in `sitemap_post_types`. Includes every Livada SEO
	 * meta field plus the Schema location pointer. Used for backups, agency
	 * handoffs, or content-team audits in Excel.
	 */
	public static function export_seo_csv(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'livada_seo_export_seo_csv' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'livada-seo' ) );
		}

		$post_types = (array) Livada_SEO_Loader::get_setting( 'sitemap_post_types', [ 'post', 'page' ] );
		if ( class_exists( 'Livada_SEO_Sitemap' ) ) {
			$deny = Livada_SEO_Sitemap::get_template_cpts();
			$post_types = array_values( array_diff( $post_types, $deny ) );
		}
		if ( empty( $post_types ) ) {
			$post_types = [ 'post', 'page' ];
		}

		nocache_headers();
		header( 'Content-Type: text/csv; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="livada-seo-' . gmdate( 'Y-m-d' ) . '.csv"' );

		$out = fopen( 'php://output', 'w' );
		fwrite( $out, "\xEF\xBB\xBF" ); // UTF-8 BOM for Excel.

		fputcsv( $out, [
			'id', 'post_type', 'status', 'title', 'permalink',
			'seo_title', 'meta_description', 'focus_keyword',
			'seo_score', 'readability', 'noindex', 'nofollow', 'cornerstone',
			'canonical', 'og_image',
			'schema_type', 'location_id',
			'sitemap_excluded',
		] );

		// Stream in batches so we don't blow memory on big sites.
		$paged = 1;
		do {
			$query = new WP_Query( [
				'post_type'              => $post_types,
				'post_status'            => [ 'publish', 'draft', 'pending', 'private', 'future' ],
				'posts_per_page'         => 200,
				'paged'                  => $paged,
				'orderby'                => 'ID',
				'order'                  => 'ASC',
				'no_found_rows'          => false,
				'update_post_term_cache' => false,
				'update_post_meta_cache' => true,
			] );

			foreach ( $query->posts as $p ) {
				$pid = (int) $p->ID;
				fputcsv( $out, [
					$pid,
					$p->post_type,
					$p->post_status,
					$p->post_title,
					(string) get_permalink( $pid ),
					(string) get_post_meta( $pid, '_livada_seo_title',                true ),
					(string) get_post_meta( $pid, '_livada_seo_description',          true ),
					(string) get_post_meta( $pid, '_livada_seo_focus_keyword',        true ),
					(string) get_post_meta( $pid, '_livada_seo_score',                true ),
					(string) get_post_meta( $pid, '_livada_seo_readability_score',    true ),
					(string) get_post_meta( $pid, '_livada_seo_noindex',              true ),
					(string) get_post_meta( $pid, '_livada_seo_nofollow',             true ),
					(string) get_post_meta( $pid, '_livada_seo_cornerstone',          true ),
					(string) get_post_meta( $pid, '_livada_seo_canonical',            true ),
					(string) get_post_meta( $pid, '_livada_seo_og_image',             true ),
					(string) get_post_meta( $pid, '_livada_seo_schema_type',          true ),
					(string) get_post_meta( $pid, '_livada_seo_location_id',          true ),
					(string) get_post_meta( $pid, '_livada_seo_sitemap_exclude',      true ),
				] );
			}

			wp_reset_postdata();
			$last = (int) $query->max_num_pages;
			$paged++;
		} while ( $paged <= $last );

		fclose( $out );
		exit;
	}

	// ── Audit CSV export ───────────────────────────────────────────────────────

	public static function export_audit_csv(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'livada_seo_audit_export' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'livada-seo' ) );
		}

		if ( ! class_exists( 'Livada_SEO_Site_Audit' ) ) {
			wp_die( esc_html__( 'Site Audit module is not active.', 'livada-seo' ) );
		}

		$results  = Livada_SEO_Site_Audit::get_results();
		$per_post = isset( $results['per_post'] ) && is_array( $results['per_post'] ) ? $results['per_post'] : [];

		nocache_headers();
		header( 'Content-Type: text/csv; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="livada-seo-audit-' . gmdate( 'Y-m-d' ) . '.csv"' );

		$out = fopen( 'php://output', 'w' );
		fwrite( $out, "\xEF\xBB\xBF" ); // UTF-8 BOM for Excel.

		fputcsv( $out, [ 'post_id', 'title', 'status', 'check', 'check_status', 'check_message', 'edit_url' ] );

		foreach ( $per_post as $row ) {
			$post_id  = isset( $row['post_id'] ) ? (int) $row['post_id'] : 0;
			$title    = isset( $row['title'] )   ? (string) $row['title']   : '';
			$status   = isset( $row['status'] )  ? (string) $row['status']  : '';
			$edit_url = isset( $row['edit_url'] ) ? (string) $row['edit_url'] : '';
			$checks   = isset( $row['checks'] ) && is_array( $row['checks'] ) ? $row['checks'] : [];

			if ( empty( $checks ) ) {
				fputcsv( $out, [ $post_id, $title, $status, '', '', '', $edit_url ] );
				continue;
			}

			foreach ( $checks as $c ) {
				fputcsv(
					$out,
					[
						$post_id,
						$title,
						$status,
						isset( $c['check'] )   ? (string) $c['check']   : '',
						isset( $c['status'] )  ? (string) $c['status']  : '',
						isset( $c['message'] ) ? (string) $c['message'] : '',
						$edit_url,
					]
				);
			}
		}

		fclose( $out );
		exit;
	}

	// ── Menu registration ──────────────────────────────────────────────────────

	public static function register_menus(): void {
		add_menu_page(
			__( 'Livada SEO', 'livada-seo' ),
			__( 'Livada SEO', 'livada-seo' ),
			'manage_options',
			'livada-seo',
			[ self::class, 'render_page' ],
			'dashicons-chart-area',
			80
		);

		// Dashboard (re-register to control the label shown in the sub-menu).
		add_submenu_page(
			'livada-seo',
			__( 'Dashboard', 'livada-seo' ),
			__( 'Dashboard', 'livada-seo' ),
			'manage_options',
			'livada-seo',
			[ self::class, 'render_page' ]
		);

		$sub_pages = [
			'livada-seo-general'    => __( 'General',          'livada-seo' ),
			'livada-seo-titles'     => __( 'Titles & Metas',   'livada-seo' ),
			'livada-seo-schema'     => __( 'Schema',           'livada-seo' ),
			'livada-seo-locations'  => __( 'Locations',        'livada-seo' ),
			'livada-seo-social'     => __( 'Social',           'livada-seo' ),
			'livada-seo-sitemap'    => __( 'Sitemap',          'livada-seo' ),
			'livada-seo-redirects'  => __( 'Redirections',     'livada-seo' ),
			'livada-seo-robots'     => __( 'Robots.txt',       'livada-seo' ),
			'livada-seo-audit'      => __( 'Site Audit',       'livada-seo' ),
			'livada-seo-ai-visibility' => __( 'AI Visibility',   'livada-seo' ),
			'livada-seo-ai-bots'    => __( 'AI Bots Access',   'livada-seo' ),
			'livada-seo-tools'      => __( 'Tools',            'livada-seo' ),
			'livada-seo-license'    => __( 'Licence',          'livada-seo' ),
		];

		foreach ( $sub_pages as $slug => $label ) {
			add_submenu_page(
				'livada-seo',
				$label . ' — ' . __( 'Livada SEO', 'livada-seo' ),
				$label,
				'manage_options',
				$slug,
				[ self::class, 'render_page' ]
			);
		}
	}

	// ── Page renderer ──────────────────────────────────────────────────────────

	public static function render_page(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'livada-seo' ) );
		}

		// Determine which sub-page we are on.
		$slug = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : 'livada-seo'; // phpcs:ignore

		$view_map = [
			'livada-seo'            => 'dashboard',
			'livada-seo-general'    => 'settings-general',
			'livada-seo-titles'     => 'settings-titles',
			'livada-seo-schema'     => 'settings-schema',
			'livada-seo-locations'  => 'settings-locations',
			'livada-seo-social'     => 'settings-social',
			'livada-seo-sitemap'    => 'settings-sitemap',
			'livada-seo-redirects'  => 'settings-redirects',
			'livada-seo-robots'     => 'settings-robots',
			'livada-seo-tools'      => 'settings-tools',
			'livada-seo-audit'      => 'audit',
			'livada-seo-ai-visibility' => 'ai-visibility',
			'livada-seo-ai-bots'    => 'settings-ai-bots',
			'livada-seo-license'    => 'settings-license',
		];

		$view = $view_map[ $slug ] ?? 'dashboard';
		$file = LIVADA_SEO_PATH . 'includes/admin/views/' . $view . '.php';

		if ( file_exists( $file ) ) {
			require $file;
		} else {
			echo '<div class="wrap"><h1>' . esc_html__( 'Page not found', 'livada-seo' ) . '</h1></div>';
		}
	}

	// ── Asset enqueue ──────────────────────────────────────────────────────────

	public static function enqueue_assets( string $hook_suffix ): void {
		// Only load on plugin pages.
		if ( strpos( $hook_suffix, 'livada-seo' ) === false ) {
			return;
		}

		wp_enqueue_style(
			'livada-seo-admin',
			LIVADA_SEO_URL . 'assets/css/admin.css',
			[],
			LIVADA_SEO_VERSION
		);

		wp_enqueue_script(
			'livada-seo-admin',
			LIVADA_SEO_URL . 'assets/js/admin.js',
			[],
			LIVADA_SEO_VERSION,
			true
		);

		// Enqueue WP media uploader for pages that use the media picker.
		$media_pages = [ 'livada-seo-social', 'livada-seo-schema' ];
		$current_page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore
		if ( in_array( $current_page, $media_pages, true ) ) {
			wp_enqueue_media();
		}

		wp_localize_script(
			'livada-seo-admin',
			'livadaSEO',
			[
				'ajaxUrl'  => admin_url( 'admin-ajax.php' ),
				'nonce'    => wp_create_nonce( 'livada_seo_admin_nonce' ),
				'strings'  => [
					'confirmReset'       => __( 'Are you sure you want to reset ALL settings to defaults? This cannot be undone.', 'livada-seo' ),
					'confirmDelete'      => __( 'Delete this redirect?', 'livada-seo' ),
					'importing'          => __( 'Importing…', 'livada-seo' ),
					'importDone'         => __( 'Import complete!', 'livada-seo' ),
					'importError'        => __( 'Import failed. Please try again.', 'livada-seo' ),
					'regenerating'       => __( 'Regenerating…', 'livada-seo' ),
					'chooseImage'        => __( 'Choose Image', 'livada-seo' ),
					'useImage'           => __( 'Use this image', 'livada-seo' ),
				],
			]
		);
	}

	// ── Settings save ──────────────────────────────────────────────────────────

	/**
	 * Generic save handler — dispatches by section name extracted from the action.
	 */
	public static function handle_save(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		// Extract section from action name, e.g. livada_seo_save_general → general.
		$action  = isset( $_POST['action'] ) ? sanitize_key( $_POST['action'] ) : ''; // phpcs:ignore
		$section = str_replace( 'livada_seo_save_', '', $action );

		if ( ! wp_verify_nonce(
			isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '', // phpcs:ignore
			'livada_seo_save_' . $section
		) ) {
			wp_die( esc_html__( 'Security check failed.', 'livada-seo' ) );
		}

		$input = isset( $_POST['livada_seo'] ) ? (array) wp_unslash( $_POST['livada_seo'] ) : []; // phpcs:ignore

		$sanitized = self::sanitize_section( $section, $input );

		// Merge into existing settings.
		$existing = get_option( 'livada_seo_settings', [] );
		if ( ! is_array( $existing ) ) {
			$existing = [];
		}

		$updated = array_merge( $existing, $sanitized );
		update_option( 'livada_seo_settings', $updated, false );

		// Settings changes can move the AI Visibility score — drop its cache.
		if ( class_exists( 'Livada_SEO_AI_Visibility' ) ) {
			Livada_SEO_AI_Visibility::flush();
		}

		// Determine the correct page slug to redirect back to.
		$page_map = [
			'general'   => 'livada-seo-general',
			'titles'    => 'livada-seo-titles',
			'schema'    => 'livada-seo-schema',
			'social'    => 'livada-seo-social',
			'sitemap'   => 'livada-seo-sitemap',
			'redirects' => 'livada-seo-redirects',
			'robots'    => 'livada-seo-robots',
			'tools'     => 'livada-seo-tools',
			'license'   => 'livada-seo-license',
			'ai_bots'   => 'livada-seo-ai-bots',
		];

		$page = $page_map[ $section ] ?? 'livada-seo';

		wp_safe_redirect( admin_url( 'admin.php?page=' . $page . '&updated=true' ) );
		exit;
	}

	/**
	 * Sanitizes settings for a given section.
	 *
	 * @param string               $section
	 * @param array<string, mixed> $input
	 * @return array<string, mixed>
	 */
	private static function sanitize_section( string $section, array $input ): array {
		switch ( $section ) {

			case 'general':
				$all_modules = [
					'meta', 'sitemap', 'sitemap_html', 'schema', 'opengraph', 'redirects',
					'breadcrumbs', 'analysis', 'robots', 'indexnow', 'toc', 'image_seo',
					'analytics', 'geo', 'aeo', 'site_audit', 'woocommerce', 'og_image',
				];
				$enabled = [];
				if ( isset( $input['modules'] ) && is_array( $input['modules'] ) ) {
					foreach ( $input['modules'] as $mod ) {
						if ( in_array( $mod, $all_modules, true ) ) {
							$enabled[] = $mod;
						}
					}
				}

				$out = [
					'site_name' => sanitize_text_field( $input['site_name'] ?? '' ),
					'separator' => sanitize_text_field( $input['separator'] ?? '|' ),
					'modules'   => $enabled,
				];

				// Anthropic API key: only update when the user typed a new value
				// or explicitly ticked the "remove" box. Empty input keeps the
				// existing stored key intact.
				if ( ! empty( $input['anthropic_api_key_remove'] ) ) {
					$out['anthropic_api_key'] = '';
				} elseif ( isset( $input['anthropic_api_key'] ) && '' !== trim( (string) $input['anthropic_api_key'] ) ) {
					$plain = trim( (string) $input['anthropic_api_key'] );
					if ( class_exists( 'Livada_SEO_Image_Bulk' ) ) {
						$out['anthropic_api_key'] = Livada_SEO_Image_Bulk::encrypt_key_for_storage( $plain );
					} else {
						$out['anthropic_api_key'] = $plain;
					}
				}
				// else: leave anthropic_api_key untouched in the merged settings.

				// AEO — Knowledge Graph fields. These are the source of truth for both
				// the Organization JSON-LD entity and the /llms.txt content.
				$out['aeo_org_description']   = sanitize_textarea_field( (string) ( $input['aeo_org_description'] ?? '' ) );

				// foundingDate — must be a 4-digit year or empty.
				$founding = trim( (string) ( $input['aeo_org_founding_date'] ?? '' ) );
				$out['aeo_org_founding_date'] = preg_match( '/^\d{4}$/', $founding ) ? $founding : '';

				$out['aeo_org_founder']       = sanitize_text_field( (string) ( $input['aeo_org_founder'] ?? '' ) );

				// knowsAbout — textarea, one topic per line → array of clean strings.
				$knows_raw   = (string) ( $input['aeo_org_knows_about'] ?? '' );
				$knows_lines = preg_split( '/[\r\n]+/', $knows_raw );
				$knows_clean = [];
				if ( is_array( $knows_lines ) ) {
					foreach ( $knows_lines as $line ) {
						$line = sanitize_text_field( $line );
						if ( '' !== $line ) {
							$knows_clean[] = $line;
						}
					}
				}
				$out['aeo_org_knows_about'] = $knows_clean;

				// Wikidata entity ID — must be Q followed by digits (e.g. Q140007358).
				// Empty input clears the setting (admin explicitly removing the Q-id).
				// Non-empty but malformed input is treated as browser autofill (Chrome
				// happily fills any plain text input with the admin email if no
				// autocomplete="off" — observed in production on hotel-du-roc04.fr):
				// in that case we PRESERVE the previously stored value rather than
				// silently wiping it. The HTML side now also carries autocomplete="off"
				// + data-lpignore + data-1p-ignore + spellcheck=false to block the
				// autofill at the source.
				$wd_raw = trim( (string) ( $input['aeo_wikidata_id'] ?? '' ) );
				if ( '' === $wd_raw ) {
					$out['aeo_wikidata_id'] = '';
				} else {
					$wd_clean = preg_replace( '/[^Q0-9]/', '', $wd_raw );
					if ( preg_match( '/^Q\d+$/', (string) $wd_clean ) ) {
						$out['aeo_wikidata_id'] = (string) $wd_clean;
					} else {
						// Malformed (likely autofill) — keep what was already stored.
						$out['aeo_wikidata_id'] = (string) Livada_SEO_Loader::get_setting( 'aeo_wikidata_id', '' );
					}
				}

				return $out;

			case 'titles':
				// Title/description templates contain %variable% placeholders.
				// sanitize_text_field() strips any %XX where XX is valid hex
				// (e.g. %ca in %category%, %ta in %tag%). Use wp_strip_all_tags()
				// to preserve placeholders while removing HTML.
				$sanitized = [];
				$title_keys = [
					'title_home', 'title_post', 'title_page',
					'title_category', 'title_tag', 'title_taxonomy',
					'desc_home',
				];
				foreach ( $title_keys as $key ) {
					if ( isset( $input[ $key ] ) ) {
						$sanitized[ $key ] = wp_strip_all_tags( (string) $input[ $key ] );
					}
				}
				// Dynamic CPT/taxonomy title templates.
				foreach ( $input as $key => $value ) {
					if ( strpos( $key, 'title_cpt_' ) === 0 || strpos( $key, 'title_tax_' ) === 0 ) {
						$sanitized[ sanitize_key( $key ) ] = wp_strip_all_tags( (string) $value );
					}
				}
				return $sanitized;

			case 'schema':
				$allowed_types = [
					'LocalBusiness', 'Organization',
					'Campground', 'LodgingBusiness', 'Hotel', 'BedAndBreakfast', 'Resort', 'Hostel', 'Motel',
					'Restaurant', 'Store', 'TouristAttraction',
				];
				$type_in       = sanitize_text_field( $input['schema_type'] ?? 'LocalBusiness' );

				$sanitized = [
					'schema_type'        => in_array( $type_in, $allowed_types, true ) ? $type_in : 'LocalBusiness',
					'schema_name'        => sanitize_text_field( $input['schema_name'] ?? '' ),
					'schema_address'     => sanitize_text_field( $input['schema_address'] ?? '' ),
					'schema_city'        => sanitize_text_field( $input['schema_city'] ?? '' ),
					'schema_postal'      => sanitize_text_field( $input['schema_postal'] ?? '' ),
					'schema_country'     => sanitize_text_field( $input['schema_country'] ?? '' ),
					'schema_phone'       => sanitize_text_field( $input['schema_phone'] ?? '' ),
					'schema_email'       => sanitize_email( $input['schema_email'] ?? '' ),
					'schema_url'         => esc_url_raw( $input['schema_url'] ?? '' ),
					'schema_logo'        => esc_url_raw( $input['schema_logo'] ?? '' ),
					'schema_geo_lat'     => sanitize_text_field( $input['schema_geo_lat'] ?? '' ),
					'schema_geo_lng'     => sanitize_text_field( $input['schema_geo_lng'] ?? '' ),
					'schema_price_range' => sanitize_text_field( $input['schema_price_range'] ?? '' ),

					// Tourism / lodging — emitted only when set.
					'schema_star_rating'     => sanitize_text_field( $input['schema_star_rating']     ?? '' ),
					'schema_number_of_rooms' => sanitize_text_field( $input['schema_number_of_rooms'] ?? '' ),
					'schema_pets_allowed'    => isset( $input['schema_pets_allowed'] ) && '' !== $input['schema_pets_allowed']
						? ( filter_var( $input['schema_pets_allowed'], FILTER_VALIDATE_BOOLEAN ) ? '1' : '0' )
						: '',
					'schema_checkin_time'    => sanitize_text_field( $input['schema_checkin_time']    ?? '' ),
					'schema_checkout_time'   => sanitize_text_field( $input['schema_checkout_time']   ?? '' ),
					'schema_rating_value'    => sanitize_text_field( $input['schema_rating_value']    ?? '' ),
					'schema_review_count'    => sanitize_text_field( $input['schema_review_count']    ?? '' ),
				];

				// Amenities (repeater of name + boolean value).
				$amenities = [];
				if ( isset( $input['schema_amenities'] ) && is_array( $input['schema_amenities'] ) ) {
					foreach ( $input['schema_amenities'] as $row ) {
						if ( ! is_array( $row ) ) {
							continue;
						}
						$name = sanitize_text_field( $row['name'] ?? '' );
						if ( '' === $name ) {
							continue;
						}
						$amenities[] = [
							'name'  => $name,
							'value' => ! empty( $row['value'] ) ? '1' : '0',
						];
					}
				}
				$sanitized['schema_amenities'] = $amenities;

				// Opening hours (array of day/open/close).
				$hours = [];
				if ( isset( $input['schema_opening_hours'] ) && is_array( $input['schema_opening_hours'] ) ) {
					foreach ( $input['schema_opening_hours'] as $row ) {
						if ( ! is_array( $row ) ) {
							continue;
						}
						$hours[] = [
							'day'   => sanitize_text_field( $row['day']   ?? '' ),
							'open'  => sanitize_text_field( $row['open']  ?? '' ),
							'close' => sanitize_text_field( $row['close'] ?? '' ),
						];
					}
				}
				$sanitized['schema_opening_hours'] = $hours;

				// SameAs URLs.
				$same_as = [];
				if ( isset( $input['schema_same_as'] ) && is_array( $input['schema_same_as'] ) ) {
					foreach ( $input['schema_same_as'] as $url ) {
						$url = esc_url_raw( sanitize_text_field( $url ) );
						if ( $url !== '' ) {
							$same_as[] = $url;
						}
					}
				}
				$sanitized['schema_same_as'] = $same_as;

				// Services.
				$services = [];
				if ( isset( $input['schema_services'] ) && is_array( $input['schema_services'] ) ) {
					foreach ( $input['schema_services'] as $svc ) {
						if ( ! is_array( $svc ) ) {
							continue;
						}
						$services[] = [
							'name'        => sanitize_text_field( $svc['name'] ?? '' ),
							'description' => sanitize_textarea_field( $svc['description'] ?? '' ),
						];
					}
				}
				$sanitized['schema_services'] = $services;

				return $sanitized;

			case 'social':
				return [
					'og_default_image'  => esc_url_raw( $input['og_default_image'] ?? '' ),
					'twitter_card_type' => in_array( $input['twitter_card_type'] ?? '', [ 'summary', 'summary_large_image' ], true )
						? $input['twitter_card_type']
						: 'summary_large_image',
					'social_facebook'   => esc_url_raw( $input['social_facebook'] ?? '' ),
					'social_twitter'    => esc_url_raw( $input['social_twitter'] ?? '' ),
					'social_linkedin'   => esc_url_raw( $input['social_linkedin'] ?? '' ),
					'social_instagram'  => esc_url_raw( $input['social_instagram'] ?? '' ),
				];

			case 'sitemap':
				$post_types = [];
				if ( isset( $input['sitemap_post_types'] ) && is_array( $input['sitemap_post_types'] ) ) {
					foreach ( $input['sitemap_post_types'] as $pt ) {
						$post_types[] = sanitize_key( $pt );
					}
				}
				$taxonomies = [];
				if ( isset( $input['sitemap_taxonomies'] ) && is_array( $input['sitemap_taxonomies'] ) ) {
					foreach ( $input['sitemap_taxonomies'] as $tax ) {
						$taxonomies[] = sanitize_key( $tax );
					}
				}

				$news_post_types = [];
				if ( isset( $input['sitemap_news_post_types'] ) && is_array( $input['sitemap_news_post_types'] ) ) {
					foreach ( $input['sitemap_news_post_types'] as $pt ) {
						$news_post_types[] = sanitize_key( $pt );
					}
				}

				// Language: keep only ASCII letters / underscores / dashes; max 5 chars.
				$lang = sanitize_text_field( $input['sitemap_news_language'] ?? '' );
				$lang = preg_replace( '/[^A-Za-z_-]/', '', $lang ) ?? '';
				$lang = substr( $lang, 0, 5 );

				return [
					'sitemap_post_types'             => $post_types,
					'sitemap_taxonomies'             => $taxonomies,
					'sitemap_max_urls'               => absint( $input['sitemap_max_urls'] ?? 1000 ),
					'sitemap_news_post_types'        => $news_post_types,
					'sitemap_news_publication_name'  => sanitize_text_field( $input['sitemap_news_publication_name'] ?? '' ),
					'sitemap_news_language'          => $lang,
				];

			case 'redirects':
				// Handled via AJAX; form-submit here is a no-op.
				return [];

			case 'robots':
				return [
					'robots_txt' => sanitize_textarea_field( $input['robots_txt'] ?? '' ),
				];

			case 'tools':
				// Settings reset and other tool actions are AJAX; nothing to merge here.
				return [];

			case 'license':
				// Licence activation / deactivation is handled via AJAX in
				// Livada_SEO_License. Nothing to save here via normal form POST.
				return [];

			case 'ai_bots':
				// Form posts livada_seo[ai_bots][bot_key] = '1' only for checked
				// boxes. Unchecked boxes are simply absent — so we build the map
				// from the canonical registry, defaulting OFF for everything the
				// form didn't include.
				if ( ! class_exists( 'Livada_SEO_AI_Bots' ) ) {
					require_once LIVADA_SEO_PATH . 'includes/class-ai-bots.php';
				}

				$registry   = Livada_SEO_AI_Bots::registry();
				$submitted  = isset( $input['ai_bots'] ) && is_array( $input['ai_bots'] ) ? $input['ai_bots'] : [];

				$cleaned = [];
				foreach ( $registry as $key => $_bot ) {
					$cleaned[ $key ] = ! empty( $submitted[ $key ] );
				}

				// Drop the per-bot diagnostic cache so the next view reflects
				// the new state if the operator runs the live test.
				Livada_SEO_AI_Bots::flush_test_cache();

				// Purge LiteSpeed Cache (if active) so robots.txt updates
				// without requiring a manual purge.
				if ( has_action( 'litespeed_purge_all' ) ) {
					do_action( 'litespeed_purge_all' );
				}

				return [
					'ai_bots'            => $cleaned,
					'ai_bots_hard_block' => ! empty( $input['ai_bots_hard_block'] ),
				];

			default:
				return [];
		}
	}

	// ── Quick-wins admin notice ────────────────────────────────────────────────

	/**
	 * Renders a single dismissible "quick wins" notice on Livada SEO admin
	 * pages when the operator has obvious unfinished setup steps:
	 *   - LocalBusiness data missing
	 *   - Anthropic API key missing while AI buttons are advertised
	 *   - No focus keyword on >50 % of published posts
	 *
	 * Only fires on the plugin's own admin pages and on the Dashboard, so it
	 * doesn't pollute regular WP admin screens. The notice respects per-user
	 * dismissal via the `livada_seo_quick_wins_dismissed` user meta.
	 */
	public static function render_quick_wins(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen ) {
			return;
		}

		// Show only on Livada SEO admin pages or the WP Dashboard.
		$id  = (string) ( $screen->id ?? '' );
		$ok  = ( 'dashboard' === $id ) || ( false !== strpos( $id, 'livada-seo' ) );
		if ( ! $ok ) {
			return;
		}

		$user_id = get_current_user_id();
		if ( get_user_meta( $user_id, 'livada_seo_quick_wins_dismissed', true ) ) {
			return;
		}

		// Allow ?livada_seo_dismiss_qw=1 to dismiss persistently.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['livada_seo_dismiss_qw'] ) && '1' === $_GET['livada_seo_dismiss_qw'] ) {
			update_user_meta( $user_id, 'livada_seo_quick_wins_dismissed', 1 );
			return;
		}

		$tips = [];

		// Tip 1: LocalBusiness data missing.
		$has_locations = false;
		if ( class_exists( 'Livada_SEO_Schema' ) ) {
			$has_locations = ! empty( Livada_SEO_Schema::get_locations() );
		}
		if ( ! $has_locations ) {
			$tips[] = sprintf(
				'<a href="%s">%s</a>',
				esc_url( admin_url( 'admin.php?page=livada-seo-locations' ) ),
				esc_html__( 'Configure your business name, address, and geo coordinates → Locations', 'livada-seo' )
			);
		}

		// Tip 2: Anthropic API key missing.
		$has_ai = false;
		if ( class_exists( 'Livada_SEO_Image_Bulk' ) ) {
			$has_ai = '' !== Livada_SEO_Image_Bulk::get_anthropic_key();
		}
		if ( ! $has_ai ) {
			$tips[] = sprintf(
				'<a href="%s">%s</a>',
				esc_url( admin_url( 'admin.php?page=livada-seo-general#livada_anthropic_api_key' ) ),
				esc_html__( 'Add an Anthropic API key to unlock AI alt-text + title/description suggestions', 'livada-seo' )
			);
		}

		// Tip 3: more than half of published posts have no focus keyword.
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$total = (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->posts}
			 WHERE post_status = 'publish'
			   AND post_type IN ('post','page')"
		);
		if ( $total > 0 ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$missing_kw = (int) $wpdb->get_var(
				"SELECT COUNT(DISTINCT p.ID)
				 FROM {$wpdb->posts} p
				 LEFT JOIN {$wpdb->postmeta} pm
				        ON pm.post_id = p.ID
				       AND pm.meta_key = '_livada_seo_focus_keyword'
				 WHERE p.post_status = 'publish'
				   AND p.post_type IN ('post','page')
				   AND ( pm.meta_value IS NULL OR pm.meta_value = '' )"
			);

			if ( $missing_kw > 0 && ( $missing_kw / $total ) > 0.5 ) {
				$tips[] = sprintf(
					/* translators: 1: filtered URL, 2: count, 3: total */
					'<a href="%1$s">%2$s</a>',
					esc_url( class_exists( 'Livada_SEO_Admin_Filters' ) ? Livada_SEO_Admin_Filters::url_for( 'no_keyword', 'post' ) : '#' ),
					sprintf(
						/* translators: 1: count, 2: total */
						esc_html__( '%1$d / %2$d posts have no focus keyword — review them', 'livada-seo' ),
						(int) $missing_kw,
						(int) $total
					)
				);
			}
		}

		if ( empty( $tips ) ) {
			return;
		}

		$dismiss_url = esc_url(
			add_query_arg( 'livada_seo_dismiss_qw', '1' )
		);

		echo '<div class="notice notice-info" style="border-left-color:#2271b1;">';
		echo '<p><strong>' . esc_html__( 'Livada SEO — quick wins:', 'livada-seo' ) . '</strong></p>';
		echo '<ul style="margin:0 0 8px 18px; list-style:disc;">';
		foreach ( $tips as $tip ) {
			// $tip already contains a sanitized <a> tag built above.
			echo '<li>' . wp_kses( $tip, [ 'a' => [ 'href' => [] ] ] ) . '</li>';
		}
		echo '</ul>';
		echo '<p style="margin:0;"><a href="' . $dismiss_url . '">' . esc_html__( 'Dismiss this notice', 'livada-seo' ) . '</a></p>';
		echo '</div>';
	}

	// ── Conflict detection ─────────────────────────────────────────────────────

	public static function detect_seo_conflicts(): void {
		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$conflicts = [];

		if ( is_plugin_active( 'wp-seopress/seopress.php' ) ) {
			$conflicts[] = 'SEOPress';
		}
		if ( is_plugin_active( 'wordpress-seo/wp-seo.php' ) ) {
			$conflicts[] = 'Yoast SEO';
		}
		if ( is_plugin_active( 'seo-by-rank-math/rank-math.php' ) ) {
			$conflicts[] = 'Rank Math SEO';
		}

		if ( ! empty( $conflicts ) ) {
			add_action( 'admin_notices', static function() use ( $conflicts ): void {
				$list = implode( ', ', array_map( 'esc_html', $conflicts ) );
				printf(
					'<div class="notice notice-warning is-dismissible"><p><strong>%s</strong> %s</p></div>',
					esc_html__( 'Livada SEO conflict detected:', 'livada-seo' ),
					sprintf(
						/* translators: %s: comma-separated plugin names */
						esc_html__( 'The following SEO plugins are also active and may conflict with Livada SEO: %s. Consider deactivating them.', 'livada-seo' ),
						$list
					)
				);
			} );
		}
	}

	// ── AJAX handlers ──────────────────────────────────────────────────────────

	public static function ajax_reset_settings(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ] );
		}

		$defaults = livada_seo_default_settings();
		// Preserve the IndexNow key so the key file doesn't break.
		$existing = get_option( 'livada_seo_settings', [] );
		if ( is_array( $existing ) && ! empty( $existing['indexnow_key'] ) ) {
			$defaults['indexnow_key'] = $existing['indexnow_key'];
		}

		update_option( 'livada_seo_settings', $defaults, false );

		wp_send_json_success( [ 'message' => __( 'Settings reset to defaults.', 'livada-seo' ) ] );
	}

	public static function ajax_regenerate_indexnow(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ] );
		}

		$new_key  = livada_seo_generate_uuid();
		$existing = get_option( 'livada_seo_settings', [] );
		if ( ! is_array( $existing ) ) {
			$existing = [];
		}
		$existing['indexnow_key'] = $new_key;
		update_option( 'livada_seo_settings', $existing, false );

		wp_send_json_success( [ 'key' => $new_key ] );
	}

	public static function ajax_add_redirect(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ] );
		}

		$source = sanitize_text_field( wp_unslash( $_POST['source_url'] ?? '' ) ); // phpcs:ignore
		$target = esc_url_raw( wp_unslash( $_POST['target_url'] ?? '' ) ); // phpcs:ignore
		$type   = absint( $_POST['type'] ?? 301 ); // phpcs:ignore

		if ( empty( $source ) || empty( $target ) ) {
			wp_send_json_error( [ 'message' => __( 'Source and target URLs are required.', 'livada-seo' ) ] );
		}

		$id = Livada_SEO_Redirects::add_redirect( $source, $target, $type );
		if ( false === $id ) {
			wp_send_json_error( [ 'message' => __( 'Failed to add redirect. The source URL may already exist.', 'livada-seo' ) ] );
		}

		wp_send_json_success( [ 'id' => $id ] );
	}

	public static function ajax_update_redirect(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ] );
		}

		$id = absint( $_POST['id'] ?? 0 ); // phpcs:ignore
		if ( ! $id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid redirect ID.', 'livada-seo' ) ] );
		}

		$data = [];
		if ( isset( $_POST['source_url'] ) ) { // phpcs:ignore
			$data['source_url'] = sanitize_text_field( wp_unslash( $_POST['source_url'] ) ); // phpcs:ignore
		}
		if ( isset( $_POST['target_url'] ) ) { // phpcs:ignore
			$data['target_url'] = esc_url_raw( wp_unslash( $_POST['target_url'] ) ); // phpcs:ignore
		}
		if ( isset( $_POST['type'] ) ) { // phpcs:ignore
			$data['type'] = absint( $_POST['type'] ); // phpcs:ignore
		}

		$ok = Livada_SEO_Redirects::update_redirect( $id, $data );
		if ( ! $ok ) {
			wp_send_json_error( [ 'message' => __( 'Failed to update redirect.', 'livada-seo' ) ] );
		}

		wp_send_json_success( [] );
	}

	public static function ajax_delete_redirect(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ] );
		}

		$id = absint( $_POST['id'] ?? 0 ); // phpcs:ignore
		if ( ! $id ) {
			wp_send_json_error( [ 'message' => __( 'Invalid redirect ID.', 'livada-seo' ) ] );
		}

		$ok = Livada_SEO_Redirects::delete_redirect( $id );
		if ( ! $ok ) {
			wp_send_json_error( [ 'message' => __( 'Failed to delete redirect.', 'livada-seo' ) ] );
		}

		wp_send_json_success( [] );
	}
}
