<?php
/**
 * Importer Utilities
 *
 * Shared helper functions for all importer classes.
 * Handles response building, metadata persistence, DB queries for batch
 * processing, and migration tracking.
 *
 * @package SEOvaultSEO\Migration
 */

namespace SEOvaultSEO\Migration;

use SEOvaultSEO\Core\PostMeta;

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

final class ImporterUtils {

	const MIGRATION_FLAG = 'seovaultseo_migration';

	/**
	 * Build a standard response array.
	 *
	 * @param string $message       Human-readable message.
	 * @param bool   $success       Whether the operation succeeded.
	 * @param mixed  $data          Optional additional data.
	 * @param bool   $no_data_found Whether no data was found.
	 * @return array
	 */
	public static function build_response( $message, $success = true, $data = array(), $no_data_found = false ) {
		return array(
			'success'       => $success,
			'message'       => $message,
			'data'          => $data,
			'no_data_found' => $no_data_found,
		);
	}

	/**
	 * Update SEOvault post meta for a given post.
	 *
	 * Robots flags: only write when the key is present in $data. Importers must
	 * omit robots_noindex/robots_nofollow for blank/default source values so a
	 * falsy write does not create a per-post override that defeats post-type
	 * noindex (see Frontend\Robots singular precedence).
	 *
	 * @param int   $post_id Post ID.
	 * @param array $data    Prepared meta values to write.
	 * @return array
	 */
	public static function update_post_meta_data( $post_id, array $data ) {
		try {
			if ( isset( $data['seo_title'] ) ) {
				PostMeta::set_seo_title( $post_id, $data['seo_title'] );
			}
			if ( isset( $data['seo_description'] ) ) {
				PostMeta::set_seo_description( $post_id, $data['seo_description'] );
			}
			if ( isset( $data['focus_keyword'] ) ) {
				PostMeta::set_focus_keyword( $post_id, $data['focus_keyword'] );
			}
			if ( isset( $data['canonical_url'] ) ) {
				PostMeta::set_canonical_url( $post_id, $data['canonical_url'] );
			}
			if ( array_key_exists( 'robots_noindex', $data ) ) {
				PostMeta::set_noindex( $post_id, $data['robots_noindex'] );
			}
			if ( array_key_exists( 'robots_nofollow', $data ) ) {
				PostMeta::set_nofollow( $post_id, $data['robots_nofollow'] );
			}
			if ( isset( $data['og_title'] ) ) {
				PostMeta::set_og_title( $post_id, $data['og_title'] );
			}
			if ( isset( $data['og_description'] ) ) {
				PostMeta::set_og_description( $post_id, $data['og_description'] );
			}
			if ( isset( $data['og_image'] ) ) {
				PostMeta::set_og_image( $post_id, $data['og_image'] );
			}

			self::update_migration_flag( $post_id );

			return self::build_response(
				sprintf(
					/* translators: %s: post ID */
					__( 'Successfully updated SEOvault settings for post ID %s.', 'seovault-ai' ),
					$post_id
				)
			);
		} catch ( \Exception $e ) {
			return self::build_response(
				sprintf(
					/* translators: 1: post ID, 2: error message */
					__( 'Failed to update SEOvault settings for post ID %1$s. Error: %2$s', 'seovault-ai' ),
					$post_id,
					$e->getMessage()
				),
				false
			);
		}
	}

	/**
	 * Update SEOvault term meta for a given term.
	 *
	 * @param int   $term_id Term ID.
	 * @param array $data    Prepared meta values to write.
	 * @return array
	 */
	public static function update_term_meta_data( $term_id, array $data ) {
		try {
			if ( isset( $data['seo_title'] ) ) {
				update_term_meta( $term_id, '_seovault_seo_title', $data['seo_title'] );
			}
			if ( isset( $data['seo_description'] ) ) {
				update_term_meta( $term_id, '_seovault_seo_description', $data['seo_description'] );
			}
			if ( isset( $data['canonical_url'] ) ) {
				update_term_meta( $term_id, '_seovault_canonical_url', $data['canonical_url'] );
			}
			if ( isset( $data['robots_noindex'] ) ) {
				update_term_meta( $term_id, '_seovault_robots_noindex', (bool) $data['robots_noindex'] );
			}
			if ( isset( $data['og_title'] ) ) {
				update_term_meta( $term_id, '_seovault_og_title', $data['og_title'] );
			}
			if ( isset( $data['og_description'] ) ) {
				update_term_meta( $term_id, '_seovault_og_description', $data['og_description'] );
			}
			if ( isset( $data['og_image'] ) ) {
				update_term_meta( $term_id, '_seovault_og_image', $data['og_image'] );
			}

			self::update_migration_flag( $term_id, false );

			return self::build_response(
				sprintf(
					/* translators: %s: term ID */
					__( 'Successfully updated SEOvault settings for term ID %s.', 'seovault-ai' ),
					$term_id
				)
			);
		} catch ( \Exception $e ) {
			return self::build_response(
				sprintf(
					/* translators: 1: term ID, 2: error message */
					__( 'Failed to update SEOvault settings for term ID %1$s. Error: %2$s', 'seovault-ai' ),
					$term_id,
					$e->getMessage()
				),
				false
			);
		}
	}

	/**
	 * Mark an item as migrated.
	 *
	 * @param int  $id      Post or term ID.
	 * @param bool $is_post Whether the ID is a post (true) or term (false).
	 */
	public static function update_migration_flag( $id, $is_post = true ) {
		if ( $is_post ) {
			update_post_meta( $id, self::MIGRATION_FLAG, 1 );
		} else {
			update_term_meta( $id, self::MIGRATION_FLAG, 1 );
		}
	}

	/**
	 * Check if an item has already been migrated.
	 *
	 * @param int  $id      Post or term ID.
	 * @param bool $is_post Whether the ID is a post (true) or term (false).
	 * @return bool
	 */
	public static function is_migrated( $id, $is_post = true ) {
		if ( $is_post ) {
			return (bool) get_post_meta( $id, self::MIGRATION_FLAG, true );
		}
		return (bool) get_term_meta( $id, self::MIGRATION_FLAG, true );
	}

	/**
	 * Check if the given type is valid.
	 *
	 * @param string $type            The type to check.
	 * @param array  $not_valid_types List of invalid types.
	 * @param array  $public_types    List of public types.
	 * @param bool   $is_taxonomy     Whether the type is a taxonomy.
	 * @return bool
	 */
	public static function is_type_valid( $type, $not_valid_types = array(), $public_types = array(), $is_taxonomy = false ) {
		if ( empty( $type ) ) {
			return false;
		}
		if ( in_array( $type, $not_valid_types, true ) ) {
			return false;
		}
		if ( $is_taxonomy && ! isset( $public_types[ $type ] ) ) {
			return false;
		}
		if ( ! $is_taxonomy && ! in_array( $type, $public_types, true ) ) {
			return false;
		}
		return true;
	}

	/**
	 * Format a migration import status message for posts and terms.
	 *
	 * @param bool   $imported    Whether data was imported.
	 * @param int    $object_id   Post or term ID.
	 * @param string $object_kind 'post' or 'term'.
	 * @param string $feature     'general', 'meta_robots', or 'social'.
	 * @return string
	 */
	public static function import_status_message( $imported, $object_id, $object_kind, $feature ) {
		$object_id = absint( $object_id );
		$is_post   = ( 'post' === $object_kind );

		if ( $is_post ) {
			if ( 'general' === $feature ) {
				if ( $imported ) {
					return sprintf(
						/* translators: %d: post ID */
						__( 'General settings imported for post %d.', 'seovault-ai' ),
						$object_id
					);
				}
				return sprintf(
					/* translators: %d: post ID */
					__( 'No general settings to import for post %d.', 'seovault-ai' ),
					$object_id
				);
			}
			if ( 'meta_robots' === $feature ) {
				if ( $imported ) {
					return sprintf(
						/* translators: %d: post ID */
						__( 'Meta-robots imported for post %d.', 'seovault-ai' ),
						$object_id
					);
				}
				return sprintf(
					/* translators: %d: post ID */
					__( 'No meta-robots to import for post %d.', 'seovault-ai' ),
					$object_id
				);
			}
			if ( 'social' === $feature ) {
				if ( $imported ) {
					return sprintf(
						/* translators: %d: post ID */
						__( 'Social metadata imported for post %d.', 'seovault-ai' ),
						$object_id
					);
				}
				return sprintf(
					/* translators: %d: post ID */
					__( 'No social metadata to import for post %d.', 'seovault-ai' ),
					$object_id
				);
			}
		} else {
			if ( 'general' === $feature ) {
				if ( $imported ) {
					return sprintf(
						/* translators: %d: term ID */
						__( 'General settings imported for term %d.', 'seovault-ai' ),
						$object_id
					);
				}
				return sprintf(
					/* translators: %d: term ID */
					__( 'No general settings to import for term %d.', 'seovault-ai' ),
					$object_id
				);
			}
			if ( 'meta_robots' === $feature ) {
				if ( $imported ) {
					return sprintf(
						/* translators: %d: term ID */
						__( 'Meta-robots imported for term %d.', 'seovault-ai' ),
						$object_id
					);
				}
				return sprintf(
					/* translators: %d: term ID */
					__( 'No meta-robots to import for term %d.', 'seovault-ai' ),
					$object_id
				);
			}
			if ( 'social' === $feature ) {
				if ( $imported ) {
					return sprintf(
						/* translators: %d: term ID */
						__( 'Social metadata imported for term %d.', 'seovault-ai' ),
						$object_id
					);
				}
				return sprintf(
					/* translators: %d: term ID */
					__( 'No social metadata to import for term %d.', 'seovault-ai' ),
					$object_id
				);
			}
		}

		return '';
	}

	/**
	 * Return a response indicating an invalid post/term type.
	 *
	 * @param int  $id          The ID of the post or term.
	 * @param bool $is_taxonomy Whether the ID is a taxonomy.
	 * @return array
	 */
	public static function not_valid_response( $id, $is_taxonomy = false ) {
		$message = $is_taxonomy
			? sprintf( /* translators: %d: Term ID */ __( 'Invalid Term ID %d.', 'seovault-ai' ), $id )
			: sprintf( /* translators: %d: Post ID */ __( 'Invalid Post ID %d.', 'seovault-ai' ), $id );

		return self::build_response( $message, false, array(), true );
	}

	/**
	 * Get the taxonomies that should be included in import.
	 *
	 * @return array Taxonomy objects keyed by name.
	 */
	public static function get_public_taxonomies() {
		$taxonomies = get_taxonomies( array( 'public' => true ), 'objects' );

		$unsupported = array( 'wp_theme', 'wp_template_part_area', 'link_category', 'nav_menu', 'post_format' );
		foreach ( $unsupported as $slug ) {
			unset( $taxonomies[ $slug ] );
		}

		return $taxonomies;
	}

	/**
	 * Prepare SQL query parameters for batch queries.
	 *
	 * @param array    $items          Items for placeholders.
	 * @param array    $excluded_keys  Excluded meta keys.
	 * @param string   $meta_prefix    Meta key prefix.
	 * @param int|null $batch_size     Batch size.
	 * @param int|null $offset         Offset.
	 * @return array
	 */
	public static function prepare_query_params( array $items, array $excluded_keys, $meta_prefix, $batch_size = null, $offset = null ) {
		$placeholders = array();
		$params       = array_merge(
			array( self::MIGRATION_FLAG, $meta_prefix ),
			$excluded_keys,
			$items
		);

		$placeholders[] = implode( ',', array_fill( 0, count( $excluded_keys ), '%s' ) );
		$placeholders[] = implode( ',', array_fill( 0, count( $items ), '%s' ) );

		if ( null !== $batch_size && null !== $offset ) {
			$params[] = $batch_size;
			$params[] = $offset;
		}

		return array(
			'placeholders' => $placeholders,
			'params'       => $params,
		);
	}

	/**
	 * Get total count of posts with source meta.
	 *
	 * @param array  $post_types     Post types.
	 * @param array  $excluded_keys  Excluded meta keys.
	 * @param string $meta_prefix    Meta key prefix.
	 * @return int
	 */
	public static function get_total_posts_count( array $post_types, array $excluded_keys, $meta_prefix ) {
		global $wpdb;

		$query_data = self::prepare_query_params( $post_types, $excluded_keys, $meta_prefix );

		$sql = "SELECT COUNT(DISTINCT p.ID)
                FROM {$wpdb->posts} p
                LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    AND pm.meta_key = %s
                INNER JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id
                    AND pm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND pm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE p.post_type IN ({$query_data['placeholders'][1]})
                 AND p.post_status != 'auto-draft'
                 AND pm.meta_id IS NULL";

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN placeholders built via prepare_query_params(); values passed to prepare().
		return (int) $wpdb->get_var( $wpdb->prepare( $sql, $query_data['params'] ) );
	}

	/**
	 * Get post IDs for batch processing.
	 *
	 * @param array  $post_types     Post types.
	 * @param array  $excluded_keys  Excluded meta keys.
	 * @param string $meta_prefix    Meta key prefix.
	 * @param int    $batch_size     Batch size.
	 * @param int    $offset         Offset.
	 * @return array
	 */
	public static function get_post_ids( array $post_types, array $excluded_keys, $meta_prefix, $batch_size, $offset ) {
		global $wpdb;

		$query_data = self::prepare_query_params( $post_types, $excluded_keys, $meta_prefix, $batch_size, $offset );

		$sql = "SELECT DISTINCT p.ID
                FROM {$wpdb->posts} p
                LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    AND pm.meta_key = %s
                INNER JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id
                    AND pm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND pm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE p.post_type IN ({$query_data['placeholders'][1]})
                 AND p.post_status != 'auto-draft'
                 AND pm.meta_id IS NULL
                 ORDER BY p.ID
                 LIMIT %d OFFSET %d";

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN placeholders built via prepare_query_params(); values passed to prepare().
		return $wpdb->get_col( $wpdb->prepare( $sql, $query_data['params'] ) );
	}

	/**
	 * Get total count of terms with source meta.
	 *
	 * @param array  $taxonomies     Taxonomy names.
	 * @param array  $excluded_keys  Excluded meta keys.
	 * @param string $meta_prefix    Meta key prefix.
	 * @return int
	 */
	public static function get_total_terms_count( array $taxonomies, array $excluded_keys, $meta_prefix ) {
		global $wpdb;

		$query_data = self::prepare_query_params( $taxonomies, $excluded_keys, $meta_prefix );

		$sql = "SELECT COUNT(DISTINCT t.term_id)
                FROM {$wpdb->terms} t
                INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                LEFT JOIN {$wpdb->termmeta} tm ON t.term_id = tm.term_id
                    AND tm.meta_key = %s
                INNER JOIN {$wpdb->termmeta} tm2 ON t.term_id = tm2.term_id
                    AND tm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND tm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE tt.taxonomy IN ({$query_data['placeholders'][1]})
                 AND tm.meta_id IS NULL";

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN placeholders built via prepare_query_params(); values passed to prepare().
		return (int) $wpdb->get_var( $wpdb->prepare( $sql, $query_data['params'] ) );
	}

	/**
	 * Get term IDs for batch processing.
	 *
	 * @param array  $taxonomies     Taxonomy names.
	 * @param array  $excluded_keys  Excluded meta keys.
	 * @param string $meta_prefix    Meta key prefix.
	 * @param int    $batch_size     Batch size.
	 * @param int    $offset         Offset.
	 * @return array
	 */
	public static function get_term_ids( array $taxonomies, array $excluded_keys, $meta_prefix, $batch_size, $offset ) {
		global $wpdb;

		$query_data = self::prepare_query_params( $taxonomies, $excluded_keys, $meta_prefix, $batch_size, $offset );

		$sql = "SELECT DISTINCT t.term_id
                FROM {$wpdb->terms} t
                INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
                LEFT JOIN {$wpdb->termmeta} tm ON t.term_id = tm.term_id
                    AND tm.meta_key = %s
                INNER JOIN {$wpdb->termmeta} tm2 ON t.term_id = tm2.term_id
                    AND tm2.meta_key LIKE %s";

		if ( ! empty( $excluded_keys ) ) {
			$sql .= " AND tm2.meta_key NOT IN ({$query_data['placeholders'][0]})";
		}

		$sql .= " WHERE tt.taxonomy IN ({$query_data['placeholders'][1]})
                 AND tm.meta_id IS NULL
                 ORDER BY t.term_id
                 LIMIT %d OFFSET %d";

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- IN placeholders built via prepare_query_params(); values passed to prepare().
		return $wpdb->get_col( $wpdb->prepare( $sql, $query_data['params'] ) );
	}

	/**
	 * Get the list of known SEO plugins for migration.
	 *
	 * @return array Map of plugin file => [name, class, pro_slug].
	 */
	public static function get_seo_plugins_list() {
		return array(
			'surerank/surerank.php'                       => array(
				'name'     => 'SureRank',
				'class'    => \SEOvaultSEO\Migration\Providers\SureRank::class,
				'pro_slug' => 'surerank-pro/surerank-pro.php',
			),
			'wp-seopress/seopress.php'                    => array(
				'name'     => 'SEOPress',
				'class'    => \SEOvaultSEO\Migration\Providers\SEOPress::class,
				'pro_slug' => 'wp-seopress-pro/seopress-pro.php',
			),
			'squirrly-seo/squirrly.php'                   => array(
				'name'     => 'Squirrly SEO',
				'class'    => \SEOvaultSEO\Migration\Providers\Squirrly::class,
				'pro_slug' => '',
			),
			'siteseo/siteseo.php'                         => array(
				'name'     => 'SiteSEO',
				'class'    => \SEOvaultSEO\Migration\Providers\SiteSEO::class,
				'pro_slug' => '',
			),
			'wordpress-seo/wp-seo.php'                    => array(
				'name'     => 'Yoast SEO',
				'class'    => \SEOvaultSEO\Migration\Providers\Yoast::class,
				'pro_slug' => 'wordpress-seo-premium/wp-seo-premium.php',
			),
			'seo-by-rank-math/rank-math.php'              => array(
				'name'     => 'Rank Math',
				'class'    => \SEOvaultSEO\Migration\Providers\RankMath::class,
				'pro_slug' => 'seo-by-rank-math-pro/rank-math-pro.php',
			),
			'all-in-one-seo-pack/all_in_one_seo_pack.php' => array(
				'name'     => 'AIOSEO',
				'class'    => \SEOvaultSEO\Migration\Providers\AIOSEO::class,
				'pro_slug' => 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php',
			),
			'slim-seo/slim-seo.php'                       => array(
				'name'     => 'Slim SEO',
				'class'    => \SEOvaultSEO\Migration\Providers\SlimSEO::class,
				'pro_slug' => 'slim-seo-pro/slim-seo-pro.php',
			),
			'autodescription/autodescription.php'         => array(
				'name'     => 'The SEO Framework',
				'class'    => \SEOvaultSEO\Migration\Providers\TheSeoFramework::class,
				'pro_slug' => '',
			),
		);
	}

	/**
	 * Extract the primary keyword from a comma-separated keywords string.
	 *
	 * @param mixed $keywords Raw keywords value.
	 * @return string
	 */
	public static function get_primary_keyword( $keywords ) {
		if ( ! is_string( $keywords ) ) {
			return '';
		}
		$keywords = trim( $keywords );
		if ( '' === $keywords ) {
			return '';
		}
		return trim( (string) strtok( $keywords, ',' ) );
	}
}
