<?php
/**
 * `wp blaminhor seo ...` — SEO Manager commands.
 *
 * Scope: indexing controls, sitemap, AI bot blocking, schema/JSON-LD,
 * and ToC toggles. Advanced features (per-post meta editor, social tag
 * overrides, JSON-LD author E-E-A-T) remain in the admin UI for now.
 *
 * @package BlaminhorEssentials
 */

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

if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
	return;
}

/**
 * Configure SEO/GEO settings, sitemap and AI bot blockers from the command line.
 */
class Blaminhor_Essentials_CLI_SEO_Manager extends Blaminhor_Essentials_CLI_Module {

	/**
	 * @var string
	 */
	protected $module_id = 'seo-manager';

	/**
	 * Show the SEO Manager configuration.
	 *
	 * Outputs the most actionable subset of the settings (indexing, AI
	 * blockers, sitemap, schema). Full JSON-LD / per-post fields are not
	 * surfaced here — use the admin UI for those.
	 *
	 * ## OPTIONS
	 *
	 * [--format=<format>]
	 * : Render format (table, json, csv, yaml). Default: table.
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function get( $args, $assoc_args ) {
		$module = $this->get_module_instance();
		$s      = $module->get_effective_settings();

		$rows = array();
		$bool = function ( $v ) { return $v ? 'true' : 'false'; };

		// Indexing.
		$rows[] = array( 'key' => 'noindex_entire_site',     'value' => $bool( $s['noindex_entire_site'] ) );
		$rows[] = array( 'key' => 'indexable_post_types',    'value' => implode( ',', (array) $s['indexable_post_types'] ) );
		$rows[] = array( 'key' => 'indexable_taxonomies',    'value' => implode( ',', (array) $s['indexable_taxonomies'] ) );
		$rows[] = array( 'key' => 'noindex_archives',        'value' => $bool( $s['noindex_archives'] ) );
		$rows[] = array( 'key' => 'noindex_author_archives', 'value' => $bool( $s['noindex_author_archives'] ) );
		$rows[] = array( 'key' => 'noindex_date_archives',   'value' => $bool( $s['noindex_date_archives'] ) );
		$rows[] = array( 'key' => 'noindex_search',          'value' => $bool( $s['noindex_search'] ) );

		// AI bot blockers.
		$rows[] = array( 'key' => 'ai_noai',                 'value' => $bool( $s['ai_noai'] ) );
		$rows[] = array( 'key' => 'ai_block_chatgpt',        'value' => $bool( $s['ai_block_chatgpt'] ) );
		$rows[] = array( 'key' => 'ai_block_google_ai',      'value' => $bool( $s['ai_block_google_ai'] ) );
		$rows[] = array( 'key' => 'ai_block_claude',         'value' => $bool( $s['ai_block_claude'] ) );
		$rows[] = array( 'key' => 'ai_block_perplexity',     'value' => $bool( $s['ai_block_perplexity'] ) );
		$rows[] = array( 'key' => 'ai_block_common_crawl',   'value' => $bool( $s['ai_block_common_crawl'] ) );

		// Sitemap.
		$rows[] = array( 'key' => 'sitemap_enabled',         'value' => $bool( $s['sitemap_enabled'] ) );
		$rows[] = array( 'key' => 'sitemap_post_types',      'value' => implode( ',', (array) $s['sitemap_post_types'] ) );
		$rows[] = array( 'key' => 'sitemap_taxonomies',      'value' => implode( ',', (array) $s['sitemap_taxonomies'] ) );
		$rows[] = array( 'key' => 'sitemap_include_lastmod', 'value' => $bool( $s['sitemap_include_lastmod'] ) );
		$rows[] = array( 'key' => 'sitemap_include_images',  'value' => $bool( $s['sitemap_include_images'] ) );
		$rows[] = array( 'key' => 'sitemap_posts_per_file',  'value' => (string) (int) $s['sitemap_posts_per_file'] );
		$rows[] = array( 'key' => 'sitemap_ping_engines',    'value' => $bool( $s['sitemap_ping_engines'] ) );

		// Schema / ToC.
		$rows[] = array( 'key' => 'schema_enabled',          'value' => $bool( $s['schema_enabled'] ) );
		$rows[] = array( 'key' => 'schema_site_type',        'value' => (string) $s['schema_site_type'] );
		$rows[] = array( 'key' => 'toc_enabled',             'value' => $bool( $s['toc_enabled'] ) );
		$rows[] = array( 'key' => 'title_separator',         'value' => (string) $s['title_separator'] );

		$this->format_for_output( $rows, $assoc_args, array( 'key', 'value' ) );
	}

	/**
	 * Update the most-used SEO Manager settings.
	 *
	 * For settings not exposed here (per-post title templates, JSON-LD
	 * author fields, etc.), use the admin UI.
	 *
	 * ## OPTIONS
	 *
	 * [--noindex-entire-site=<bool>]
	 * : Add noindex,nofollow to every page (kill switch).
	 *
	 * [--sitemap-enabled=<bool>]
	 * : Generate /sitemap.xml and per-post-type sub-sitemaps.
	 *
	 * [--sitemap-ping-engines=<bool>]
	 * : Notify search engines on publish.
	 *
	 * [--sitemap-include-lastmod=<bool>]
	 * : Include <lastmod> tags in sitemap entries.
	 *
	 * [--sitemap-include-images=<bool>]
	 * : Include image references in sitemap entries.
	 *
	 * [--sitemap-posts-per-file=<n>]
	 * : Max number of URLs per sub-sitemap file (default 1000).
	 *
	 * [--schema-enabled=<bool>]
	 * : Emit JSON-LD schema markup.
	 *
	 * [--toc-enabled=<bool>]
	 * : Auto-insert a table of contents on long posts.
	 *
	 * [--ai-noai=<bool>]
	 * : Set the global noai/noimageai meta tag.
	 *
	 * [--ai-block-chatgpt=<bool>]
	 * : Add GPTBot to robots.txt.
	 *
	 * [--ai-block-google-ai=<bool>]
	 * : Block Google-Extended (Bard / Gemini).
	 *
	 * [--ai-block-claude=<bool>]
	 * : Block ClaudeBot.
	 *
	 * [--ai-block-perplexity=<bool>]
	 * : Block PerplexityBot.
	 *
	 * [--ai-block-common-crawl=<bool>]
	 * : Block CCBot.
	 *
	 * [--title-separator=<sep>]
	 * : One of: -, –, —, |, >, ·, ~ (validated server-side).
	 *
	 * [--title-home=<template>]
	 * : Title template for the home page (supports {site_name} {sep} {tagline}).
	 *
	 * [--title-search=<template>]
	 * : Title template for search results pages.
	 *
	 * [--title-404=<template>]
	 * : Title template for 404 pages.
	 *
	 * [--og-default-image=<url>]
	 * : Fallback Open Graph image URL when a post does not set one.
	 *
	 * [--twitter-card-type=<type>]
	 * : Twitter Card type. One of: summary, summary_large_image.
	 *
	 * [--twitter-site=<handle>]
	 * : Twitter handle for the site (e.g. @example).
	 *
	 * [--schema-site-type=<type>]
	 * : Site-level schema type. One of: Organization, Person.
	 *
	 * [--schema-org-name=<name>]
	 * : Organization / person name for JSON-LD.
	 *
	 * [--schema-org-logo=<url>]
	 * : Organization logo URL for JSON-LD.
	 *
	 * [--schema-org-url=<url>]
	 * : Organization URL for JSON-LD.
	 *
	 * [--schema-same-as=<urls>]
	 * : Newline-separated sameAs URLs (social profiles, Wikipedia, etc.).
	 *
	 * [--schema-default-article-type=<type>]
	 * : Default article schema type. One of: Article, BlogPosting, NewsArticle, TechArticle.
	 *
	 * [--schema-default-page-type=<type>]
	 * : Default page schema type. One of: WebPage, AboutPage, ContactPage, FAQPage.
	 *
	 * [--schema-add-website=<bool>]
	 * : Emit WebSite JSON-LD with potentialAction.
	 *
	 * [--schema-add-breadcrumbs=<bool>]
	 * : Emit BreadcrumbList JSON-LD.
	 *
	 * [--schema-auto-faq=<bool>]
	 * : Auto-detect FAQ blocks and emit FAQPage schema.
	 *
	 * [--schema-auto-howto=<bool>]
	 * : Auto-detect How-To blocks and emit HowTo schema.
	 *
	 * [--schema-auto-video=<bool>]
	 * : Auto-detect videos and emit VideoObject schema.
	 *
	 * [--toc-depth=<depth>]
	 * : Heading depth. One of: h2, h2-h3, h2-h4.
	 *
	 * [--toc-style=<style>]
	 * : Visual style. One of: numbered, bullets, plain.
	 *
	 * [--toc-title=<title>]
	 * : Heading text for the ToC block.
	 *
	 * [--takeaways-enabled=<bool>]
	 * : Render the Key Takeaways block at the top of posts that define one.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor seo set --sitemap-enabled=true --sitemap-ping-engines=true
	 *     $ wp blaminhor seo set --ai-block-chatgpt=true --ai-block-claude=true
	 *     $ wp blaminhor seo set --schema-site-type=Organization --schema-org-name="Acme Inc"
	 *     $ wp blaminhor seo set --toc-enabled=true --toc-style=numbered --toc-depth=h2-h3
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function set( $args, $assoc_args ) {
		$this->ensure_capabilities();
		$module  = $this->get_module_instance();
		$current = $module->get_effective_settings();

		// Carry-forward of fields we don't expose so process_settings doesn't
		// reset them.
		$data = array(
			'indexable_post_types'        => (array) $current['indexable_post_types'],
			'indexable_taxonomies'        => (array) $current['indexable_taxonomies'],
			'sitemap_post_types'          => (array) $current['sitemap_post_types'],
			'sitemap_taxonomies'          => (array) $current['sitemap_taxonomies'],
			'schema_post_types'           => (array) ( isset( $current['schema_post_types'] ) ? $current['schema_post_types'] : array() ),
			'title_templates'             => (array) ( isset( $current['title_templates'] ) ? $current['title_templates'] : array() ),
			'title_separator'             => (string) $current['title_separator'],
			'title_home'                  => (string) $current['title_home'],
			'title_search'                => (string) $current['title_search'],
			'title_404'                   => (string) $current['title_404'],
			'title_author_archive'        => (string) $current['title_author_archive'],
			'title_date_archive'          => (string) $current['title_date_archive'],
			'og_default_image'            => (string) $current['og_default_image'],
			'twitter_card_type'           => (string) $current['twitter_card_type'],
			'twitter_site'                => (string) $current['twitter_site'],
			'schema_site_type'            => (string) $current['schema_site_type'],
			'schema_org_name'             => (string) $current['schema_org_name'],
			'schema_org_logo'             => (string) $current['schema_org_logo'],
			'schema_org_url'              => (string) $current['schema_org_url'],
			'schema_same_as'              => (string) $current['schema_same_as'],
			'schema_default_article_type' => (string) $current['schema_default_article_type'],
			'schema_default_page_type'    => (string) $current['schema_default_page_type'],
			'schema_author_job_title'     => (string) $current['schema_author_job_title'],
			'schema_author_description'   => (string) $current['schema_author_description'],
			'schema_author_knows_about'   => (string) $current['schema_author_knows_about'],
			'schema_author_alumni_of'     => (string) $current['schema_author_alumni_of'],
			'schema_author_same_as'       => (string) $current['schema_author_same_as'],
			'toc_depth'                   => (string) $current['toc_depth'],
			'toc_style'                   => (string) $current['toc_style'],
			'toc_title'                   => (string) $current['toc_title'],
			'sitemap_posts_per_file'      => (string) (int) $current['sitemap_posts_per_file'],
		);

		// Scalar overrides — each new flag maps 1:1 to a setting key.
		$scalar_map = array(
			'title-separator'             => 'title_separator',
			'title-home'                  => 'title_home',
			'title-search'                => 'title_search',
			'title-404'                   => 'title_404',
			'og-default-image'            => 'og_default_image',
			'twitter-card-type'           => 'twitter_card_type',
			'twitter-site'                => 'twitter_site',
			'schema-site-type'            => 'schema_site_type',
			'schema-org-name'             => 'schema_org_name',
			'schema-org-logo'             => 'schema_org_logo',
			'schema-org-url'              => 'schema_org_url',
			'schema-same-as'              => 'schema_same_as',
			'schema-default-article-type' => 'schema_default_article_type',
			'schema-default-page-type'    => 'schema_default_page_type',
			'toc-depth'                   => 'toc_depth',
			'toc-style'                   => 'toc_style',
			'toc-title'                   => 'toc_title',
		);
		foreach ( $scalar_map as $cli_flag => $setting_key ) {
			if ( isset( $assoc_args[ $cli_flag ] ) ) {
				$data[ $setting_key ] = (string) $assoc_args[ $cli_flag ];
			}
		}
		if ( isset( $assoc_args['sitemap-posts-per-file'] ) ) {
			$data['sitemap_posts_per_file'] = (string) (int) $assoc_args['sitemap-posts-per-file'];
		}

		// Booleans (checkboxes — set key to '1' to enable, omit to disable).
		$bool_map = array(
			'noindex-entire-site'    => 'noindex_entire_site',
			'sitemap-enabled'        => 'sitemap_enabled',
			'sitemap-ping-engines'   => 'sitemap_ping_engines',
			'sitemap-include-lastmod'=> 'sitemap_include_lastmod',
			'sitemap-include-images' => 'sitemap_include_images',
			'schema-enabled'         => 'schema_enabled',
			'schema-add-website'     => 'schema_add_website',
			'schema-add-breadcrumbs' => 'schema_add_breadcrumbs',
			'schema-auto-faq'        => 'schema_auto_faq',
			'schema-auto-howto'      => 'schema_auto_howto',
			'schema-auto-video'      => 'schema_auto_video',
			'toc-enabled'            => 'toc_enabled',
			'takeaways-enabled'      => 'takeaways_enabled',
			'ai-noai'                => 'ai_noai',
			'ai-block-chatgpt'       => 'ai_block_chatgpt',
			'ai-block-google-ai'     => 'ai_block_google_ai',
			'ai-block-claude'        => 'ai_block_claude',
			'ai-block-perplexity'    => 'ai_block_perplexity',
			'ai-block-common-crawl'  => 'ai_block_common_crawl',
		);

		foreach ( $bool_map as $cli_flag => $setting_key ) {
			if ( array_key_exists( $cli_flag, $assoc_args ) ) {
				if ( $this->parse_bool( $assoc_args[ $cli_flag ] ) ) {
					$data[ $setting_key ] = '1';
				}
			} elseif ( ! empty( $current[ $setting_key ] ) ) {
				$data[ $setting_key ] = '1';
			}
		}

		// `noindex_archives/author/date/search` use the INVERTED pattern in
		// process_settings(): the form has positive-sense checkboxes (`index_X`)
		// and the module flips them. Carry the current "non-noindex" state
		// forward so the absence of a CLI flag preserves the UI value.
		$inverted = array(
			'index_archives'        => 'noindex_archives',
			'index_author_archives' => 'noindex_author_archives',
			'index_date_archives'   => 'noindex_date_archives',
			'index_search'          => 'noindex_search',
		);
		foreach ( $inverted as $cb_key => $setting_key ) {
			// "Indexed" means noindex flag is false.
			if ( empty( $current[ $setting_key ] ) ) {
				$data[ $cb_key ] = '1';
			}
		}

		$result = $module->apply_settings( $data );
		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		}

		blaminhor_essentials_log( 'SEO Manager settings updated via WP-CLI.', 'info', 'cli', array( 'event' => 'seo_set' ) );
		WP_CLI::success( __( 'SEO Manager settings updated.', 'blaminhor-essentials' ) );
	}

	/**
	 * Print every URL that the dynamic sitemap currently exposes.
	 *
	 * ## OPTIONS
	 *
	 * [--format=<format>]
	 * : Render format (table, json, csv, yaml, count, ids). Default: table.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor seo list-urls --format=count
	 *     $ wp blaminhor seo list-urls --format=ids | head -20
	 *
	 * @subcommand list-urls
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function list_urls( $args, $assoc_args ) {
		$module = $this->get_module_instance();
		if ( ! method_exists( $module, 'get_sitemap_urls' ) ) {
			WP_CLI::error( __( 'This build of the SEO Manager does not expose get_sitemap_urls().', 'blaminhor-essentials' ) );
		}
		$urls = (array) $module->get_sitemap_urls();

		$rows = array();
		foreach ( $urls as $url ) {
			$rows[] = array( 'url' => (string) $url );
		}
		$this->format_for_output( $rows, $assoc_args, array( 'url' ) );
	}

	/**
	 * Force a refresh of the sitemap rewrite rules and bust the cache.
	 *
	 * Useful after manual database tinkering or to confirm that a fresh
	 * /sitemap.xml will be served on the next request. The XML itself is
	 * regenerated on-demand by the module at request time — there is no
	 * pre-cached file to delete.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor seo regenerate-sitemap
	 *
	 * @subcommand regenerate-sitemap
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args (unused).
	 */
	public function regenerate_sitemap( $args, $assoc_args ) {
		$this->ensure_capabilities();
		$module = $this->get_module_instance();

		if ( method_exists( $module, 'add_sitemap_rewrite_rules' ) ) {
			$module->add_sitemap_rewrite_rules();
		}
		flush_rewrite_rules( false );
		do_action( 'blaminhor_essentials_cache_purge_request' );

		blaminhor_essentials_log( 'SEO sitemap refresh triggered via WP-CLI.', 'info', 'cli', array( 'event' => 'seo_regenerate_sitemap' ) );
		WP_CLI::success( __( 'Sitemap rewrite rules flushed and cache purge dispatched.', 'blaminhor-essentials' ) );
	}

	/**
	 * Ping search engines about the sitemap URL.
	 *
	 * Equivalent to checking the box in admin and saving — but instant.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor seo ping-engines
	 *
	 * @subcommand ping-engines
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args (unused).
	 */
	public function ping_engines( $args, $assoc_args ) {
		$this->ensure_capabilities();
		$module = $this->get_module_instance();
		if ( ! method_exists( $module, 'ping_search_engines' ) ) {
			WP_CLI::error( __( 'This build of the SEO Manager does not expose ping_search_engines().', 'blaminhor-essentials' ) );
		}
		$module->ping_search_engines();

		blaminhor_essentials_log( 'SEO ping_search_engines() triggered via WP-CLI.', 'info', 'cli', array( 'event' => 'seo_ping' ) );
		WP_CLI::success( __( 'Search engines pinged.', 'blaminhor-essentials' ) );
	}

	/**
	 * Show every SEO meta field stored on a given post.
	 *
	 * Reads the per-post _blaminhor_essentials_* meta keys (seo_title,
	 * seo_description, OG/Twitter overrides, canonical, noindex/nofollow/noai
	 * flags, schema overrides). Read-only.
	 *
	 * ## OPTIONS
	 *
	 * <post-id>
	 * : Numeric post / page / CPT ID.
	 *
	 * [--format=<format>]
	 * : Render format (table, json, csv, yaml). Default: table.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor seo meta-get 42
	 *     $ wp blaminhor seo meta-get 42 --format=json
	 *
	 * @subcommand meta-get
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args: [<post-id>].
	 * @param array $assoc_args Associative args.
	 */
	public function meta_get( $args, $assoc_args ) {
		if ( empty( $args[0] ) || ! is_numeric( $args[0] ) ) {
			WP_CLI::error( __( 'Missing or invalid post ID.', 'blaminhor-essentials' ) );
		}
		$post_id = (int) $args[0];
		if ( ! get_post( $post_id ) ) {
			WP_CLI::error( sprintf(
				/* translators: %d: post ID */
				__( 'No post found with ID %d.', 'blaminhor-essentials' ),
				$post_id
			) );
		}

		$fields = array(
			'seo_title',
			'seo_description',
			'seo_keywords',
			'og_title',
			'og_description',
			'og_image',
			'twitter_title',
			'twitter_description',
			'canonical_url',
			'noindex',
			'nofollow',
			'noai',
			'schema_type',
			'schema_description',
			'schema_custom_json',
		);

		$rows = array();
		foreach ( $fields as $field ) {
			$rows[] = array(
				'key'   => $field,
				'value' => (string) get_post_meta( $post_id, '_blaminhor_essentials_' . $field, true ),
			);
		}
		$this->format_for_output( $rows, $assoc_args, array( 'key', 'value' ) );
	}

	/**
	 * Audit SEO completeness across posts and surface issues.
	 *
	 * Walks every published post of the requested post types and flags
	 * missing/over-long SEO titles and descriptions, missing OG image
	 * fallback, and noindex contradictions (post indexable but flagged
	 * noindex). Read-only — does not mutate any setting or meta.
	 *
	 * ## OPTIONS
	 *
	 * [--post-type=<csv>]
	 * : Limit the audit to these post types (comma-separated). Defaults to
	 *   the post types configured as indexable in the module.
	 *
	 * [--issues-only]
	 * : Skip rows that have no detected issue.
	 *
	 * [--limit=<n>]
	 * : Cap the number of posts examined (0 = no cap). Default: 0.
	 *
	 * [--format=<format>]
	 * : Render format (table, json, csv, yaml, count, ids). Default: table.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor seo audit
	 *     $ wp blaminhor seo audit --post-type=post --issues-only
	 *     $ wp blaminhor seo audit --limit=200 --format=csv > audit.csv
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function audit( $args, $assoc_args ) {
		$module = $this->get_module_instance();

		$post_types = ! empty( $assoc_args['post-type'] )
			? array_filter( array_map( 'sanitize_key', explode( ',', (string) $assoc_args['post-type'] ) ) )
			: (array) $module->get_setting( 'indexable_post_types', array( 'post', 'page' ) );
		if ( empty( $post_types ) ) {
			$post_types = array( 'post', 'page' );
		}

		$limit       = isset( $assoc_args['limit'] ) ? (int) $assoc_args['limit'] : 0;
		$issues_only = isset( $assoc_args['issues-only'] );

		$query = new WP_Query( array(
			'post_type'      => $post_types,
			'post_status'    => 'publish',
			'posts_per_page' => $limit > 0 ? $limit : -1,
			'fields'         => 'ids',
			'no_found_rows'  => true,
		) );

		$rows = array();
		foreach ( $query->posts as $post_id ) {
			$post = get_post( $post_id );
			if ( ! $post ) {
				continue;
			}
			$seo_title       = (string) get_post_meta( $post_id, '_blaminhor_essentials_seo_title', true );
			$seo_description = (string) get_post_meta( $post_id, '_blaminhor_essentials_seo_description', true );
			$og_image        = (string) get_post_meta( $post_id, '_blaminhor_essentials_og_image', true );
			$noindex         = (string) get_post_meta( $post_id, '_blaminhor_essentials_noindex', true );

			$issues = array();
			if ( '' === trim( $seo_title ) ) {
				$issues[] = 'seo_title_missing';
			} elseif ( strlen( $seo_title ) > 60 ) {
				$issues[] = 'seo_title_too_long';
			}
			if ( '' === trim( $seo_description ) ) {
				$issues[] = 'seo_description_missing';
			} elseif ( strlen( $seo_description ) > 160 ) {
				$issues[] = 'seo_description_too_long';
			}
			if ( '' === trim( $og_image ) && '' === (string) $module->get_setting( 'og_default_image', '' ) ) {
				$issues[] = 'og_image_missing';
			}
			if ( '1' === $noindex && in_array( $post->post_type, $post_types, true ) ) {
				$issues[] = 'noindex_on_indexable_type';
			}

			if ( $issues_only && empty( $issues ) ) {
				continue;
			}

			$rows[] = array(
				'id'             => (string) (int) $post_id,
				'post_type'      => (string) $post->post_type,
				'title'          => (string) get_the_title( $post_id ),
				'seo_title_len'  => (string) strlen( $seo_title ),
				'seo_desc_len'   => (string) strlen( $seo_description ),
				'has_og_image'   => '' !== $og_image ? 'true' : 'false',
				'noindex'        => '1' === $noindex ? 'true' : 'false',
				'issues'         => implode( ',', $issues ),
			);
		}

		$this->format_for_output(
			$rows,
			$assoc_args,
			array( 'id', 'post_type', 'title', 'seo_title_len', 'seo_desc_len', 'has_og_image', 'noindex', 'issues' )
		);

		if ( $issues_only ) {
			WP_CLI::log( sprintf(
				/* translators: %d: number of posts with issues */
				__( '%d post(s) with detected issues.', 'blaminhor-essentials' ),
				count( $rows )
			) );
		}
	}

	/**
	 * Dump SEO Manager settings as a JSON envelope on STDOUT.
	 *
	 * Read-only.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp blaminhor seo export > seo-manager.json
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args (unused).
	 */
	public function export( $args, $assoc_args ) {
		$module  = $this->get_module_instance();
		$payload = array(
			'module'         => 'seo-manager',
			'plugin_version' => defined( 'BLAMINHOR_ESSENTIALS_VERSION' ) ? BLAMINHOR_ESSENTIALS_VERSION : '',
			'exported_at'    => gmdate( 'c' ),
			'settings'       => $module->get_effective_settings(),
		);
		WP_CLI::line( wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
	}
}
