<?php
/**
 * Pure helpers for founder-task verification and trace-safe recovery.
 *
 * @package Niwa_AI
 */

defined( 'ABSPATH' ) || exit;

class Niwa_AI_Admin_Autonomy_Task_Integrity {

	/**
	 * Reuse a valid continuation frame or build the normal-turn frame once.
	 *
	 * @param array  $candidate           Existing frame, when continuing.
	 * @param array  $messages            Initial runtime messages.
	 * @param string $turn_objective      Exact current runtime input.
	 * @param string $current_user_prompt Raw founder message.
	 * @return array
	 */
	public static function resolve_turn_frame( $candidate, $messages, $turn_objective, $current_user_prompt ) {
		$candidate = is_array( $candidate ) ? $candidate : array();
		if ( array_key_exists( 'turn_objective', $candidate ) ) {
			$candidate['turn_objective'] = self::canonicalize_content( $candidate['turn_objective'] );
		}
		if ( array_key_exists( 'current_user_prompt', $candidate ) ) {
			$candidate['current_user_prompt'] = self::canonicalize_content( $candidate['current_user_prompt'] );
		}
		if ( '' !== trim( (string) ( $candidate['turn_objective'] ?? '' ) ) ) {
			return self::normalize_turn_frame( $candidate );
		}
		return self::build_turn_frame( $messages, $turn_objective, $current_user_prompt );
	}

	/**
	 * Convert text or multimodal message content into one stable frame value.
	 *
	 * @param mixed $content Message content.
	 * @return string
	 */
	public static function canonicalize_content( $content ) {
		if ( is_string( $content ) || is_numeric( $content ) ) {
			return trim( (string) $content );
		}
		if ( null === $content ) {
			return '';
		}
		$encoded = wp_json_encode( $content, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		return is_string( $encoded ) ? trim( $encoded ) : '';
	}

	/**
	 * Capture immutable turn identity before runtime control messages and tool
	 * traffic are appended.
	 *
	 * @param array  $messages            Initial runtime messages.
	 * @param string $turn_objective      Exact current runtime input.
	 * @param string $current_user_prompt Raw founder message.
	 * @return array
	 */
	public static function build_turn_frame( $messages, $turn_objective, $current_user_prompt ) {
		$messages            = is_array( $messages ) ? array_values( $messages ) : array();
		$turn_objective      = self::canonicalize_content( $turn_objective );
		$current_user_prompt = self::canonicalize_content( $current_user_prompt );
		$current_index       = -1;

		for ( $index = count( $messages ) - 1; $index >= 0; $index-- ) {
			$message = is_array( $messages[ $index ] ) ? $messages[ $index ] : array();
			$content = self::message_text( $message );
			if ( 'user' !== (string) ( $message['role'] ?? '' ) || '' === $content ) {
				continue;
			}
			if ( '' === $turn_objective || hash_equals( $turn_objective, $content ) ) {
				$current_index = $index;
				if ( '' === $turn_objective ) {
					$turn_objective = $content;
				}
				break;
			}
		}

		$previous_dialogue = array();
		if ( $current_index > 0 ) {
			$assistant_index = -1;
			for ( $index = $current_index - 1; $index >= 0; $index-- ) {
				$message = is_array( $messages[ $index ] ) ? $messages[ $index ] : array();
				if ( self::is_dialogue_message( $message, 'assistant' ) ) {
					$assistant_index = $index;
					break;
				}
			}

			if ( $assistant_index > 0 ) {
				for ( $index = $assistant_index - 1; $index >= 0; $index-- ) {
					$message = is_array( $messages[ $index ] ) ? $messages[ $index ] : array();
					if ( ! self::is_dialogue_message( $message, 'user' ) ) {
						continue;
					}
					$previous_dialogue[] = array(
						'role'    => 'user',
						'content' => self::message_text( $message ),
					);
					$previous_dialogue[] = array(
						'role'    => 'assistant',
						'content' => self::message_text( $messages[ $assistant_index ] ),
					);
					break;
				}
			}
		}

		return self::normalize_turn_frame( array(
			'current_user_prompt' => $current_user_prompt,
			'turn_objective'      => $turn_objective,
			'previous_dialogue'   => $previous_dialogue,
		) );
	}

	/**
	 * Add stable same-turn identity and the native compaction contract.
	 *
	 * @param array $frame Turn frame.
	 * @return array
	 */
	private static function normalize_turn_frame( $frame ) {
		$frame                        = is_array( $frame ) ? $frame : array();
		$frame['current_user_prompt'] = self::canonicalize_content( $frame['current_user_prompt'] ?? '' );
		$frame['turn_objective']      = self::canonicalize_content( $frame['turn_objective'] ?? '' );
		$frame['previous_dialogue']   = isset( $frame['previous_dialogue'] ) && is_array( $frame['previous_dialogue'] )
			? array_values( $frame['previous_dialogue'] )
			: array();

		if ( empty( $frame['frame_id'] ) ) {
			$seed = wp_json_encode(
				array(
					$frame['current_user_prompt'],
					$frame['turn_objective'],
					$frame['previous_dialogue'],
				),
				JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
			);
			$frame['frame_id'] = 'turn_' . substr( hash( 'sha256', (string) $seed ), 0, 24 );
		}

		$frame['compaction_contract'] = array(
			'summary_mode'              => 'split_before_after_current_turn',
			'continuation_scope'        => 'same_turn',
			'preserve_current_prompt'   => true,
			'preserve_previous_dialogue'=> true,
			'preserve_latest_tool_group'=> true,
		);

		return $frame;
	}

	/**
	 * Build compact chronological evidence for the adequacy judge.
	 *
	 * @param array  $context Turn context.
	 * @param string $prompt  Current prompt.
	 * @return array
	 */
	public static function build_evaluation_context( $context, $prompt ) {
		$context = is_array( $context ) ? $context : array();
		$history = isset( $context['history'] ) && is_array( $context['history'] ) ? $context['history'] : array();
		$recent  = array();

		foreach ( array_slice( array_values( $history ), -12 ) as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$role = sanitize_key( (string) ( $item['role'] ?? '' ) );
			$text = trim( (string) ( $item['text'] ?? '' ) );
			if ( '' === $text || ! in_array( $role, array( 'user', 'assistant', 'bot' ), true ) ) {
				continue;
			}
			$recent[] = array(
				'role' => in_array( $role, array( 'assistant', 'bot' ), true ) ? 'assistant' : 'user',
				'text' => self::truncate( $text, 1200 ),
			);
		}

		$task_state = array();
		foreach ( array(
			'session_current_topic',
			'session_immediate_goals',
			'session_recent_progress',
			'session_next_steps',
			'session_restart_recap',
			'agentic_growth_retry_context',
		) as $key ) {
			$value = trim( (string) ( $context[ $key ] ?? '' ) );
			if ( '' !== $value ) {
				$task_state[ $key ] = self::truncate( $value, 1200 );
			}
		}

		return array(
			'recent_conversation' => $recent,
			'current_message'     => self::truncate( (string) $prompt, 2400 ),
			'session_memory_advisory' => $task_state,
		);
	}

	/**
	 * Continue from the exact model/tool trace instead of restarting a task.
	 *
	 * @param array  $context          Original turn context.
	 * @param array  $result           Completed candidate result.
	 * @param string $recovery_message Integrity recovery instruction.
	 * @return array
	 */
	public static function build_recovery_context( $context, $result, $recovery_message ) {
		$context  = is_array( $context ) ? $context : array();
		$result   = is_array( $result ) ? $result : array();
		$snapshot = isset( $result['context_snapshot'] ) && is_array( $result['context_snapshot'] )
			? $result['context_snapshot']
			: array();

		if ( ! empty( $snapshot['messages'] ) && is_array( $snapshot['messages'] ) ) {
			$snapshot['messages'][] = array(
				'role'                 => 'system',
				'content'              => (string) $recovery_message,
				'ephemeral'            => true,
				'niwa_runtime_control' => true,
			);
			$snapshot['continuation_reason'] = 'task_integrity_recovery';
			$context['continuation_context'] = $snapshot;
		}

		return $context;
	}

	/**
	 * Append one bounded, structured tool outcome for final-result checking.
	 *
	 * Raw payloads can contain large documents or secrets, so the integrity
	 * gate receives only stable target IDs, requested field names, success,
	 * compact summaries, and executed action metadata.
	 *
	 * @param array $ledger          Existing ledger.
	 * @param array $call            Tool call.
	 * @param array $decoded_payload Decoded tool result.
	 * @param bool  $succeeded       Whether the tool succeeded.
	 * @param bool  $is_write        Whether the tool mutates state.
	 * @return array
	 */
	public static function append_tool_evidence( $ledger, $call, $decoded_payload, $succeeded, $is_write ) {
		$ledger          = is_array( $ledger ) ? array_values( $ledger ) : array();
		$call            = is_array( $call ) ? $call : array();
		$decoded_payload = is_array( $decoded_payload ) ? $decoded_payload : array();
		$arguments       = self::decode_call_arguments( $call );
		$tool_name       = sanitize_key( (string) ( $call['name'] ?? ( $call['function']['name'] ?? '' ) ) );
		$target          = array();

		foreach ( array( 'product_id', 'variation_id', 'parent_id', 'post_id', 'category_id', 'term_id', 'sku', 'product_query', 'query', 'slug', 'name' ) as $key ) {
			if ( ! array_key_exists( $key, $arguments ) || is_array( $arguments[ $key ] ) || is_object( $arguments[ $key ] ) ) {
				continue;
			}
			$value = self::truncate( (string) $arguments[ $key ], 180 );
			if ( '' !== trim( $value ) ) {
				$target[ $key ] = $value;
			}
		}

		$control_keys = array(
			'product_id',
			'variation_id',
			'parent_id',
			'post_id',
			'category_id',
			'term_id',
			'sku',
			'product_query',
			'query',
			'slug',
			'name',
			'limit',
			'offset',
			'page',
			'exact_only',
			'include_trash',
			'dry_run',
		);
		$requested_fields = array_values( array_diff( array_map( 'strval', array_keys( $arguments ) ), $control_keys ) );
		$outcome          = array();
		foreach ( array( 'summary', 'message', 'error', 'status' ) as $key ) {
			if ( isset( $decoded_payload[ $key ] ) && is_scalar( $decoded_payload[ $key ] ) ) {
				$value = (string) $decoded_payload[ $key ];
				if ( 'image_analyze' === $tool_name && ! empty( $arguments['image_urls'] ) && is_array( $arguments['image_urls'] ) ) {
					foreach ( $arguments['image_urls'] as $image_url ) {
						if ( is_scalar( $image_url ) && '' !== (string) $image_url ) {
							$value = str_replace( (string) $image_url, '[image:' . substr( hash( 'sha256', (string) $image_url ), 0, 16 ) . ']', $value );
						}
					}
				}
				$outcome[ $key ] = self::truncate( $value, 420 );
			}
		}
		if ( 'image_analyze' === $tool_name && isset( $decoded_payload['analysis'] ) && is_scalar( $decoded_payload['analysis'] ) ) {
			$analysis = (string) $decoded_payload['analysis'];
			foreach ( (array) ( $arguments['image_urls'] ?? array() ) as $image_url ) {
				if ( is_scalar( $image_url ) && '' !== (string) $image_url ) {
					$analysis = str_replace(
						(string) $image_url,
						'[image:' . substr( hash( 'sha256', (string) $image_url ), 0, 16 ) . ']',
						$analysis
					);
				}
			}
			$outcome['analysis'] = self::truncate( $analysis, 1600 );
		}

		$actions = array();
		if ( ! empty( $decoded_payload['executed_actions'] ) && is_array( $decoded_payload['executed_actions'] ) ) {
			foreach ( array_slice( $decoded_payload['executed_actions'], 0, 8 ) as $action ) {
				if ( ! is_array( $action ) ) {
					continue;
				}
				$actions[] = array_filter(
					array(
						'type'         => sanitize_key( (string) ( $action['type'] ?? '' ) ),
						'product_id'   => intval( $action['product_id'] ?? 0 ),
						'variation_id' => intval( $action['variation_id'] ?? 0 ),
						'post_id'      => intval( $action['post_id'] ?? 0 ),
						'changes'      => isset( $action['changes'] ) && is_array( $action['changes'] )
							? array_slice( array_values( array_map( 'strval', $action['changes'] ) ), 0, 20 )
							: array(),
					)
				);
			}
		}

		$entry = array(
			'call_id'          => self::truncate( (string) ( $call['id'] ?? '' ), 120 ),
			'tool'             => $tool_name,
			'succeeded'        => (bool) $succeeded,
			'write'            => (bool) $is_write,
			'target'           => $target,
			'requested_fields' => array_slice( $requested_fields, 0, 30 ),
			'result_keys'      => array_slice( array_values( array_map( 'strval', array_keys( $decoded_payload ) ) ), 0, 30 ),
			'scope'            => array(
				'arguments' => self::build_argument_scope( $tool_name, $arguments ),
				'result'    => self::build_result_scope( $tool_name, $decoded_payload ),
			),
			'outcome'          => $outcome,
			'executed_actions' => $actions,
		);
		if ( $is_write ) {
			$entry['requested_changes'] = self::build_write_change_evidence( $arguments );
		}

		$ledger[] = $entry;
		return array_slice( $ledger, -40 );
	}

	private static function build_write_change_evidence( $arguments ) {
		$arguments = is_array( $arguments ) ? $arguments : array();
		$allowed = array_fill_keys(
			array(
				'title',
				'name',
				'description',
				'short_description',
				'sku',
				'status',
				'regular_price',
				'sale_price',
				'stock_quantity',
				'stock_status',
				'weight',
				'length',
				'width',
				'height',
				'categories',
				'category_ids',
				'category_names',
				'category_action',
				'tags',
				'tag_ids',
				'tag_names',
				'tag_action',
				'attributes',
				'variation_attributes',
			),
			true
		);
		$changes = array();
		foreach ( $arguments as $key => $value ) {
			$key = sanitize_key( (string) $key );
			if ( ! isset( $allowed[ $key ] ) || null === $value ) {
				continue;
			}
			$changes[ $key ] = self::compact_evidence_value( $value, 0 );
		}
		return $changes;
	}

	private static function compact_evidence_value( $value, $depth ) {
		if ( is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
			return $value;
		}
		if ( is_string( $value ) || is_numeric( $value ) ) {
			return self::truncate( (string) $value, 500 );
		}
		if ( ! is_array( $value ) || $depth >= 2 ) {
			return is_array( $value ) ? array( 'count' => count( $value ) ) : '';
		}

		$compacted = array();
		foreach ( array_slice( $value, 0, 20, true ) as $key => $item ) {
			$safe_key = is_int( $key ) ? $key : sanitize_key( (string) $key );
			if ( '' === (string) $safe_key ) {
				continue;
			}
			$compacted[ $safe_key ] = self::compact_evidence_value( $item, $depth + 1 );
		}
		if ( count( $value ) > count( $compacted ) ) {
			$compacted['_total_count'] = count( $value );
		}
		return $compacted;
	}

	private static function decode_call_arguments( $call ) {
		$arguments = $call['arguments'] ?? ( $call['function']['arguments'] ?? array() );
		if ( is_string( $arguments ) ) {
			$decoded = json_decode( $arguments, true );
			return is_array( $decoded ) ? $decoded : array();
		}
		return is_array( $arguments ) ? $arguments : array();
	}

	private static function build_argument_scope( $tool_name, $arguments ) {
		$scope             = array();
		$collection_counts = array();
		$collection_ids    = array();
		$id_list_keys      = array(
			'ids'            => 'id',
			'product_ids'    => 'product_id',
			'variation_ids'  => 'variation_id',
			'parent_ids'     => 'parent_id',
			'post_ids'       => 'post_id',
			'term_ids'       => 'term_id',
			'category_ids'   => 'category_id',
			'attachment_ids' => 'attachment_id',
			'order_ids'      => 'order_id',
			'user_ids'       => 'user_id',
		);

		foreach ( $arguments as $key => $value ) {
			if ( ! is_array( $value ) ) {
				continue;
			}
			$key = sanitize_key( (string) $key );
			if ( '' === $key ) {
				continue;
			}
			if ( count( $collection_counts ) < 30 ) {
				$collection_counts[ $key ] = count( $value );
			}
			$ids = isset( $id_list_keys[ $key ] )
				? self::collect_stable_ids( $value, $id_list_keys[ $key ] )
				: self::collect_stable_ids( $value );
			if ( ! empty( $ids ) && count( $collection_ids ) < 20 ) {
				$collection_ids[ $key ] = $ids;
			}
		}

		if ( ! empty( $collection_counts ) ) {
			$scope['collection_counts'] = $collection_counts;
		}
		if ( ! empty( $collection_ids ) ) {
			$scope['ids'] = $collection_ids;
		}

		foreach ( array( 'limit', 'offset', 'page' ) as $key ) {
			if ( isset( $arguments[ $key ] ) && is_numeric( $arguments[ $key ] ) ) {
				$scope[ $key ] = intval( $arguments[ $key ] );
			}
		}

		if ( 'image_analyze' === $tool_name ) {
			$image_urls = isset( $arguments['image_urls'] ) && is_array( $arguments['image_urls'] )
				? array_values(
					array_filter(
						$arguments['image_urls'],
						static function ( $value ) {
							return is_scalar( $value ) && '' !== trim( (string) $value );
						}
					)
				)
				: array();
			$scope['image_count']           = count( $image_urls );
			$scope['image_sha256_prefixes'] = array_map(
				static function ( $image_url ) {
					return substr( hash( 'sha256', (string) $image_url ), 0, 16 );
				},
				array_slice( $image_urls, 0, 20 )
			);
		}

		return $scope;
	}

	private static function build_result_scope( $tool_name, $decoded_payload ) {
		$scope       = array();
		$collections = array();

		foreach ( array( 'products', 'results', 'matches', 'executed_actions' ) as $key ) {
			if ( ! isset( $decoded_payload[ $key ] ) || ! is_array( $decoded_payload[ $key ] ) ) {
				continue;
			}
			$collections[ $key ] = array(
				'count' => count( $decoded_payload[ $key ] ),
				'ids'   => self::collect_stable_ids( $decoded_payload[ $key ] ),
			);
		}
		if ( ! empty( $collections ) ) {
			$scope['collections'] = $collections;
		}

		if ( 'woo_product_lookup' === $tool_name ) {
			$scope['product_ids'] = array_values(
				array_filter(
					array_map(
						static function ( $id ) {
							return intval( $id['product_id'] ?? ( $id['post_id'] ?? ( $id['id'] ?? 0 ) ) );
						},
						$collections['products']['ids'] ?? array()
					)
				)
			);
			if ( isset( $decoded_payload['returned'] ) && is_numeric( $decoded_payload['returned'] ) ) {
				$scope['returned'] = intval( $decoded_payload['returned'] );
			}
			if ( array_key_exists( 'maybe_more', $decoded_payload ) ) {
				$scope['maybe_more'] = (bool) $decoded_payload['maybe_more'];
			}
			foreach ( array( 'limit', 'offset', 'page' ) as $key ) {
				if ( isset( $decoded_payload[ $key ] ) && is_numeric( $decoded_payload[ $key ] ) ) {
					$scope[ $key ] = intval( $decoded_payload[ $key ] );
				}
			}
		}
		if ( 'image_analyze' === $tool_name ) {
			foreach ( array( 'requested_count', 'processed_count', 'omitted_count' ) as $key ) {
				if ( isset( $decoded_payload[ $key ] ) && is_numeric( $decoded_payload[ $key ] ) ) {
					$scope[ $key ] = max( 0, intval( $decoded_payload[ $key ] ) );
				}
			}
			foreach ( array( 'partial', 'continuation_required' ) as $key ) {
				if ( array_key_exists( $key, $decoded_payload ) ) {
					$scope[ $key ] = (bool) $decoded_payload[ $key ];
				}
			}
		}

		return $scope;
	}

	private static function collect_stable_ids( $items, $scalar_key = '' ) {
		$ids       = array();
		$seen      = array();
		$id_fields = array(
			'product_id',
			'variation_id',
			'parent_id',
			'post_id',
			'term_id',
			'category_id',
			'attachment_id',
			'order_id',
			'user_id',
			'comment_id',
			'template_id',
			'snippet_id',
			'id',
		);

		foreach ( array_values( (array) $items ) as $item ) {
			if ( '' !== $scalar_key && is_scalar( $item ) ) {
				$item = array( $scalar_key => $item );
			}
			if ( ! is_array( $item ) ) {
				continue;
			}
			foreach ( $id_fields as $field ) {
				if ( ! isset( $item[ $field ] ) || ! is_scalar( $item[ $field ] ) || '' === trim( (string) $item[ $field ] ) ) {
					continue;
				}
				$value = is_numeric( $item[ $field ] )
					? intval( $item[ $field ] )
					: self::truncate( (string) $item[ $field ], 120 );
				$hash  = $field . ':' . (string) $value;
				if ( isset( $seen[ $hash ] ) ) {
					continue;
				}
				$seen[ $hash ] = true;
				$ids[]         = array( $field => $value );
				if ( count( $ids ) >= 20 ) {
					return $ids;
				}
			}
		}

		return $ids;
	}

	private static function is_dialogue_message( $message, $role ) {
		if ( ! is_array( $message ) || $role !== (string) ( $message['role'] ?? '' ) || '' === self::message_text( $message ) ) {
			return false;
		}
		if ( 'assistant' === $role && ( ! empty( $message['tool_calls'] ) || self::has_function_call_response_item( $message ) ) ) {
			return false;
		}
		return true;
	}

	private static function has_function_call_response_item( $message ) {
		if ( empty( $message['response_items'] ) || ! is_array( $message['response_items'] ) ) {
			return false;
		}
		foreach ( $message['response_items'] as $item ) {
			if ( is_array( $item ) && 'function_call' === (string) ( $item['type'] ?? '' ) ) {
				return true;
			}
		}
		return false;
	}

	private static function message_text( $message ) {
		if ( ! is_array( $message ) || ! array_key_exists( 'content', $message ) ) {
			return '';
		}
		return self::canonicalize_content( $message['content'] );
	}

	private static function truncate( $text, $limit ) {
		$text  = (string) $text;
		$limit = max( 1, intval( $limit ) );
		if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) {
			return mb_strlen( $text, 'UTF-8' ) > $limit ? mb_substr( $text, 0, $limit, 'UTF-8' ) : $text;
		}
		return strlen( $text ) > $limit ? substr( $text, 0, $limit ) : $text;
	}
}
