<?php
/**
 * Presentation adapter for structured Schema Health issues.
 *
 * @package SEOvaultSEO\Schema\Validation
 */

namespace SEOvaultSEO\Schema\Validation;

use SEOvaultSEO\Schema\SchemaExtensionApi;

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

/**
 * Converts stable validator keys into translated, user-facing editor copy.
 */
class ValidationIssuePresenter {

	/**
	 * Present one exported ValidationIssue.
	 *
	 * Technical code/path stay in dedicated keys for secondary disclosure.
	 *
	 * @param array $issue Exported issue data.
	 * @return array
	 */
	public function present( $issue ) {
		$issue         = is_array( $issue ) ? $issue : array();
		$code          = isset( $issue['code'] ) ? (string) $issue['code'] : '';
		$severity      = isset( $issue['severity'] ) ? (string) $issue['severity'] : Severity::INFO;
		$path          = isset( $issue['path'] ) ? (string) $issue['path'] : '';
		$property      = $this->property_from_path( $path );
		$copy          = $this->get_copy( $issue, $code, $severity, $property );
		$entity        = isset( $issue['entity_type'] ) ? (string) $issue['entity_type'] : '';
		$entity        = '' !== $entity ? $entity : __( 'Schema graph', 'seovault-ai' );
		$documentation = isset( $issue['documentation'] ) ? (string) $issue['documentation'] : '';
		$is_fault      = $this->is_plugin_generation_fault( $issue, $code );

		return array(
			'severity'       => $severity,
			'severity_label' => $this->get_severity_label( $severity ),
			'title'          => $copy['title'],
			'message'        => $copy['message'],
			'action'         => $copy['action'],
			'entity_type'    => $entity,
			'path'           => $path,
			'code'           => $code,
			'is_plugin_fault' => $is_fault,
			'repair_actions' => $is_fault ? $this->get_repair_actions() : array(),
			'learn_more_url' => ValidationDocumentationRegistry::resolve( $documentation, $issue ),
		);
	}

	/**
	 * Get translated issue copy.
	 *
	 * @param array  $issue    Exported issue data.
	 * @param string $code     Stable issue code.
	 * @param string $severity Issue severity.
	 * @param string $property Human-readable property.
	 * @return array
	 */
	private function get_copy( $issue, $code, $severity, $property ) {
		if ( $this->is_plugin_generation_fault( $issue, $code ) ) {
			return $this->get_plugin_fault_copy( $code, $issue );
		}

		$copy = array(
			'title'   => __( 'Schema issue found', 'seovault-ai' ),
			'message' => __( 'Review this part of the schema and correct the reported value.', 'seovault-ai' ),
			'action'  => __( 'Review the affected content or settings.', 'seovault-ai' ),
		);

		$google_image = $this->get_google_image_improvement_copy( $code, $issue );
		if ( null !== $google_image ) {
			return $google_image;
		}

		if ( 0 === strpos( $code, 'google.' ) && '.missing' === substr( $code, -8 ) ) {
			$required = Severity::ERROR === $severity;
			$field    = $this->friendly_property_label( $property );
			return array(
				'title'   => $required ? __( 'Required rich-result property is missing', 'seovault-ai' ) : __( 'Recommended rich-result property is missing', 'seovault-ai' ),
				'message' => sprintf(
					/* translators: %s: human field label. */
					$required ? __( 'Google documents %s as required for this rich-result type.', 'seovault-ai' ) : __( 'Google recommends %s for a more complete rich-result implementation.', 'seovault-ai' ),
					$field
				),
				'action'  => sprintf(
					/* translators: %s: human field label. */
					__( 'Add a valid %s.', 'seovault-ai' ),
					$field
				),
			);
		}

		$catalog = array(
			'json.syntax.invalid'        => array(
				__( 'Invalid JSON syntax', 'seovault-ai' ),
				__( 'The custom schema is not valid JSON, so SEOvault cannot publish it safely.', 'seovault-ai' ),
				__( 'Correct the JSON syntax and check the schema again.', 'seovault-ai' ),
			),
			'json.input.too_large'       => array(
				__( 'Custom schema is too large', 'seovault-ai' ),
				__( 'The custom schema exceeds the local validation size limit.', 'seovault-ai' ),
				__( 'Reduce the custom JSON size before checking it again.', 'seovault-ai' ),
			),
			'json.root.invalid_type'     => array(
				__( 'Invalid JSON-LD root', 'seovault-ai' ),
				__( 'The custom schema root must be an object or a list of schema objects.', 'seovault-ai' ),
				__( 'Wrap the schema data in a JSON object or a valid object list.', 'seovault-ai' ),
			),
			'schema.context_missing'     => array(
				__( 'Schema context is missing', 'seovault-ai' ),
				__( 'This schema does not declare a Schema.org context.', 'seovault-ai' ),
				__( 'Add a Schema.org context to the custom schema, or let SEOvault generate the graph.', 'seovault-ai' ),
			),
			'schema.context_unsupported' => array(
				__( 'Schema context is unsupported', 'seovault-ai' ),
				__( 'The declared JSON-LD context is not the supported Schema.org context.', 'seovault-ai' ),
				__( 'Use the Schema.org context, or remove the custom override.', 'seovault-ai' ),
			),
			'schema.type.missing'        => array(
				__( 'Schema type is missing', 'seovault-ai' ),
				__( 'A schema entity does not declare its type.', 'seovault-ai' ),
				__( 'Choose a schema type or remove the incomplete entity.', 'seovault-ai' ),
			),
			'schema.type.invalid'        => array(
				__( 'Schema type is invalid', 'seovault-ai' ),
				__( 'An entity has a type value in an unsupported format.', 'seovault-ai' ),
				__( 'Use a non-empty Schema.org type name.', 'seovault-ai' ),
			),
			'schema.type_unknown'        => array(
				__( 'Type not in bundled vocabulary', 'seovault-ai' ),
				__( 'This type is outside SEOvault’s bundled Schema.org vocabulary subset. That does not prove the type is invalid Schema.org — only that it was not verified against the local list.', 'seovault-ai' ),
				__( 'Use a SEOvault-supported type when possible, or review the custom type in technical details.', 'seovault-ai' ),
			),
			'schema.property_unknown'    => array(
				__( 'Property not in bundled vocabulary', 'seovault-ai' ),
				__( 'This property is outside SEOvault’s bundled Schema.org vocabulary subset. The local checker uses a flat known-property list and does not prove the term is invalid for this entity.', 'seovault-ai' ),
				__( 'Confirm the property only if you expected a SEOvault-supported term; otherwise treat this as technical information.', 'seovault-ai' ),
			),
			'graph.id_duplicate'         => array(
				__( 'Duplicate entity identifier', 'seovault-ai' ),
				__( 'More than one entity uses the same identifier.', 'seovault-ai' ),
				__( 'Give each distinct entity a unique identifier.', 'seovault-ai' ),
			),
			'graph.reference_unresolved' => array(
				__( 'Unresolved entity reference', 'seovault-ai' ),
				__( 'A schema reference does not match an entity in this graph.', 'seovault-ai' ),
				__( 'Add the referenced entity or correct the reference.', 'seovault-ai' ),
			),
			'schema.value_empty'         => array(
				__( 'Schema value is empty', 'seovault-ai' ),
				__( 'An included schema property has no usable value.', 'seovault-ai' ),
				__( 'Add a value or remove the empty property.', 'seovault-ai' ),
			),
			'schema.value_invalid_url'   => array(
				__( 'Invalid URL value', 'seovault-ai' ),
				__( 'A schema URL property does not contain a valid HTTP or HTTPS URL.', 'seovault-ai' ),
				__( 'Replace it with a complete public URL.', 'seovault-ai' ),
			),
			'schema.value_invalid_date'  => array(
				__( 'Invalid date value', 'seovault-ai' ),
				__( 'A schema date property is not in a supported ISO 8601 format.', 'seovault-ai' ),
				__( 'Use a valid date or date-time value.', 'seovault-ai' ),
			),
			'validator.rule.failed'      => array(
				__( 'One validation check was unavailable', 'seovault-ai' ),
				__( 'SEOvault could not complete one local rule, but the remaining checks continued.', 'seovault-ai' ),
				__( 'Run the check again. If it persists, review the schema manually.', 'seovault-ai' ),
			),
			'plugin.generation.breadcrumb_invalid' => array(
				__( 'Breadcrumb generation problem', 'seovault-ai' ),
				__( 'SEOvault could not build one breadcrumb item correctly.', 'seovault-ai' ),
				__( 'Rebuild schema, then review technical details if the problem remains.', 'seovault-ai' ),
			),
			'plugin.generation.reference_unresolved' => array(
				__( 'Schema reference generation problem', 'seovault-ai' ),
				__( 'SEOvault generated a reference that does not resolve inside the published graph.', 'seovault-ai' ),
				__( 'Rebuild schema, then review technical details if the problem remains.', 'seovault-ai' ),
			),
			'plugin.generation.fault' => array(
				__( 'Schema generation problem', 'seovault-ai' ),
				__( 'SEOvault could not build part of the automatic schema correctly.', 'seovault-ai' ),
				__( 'Rebuild schema, then review technical details if the problem remains.', 'seovault-ai' ),
			),
			'faq.main_entity.missing' => array(
				__( 'FAQ questions are missing', 'seovault-ai' ),
				__( 'FAQPage markup has no question and answer pairs.', 'seovault-ai' ),
				__( 'Add at least one complete question and answer, or remove the incomplete FAQ markup.', 'seovault-ai' ),
			),
			'faq.incomplete_pair' => array(
				__( 'FAQ is incomplete', 'seovault-ai' ),
				__( 'FAQPage needs at least one complete question with an accepted answer.', 'seovault-ai' ),
				__( 'Add a complete question and answer pair in the FAQ block.', 'seovault-ai' ),
			),
			'faq.question.incomplete' => array(
				__( 'FAQ pair is incomplete', 'seovault-ai' ),
				__( 'A FAQ entry is missing a question or an accepted answer.', 'seovault-ai' ),
				__( 'Complete the unfinished FAQ pair or remove it.', 'seovault-ai' ),
			),
			'faq.duplicate_question' => array(
				__( 'Duplicate FAQ question', 'seovault-ai' ),
				__( 'The FAQ markup includes duplicate questions. This is an improvement, not a blocker.', 'seovault-ai' ),
				__( 'Remove duplicate questions so each answer appears once.', 'seovault-ai' ),
			),
			'howto.name.missing' => array(
				__( 'How-to name is missing', 'seovault-ai' ),
				__( 'HowTo markup needs a name.', 'seovault-ai' ),
				__( 'Add a How-to name in the How-to block.', 'seovault-ai' ),
			),
			'howto.steps.missing' => array(
				__( 'How-to steps are missing', 'seovault-ai' ),
				__( 'HowTo markup needs at least one complete step.', 'seovault-ai' ),
				__( 'Add a complete step with a name or instructions.', 'seovault-ai' ),
			),
			'howto.step.incomplete' => array(
				__( 'How-to step is incomplete', 'seovault-ai' ),
				__( 'A HowTo step is missing both a name and instructions.', 'seovault-ai' ),
				__( 'Complete the unfinished step or remove it.', 'seovault-ai' ),
			),
		);
		$defaults = $catalog;
		$filtered = apply_filters( 'seovaultseo_schema_validation_presentation_strings', $catalog, $issue );
		$sanitized = SchemaExtensionApi::sanitize_presentation_catalog(
			is_array( $filtered ) ? $filtered : array()
		);
		// Extensions may override copy; hostile wipe falls back to built-ins.
		$catalog = ! empty( $sanitized ) ? array_merge( $defaults, $sanitized ) : $defaults;

		$title_key   = isset( $issue['title_key'] ) ? $this->strip_key_suffix( (string) $issue['title_key'] ) : '';
		$message_key = isset( $issue['message_key'] ) ? $this->strip_key_suffix( (string) $issue['message_key'] ) : '';
		$lookup_keys = array_unique( array_filter( array( $title_key, $message_key, $code ) ) );
		$matched_key = '';
		foreach ( $lookup_keys as $lookup_key ) {
			if ( isset( $catalog[ $lookup_key ] ) && is_array( $catalog[ $lookup_key ] ) ) {
				$matched_key = $lookup_key;
				break;
			}
		}

		if ( '' !== $matched_key ) {
			$copy = array(
				'title'   => $catalog[ $matched_key ][0],
				'message' => $catalog[ $matched_key ][1],
				'action'  => $catalog[ $matched_key ][2],
			);
		} elseif ( 0 === strpos( $code, 'graph.' ) ) {
			$copy = array(
				'title'   => __( 'Invalid schema graph structure', 'seovault-ai' ),
				'message' => __( 'Part of the published schema has an unsupported structure.', 'seovault-ai' ),
				'action'  => __( 'Review the affected schema content and correct the structure.', 'seovault-ai' ),
			);
		}

		if ( 'json.input.too_large' === $code && ! empty( $issue['message_args']['max_bytes'] ) ) {
			$copy['message'] = sprintf(
				/* translators: %s: maximum JSON size in bytes. */
				__( 'The custom schema exceeds the local validation limit of %s bytes.', 'seovault-ai' ),
				number_format_i18n( (int) $issue['message_args']['max_bytes'] )
			);
		}

		return $copy;
	}

	/**
	 * Prefer author-facing image improvement copy for common Google recommendations.
	 *
	 * @param string $code  Issue code.
	 * @param array  $issue Issue data.
	 * @return array|null
	 */
	private function get_google_image_improvement_copy( $code, array $issue ) {
		if ( ! preg_match( '/^google\.(article|blog_posting|news_article)\.image\.missing$/', $code ) ) {
			return null;
		}
		if ( isset( $issue['severity'] ) && Severity::ERROR === $issue['severity'] ) {
			return null;
		}

		return array(
			'title'   => __( 'Featured image recommended', 'seovault-ai' ),
			'message' => __( 'Article markup is valid. A featured image makes the markup more complete.', 'seovault-ai' ),
			'action'  => __( 'Add a featured image to improve Article markup.', 'seovault-ai' ),
		);
	}

	/**
	 * Plugin-generation fault copy.
	 *
	 * @param string $code  Issue code.
	 * @param array  $issue Issue data.
	 * @return array
	 */
	private function get_plugin_fault_copy( $code, array $issue ) {
		if ( false !== strpos( $code, 'breadcrumb' ) || false !== strpos( (string) ( $issue['path'] ?? '' ), 'itemListElement' ) ) {
			return array(
				'title'   => __( 'Breadcrumb generation problem', 'seovault-ai' ),
				'message' => __( 'SEOvault could not build one breadcrumb item correctly.', 'seovault-ai' ),
				'action'  => __( 'Rebuild schema, then review technical details if the problem remains.', 'seovault-ai' ),
			);
		}
		if ( false !== strpos( $code, 'reference' ) || 'graph.reference_unresolved' === $code ) {
			return array(
				'title'   => __( 'Schema reference generation problem', 'seovault-ai' ),
				'message' => __( 'SEOvault generated a reference that does not resolve inside the published graph.', 'seovault-ai' ),
				'action'  => __( 'Rebuild schema, then review technical details if the problem remains.', 'seovault-ai' ),
			);
		}

		return array(
			'title'   => __( 'Schema generation problem', 'seovault-ai' ),
			'message' => __( 'SEOvault could not build part of the automatic schema correctly.', 'seovault-ai' ),
			'action'  => __( 'Rebuild schema, then review technical details if the problem remains.', 'seovault-ai' ),
		);
	}

	/**
	 * Repair actions for plugin-generation faults.
	 *
	 * @return string[]
	 */
	private function get_repair_actions() {
		return array(
			__( 'Rebuild schema', 'seovault-ai' ),
			__( 'Review technical details', 'seovault-ai' ),
		);
	}

	/**
	 * Detect SEOvault generator faults that authors should not “fix” via @graph paths.
	 *
	 * @param array  $issue Issue data.
	 * @param string $code  Issue code.
	 * @return bool
	 */
	private function is_plugin_generation_fault( array $issue, $code ) {
		if ( 0 === strpos( $code, 'plugin.generation.' ) ) {
			return true;
		}
		if ( 'plugin_generation' === ( $issue['category'] ?? '' ) ) {
			return true;
		}
		$fix_action = isset( $issue['fix_action'] ) ? (string) $issue['fix_action'] : '';
		if ( 0 === strpos( $fix_action, 'rebuild_' ) || 'rebuild_schema' === $fix_action ) {
			return true;
		}

		return false;
	}

	/**
	 * Remove conventional presentation suffixes from a translation key.
	 *
	 * @param string $key Presentation key.
	 * @return string
	 */
	private function strip_key_suffix( $key ) {
		return preg_replace( '/\.(?:title|message|action)$/', '', $key );
	}

	/**
	 * Convert a path tail to a readable property label.
	 *
	 * @param string $path Property path.
	 * @return string
	 */
	private function property_from_path( $path ) {
		$parts    = array_values(
			array_filter(
				explode( '.', $path ),
				function ( $part ) {
					return '' !== $part && ! preg_match( '/^@graph\[\d+\]$/', $part ) && '@graph' !== $part;
				}
			)
		);
		$property = ! empty( $parts ) ? end( $parts ) : __( 'property', 'seovault-ai' );

		return (string) $property;
	}

	/**
	 * Friendly field label for author-facing copy.
	 *
	 * @param string $property Raw property.
	 * @return string
	 */
	private function friendly_property_label( $property ) {
		$map = array(
			'image'           => __( 'image', 'seovault-ai' ),
			'author'          => __( 'author', 'seovault-ai' ),
			'headline'        => __( 'headline', 'seovault-ai' ),
			'datePublished'   => __( 'publish date', 'seovault-ai' ),
			'dateModified'    => __( 'modified date', 'seovault-ai' ),
			'description'     => __( 'description', 'seovault-ai' ),
			'name'            => __( 'name', 'seovault-ai' ),
			'startDate'       => __( 'start date', 'seovault-ai' ),
			'location'        => __( 'location', 'seovault-ai' ),
			'operatingSystem' => __( 'operating system', 'seovault-ai' ),
		);

		return isset( $map[ $property ] ) ? $map[ $property ] : $property;
	}

	/**
	 * Get a visible severity label.
	 *
	 * Recommendations stay labeled as recommendations (never errors).
	 *
	 * @param string $severity Severity.
	 * @return string
	 */
	private function get_severity_label( $severity ) {
		$labels = array(
			Severity::ERROR          => __( 'Error', 'seovault-ai' ),
			Severity::WARNING        => __( 'Warning', 'seovault-ai' ),
			Severity::RECOMMENDATION => __( 'Recommendation', 'seovault-ai' ),
			Severity::INFO           => __( 'Information', 'seovault-ai' ),
		);

		return isset( $labels[ $severity ] ) ? $labels[ $severity ] : $labels[ Severity::INFO ];
	}
}
