<?php
/**
 * TOON Encoder Service
 *
 * Transforms PHP arrays to TOON (Token-Oriented Object Notation) format.
 * Pure PHP implementation with no WordPress dependencies.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.5.0
 */

namespace AIPT\Api\StoreIndex;

/**
 * ToonEncoder class
 *
 * Encodes the AI Store Index array into TOON format for LLM consumption.
 * Produces deterministic output with consistent field ordering.
 */
class ToonEncoder implements ToonEncoderInterface {

	/**
	 * Indentation string (2 spaces).
	 *
	 * @var string
	 */
	private const INDENT = '  ';

	/**
	 * Encode an index array to TOON format.
	 *
	 * @param array $index Index data with 'meta', 'products', 'categories' keys.
	 * @return string TOON-formatted string.
	 * @throws \InvalidArgumentException If index structure is invalid.
	 */
	public function encode( array $index ): string {
		$this->validateIndex( $index );

		$parts   = array();
		$parts[] = $this->encodeMeta( $index['meta'] );
		$parts[] = ''; 
		$parts[] = $this->encodeProducts( $index['products'] );
		$parts[] = ''; 
		$parts[] = $this->encodeCategories( $index['categories'] );

		return implode( "\n", $parts );
	}

	/**
	 * Encode the meta section using nested object layout.
	 *
	 * @param array $meta Meta data array.
	 * @return string TOON-formatted meta section.
	 */
	public function encodeMeta( array $meta ): string {
		$lines   = array();
		$lines[] = 'meta:';

		ksort( $meta );

		foreach ( $meta as $key => $value ) {
			if ( $key === 'settings' && is_array( $value ) ) {

				$lines[] = self::INDENT . 'settings:';
				ksort( $value );
				foreach ( $value as $setting_key => $setting_value ) {
					$lines[] = self::INDENT . self::INDENT . $this->encodeKeyValue( $setting_key, $setting_value );
				}
			} else {
				$lines[] = self::INDENT . $this->encodeKeyValue( $key, $value );
			}
		}

		return implode( "\n", $lines );
	}

	/**
	 * Encode the products section using tabular layout.
	 *
	 * Format: products[N]{field1,field2,...}:
	 * Schema is dynamically built from actual product fields.
	 *
	 * @param array $products Products keyed by ID.
	 * @return string TOON-formatted products section.
	 */
	public function encodeProducts( array $products ): string {
		$count = count( $products );

		$all_fields = $this->collectProductFields( $products );

		$header = 'products[' . $count . ']{' . implode( ',', $all_fields ) . '}:';

		if ( $count === 0 ) {
			return $header;
		}

		$lines   = array();
		$lines[] = $header;

		ksort( $products, SORT_NUMERIC );

		foreach ( $products as $product ) {
			$row_values = array();
			foreach ( $all_fields as $field ) {
				$value        = $product[ $field ] ?? null;
				$row_values[] = $this->encodeValue( $value, true );
			}
			$lines[] = self::INDENT . implode( ',', $row_values );
		}

		return implode( "\n", $lines );
	}

	/**
	 * Encode the categories section using keyed object layout.
	 *
	 * @param array $categories Categories keyed by ID.
	 * @return string TOON-formatted categories section.
	 */
	public function encodeCategories( array $categories ): string {
		$lines   = array();
		$lines[] = 'categories:';

		if ( empty( $categories ) ) {
			return $lines[0];
		}

		ksort( $categories, SORT_NUMERIC );

		foreach ( $categories as $cat_id => $category ) {
			$lines[] = self::INDENT . $cat_id . ':';

			ksort( $category );

			foreach ( $category as $key => $value ) {
				$lines[] = self::INDENT . self::INDENT . $this->encodeKeyValue( $key, $value );
			}
		}

		return implode( "\n", $lines );
	}

	/**
	 * Encode a key-value pair.
	 *
	 * @param string $key   The key.
	 * @param mixed  $value The value.
	 * @return string Encoded key: value string.
	 */
	private function encodeKeyValue( string $key, $value ): string {

		if ( is_array( $value ) ) {
			$count = count( $value );
			if ( $count === 0 ) {
				return $key . '[0]:';
			}

			$encoded_values = array_map(
				function ( $v ) {
					return $this->encodeValue( $v, false );
				},
				$value
			);
			return $key . '[' . $count . ']: ' . implode( ',', $encoded_values );
		}

		return $key . ': ' . $this->encodeValue( $value, false );
	}

	/**
	 * Encode a single value for TOON output.
	 *
	 * @param mixed $value   The value to encode.
	 * @param bool  $in_table Whether this value is inside a tabular row.
	 * @return string Encoded value.
	 */
	private function encodeValue( $value, bool $in_table = false ): string {
		if ( $value === null ) {
			return 'null';
		}

		if ( is_bool( $value ) ) {
			return $value ? 'true' : 'false';
		}

		if ( is_int( $value ) ) {
			return (string) $value;
		}

		if ( is_float( $value ) ) {

			$str = (string) $value;
			if ( strpos( $str, '.' ) === false && strpos( $str, 'e' ) === false && strpos( $str, 'E' ) === false ) {
				$str .= '.0';
			}
			return $str;
		}

		if ( is_array( $value ) ) {

			$json = wp_json_encode( array_values( $value ) );
			return $this->quoteString( $json );
		}

		$str = (string) $value;

		if ( $this->needsQuoting( $str ) ) {
			return $this->quoteString( $str );
		}

		return $str;
	}

	/**
	 * Check if a string needs quoting.
	 *
	 * Checks for: empty string, delimiters (comma, colon), quotes, backslashes,
	 * control characters (newline, carriage return, tab), and leading/trailing whitespace.
	 *
	 * @param string $value The string to check.
	 * @return bool True if quoting is needed.
	 */
	private function needsQuoting( string $value ): bool {

		if ( $value === '' ) {
			return true;
		}

		return preg_match( '/[,:"\\\\\n\r\t]|^\s|\s$/', $value ) === 1;
	}

	/**
	 * Quote a string for TOON output.
	 *
	 * @param string $value The string to quote.
	 * @return string Quoted string with escaped characters.
	 */
	private function quoteString( string $value ): string {

		$escaped = str_replace( '\\', '\\\\', $value );

		$escaped = str_replace(
			array( '"', "\n", "\r", "\t" ),
			array( '\\"', '\\n', '\\r', '\\t' ),
			$escaped
		);

		return '"' . $escaped . '"';
	}

	/**
	 * Collect all product field names dynamically from actual products.
	 *
	 * Schema is built from actual product data - only includes fields that exist.
	 * 'id' is always first, other fields are sorted alphabetically.
	 *
	 * @param array $products Products array.
	 * @return array Ordered field names.
	 */
	private function collectProductFields( array $products ): array {
		$all_fields = array();

		foreach ( $products as $product ) {
			foreach ( array_keys( $product ) as $key ) {
				$all_fields[ $key ] = true;
			}
		}

		$field_names = array_keys( $all_fields );
		$field_names = array_diff( $field_names, array( 'id' ) );

		sort( $field_names );

		return array_merge( array( 'id' ), $field_names );
	}

	/**
	 * Validate index structure.
	 *
	 * @param array $index Index data.
	 * @throws \InvalidArgumentException If structure is invalid.
	 */
	private function validateIndex( array $index ): void {
		if ( ! isset( $index['meta'] ) || ! is_array( $index['meta'] ) ) {
			throw new \InvalidArgumentException( 'Index must contain a "meta" array' );
		}

		if ( ! isset( $index['products'] ) || ! is_array( $index['products'] ) ) {
			throw new \InvalidArgumentException( 'Index must contain a "products" array' );
		}

		if ( ! isset( $index['categories'] ) || ! is_array( $index['categories'] ) ) {
			throw new \InvalidArgumentException( 'Index must contain a "categories" array' );
		}
	}
}
