<?php
namespace AIOSEO\Plugin\Common\Migration;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound

use AIOSEO\Plugin\Common\Models;

/**
 * Migrates the post meta from V3.
 *
 * @since 4.0.0
 */
class Meta {
	/**
	 * Holds the old options array.
	 *
	 * @since 4.0.3
	 *
	 * @var array|null
	 */
	protected static $oldOptions = null;

	/**
	 * Migrates the plugin meta data.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function migrateMeta() {
		try {
			if ( as_next_scheduled_action( 'aioseo_migrate_post_meta' ) ) {
				return;
			}

			as_schedule_single_action( time() + 30, 'aioseo_migrate_post_meta', [], 'aioseo' );
		} catch ( \Exception $e ) {
			// Do nothing.
		}
	}

	/**
	 * Migrates the post meta data from V3.
	 *
	 * @since 4.0.0
	 *
	 * @return void
	 */
	public function migratePostMeta() {
		if ( aioseo()->core->cache->get( 'v3_migration_in_progress_settings' ) ) {
			aioseo()->actionScheduler->scheduleSingle( 'aioseo_migrate_post_meta', 30, [], true );

			return;
		}

		$postsPerAction  = 50;
		$publicPostTypes = aioseo()->helpers->getPublicPostTypes( true );
		$timeStarted     = esc_sql( gmdate( 'Y-m-d H:i:s', aioseo()->core->cache->get( 'v3_migration_in_progress_posts' ) ) );

		$postsToMigrate = aioseo()->core->db
			->start( 'posts' . ' as p' )
			->select( 'p.ID' )
			->leftJoin( 'aioseo_posts as ap', '`p`.`ID` = `ap`.`post_id`' )
			->whereRaw( "( ap.post_id IS NULL OR ap.updated < '$timeStarted' )" )
			->whereIn( 'p.post_type', $publicPostTypes )
			->whereNotIn( 'p.post_status', [ 'auto-draft' ] )
			->orderBy( 'p.ID DESC' )
			->limit( $postsPerAction )
			->run()
			->result();

		if ( ! $postsToMigrate || ! count( $postsToMigrate ) ) {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );

			return;
		}

		foreach ( $postsToMigrate as $post ) {
			$newPostMeta = $this->getMigratedPostMeta( $post->ID );

			$aioseoPost = Models\Post::getPost( $post->ID );
			$aioseoPost->set( $newPostMeta );
			$aioseoPost->save();

			$this->updateLocalizedPostMeta( $post->ID, $newPostMeta );
			$this->migrateAdditionalPostMeta( $post->ID );
		}

		if ( count( $postsToMigrate ) === $postsPerAction ) {
			try {
				as_schedule_single_action( time() + 30, 'aioseo_migrate_post_meta', [], 'aioseo' );
			} catch ( \Exception $e ) {
				// Do nothing.
			}
		} else {
			aioseo()->core->cache->delete( 'v3_migration_in_progress_posts' );
		}
	}

	/**
	 * Returns the migrated post meta for a given post.
	 *
	 * @since 4.0.3
	 *
	 * @param  int   $postId The post ID.
	 * @return array         The post meta.
	 */
	public function getMigratedPostMeta( $postId ) {
		if ( is_category() || is_tag() || is_tax() || ! is_numeric( $postId ) ) {
			return [];
		}

		if ( null === self::$oldOptions ) {
			self::$oldOptions = get_option( 'aioseop_options' );
		}

		if ( empty( self::$oldOptions ) ) {
			return [];
		}

		$postMeta = aioseo()->core->db
			->start( 'postmeta' . ' as pm' )
			->select( 'pm.meta_key, pm.meta_value' )
			->where( 'pm.post_id', $postId )
			->whereLike( 'pm.meta_key', '_aioseop_%', true )
			->run()
			->result();

		$mappedMeta = [
			'_aioseop_title'              => 'title',
			'_aioseop_description'        => 'description',
			'_aioseop_custom_link'        => 'canonical_url',
			'_aioseop_sitemap_exclude'    => '',
			'_aioseop_disable'            => '',
			'_aioseop_noindex'            => 'robots_noindex',
			'_aioseop_nofollow'           => 'robots_nofollow',
			'_aioseop_sitemap_priority'   => 'priority',
			'_aioseop_sitemap_frequency'  => 'frequency',
			'_aioseop_keywords'           => 'keywords',
			'_aioseop_opengraph_settings' => ''
		];

		$meta = [
			'post_id' => $postId,
		];

		if ( ! $postMeta || ! count( $postMeta ) ) {
			return $meta;
		}

		foreach ( $postMeta as $record ) {
			$name  = $record->meta_key;
			$value = $record->meta_value;

			if ( ! in_array( $name, array_keys( $mappedMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case '_aioseop_description':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $value ) );
					break;
				case '_aioseop_title':
					if ( ! empty( $value ) ) {
						$meta[ $mappedMeta[ $name ] ] = $this->getPostTitle( $postId, $value );
					}
					break;
				case '_aioseop_sitemap_exclude':
					if ( empty( $value ) ) {
						break;
					}
					$this->migrateExcludedPost( $postId );
					break;
				case '_aioseop_disable':
					if ( empty( $value ) ) {
						break;
					}
					$this->migrateSitemapExcludedPost( $postId );
					break;
				case '_aioseop_noindex':
				case '_aioseop_nofollow':
					if ( 'on' === (string) $value ) {
						$meta['robots_default']       = false;
						$meta[ $mappedMeta[ $name ] ] = true;
					} elseif ( 'off' === (string) $value ) {
						$meta['robots_default'] = false;
					}
					break;
				case '_aioseop_keywords':
					$meta[ $mappedMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				case '_aioseop_opengraph_settings':
					$meta += $this->convertOpenGraphMeta( $value );
					break;
				case '_aioseop_sitemap_priority':
				case '_aioseop_sitemap_frequency':
					if ( empty( $value ) ) {
						$meta[ $mappedMeta[ $name ] ] = 'default';
						break;
					}
					$meta[ $mappedMeta[ $name ] ] = $value;
					break;
				default:
					$meta[ $mappedMeta[ $name ] ] = esc_html( wp_strip_all_tags( strval( $value ) ) );
					break;
			}
		}

		return $meta;
	}

	/**
	 * Migrates a given disabled post from V3.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	private function migrateExcludedPost( $postId ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return;
		}

		aioseo()->options->sitemap->general->advancedSettings->enable = true;
		$excludedPosts = aioseo()->options->sitemap->general->advancedSettings->excludePosts;

		foreach ( $excludedPosts as $excludedPost ) {
			$excludedPost = json_decode( $excludedPost );
			if ( $excludedPost->value === $postId ) {
				return;
			}
		}

		$excludedPost = [
			'value' => $post->ID,
			'type'  => $post->post_type,
			'label' => $post->post_title,
			'link'  => get_permalink( $post )
		];

		$excludedPosts[] = wp_json_encode( $excludedPost );
		aioseo()->options->sitemap->general->advancedSettings->excludePosts = $excludedPosts;

		$deprecatedOptions = aioseo()->internalOptions->internal->deprecatedOptions;
		if ( ! in_array( 'excludePosts', $deprecatedOptions, true ) ) {
			array_push( $deprecatedOptions, 'excludePosts' );
			aioseo()->internalOptions->internal->deprecatedOptions = $deprecatedOptions;
		}
	}

	/**
	 * Migrates a given sitemap excluded post from V3.
	 *
	 * @since 4.0.3
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	private function migrateSitemapExcludedPost( $postId ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return;
		}

		$excludedPosts = aioseo()->options->deprecated->searchAppearance->advanced->excludePosts;
		foreach ( $excludedPosts as $excludedPost ) {
			$excludedPost = json_decode( $excludedPost );
			if ( $excludedPost->value === $postId ) {
				return;
			}
		}

		$excludedPost = [
			'value' => $post->ID,
			'type'  => $post->post_type,
			'label' => $post->post_title,
			'link'  => get_permalink( $post )
		];

		$excludedPosts[] = wp_json_encode( $excludedPost );
		aioseo()->options->deprecated->searchAppearance->advanced->excludePosts = $excludedPosts;
	}

	/**
	 * Updates the traditional post meta table with the new data.
	 *
	 * @since 4.1.0
	 *
	 * @param  int   $postId  The post ID.
	 * @param  array $newMeta The new meta data.
	 * @return void
	 */
	protected function updateLocalizedPostMeta( $postId, $newMeta ) {
		$localizedFields = [
			'title',
			'description',
			'keywords',
			'og_title',
			'og_description',
			'og_article_section',
			'og_article_tags',
			'twitter_title',
			'twitter_description'
		];

		foreach ( $newMeta as $k => $v ) {
			if ( ! in_array( $k, $localizedFields, true ) ) {
				continue;
			}

			if ( in_array( $k, [ 'keywords', 'og_article_tags' ], true ) ) {
				$v = ! empty( $v ) ? aioseo()->helpers->jsonTagsToCommaSeparatedList( $v ) : '';
			}

			update_post_meta( $postId, "_aioseo_{$k}", $v );
		}
	}

	/**
	 * Migrates additional post meta data.
	 *
	 * @since 4.0.2
	 *
	 * @param  int  $postId The post ID.
	 * @return void
	 */
	public function migrateAdditionalPostMeta( $postId ) {
		static $disabled = null;

		if ( null === $disabled ) {
			$disabled = (
				! aioseo()->options->sitemap->general->enable ||
				(
					aioseo()->options->sitemap->general->advancedSettings->enable &&
					aioseo()->options->sitemap->general->advancedSettings->excludeImages
				)
			);
		}
		if ( $disabled ) {
			return;
		}

		aioseo()->sitemap->image->scanPost( $postId );
	}

	/**
	 * Maps the old Open Graph meta to the social meta columns in V4.
	 *
	 * @since 4.0.0
	 *
	 * @param  array $ogMeta The old V3 Open Graph meta.
	 * @return array $meta   The mapped meta.
	 */
	public function convertOpenGraphMeta( $ogMeta ) {
		$ogMeta = aioseo()->helpers->maybeUnserialize( $ogMeta );

		if ( ! is_array( $ogMeta ) ) {
			return [];
		}

		$mappedSocialMeta = [
			'aioseop_opengraph_settings_title'             => 'og_title',
			'aioseop_opengraph_settings_desc'              => 'og_description',
			'aioseop_opengraph_settings_image'             => 'og_image_custom_url',
			'aioseop_opengraph_settings_imagewidth'        => 'og_image_width',
			'aioseop_opengraph_settings_imageheight'       => 'og_image_height',
			'aioseop_opengraph_settings_video'             => 'og_video',
			'aioseop_opengraph_settings_videowidth'        => 'og_video_width',
			'aioseop_opengraph_settings_videoheight'       => 'og_video_height',
			'aioseop_opengraph_settings_category'          => 'og_object_type',
			'aioseop_opengraph_settings_section'           => 'og_article_section',
			'aioseop_opengraph_settings_tag'               => 'og_article_tags',
			'aioseop_opengraph_settings_setcard'           => 'twitter_card',
			'aioseop_opengraph_settings_customimg_twitter' => 'twitter_image_custom_url',
		];

		$meta = [];
		foreach ( $ogMeta as $name => $value ) {
			if ( ! in_array( $name, array_keys( $mappedSocialMeta ), true ) ) {
				continue;
			}

			switch ( $name ) {
				case 'aioseop_opengraph_settings_desc':
				case 'aioseop_opengraph_settings_title':
					$meta[ $mappedSocialMeta[ $name ] ] = aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $value ) );
					break;
				case 'aioseop_opengraph_settings_image':
					$value = strval( $value );
					if ( empty( $value ) ) {
						break;
					}

					$meta['og_image_type']              = 'custom_image';
					$meta[ $mappedSocialMeta[ $name ] ] = strval( $value );
					break;
				case 'aioseop_opengraph_settings_video':
					$meta[ $mappedSocialMeta[ $name ] ] = esc_url( $value );
					break;
				case 'aioseop_opengraph_settings_customimg_twitter':
					$value = strval( $value );
					if ( empty( $value ) ) {
						break;
					}
					$meta['twitter_image_type']         = 'custom_image';
					$meta['twitter_use_og']             = false;
					$meta[ $mappedSocialMeta[ $name ] ] = strval( $value );
					break;
				case 'aioseop_opengraph_settings_imagewidth':
				case 'aioseop_opengraph_settings_imageheight':
				case 'aioseop_opengraph_settings_videowidth':
				case 'aioseop_opengraph_settings_videoheight':
					$value = intval( $value );
					if ( ! $value || $value <= 0 ) {
						break;
					}
					$meta[ $mappedSocialMeta[ $name ] ] = $value;
					break;
				case 'aioseop_opengraph_settings_tag':
					$meta[ $mappedSocialMeta[ $name ] ] = aioseo()->migration->helpers->oldKeywordsToNewKeywords( $value );
					break;
				default:
					$meta[ $mappedSocialMeta[ $name ] ] = esc_html( strval( $value ) );
					break;
			}
		}

		return $meta;
	}

	/**
	 * Returns the title as it was in V3.
	 *
	 * @since 4.0.0
	 *
	 * @param  int    $postId   The post ID.
	 * @param  string $seoTitle The old SEO title.
	 * @return string           The title.
	 */
	protected function getPostTitle( $postId, $seoTitle = '' ) {
		$post = get_post( $postId );
		if ( ! is_object( $post ) ) {
			return '';
		}

		$postType    = $post->post_type;
		$oldOptions  = get_option( 'aioseo_options_v3' );
		$titleFormat = isset( $oldOptions[ "aiosp_{$postType}_title_format" ] ) ? $oldOptions[ "aiosp_{$postType}_title_format" ] : '';

		$seoTitle = aioseo()->helpers->pregReplace( '/(%post_title%|%page_title%)/', $seoTitle, $titleFormat );

		return aioseo()->helpers->sanitizeOption( aioseo()->migration->helpers->macrosToSmartTags( $seoTitle ) );
	}
}