<?php
/**
 * Non-mutating JSON-LD graph normalizer.
 *
 * @package SEOvaultSEO\Schema\Validation
 */

namespace SEOvaultSEO\Schema\Validation;

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

/**
 * Normalizes root forms while retaining enough shape information for rules.
 */
class GraphNormalizer {

	/**
	 * Normalize a parsed JSON-LD object or list.
	 *
	 * @param array $data   Parsed JSON-LD data.
	 * @param array $limits Effective validation limits.
	 * @return array
	 */
	public function normalize( $data, $limits = array() ) {
		$limits   = ValidationLimits::get( $limits );
		$context  = null;
		$nodes    = array();
		$problems = array();

		if ( ValueInspector::is_list( $data ) ) {
			$nodes = $data;
		} elseif ( array_key_exists( '@graph', $data ) ) {
			$context = $data['@context'] ?? null;
			if ( is_array( $data['@graph'] ) ) {
				$nodes = ValueInspector::is_list( $data['@graph'] ) ? $data['@graph'] : array( $data['@graph'] );
			} else {
				$problems[] = array(
					'code' => 'graph.shape.invalid',
					'path' => '@graph',
				);
			}
		} else {
			$context = $data['@context'] ?? null;
			$nodes[] = $data;
		}

		$entities   = array();
		$references = array();
		foreach ( $nodes as $index => $node ) {
			$path = '@graph[' . $index . ']';
			if ( ! is_array( $node ) || ValueInspector::is_list( $node ) ) {
				$problems[] = array(
					'code' => 'graph.node.invalid',
					'path' => $path,
				);
				continue;
			}

			$this->collect( $node, $path, 0, true, $limits, $entities, $references, $problems );
		}

		return array(
			'graph'    => array(
				'@context'       => $context,
				'@graph'         => $nodes,
				'references'     => $references,
				'shape_problems' => $problems,
			),
			'entities' => $entities,
		);
	}

	/**
	 * Collect explicitly described entities and @id-only references.
	 *
	 * @param array  $value      Current object.
	 * @param string $path      Current path.
	 * @param int    $depth     Current depth.
	 * @param bool   $top_level Whether this is a top-level graph node.
	 * @param array  $limits    Effective validation limits.
	 * @param array  $entities  Normalized entity records.
	 * @param array  $references Normalized reference records.
	 * @param array  $problems  Shape problems.
	 * @return void
	 */
	private function collect( $value, $path, $depth, $top_level, $limits, &$entities, &$references, &$problems ) {
		if ( $depth > $limits['max_depth'] ) {
			$problems[] = array(
				'code' => 'graph.depth.exceeded',
				'path' => $path,
			);
			return;
		}

		if ( count( $value ) > $limits['max_properties'] ) {
			$problems[] = array(
				'code' => 'graph.properties.exceeded',
				'path' => $path,
			);
		}

		if ( ValueInspector::is_reference( $value ) ) {
			$references[] = array(
				'id'   => ValueInspector::get_id( $value ),
				'path' => $path,
			);
			return;
		}

		if ( $top_level || ValueInspector::is_entity( $value ) ) {
			if ( count( $entities ) >= $limits['max_entities'] ) {
				$problems[] = array(
					'code' => 'graph.entities.exceeded',
					'path' => $path,
				);
				return;
			}

			$entities[] = array(
				'data'         => $value,
				'path'         => $path,
				'id'           => ValueInspector::get_id( $value ),
				'types'        => ValueInspector::get_types( $value ),
				'is_top_level' => $top_level,
			);
		}

		foreach ( $value as $property => $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			$property_path = $path . '.' . $property;
			if ( ValueInspector::is_list( $item ) ) {
				foreach ( $item as $index => $list_item ) {
					if ( is_array( $list_item ) && ! ValueInspector::is_list( $list_item ) ) {
						$this->collect( $list_item, $property_path . '[' . $index . ']', $depth + 1, false, $limits, $entities, $references, $problems );
					}
				}
			} else {
				$this->collect( $item, $property_path, $depth + 1, false, $limits, $entities, $references, $problems );
			}
		}
	}
}
