<?php
/**
 * Extract JSON-LD blocks from public HTML.
 *
 * @package SEOvaultSEO\Schema\Validation
 */

namespace SEOvaultSEO\Schema\Validation;

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

/**
 * Finds application/ld+json script blocks with provenance metadata.
 */
class JsonLdHtmlExtractor {

	/**
	 * Extract JSON-LD script blocks from HTML.
	 *
	 * @param string $html HTML document body.
	 * @return array{
	 *   blocks: array<int, array{index:int,raw:string,data:mixed,error:string}>,
	 *   entities: array<int, array>,
	 *   decode_errors: array<int, array{index:int,message:string}>
	 * }
	 */
	public function extract( $html ) {
		$html = is_string( $html ) ? $html : '';
		if ( '' === $html ) {
			return array(
				'blocks'        => array(),
				'entities'      => array(),
				'decode_errors' => array(),
			);
		}

		$blocks = array();
		$pattern = '#<script\b[^>]*\btype\s*=\s*(["\'])application/ld\+json\1[^>]*>(.*?)</script>#is';
		if ( ! preg_match_all( $pattern, $html, $matches, PREG_SET_ORDER ) ) {
			// Fallback: type attribute before or after other attributes with flexible quoting.
			$pattern = '#<script\b(?=[^>]*\btype\s*=\s*(["\']?)application/ld\+json\1)[^>]*>(.*?)</script>#is';
			preg_match_all( $pattern, $html, $matches, PREG_SET_ORDER );
		}

		$decode_errors = array();
		$entities      = array();

		foreach ( $matches as $index => $match ) {
			$raw  = isset( $match[2] ) ? trim( html_entity_decode( $match[2], ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) : '';
			$block = array(
				'index' => $index,
				'raw'   => $raw,
				'data'  => null,
				'error' => '',
			);

			if ( '' === $raw ) {
				$block['error'] = 'empty';
				$decode_errors[] = array(
					'index'   => $index,
					'message' => 'empty',
				);
				$blocks[] = $block;
				continue;
			}

			$decoded = json_decode( $raw, true );
			if ( JSON_ERROR_NONE !== json_last_error() || ( ! is_array( $decoded ) && ! is_object( $decoded ) ) ) {
				$block['error'] = json_last_error_msg();
				$decode_errors[] = array(
					'index'   => $index,
					'message' => $block['error'],
				);
				$blocks[] = $block;
				continue;
			}

			if ( is_object( $decoded ) ) {
				$decoded = (array) $decoded;
			}

			$block['data'] = $decoded;
			$blocks[]      = $block;

			foreach ( $this->flatten_to_entities( $decoded, $index ) as $entity ) {
				$entities[] = $entity;
			}
		}

		return array(
			'blocks'        => $blocks,
			'entities'      => $entities,
			'decode_errors' => $decode_errors,
		);
	}

	/**
	 * Build a combined observed graph from extracted entities.
	 *
	 * @param array $extraction Extractor result.
	 * @return array
	 */
	public function to_graph( $extraction ) {
		$entities = isset( $extraction['entities'] ) && is_array( $extraction['entities'] ) ? $extraction['entities'] : array();
		$nodes    = array();

		foreach ( $entities as $entity ) {
			if ( ! is_array( $entity ) || ! isset( $entity['data'] ) || ! is_array( $entity['data'] ) ) {
				continue;
			}
			$nodes[] = $entity['data'];
		}

		return array(
			'@context' => 'https://schema.org',
			'@graph'   => $nodes,
		);
	}

	/**
	 * Flatten a decoded JSON-LD value into top-level entity records.
	 *
	 * @param mixed $data  Decoded JSON-LD.
	 * @param int   $index Script block index.
	 * @return array<int, array{block_index:int,data:array}>
	 */
	private function flatten_to_entities( $data, $index ) {
		$out = array();

		if ( ! is_array( $data ) ) {
			return $out;
		}

		if ( ValueInspector::is_list( $data ) ) {
			foreach ( $data as $item ) {
				foreach ( $this->flatten_to_entities( $item, $index ) as $entity ) {
					$out[] = $entity;
				}
			}
			return $out;
		}

		if ( array_key_exists( '@graph', $data ) ) {
			$graph = $data['@graph'];
			if ( is_array( $graph ) ) {
				$items = ValueInspector::is_list( $graph ) ? $graph : array( $graph );
				foreach ( $items as $item ) {
					if ( is_array( $item ) && ! ValueInspector::is_list( $item ) ) {
						$out[] = array(
							'block_index' => $index,
							'data'        => $item,
						);
					}
				}
			}
			return $out;
		}

		$out[] = array(
			'block_index' => $index,
			'data'        => $data,
		);

		return $out;
	}
}
