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

namespace AIPT\Api\StoreIndex;

/**
 * ToonDecoder class
 *
 * Decodes TOON-formatted strings back to PHP arrays for internal consumption.
 * Handles meta, products (tabular), and categories sections.
 */
class ToonDecoder implements ToonDecoderInterface {

	/**
	 * Decode a TOON string to PHP array.
	 *
	 * This method is lenient by design - it attempts to parse as much as possible
	 * and returns empty arrays for missing/invalid sections. Use validate() first
	 * if strict validation is required.
	 *
	 * @param string $toon TOON-formatted string.
	 * @return array Index data with 'meta', 'products', 'categories' keys.
	 */
	public function decode( string $toon ): array {
		$lines = explode( "\n", $toon );
		$lines = array_map( 'rtrim', $lines ); 

		$result = array(
			'meta'       => array(),
			'products'   => array(),
			'categories' => array(),
		);

		$current_section = null;
		$section_lines   = array();

		foreach ( $lines as $line ) {

			if ( trim( $line ) === '' ) {
				continue;
			}

			if ( $line !== '' && $line[0] !== ' ' && $line[0] !== "\t" ) {

				if ( $current_section !== null && ! empty( $section_lines ) ) {
					$result[ $current_section ] = $this->parseSection( $current_section, $section_lines );
				}

				if ( strpos( $line, 'meta:' ) === 0 ) {
					$current_section = 'meta';
					$section_lines   = array();
				} elseif ( strpos( $line, 'products' ) === 0 ) {
					$current_section = 'products';
					$section_lines   = array( $line ); 
				} elseif ( strpos( $line, 'categories:' ) === 0 ) {
					$current_section = 'categories';
					$section_lines   = array();
				}
				continue;
			}

			if ( $current_section !== null ) {
				$section_lines[] = $line;
			}
		}

		if ( $current_section !== null && ! empty( $section_lines ) ) {
			$result[ $current_section ] = $this->parseSection( $current_section, $section_lines );
		}

		return $result;
	}

	/**
	 * Validate TOON structure without full decode.
	 *
	 * Performs lightweight regex-based validation to check for required
	 * section headers. This is much faster than a full decode.
	 *
	 * @param string $toon TOON-formatted string.
	 * @return bool True if valid structure.
	 */
	public function validate( string $toon ): bool {

		$has_meta       = preg_match( '/^meta:/m', $toon );
		$has_products   = preg_match( '/^products\[\d+\]\{[^}]*\}:/m', $toon );
		$has_categories = preg_match( '/^categories:/m', $toon );

		if ( ! $has_meta || ! $has_products || ! $has_categories ) {
			return false;
		}

		$has_meta_content = preg_match( '/^meta:\s*\n\s+[\w-]+:/m', $toon );

		return (bool) $has_meta_content;
	}

	/**
	 * Parse a section based on its type.
	 *
	 * @param string   $section Section name.
	 * @param string[] $lines   Section lines.
	 * @return array Parsed section data.
	 */
	private function parseSection( string $section, array $lines ): array {
		switch ( $section ) {
			case 'meta':
				return $this->parseNestedObject( $lines, 1 );
			case 'products':
				return $this->parseTabularProducts( $lines );
			case 'categories':
				return $this->parseKeyedCategories( $lines );
			default:
				return array();
		}
	}

	/**
	 * Parse nested object from indented lines.
	 *
	 * @param string[] $lines        Lines to parse.
	 * @param int      $indent_level Expected indentation level.
	 * @return array Parsed object.
	 */
	private function parseNestedObject( array $lines, int $indent_level ): array {
		$result         = array();
		$current_key    = null;
		$nested_lines   = array();
		$indent_str     = str_repeat( '  ', $indent_level );
		$next_indent    = str_repeat( '  ', $indent_level + 1 );

		foreach ( $lines as $line ) {

			if ( strpos( $line, $indent_str ) === 0 && ( ! isset( $line[ strlen( $indent_str ) ] ) || $line[ strlen( $indent_str ) ] !== ' ' ) ) {

				if ( $current_key !== null && ! empty( $nested_lines ) ) {
					$result[ $current_key ] = $this->parseNestedObject( $nested_lines, $indent_level + 1 );
					$nested_lines           = array();
				}

				$content = substr( $line, strlen( $indent_str ) );

				if ( preg_match( '/^([\w-]+):$/', $content, $matches ) ) {
					$current_key = $matches[1];
					continue;
				}

				$parsed = $this->parseKeyValue( $content );
				if ( $parsed !== null ) {
					$result[ $parsed['key'] ] = $parsed['value'];
					$current_key              = null;
				}
			} elseif ( $current_key !== null && strpos( $line, $next_indent ) === 0 ) {

				$nested_lines[] = $line;
			}
		}

		if ( $current_key !== null && ! empty( $nested_lines ) ) {
			$result[ $current_key ] = $this->parseNestedObject( $nested_lines, $indent_level + 1 );
		}

		return $result;
	}

	/**
	 * Parse tabular products section.
	 *
	 * @param string[] $lines Section lines including header.
	 * @return array Products keyed by ID.
	 */
	private function parseTabularProducts( array $lines ): array {
		if ( empty( $lines ) ) {
			return array();
		}

		$header = $lines[0];
		if ( ! preg_match( '/^products\[(\d+)\]\{([^}]+)\}:/', $header, $matches ) ) {

			if ( preg_match( '/^products\[0\]/', $header ) ) {
				return array();
			}

			throw new \RuntimeException( 'Invalid products header: ' . $header );
		}

		$count  = (int) $matches[1];
		$fields = explode( ',', $matches[2] );

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

		$products = array();

		for ( $i = 1; $i < count( $lines ); $i++ ) {
			$row_line = ltrim( $lines[ $i ] );
			if ( $row_line === '' ) {
				continue;
			}

			$values  = $this->parseTabularRow( $row_line, count( $fields ) );
			$product = array();

			foreach ( $fields as $idx => $field ) {
				$product[ $field ] = $values[ $idx ] ?? null;
			}

			$product_id              = (string) $product['id'];
			$products[ $product_id ] = $product;
		}

		return $products;
	}

	/**
	 * Parse a tabular row into values.
	 *
	 * @param string $row        Row string.
	 * @param int    $num_fields Expected number of fields (unused, kept for interface).
	 * @return array Parsed values.
	 */
	private function parseTabularRow( string $row, int $num_fields ): array {
		return $this->parseCommaSeparated( $row );
	}

	/**
	 * Parse comma-separated values with quote and escape handling.
	 *
	 * This is the core parsing logic used by both parseTabularRow and parseSimpleArray.
	 *
	 * @param string $str Input string.
	 * @return array Parsed scalar values.
	 */
	private function parseCommaSeparated( string $str ): array {
		$values   = array();
		$current  = '';
		$in_quote = false;
		$escape   = false;
		$len      = strlen( $str );

		for ( $i = 0; $i < $len; $i++ ) {
			$char = $str[ $i ];

			if ( $escape ) {

				switch ( $char ) {
					case 'n':
						$current .= "\n";
						break;
					case 'r':
						$current .= "\r";
						break;
					case 't':
						$current .= "\t";
						break;
					case '"':
						$current .= '"';
						break;
					case '\\':
						$current .= '\\';
						break;
					default:
						$current .= $char;
				}
				$escape = false;
				continue;
			}

			if ( $char === '\\' && $in_quote ) {
				$escape = true;
				continue;
			}

			if ( $char === '"' ) {
				$in_quote = ! $in_quote;
				continue;
			}

			if ( $char === ',' && ! $in_quote ) {
				$values[] = $this->parseScalarValue( $current );
				$current  = '';
				continue;
			}

			$current .= $char;
		}

		if ( $current !== '' || ! empty( $values ) ) {
			$values[] = $this->parseScalarValue( $current );
		}

		return $values;
	}

	/**
	 * Parse keyed categories section.
	 *
	 * @param string[] $lines Section lines.
	 * @return array Categories keyed by ID.
	 */
	private function parseKeyedCategories( array $lines ): array {
		$categories     = array();
		$current_cat_id = null;
		$cat_lines      = array();

		foreach ( $lines as $line ) {

			if ( preg_match( '/^  (\d+):$/', $line, $matches ) ) {

				if ( $current_cat_id !== null && ! empty( $cat_lines ) ) {
					$categories[ $current_cat_id ] = $this->parseCategoryObject( $cat_lines );
				}

				$current_cat_id = $matches[1];
				$cat_lines      = array();
				continue;
			}

			if ( $current_cat_id !== null ) {
				$cat_lines[] = $line;
			}
		}

		if ( $current_cat_id !== null && ! empty( $cat_lines ) ) {
			$categories[ $current_cat_id ] = $this->parseCategoryObject( $cat_lines );
		}

		return $categories;
	}

	/**
	 * Parse a category object from its lines.
	 *
	 * @param string[] $lines Category lines.
	 * @return array Category data.
	 */
	private function parseCategoryObject( array $lines ): array {
		$result = array();

		foreach ( $lines as $line ) {

			$content = substr( $line, 4 );
			$parsed  = $this->parseKeyValue( $content );
			if ( $parsed !== null ) {
				$result[ $parsed['key'] ] = $parsed['value'];
			}
		}

		return $result;
	}

	/**
	 * Parse a key-value line.
	 *
	 * Handles:
	 * - Simple: key: value
	 * - Array: key[N]: v1,v2,v3
	 * - Empty array: key[0]:
	 *
	 * Keys can contain alphanumeric characters, underscores, and dashes.
	 * This supports WooCommerce attribute keys like "pa_my-attr" and custom fields.
	 *
	 * @param string $content Line content.
	 * @return array|null Parsed key/value or null.
	 */
	private function parseKeyValue( string $content ): ?array {

		if ( preg_match( '/^([\w-]+)\[(\d+)\]:\s*(.*)$/', $content, $matches ) ) {
			$key   = $matches[1];
			$count = (int) $matches[2];
			$rest  = $matches[3];

			if ( $count === 0 || $rest === '' ) {
				return array(
					'key'   => $key,
					'value' => array(),
				);
			}

			$values = $this->parseSimpleArray( $rest );
			return array(
				'key'   => $key,
				'value' => $values,
			);
		}

		if ( preg_match( '/^([\w-]+):\s*(.*)$/', $content, $matches ) ) {
			$key   = $matches[1];
			$value = $this->parseScalarValue( $matches[2] );
			return array(
				'key'   => $key,
				'value' => $value,
			);
		}

		return null;
	}

	/**
	 * Parse a simple comma-separated array.
	 *
	 * @param string $str Array string.
	 * @return array Parsed values.
	 */
	private function parseSimpleArray( string $str ): array {
		return $this->parseCommaSeparated( $str );
	}

	/**
	 * Parse a scalar value from string.
	 *
	 * @param string $str Value string.
	 * @return mixed Parsed value (int, float, bool, null, string, or array).
	 */
	private function parseScalarValue( string $str ) {
		$str = trim( $str );

		if ( $str === 'null' ) {
			return null;
		}

		if ( $str === 'true' ) {
			return true;
		}
		if ( $str === 'false' ) {
			return false;
		}

		if ( preg_match( '/^-?\d+$/', $str ) ) {
			return (int) $str;
		}

		if ( preg_match( '/^-?\d+\.\d+$/', $str ) || preg_match( '/^-?\d+[eE][+-]?\d+$/', $str ) ) {
			return (float) $str;
		}

		if ( strpos( $str, '[' ) === 0 && strrpos( $str, ']' ) === strlen( $str ) - 1 ) {
			$decoded = json_decode( $str, true );
			if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
				return array_map( 'intval', $decoded );
			}
		}

		if ( strlen( $str ) >= 2 && $str[0] === '"' && $str[ strlen( $str ) - 1 ] === '"' ) {
			$unquoted = substr( $str, 1, -1 );
			

			$unquoted = preg_replace_callback(
				'/\\\\(n|r|t|"|\\\\)/',
				function ( $matches ) {
					switch ( $matches[1] ) {
						case 'n':
							return "\n";
						case 'r':
							return "\r";
						case 't':
							return "\t";
						case '"':
							return '"';
						case '\\':
							return '\\';
						default:
							return $matches[0];
					}
				},
				$unquoted
			);
			

			if ( strpos( $unquoted, '[' ) === 0 && strrpos( $unquoted, ']' ) === strlen( $unquoted ) - 1 ) {
				$decoded = json_decode( $unquoted, true );
				if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {

					$is_numeric = array_reduce(
						$decoded,
						function ( $carry, $item ) {
							return $carry && is_numeric( $item );
						},
						true
					);
					return $is_numeric ? array_map( 'intval', $decoded ) : $decoded;
				}
			}
			
			return $unquoted;
		}

		return $str;
	}
}
