<?php
/**
 * Livada SEO — Import Module (Yoast SEO)
 *
 * Provides batched AJAX-driven migration of Yoast SEO post meta and redirections
 * into Livada SEO's own data structures. Mirrors the SEOPress importer
 * architecture: progress tracked in transients, 50 posts per batch.
 *
 * Supported actions (POST param `action_type`):
 *   start        — reset progress, return total post count
 *   batch        — process next 50 posts, advance offset transient
 *   redirections — import all Yoast Premium redirections in one pass
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_Import_Yoast {

	/** Transient key that tracks the current batch offset. */
	const OFFSET_TRANSIENT = 'livada_seo_yoast_import_offset';

	/** Transient key that tracks the total post count for the current run. */
	const TOTAL_TRANSIENT = 'livada_seo_yoast_import_total';

	/** Number of posts processed per batch. */
	const BATCH_SIZE = 50;

	/**
	 * Maps Yoast SEO meta keys to their Livada SEO equivalents.
	 * Source: https://developer.yoast.com/customization/yoast-seo/post-meta/
	 *
	 * @var array<string,string>
	 */
	const META_MAP = [
		'_yoast_wpseo_title'                   => '_livada_seo_title',
		'_yoast_wpseo_metadesc'                => '_livada_seo_description',
		'_yoast_wpseo_focuskw'                 => '_livada_seo_focus_keyword',
		'_yoast_wpseo_canonical'               => '_livada_seo_canonical',
		'_yoast_wpseo_opengraph-image'         => '_livada_seo_og_image',
		// Robots flags are handled separately (see migrate_robots_meta).
	];

	/**
	 * Yoast meta keys that store URLs.
	 *
	 * @var string[]
	 */
	const URL_FIELDS = [
		'_yoast_wpseo_canonical',
		'_yoast_wpseo_opengraph-image',
	];

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

	public static function init(): void {
		add_action( 'wp_ajax_livada_seo_import_yoast', [ self::class, 'handle_ajax' ] );
	}

	// ── AJAX entry point ──────────────────────────────────────────────────────

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

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

		$action_type = isset( $_POST['action_type'] ) ? sanitize_key( $_POST['action_type'] ) : '';
		$result      = self::dispatch( $action_type );

		if ( isset( $result['_error'] ) ) {
			wp_send_json_error( [ 'message' => $result['_error'] ], 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Auth-agnostic dispatcher used by both the AJAX handler and the REST API.
	 *
	 * Returns the action result array on success, or an array with an `_error`
	 * key when the action is unknown so callers can surface a 400 response.
	 *
	 * @param  string $action_type  start|batch|redirections
	 * @return array<string,mixed>
	 */
	public static function dispatch( string $action_type ): array {
		switch ( $action_type ) {
			case 'start':
				return self::handle_start();
			case 'batch':
				return self::handle_batch();
			case 'redirections':
				return self::handle_redirections();
			default:
				return [ '_error' => __( 'Unknown action_type. Use start, batch or redirections.', 'livada-seo' ) ];
		}
	}

	// ── Action handlers ───────────────────────────────────────────────────────

	private static function handle_start(): array {
		set_transient( self::OFFSET_TRANSIENT, 0, HOUR_IN_SECONDS );
		$total = self::count_importable_posts();
		set_transient( self::TOTAL_TRANSIENT, $total, HOUR_IN_SECONDS );

		return [
			'done'      => false,
			'processed' => 0,
			'total'     => $total,
			'offset'    => 0,
			'warnings'  => [],
		];
	}

	private static function handle_batch(): array {
		$offset   = (int) get_transient( self::OFFSET_TRANSIENT );
		$total    = (int) get_transient( self::TOTAL_TRANSIENT );
		$warnings = [];

		if ( 0 === $total ) {
			$total = self::count_importable_posts();
			set_transient( self::TOTAL_TRANSIENT, $total, HOUR_IN_SECONDS );
		}

		$posts = self::get_posts_batch( $offset );

		foreach ( $posts as $post ) {
			$post_warnings = self::migrate_post( (int) $post->ID );
			foreach ( $post_warnings as $warning ) {
				$warnings[] = sprintf(
					/* translators: 1: post ID, 2: warning detail */
					__( 'Post %1$d: %2$s', 'livada-seo' ),
					$post->ID,
					$warning
				);
			}
		}

		$batch_count = count( $posts );
		$new_offset  = $offset + $batch_count;

		set_transient( self::OFFSET_TRANSIENT, $new_offset, HOUR_IN_SECONDS );

		$done = ( $batch_count < self::BATCH_SIZE ) || ( $new_offset >= $total );

		if ( $done ) {
			delete_transient( self::OFFSET_TRANSIENT );
			delete_transient( self::TOTAL_TRANSIENT );

			// Migrate site-wide Yoast options once at the end of the run.
			self::migrate_global_options();
		}

		return [
			'done'      => $done,
			'processed' => $new_offset,
			'total'     => $total,
			'offset'    => $new_offset,
			'warnings'  => $warnings,
		];
	}

	/**
	 * Imports redirections from Yoast Premium's wpseo-premium-redirects option.
	 * Yoast stores redirects in two options keyed by type:
	 *   wpseo-premium-redirects-base   (plain redirects)
	 *   wpseo-premium-redirects-export-regex (regex redirects, skipped)
	 */
	private static function handle_redirections(): array {
		$warnings = [];
		$imported = 0;
		$skipped  = 0;

		if ( ! class_exists( 'Livada_SEO_Redirects' ) ) {
			$file = LIVADA_SEO_PATH . 'includes/class-redirects.php';
			if ( file_exists( $file ) ) {
				require_once $file;
			}
		}

		if ( ! class_exists( 'Livada_SEO_Redirects' ) ) {
			return [
				'done'     => true,
				'imported' => 0,
				'skipped'  => 0,
				'warnings' => [ __( 'Redirects module unavailable.', 'livada-seo' ) ],
			];
		}

		// Yoast Premium stores plain redirects here as a serialized array.
		// Each entry: [ 'origin' => string, 'url' => string, 'type' => '301'|'302'|'307'|'410'|'451' ]
		$yoast_redirects = get_option( 'wpseo-premium-redirects-base', [] );

		if ( ! is_array( $yoast_redirects ) || empty( $yoast_redirects ) ) {
			// Fallback: some Yoast Premium versions store under a flat option.
			$yoast_redirects = get_option( 'wpseo_premium_redirects', [] );
		}

		if ( ! is_array( $yoast_redirects ) || empty( $yoast_redirects ) ) {
			return [
				'done'     => true,
				'imported' => 0,
				'skipped'  => 0,
				'warnings' => [ __( 'No Yoast Premium redirections found. Yoast Premium must be installed for this feature.', 'livada-seo' ) ],
			];
		}

		foreach ( $yoast_redirects as $redirect ) {
			if ( ! is_array( $redirect ) ) {
				$skipped++;
				continue;
			}

			$source = isset( $redirect['origin'] ) ? (string) $redirect['origin'] : '';
			$target = isset( $redirect['url'] )    ? (string) $redirect['url']    : '';
			$type   = isset( $redirect['type'] )   ? (int)    $redirect['type']   : 301;

			// Only support 301/302; skip 307/410/451 which Livada doesn't model.
			if ( ! in_array( $type, [ 301, 302 ], true ) ) {
				$skipped++;
				$warnings[] = sprintf(
					/* translators: 1: source URL, 2: redirect type */
					__( 'Skipped "%1$s" — type %2$d not supported (only 301/302).', 'livada-seo' ),
					$source,
					$type
				);
				continue;
			}

			if ( '' === $source || '' === $target ) {
				$skipped++;
				continue;
			}

			$source = '/' . ltrim( $source, '/' );
			$target = esc_url_raw( $target );

			if ( '' === $target ) {
				$skipped++;
				continue;
			}

			$result = Livada_SEO_Redirects::add_redirect( $source, $target, $type );

			if ( false === $result ) {
				$skipped++;
			} else {
				$imported++;
			}
		}

		return [
			'done'     => true,
			'imported' => $imported,
			'skipped'  => $skipped,
			'warnings' => $warnings,
		];
	}

	// ── Per-post migration ────────────────────────────────────────────────────

	/**
	 * Copies Yoast meta values to Livada SEO meta keys for a single post.
	 *
	 * Existing Livada SEO values are overwritten so a re-run stays idempotent.
	 *
	 * @param  int      $post_id WordPress post ID.
	 * @return string[] Warnings.
	 */
	private static function migrate_post( int $post_id ): array {
		$warnings = [];

		// Plain mapped fields.
		foreach ( self::META_MAP as $yoast_key => $livada_key ) {
			$raw = get_post_meta( $post_id, $yoast_key, true );

			if ( '' === $raw || false === $raw ) {
				continue;
			}

			$is_url    = in_array( $yoast_key, self::URL_FIELDS, true );
			$sanitized = self::sanitize_value( (string) $raw, $is_url );

			if ( null === $sanitized ) {
				$warnings[] = sprintf(
					/* translators: %s: Yoast field name */
					__( 'invalid %s skipped', 'livada-seo' ),
					str_replace( '_yoast_wpseo_', '', $yoast_key )
				);
				continue;
			}

			// Yoast title/description templates use %%sitename%% syntax; convert
			// them to Livada's %sitename% form so dynamic values render correctly.
			if ( in_array( $livada_key, [ '_livada_seo_title', '_livada_seo_description' ], true ) ) {
				$sanitized = self::convert_yoast_variables( $sanitized );
			}

			update_post_meta( $post_id, $livada_key, $sanitized );
		}

		// Robots: Yoast stores meta-robots-noindex (0/1/2) and meta-robots-nofollow (0/1).
		// Yoast values: 0 = default, 1 = noindex/nofollow, 2 = index.
		$noindex_raw = get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true );
		if ( '1' === (string) $noindex_raw ) {
			update_post_meta( $post_id, '_livada_seo_noindex', '1' );
		}

		$nofollow_raw = get_post_meta( $post_id, '_yoast_wpseo_meta-robots-nofollow', true );
		if ( '1' === (string) $nofollow_raw ) {
			update_post_meta( $post_id, '_livada_seo_nofollow', '1' );
		}

		return $warnings;
	}

	// ── Site-wide options migration ───────────────────────────────────────────

	/**
	 * Copies Yoast site-wide options into the Livada SEO settings array.
	 * Only runs once at the end of a successful import.
	 */
	private static function migrate_global_options(): void {
		// Yoast title templates are stored in the wpseo_titles option.
		$yoast_titles = get_option( 'wpseo_titles', [] );
		if ( ! is_array( $yoast_titles ) ) {
			return;
		}

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

		// Map Yoast template keys → Livada keys.
		$tpl_map = [
			'title-home-wpseo'     => 'title_home',
			'title-post'           => 'title_post',
			'title-page'           => 'title_page',
			'title-tax-category'   => 'title_category',
			'title-tax-post_tag'   => 'title_tag',
			'metadesc-home-wpseo'  => 'desc_home',
		];

		foreach ( $tpl_map as $y_key => $l_key ) {
			if ( ! isset( $yoast_titles[ $y_key ] ) || '' === (string) $yoast_titles[ $y_key ] ) {
				continue;
			}

			// Don't overwrite a Livada value that's already non-default.
			if ( ! empty( $existing[ $l_key ] ) ) {
				continue;
			}

			$existing[ $l_key ] = self::convert_yoast_variables( (string) $yoast_titles[ $y_key ] );
		}

		// Social URLs from wpseo_social option.
		$yoast_social = get_option( 'wpseo_social', [] );
		if ( is_array( $yoast_social ) ) {
			$social_map = [
				'facebook_site' => 'social_facebook',
				'twitter_site'  => 'social_twitter',
				'instagram_url' => 'social_instagram',
				'linkedin_url'  => 'social_linkedin',
			];

			foreach ( $social_map as $y_key => $l_key ) {
				if ( ! isset( $yoast_social[ $y_key ] ) || '' === (string) $yoast_social[ $y_key ] ) {
					continue;
				}

				if ( ! empty( $existing[ $l_key ] ) ) {
					continue;
				}

				// Twitter is a handle in Yoast — convert to URL.
				$value = (string) $yoast_social[ $y_key ];
				if ( 'twitter_site' === $y_key && '' !== $value && strpos( $value, 'http' ) !== 0 ) {
					$value = 'https://twitter.com/' . ltrim( $value, '@' );
				}

				$existing[ $l_key ] = esc_url_raw( $value );
			}
		}

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

	// ── Helpers ───────────────────────────────────────────────────────────────

	/**
	 * Converts Yoast template variables (%%var%%) into Livada's (%var%) form.
	 * Only the most common variables are mapped — Yoast-only tokens (%%term_title%%,
	 * %%post_year%%, etc.) are passed through unchanged so admins can clean up
	 * manually if needed.
	 */
	private static function convert_yoast_variables( string $value ): string {
		$map = [
			'%%title%%'     => '%title%',
			'%%sitename%%'  => '%sitename%',
			'%%sep%%'       => '%sep%',
			'%%page%%'      => '%page%',
			'%%tagline%%'   => '%tagline%',
			'%%category%%'  => '%category%',
			'%%tag%%'       => '%tag%',
			'%%date%%'      => '%date%',
			'%%term_title%%' => '%term%',
		];

		return str_replace( array_keys( $map ), array_values( $map ), $value );
	}

	private static function sanitize_value( string $value, bool $is_url ): ?string {
		if ( $is_url ) {
			$sanitized = esc_url_raw( $value );
			return '' === $sanitized ? null : $sanitized;
		}

		$sanitized = sanitize_text_field( $value );

		if ( '' === $sanitized && '' !== $value ) {
			return null;
		}

		return $sanitized;
	}

	private static function count_importable_posts(): int {
		global $wpdb;

		$keys = array_merge(
			array_keys( self::META_MAP ),
			[ '_yoast_wpseo_meta-robots-noindex', '_yoast_wpseo_meta-robots-nofollow' ]
		);

		$placeholders = implode( ', ', array_fill( 0, count( $keys ), '%s' ) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$count = (int) $wpdb->get_var(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
				"SELECT COUNT(DISTINCT p.ID)
				 FROM {$wpdb->posts} p
				 WHERE p.post_status NOT IN ('auto-draft','trash')
				   AND p.post_type   NOT IN ('revision','attachment')
				   AND EXISTS (
				       SELECT 1
				       FROM {$wpdb->postmeta} pm
				       WHERE pm.post_id = p.ID
				         AND pm.meta_key IN ({$placeholders})
				   )",
				...$keys
			)
		);

		return $count;
	}

	private static function get_posts_batch( int $offset ): array {
		global $wpdb;

		$keys = array_merge(
			array_keys( self::META_MAP ),
			[ '_yoast_wpseo_meta-robots-noindex', '_yoast_wpseo_meta-robots-nofollow' ]
		);

		$placeholders = implode( ', ', array_fill( 0, count( $keys ), '%s' ) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
				"SELECT DISTINCT p.ID
				 FROM {$wpdb->posts} p
				 WHERE p.post_status NOT IN ('auto-draft','trash')
				   AND p.post_type   NOT IN ('revision','attachment')
				   AND EXISTS (
				       SELECT 1
				       FROM {$wpdb->postmeta} pm
				       WHERE pm.post_id = p.ID
				         AND pm.meta_key IN ({$placeholders})
				   )
				 ORDER BY p.ID ASC
				 LIMIT %d OFFSET %d",
				...array_merge( $keys, [ self::BATCH_SIZE, $offset ] )
			)
		);

		return $rows ?: [];
	}
}
