<?php
/** Bounded, read-only context for the integrated chat and MCP resources. */

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

/** @return array<string,int> */
function wpagent_companion_chat_context_limits(): array {
	return array(
		'max_bytes'                    => 12000,
		'max_sections'                 => 4,
		'max_site_context_bytes'       => 2400,
		'max_site_field_bytes'         => 800,
		'max_skill_count'              => 5,
		'max_skill_instruction_bytes'  => 600,
		'max_skill_description_bytes'  => 240,
		'max_memory_count'             => 5,
		'max_memory_content_bytes'     => 700,
		'max_memory_title_bytes'       => 150,
		'max_memory_keywords'          => 12,
		'max_design_text_bytes'        => 600,
		'max_design_rule_count'        => 6,
		'max_design_rule_bytes'        => 180,
		'max_design_tokens_per_group'  => 8,
	);
}

function wpagent_companion_chat_context_bytes( string $value ): int {
	return strlen( $value );
}

/**
 * Keep UTF-8 valid even where mbstring is unavailable. The caller always gets
 * a redacted, bounded string rather than a byte-sliced invalid sequence.
 */
function wpagent_companion_chat_context_truncate( string $value, int $maximum_bytes ): string {
	if ( $maximum_bytes < 1 ) {
		return '';
	}
	if ( wpagent_companion_chat_context_bytes( $value ) <= $maximum_bytes ) {
		return $value;
	}
	$suffix = '…';
	if ( $maximum_bytes < strlen( $suffix ) ) {
		return '';
	}
	$limit  = max( 0, $maximum_bytes - strlen( $suffix ) );
	if ( function_exists( 'mb_strcut' ) ) {
		return rtrim( mb_strcut( $value, 0, $limit, 'UTF-8' ) ) . $suffix;
	}
	$characters = preg_split( '//u', $value, -1, PREG_SPLIT_NO_EMPTY );
	if ( false === $characters ) {
		return $suffix;
	}
	$result = '';
	foreach ( $characters as $character ) {
		if ( strlen( $result ) + strlen( $character ) > $limit ) {
			break;
		}
		$result .= $character;
	}
	return rtrim( $result ) . $suffix;
}

/**
 * Stored context is not an appropriate place for credentials. This is defense
 * in depth: data is already bounded and never trusted, but common secrets are
 * also removed before any third-party model or MCP client can receive them.
 */
function wpagent_companion_chat_context_redact( string $value ): string {
	$sensitive_name = '(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret|private[_ -]?key|authorization|password|passwd|secret|token)';
	$value = preg_replace( '/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----.*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/is', '[REDACTED PRIVATE KEY]', $value ) ?? $value;
	$value = preg_replace(
		'/("' . $sensitive_name . '"\\s*:\\s*)"(?:(?:\\\\.)|[^"\\\\])*"/iu',
		'$1"[REDACTED]"',
		$value
	) ?? $value;
	$value = preg_replace(
		'/\b(' . $sensitive_name . ')\b\s*([=:])\s*(?:(?:Bearer\s+)[A-Za-z0-9._~+\/=-]{8,}|"[^"]*"|\'[^\']*\'|[^\s"\'`;,)}\]]+)/iu',
		'$1$2[REDACTED]',
		$value
	) ?? $value;
	$value = preg_replace( '/([?&](?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|secret|token)=)[^&#\s]+/iu', '$1[REDACTED]', $value ) ?? $value;
	$value = preg_replace( '/\bBearer\s+[A-Za-z0-9._~+\/=-]{8,}\b/i', 'Bearer [REDACTED]', $value ) ?? $value;
	$value = preg_replace( '/\bAKIA[0-9A-Z]{16}\b/', '[REDACTED]', $value ) ?? $value;
	$value = preg_replace( '/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/', '[REDACTED]', $value ) ?? $value;
	return $value;
}

function wpagent_companion_chat_context_text( $value, int $maximum_bytes ): string {
	if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
		return '';
	}
	$value = wp_check_invalid_utf8( (string) $value, true );
	$value = wp_strip_all_tags( $value );
	$value = preg_replace( '/\r\n?|\n/', "\n", $value );
	$value = is_string( $value ) ? trim( $value ) : '';
	return wpagent_companion_chat_context_truncate( wpagent_companion_chat_context_redact( $value ), $maximum_bytes );
}

/** @return string[] */
function wpagent_companion_chat_context_keywords( string $prompt ): array {
	$prompt = wp_strip_all_tags( $prompt );
	$matches = array();
	preg_match_all( '/[\p{L}\p{N}][\p{L}\p{N}_-]{2,}/u', $prompt, $matches );
	$stopwords = array_flip( array( 'and', 'the', 'this', 'that', 'with', 'for', 'from', 'into', 'about', 'your', 'eine', 'einen', 'einer', 'einem', 'eines', 'oder', 'und', 'die', 'der', 'das', 'den', 'dem', 'des', 'mit', 'für', 'auf', 'von', 'bei', 'zur', 'zum', 'über', 'bitte' ) );
	$keywords  = array();
	foreach ( $matches[0] ?? array() as $match ) {
		$keyword = function_exists( 'mb_strtolower' ) ? mb_strtolower( $match, 'UTF-8' ) : strtolower( $match );
		if ( isset( $stopwords[ $keyword ] ) || isset( $keywords[ $keyword ] ) ) {
			continue;
		}
		$keywords[ $keyword ] = $keyword;
		if ( count( $keywords ) >= wpagent_companion_chat_context_limits()['max_memory_keywords'] ) {
			break;
		}
	}
	return array_values( $keywords );
}

function wpagent_companion_chat_context_lower( string $value ): string {
	return function_exists( 'mb_strtolower' ) ? mb_strtolower( $value, 'UTF-8' ) : strtolower( $value );
}

function wpagent_companion_chat_context_contains_keyword( string $value, string $keyword ): bool {
	$pattern = '/(?<![\p{L}\p{N}_-])' . preg_quote( $keyword, '/' ) . '(?![\p{L}\p{N}_-])/iu';
	return 1 === preg_match( $pattern, $value );
}

/** @param array<string,mixed> $memory @param string[] $keywords */
function wpagent_companion_chat_context_memory_score( array $memory, array $keywords ): int {
	if ( empty( $keywords ) ) {
		return 0;
	}
	$title   = wpagent_companion_chat_context_lower( (string) $memory['title'] );
	$content = wpagent_companion_chat_context_lower( (string) $memory['content'] );
	$tags    = wpagent_companion_chat_context_lower( implode( ' ', (array) $memory['tags'] ) );
	$score   = 0;
	foreach ( $keywords as $keyword ) {
		if ( wpagent_companion_chat_context_contains_keyword( $title, $keyword ) ) {
			$score += 10;
		}
		if ( wpagent_companion_chat_context_contains_keyword( $tags, $keyword ) ) {
			$score += 8;
		}
		if ( wpagent_companion_chat_context_contains_keyword( $content, $keyword ) ) {
			$score += 2;
		}
	}
	return $score;
}

/** @param array<string,mixed> $profile @return array<string,mixed> */
function wpagent_companion_chat_context_profile( array $profile ): array {
	$limits = wpagent_companion_chat_context_limits();
	$result = array();
	foreach ( array( 'colors', 'spacing', 'radius' ) as $group ) {
		$tokens = is_array( $profile[ $group ] ?? null ) ? $profile[ $group ] : array();
		ksort( $tokens, SORT_STRING );
		$clean = array();
		foreach ( array_slice( $tokens, 0, $limits['max_design_tokens_per_group'], true ) as $key => $value ) {
			$text = wpagent_companion_chat_context_text( $value, 100 );
			if ( '' !== $text ) {
				$clean[ (string) $key ] = $text;
			}
		}
		if ( ! empty( $clean ) ) {
			$result[ $group ] = $clean;
		}
	}
	$fonts = array();
	foreach ( array_slice( is_array( $profile['font_families'] ?? null ) ? $profile['font_families'] : array(), 0, 6 ) as $font ) {
		$font = wpagent_companion_chat_context_text( $font, 100 );
		if ( '' !== $font && ! in_array( $font, $fonts, true ) ) {
			$fonts[] = $font;
		}
	}
	if ( ! empty( $fonts ) ) {
		$result['font_families'] = $fonts;
	}
	foreach ( array( 'base_size' => 30, 'notes' => $limits['max_design_text_bytes'] ) as $field => $maximum_bytes ) {
		$text = wpagent_companion_chat_context_text( $profile[ $field ] ?? '', $maximum_bytes );
		if ( '' !== $text ) {
			$result[ $field ] = $text;
		}
	}
	return $result;
}

/** @param array<string,mixed> $profile */
function wpagent_companion_chat_context_has_profile( array $profile ): bool {
	foreach ( array( 'colors', 'spacing', 'radius', 'font_families' ) as $field ) {
		if ( ! empty( $profile[ $field ] ) ) {
			return true;
		}
	}
	return '' !== (string) ( $profile['base_size'] ?? '' ) || '' !== (string) ( $profile['notes'] ?? '' );
}

/** @param array<int,array<string,mixed>> $sections @param array<string,mixed> $section */
function wpagent_companion_chat_context_add_section( array &$sections, array $section ): bool {
	$limits = wpagent_companion_chat_context_limits();
	if ( count( $sections ) >= $limits['max_sections'] ) {
		return false;
	}
	$candidate = array( 'version' => 1, 'sections' => array_merge( $sections, array( $section ) ) );
	$json      = wp_json_encode( $candidate, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
	if ( ! is_string( $json ) || strlen( $json ) > $limits['max_bytes'] ) {
		return false;
	}
	$sections[] = $section;
	return true;
}

/** @param array<int,string> $fields @return array<string,string> */
function wpagent_companion_chat_context_site_data( array $fields, bool $preserve_empty_fields = false ): array {
	$limits = wpagent_companion_chat_context_limits();
	$used   = 0;
	$data   = array();
	$context = wpagent_normalized_site_context();
	foreach ( $fields as $field ) {
		$value = wpagent_companion_chat_context_text( $context[ $field ] ?? '', $limits['max_site_field_bytes'] );
		if ( $used + strlen( $value ) > $limits['max_site_context_bytes'] ) {
			$value = '';
		}
		if ( '' === $value && ! $preserve_empty_fields ) {
			continue;
		}
		$data[ $field ] = $value;
		$used          += strlen( $value );
	}
	return $data;
}

/** @return array<string,mixed> */
function wpagent_companion_chat_assemble_context( string $prompt = '' ): array {
	$limits   = wpagent_companion_chat_context_limits();
	$sections = array();
	$site     = wpagent_companion_chat_context_site_data( array( 'site_name', 'audience', 'voice', 'goals', 'constraints', 'notes' ) );
	if ( ! empty( $site ) ) {
		wpagent_companion_chat_context_add_section( $sections, array( 'source' => 'site_context', 'data' => $site ) );
	}

	$active_id = wpagent_active_design_direction_id();
	$directions = wpagent_normalized_design_directions();
	if ( '' !== $active_id && isset( $directions[ $active_id ] ) ) {
		$direction = $directions[ $active_id ];
		$rule_groups = array( 'rules' => array(), 'avoid' => array() );
		foreach ( array_keys( $rule_groups ) as $field ) {
			foreach ( array_slice( (array) $direction[ $field ], 0, $limits['max_design_rule_count'] ) as $rule ) {
				$rule = wpagent_companion_chat_context_text( $rule, $limits['max_design_rule_bytes'] );
				if ( '' !== $rule ) {
					$rule_groups[ $field ][] = $rule;
				}
			}
		}
		$data = array_filter(
			array(
				'id' => $direction['id'],
				'title' => wpagent_companion_chat_context_text( $direction['title'], 150 ),
				'summary' => wpagent_companion_chat_context_text( $direction['summary'], $limits['max_design_text_bytes'] ),
				'voice' => wpagent_companion_chat_context_text( $direction['voice'], $limits['max_design_text_bytes'] ),
				'rules' => $rule_groups['rules'],
				'avoid' => $rule_groups['avoid'],
				'profile' => wpagent_companion_chat_context_profile( $direction['profile'] ),
			),
			static fn( $value ): bool => is_array( $value ) ? ! empty( $value ) : '' !== $value
		);
		wpagent_companion_chat_context_add_section( $sections, array( 'source' => 'active_design_direction', 'data' => $data ) );
	} else {
		$profile = wpagent_companion_chat_context_profile( wpagent_normalized_design_profile() );
		if ( wpagent_companion_chat_context_has_profile( $profile ) ) {
			wpagent_companion_chat_context_add_section( $sections, array( 'source' => 'active_design_profile', 'data' => $profile ) );
		}
	}

	$all_skills = wpagent_normalized_skills();
	ksort( $all_skills, SORT_STRING );
	$skills = array();
	foreach ( $all_skills as $skill ) {
		if ( true !== $skill['enable_agentic'] || count( $skills ) >= $limits['max_skill_count'] ) {
			continue;
		}
		$skills[] = array_filter(
			array(
				'id' => $skill['id'],
				'title' => wpagent_companion_chat_context_text( $skill['title'], 150 ),
				'description' => wpagent_companion_chat_context_text( $skill['description'], $limits['max_skill_description_bytes'] ),
				'instructions' => wpagent_companion_chat_context_text( $skill['instructions'], $limits['max_skill_instruction_bytes'] ),
			),
			static fn( $value ): bool => '' !== $value
		);
	}
	if ( ! empty( $skills ) ) {
		wpagent_companion_chat_context_add_section( $sections, array( 'source' => 'agent_skills', 'data' => array( 'items' => $skills ) ) );
	}

	$keywords = wpagent_companion_chat_context_keywords( $prompt );
	$memories = array();
	foreach ( wpagent_normalized_memory() as $memory ) {
		$score = wpagent_companion_chat_context_memory_score( $memory, $keywords );
		if ( $score > 0 ) {
			$memory['_score'] = $score;
			$memories[]       = $memory;
		}
	}
	usort(
		$memories,
		static function ( array $left, array $right ): int {
			if ( $left['_score'] !== $right['_score'] ) {
				return $right['_score'] <=> $left['_score'];
			}
			$updated = strcmp( (string) $right['updated_at'], (string) $left['updated_at'] );
			return 0 !== $updated ? $updated : strcmp( (string) $left['id'], (string) $right['id'] );
		}
	);
	$items = array();
	foreach ( array_slice( $memories, 0, $limits['max_memory_count'] ) as $memory ) {
		$items[] = array_filter(
			array(
				'id' => $memory['id'],
				'title' => wpagent_companion_chat_context_text( $memory['title'], $limits['max_memory_title_bytes'] ),
				'content' => wpagent_companion_chat_context_text( $memory['content'], $limits['max_memory_content_bytes'] ),
				'tags' => array_values( array_filter( array_map( static fn( $tag ): string => wpagent_companion_chat_context_text( $tag, 64 ), (array) $memory['tags'] ) ) ),
			),
			static fn( $value ): bool => is_array( $value ) ? ! empty( $value ) : '' !== $value
		);
	}
	if ( ! empty( $items ) ) {
		wpagent_companion_chat_context_add_section( $sections, array( 'source' => 'relevant_memories', 'data' => array( 'items' => $items ) ) );
	}

	return array( 'version' => 1, 'sections' => $sections );
}

/** @return array<string,mixed> */
function wpagent_companion_chat_context_schema(): array {
	return array(
		'type' => 'object',
		'properties' => array(
			'version' => array( 'type' => 'integer' ),
			'sections' => array( 'type' => 'array', 'items' => array( 'type' => 'object' ) ),
		),
	);
}
