<?php
/**
 * Normalizes and compacts agent-loop tool payloads for model input.
 *
 * @package Niwa_AI
 */

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

class Niwa_AI_Agent_Tool_Payload_Normalizer {

	public static function normalize( $payloads, $call_id ) {
		$payload = is_array( $payloads ) && ! empty( $payloads[0] ) && is_array( $payloads[0] )
			? $payloads[0]
			: array(
				'role'         => 'tool',
				'tool_call_id' => $call_id,
				'content'      => self::encode_json(
					array(
						'ok'    => false,
						'error' => 'Tool call did not return a payload.',
					)
				),
			);

		if ( ! isset( $payload['role'] ) ) {
			$payload['role'] = 'tool';
		}

		if ( empty( $payload['tool_call_id'] ) ) {
			$payload['tool_call_id'] = $call_id;
		}

		return $payload;
	}

	public static function compact_for_model( $payload, $tool_name = '', $compact_threshold = 60000, $string_limit = 8000 ) {
		if ( ! is_array( $payload ) || empty( $payload['content'] ) || ! is_string( $payload['content'] ) ) {
			return $payload;
		}

		$content           = (string) $payload['content'];
		$tool_name         = self::sanitize_tool_name( (string) $tool_name );
		$compact_threshold = max( 1000, intval( $compact_threshold ) );
		$string_limit      = max( 80, intval( $string_limit ) );
		if ( 'code_read' === $tool_name ) {
			$tool_name = 'file_read';
		}
		$always_compact = in_array( $tool_name, array( 'tool_search', 'code_search', 'code_scan_run', 'file_read', 'file_list', 'batch_read', 'main_coding_edit_dry_run', 'memory_recall_all', 'session_history_get', 'scheduled_tasks_list' ), true );

		if ( ! $always_compact && strlen( $content ) <= $compact_threshold ) {
			return $payload;
		}

		$decoded = json_decode( $content, true );
		if ( ! is_array( $decoded ) ) {
			$payload['content'] = substr( $content, 0, $compact_threshold ) . '...[truncated]';
			return $payload;
		}

		if ( 'email_inbox_read' === $tool_name && isset( $decoded['messages'] ) && is_array( $decoded['messages'] ) ) {
			foreach ( array( 900, 650, 420, 260 ) as $body_limit ) {
				$encoded = self::encode_json( self::compact_email_inbox_read_for_model( $decoded, $body_limit, $string_limit ) );
				if ( is_string( $encoded ) && strlen( $encoded ) <= $compact_threshold ) {
					$payload['content'] = $encoded;
					return $payload;
				}
			}
		}

		if ( 'woo_product_lookup' === $tool_name && isset( $decoded['products'] ) && is_array( $decoded['products'] ) ) {
			foreach ( array( 8, 5, 3, 1 ) as $product_limit ) {
				$encoded = self::encode_json(
					self::compact_woo_product_lookup_for_model(
						$decoded,
						$product_limit,
						min( 1200, $string_limit )
					)
				);
				if ( is_string( $encoded ) && strlen( $encoded ) <= $compact_threshold ) {
					$payload['content'] = $encoded;
					return $payload;
				}
			}
		}

		$compact = self::compact_tool_output_value( $decoded, $tool_name, 0, $string_limit );
		$encoded = self::encode_json( $compact );

		if ( ! is_string( $encoded ) || strlen( $encoded ) > $compact_threshold ) {
			$encoded = self::encode_json( self::summarize_tool_output_for_model( $decoded, $tool_name, $string_limit ) );
		}

		if ( is_string( $encoded ) && '' !== $encoded ) {
			$payload['content'] = $encoded;
		}

		return $payload;
	}

	private static function compact_tool_output_value( $value, $tool_name, $depth, $string_limit ) {
		if ( is_string( $value ) ) {
			return self::truncate_model_string( $value, $string_limit );
		}

		if ( ! is_array( $value ) ) {
			return $value;
		}

		if ( $depth >= 4 ) {
			return array(
				'truncated' => true,
				'type'      => self::is_assoc_array_for_model( $value ) ? 'object' : 'list',
				'count'     => count( $value ),
			);
		}

		$assoc = self::is_assoc_array_for_model( $value );
		$limit = $assoc ? 20 : ( in_array( $tool_name, array( 'tool_search', 'code_search', 'file_list' ), true ) ? 6 : 10 );
		$out   = array();
		$index = 0;

		foreach ( $value as $key => $item ) {
			if ( $index >= $limit ) {
				$out[ $assoc ? '__truncated__' : $index ] = $assoc ? 'additional fields omitted' : 'additional items omitted';
				break;
			}

			$out[ $key ] = self::compact_tool_output_value( $item, $tool_name, $depth + 1, $string_limit );
			$index++;
		}

		return $out;
	}

	private static function compact_email_inbox_read_for_model( $decoded, $body_limit = 650, $string_limit = 8000 ) {
		$decoded    = is_array( $decoded ) ? $decoded : array();
		$body_limit = max( 120, intval( $body_limit ) );
		$messages   = isset( $decoded['messages'] ) && is_array( $decoded['messages'] ) ? array_values( $decoded['messages'] ) : array();
		$out        = array(
			'ok'             => ! empty( $decoded['ok'] ),
			'success'        => ! empty( $decoded['success'] ),
			'count'          => isset( $decoded['count'] ) ? intval( $decoded['count'] ) : count( $messages ),
			'messages_count' => count( $messages ),
			'messages'       => array(),
		);

		foreach ( array( 'error', 'message', 'mailbox', 'status' ) as $key ) {
			if ( isset( $decoded[ $key ] ) && ! is_array( $decoded[ $key ] ) ) {
				$out[ $key ] = self::truncate_model_string( (string) $decoded[ $key ], 700 );
			}
		}

		foreach ( array_slice( $messages, 0, 12 ) as $message ) {
			$message = is_array( $message ) ? $message : array();
			$out['messages'][] = array(
				'uid'       => isset( $message['uid'] ) ? intval( $message['uid'] ) : 0,
				'subject'   => isset( $message['subject'] ) ? self::truncate_model_string( (string) $message['subject'], 220 ) : '',
				'from'      => isset( $message['from'] ) ? self::truncate_model_string( (string) $message['from'], 220 ) : '',
				'date'      => isset( $message['date'] ) ? self::truncate_model_string( (string) $message['date'], 120 ) : '',
				'seen'      => ! empty( $message['seen'] ),
				'excerpt'   => isset( $message['excerpt'] ) ? self::truncate_model_string( (string) $message['excerpt'], 260 ) : '',
				'body_text' => isset( $message['body_text'] ) ? self::truncate_model_string( (string) $message['body_text'], $body_limit ) : '',
			);
		}

		if ( count( $messages ) > 12 ) {
			$out['messages_truncated'] = true;
			$out['messages_omitted']   = count( $messages ) - 12;
		}

		return $out;
	}

	/**
	 * Keep catalog paging and product identity coherent when a Woo page is too
	 * large for one model result. The next offset advances only past products
	 * actually exposed to the model, so compacting a 50-product page cannot
	 * silently skip the omitted products.
	 */
	private static function compact_woo_product_lookup_for_model( $decoded, $product_limit = 8, $string_limit = 1200 ) {
		$decoded       = is_array( $decoded ) ? $decoded : array();
		$products      = isset( $decoded['products'] ) && is_array( $decoded['products'] ) ? array_values( $decoded['products'] ) : array();
		$product_limit = max( 1, intval( $product_limit ) );
		$string_limit  = max( 160, intval( $string_limit ) );
		$visible       = array_slice( $products, 0, $product_limit );
		$offset        = isset( $decoded['offset'] ) ? max( 0, intval( $decoded['offset'] ) ) : 0;
		$page_limit    = isset( $decoded['limit'] ) ? max( 1, intval( $decoded['limit'] ) ) : max( 1, count( $products ) );
		$returned      = isset( $decoded['returned'] ) ? max( 0, intval( $decoded['returned'] ) ) : count( $products );
		$omitted       = max( 0, count( $products ) - count( $visible ) );
		$maybe_more    = ! empty( $decoded['maybe_more'] );
		$next_offset   = $offset + ( $omitted > 0 ? count( $visible ) : $returned );

		$out = array(
			'ok'                        => ! empty( $decoded['ok'] ),
			'success'                   => array_key_exists( 'success', $decoded ) ? ! empty( $decoded['success'] ) : ! empty( $decoded['ok'] ),
			'available'                 => ! empty( $decoded['available'] ),
			'scope'                     => isset( $decoded['scope'] ) ? (string) $decoded['scope'] : 'match',
			'query'                     => isset( $decoded['query'] ) ? self::truncate_model_string( (string) $decoded['query'], 500 ) : '',
			'limit'                     => $page_limit,
			'page'                      => isset( $decoded['page'] ) ? max( 1, intval( $decoded['page'] ) ) : 1,
			'offset'                    => $offset,
			'returned'                  => $returned,
			'maybe_more'                => $maybe_more,
			'products_count'            => count( $products ),
			'products_returned_to_model' => count( $visible ),
			'products_omitted'          => $omitted,
			'continuation_required'     => $omitted > 0 || $maybe_more,
			'next_offset'               => $next_offset,
			'next_limit'                => min( $page_limit, max( 1, count( $visible ) ) ),
			'products'                  => array(),
		);

		if ( isset( $decoded['filters'] ) && is_array( $decoded['filters'] ) ) {
			$out['filters'] = self::compact_tool_output_value( $decoded['filters'], 'woo_product_lookup', 1, 300 );
		}
		foreach ( array( 'error', 'message' ) as $key ) {
			if ( isset( $decoded[ $key ] ) && is_scalar( $decoded[ $key ] ) ) {
				$out[ $key ] = self::truncate_model_string( (string) $decoded[ $key ], 700 );
			}
		}

		foreach ( $visible as $product ) {
			if ( is_array( $product ) ) {
				$out['products'][] = self::compact_woo_product_for_model( $product, $string_limit );
			}
		}

		return $out;
	}

	private static function compact_woo_product_for_model( $product, $string_limit ) {
		$out = array();
		foreach ( array(
			'post_id',
			'post_title',
			'post_status',
			'post_name',
			'product_type',
			'sku',
			'regular_price',
			'regular_price_display',
			'sale_price',
			'sale_price_display',
			'date_on_sale_from',
			'date_on_sale_to',
			'price',
			'price_display',
			'is_on_sale',
			'manage_stock',
			'stock_quantity',
			'stock_status',
			'weight',
			'dimensions',
			'catalog_visibility',
			'featured',
			'virtual',
			'downloadable',
			'tax_status',
			'sold_individually',
			'backorders',
			'score',
			'exact_match',
			'match_confidence',
			'match_warning',
			'source',
			'featured_image_id',
		) as $key ) {
			if ( ! array_key_exists( $key, $product ) || is_array( $product[ $key ] ) || is_object( $product[ $key ] ) ) {
				continue;
			}
			$out[ $key ] = is_string( $product[ $key ] )
				? self::truncate_model_string( $product[ $key ], min( 500, $string_limit ) )
				: $product[ $key ];
		}

		foreach ( array(
			'description'        => $string_limit,
			'short_description'  => min( 700, $string_limit ),
			'featured_image_url' => min( 1200, $string_limit ),
		) as $key => $limit ) {
			if ( isset( $product[ $key ] ) && is_scalar( $product[ $key ] ) ) {
				$out[ $key ] = self::truncate_model_string( (string) $product[ $key ], $limit );
			}
		}

		foreach ( array( 'categories', 'category_ids', 'category_slugs', 'tags' ) as $key ) {
			if ( isset( $product[ $key ] ) && is_array( $product[ $key ] ) ) {
				$out[ $key ] = array_slice( array_values( $product[ $key ] ), 0, 20 );
			}
		}
		foreach ( array( 'gallery_image_ids', 'gallery_image_urls' ) as $key ) {
			if ( ! isset( $product[ $key ] ) || ! is_array( $product[ $key ] ) ) {
				continue;
			}
			$out[ $key ] = array();
			foreach ( array_slice( array_values( $product[ $key ] ), 0, 5 ) as $value ) {
				$out[ $key ][] = is_string( $value )
					? self::truncate_model_string( $value, min( 1200, $string_limit ) )
					: $value;
			}
		}

		return $out;
	}

	private static function summarize_tool_output_for_model( $decoded, $tool_name, $string_limit ) {
		$summary = array(
			'ok'        => ! empty( $decoded['ok'] ),
			'success'   => ! empty( $decoded['success'] ),
			'tool_name' => self::sanitize_tool_name( (string) $tool_name ),
		);

		foreach ( array( 'summary', 'error', 'message', 'detail', 'path', 'title', 'status', 'product_id', 'product_name', 'edit_url', 'preview_url' ) as $key ) {
			if ( isset( $decoded[ $key ] ) && ! is_array( $decoded[ $key ] ) ) {
				$summary[ $key ] = self::truncate_model_string( (string) $decoded[ $key ], 700 );
			}
		}

		foreach ( array( 'results', 'products', 'matches', 'history', 'actions', 'executed_actions', 'rollback_entries', 'active', 'loaded', 'skipped' ) as $key ) {
			if ( isset( $decoded[ $key ] ) && is_array( $decoded[ $key ] ) ) {
				$summary[ $key . '_count' ]  = count( $decoded[ $key ] );
				$summary[ $key . '_sample' ] = array_slice( self::compact_tool_output_value( $decoded[ $key ], (string) $tool_name, 1, $string_limit ), 0, 3 );
			}
		}

		return $summary;
	}

	private static function truncate_model_string( $value, $limit ) {
		$value = (string) $value;
		$limit = max( 80, intval( $limit ) );
		if ( function_exists( 'mb_strlen' ) && mb_strlen( $value, 'UTF-8' ) <= $limit ) {
			return $value;
		}
		if ( ! function_exists( 'mb_strlen' ) && strlen( $value ) <= $limit ) {
			return $value;
		}

		return function_exists( 'mb_substr' )
			? rtrim( mb_substr( $value, 0, $limit - 3, 'UTF-8' ) ) . '...'
			: rtrim( substr( $value, 0, $limit - 3 ) ) . '...';
	}

	private static function is_assoc_array_for_model( $value ) {
		if ( ! is_array( $value ) || empty( $value ) ) {
			return false;
		}

		return array_keys( $value ) !== range( 0, count( $value ) - 1 );
	}

	private static function encode_json( $value ) {
		if ( function_exists( 'wp_json_encode' ) ) {
			return wp_json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		}

		return json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	}

	private static function sanitize_tool_name( $tool_name ) {
		if ( function_exists( 'sanitize_key' ) ) {
			return sanitize_key( $tool_name );
		}

		return strtolower( preg_replace( '/[^a-z0-9_\-]/', '', (string) $tool_name ) );
	}
}
