<?php

/**
 * Prompt engine with variable injection
 *
 * Supports simple variables {{var}}, memory {{memory.*}} and profile {{profile.*}}
 *
 * @package SlideMind
 * @since 2.0.0
 */
namespace SlideMind\Core;

if ( !defined( 'ABSPATH' ) ) {
    exit;
}
class PromptEngine {
    /**
     * Pattern pour les variables {{variable}}, {{memory.key}}, {{memory.scope.key}}
     * @var string
     */
    private $variable_pattern = '/\\{\\{([\\p{L}_][\\p{L}0-9_]*(?:\\.[\\p{L}_][\\p{L}0-9_]*){0,2})(?:\\|([^}]*))?\\}\\}/u';

    /**
     * Label used to mark unresolved variables in the prompt
     * @var string
     */
    private const UNRESOLVED_LABEL = 'not available';

    /**
     * Injects variables into a prompt
     *
     * @param string $prompt The prompt with {{var}} variables
     * @param array $variables The variable values ['var' => 'value']
     * @return string The prompt with variables replaced
     */
    public function inject_variables( $prompt, array $variables ) {
        if ( empty( $prompt ) ) {
            return '';
        }
        // Sanitize the variables
        $safe_variables = $this->sanitize_variables( $variables );
        // Replace the variables
        $result = preg_replace_callback( $this->variable_pattern, function ( $matches ) use($safe_variables) {
            $var_name = $matches[1];
            $default = ( isset( $matches[2] ) ? $matches[2] : null );
            // Return the resolved value if present
            if ( isset( $safe_variables[$var_name] ) && $safe_variables[$var_name] !== '' ) {
                return $safe_variables[$var_name];
            }
            // Use the default (possibly empty string) if present
            if ( $default !== null ) {
                return $default;
            }
            // Keep the placeholder unchanged (existing behavior)
            return $matches[0];
        }, $prompt );
        return $result;
    }

    /**
     * Replaces unresolved {{...}} placeholders with an explicit indicator
     *
     * For example, transforms {{profile.first_name}} into [first name : not available]
     * so the LLM understands that the information is not available
     * instead of interpreting the placeholder as literal text.
     *
     * @param string $prompt The prompt with possible remaining placeholders
     * @return array ['prompt' => string, 'unresolved' => string[]]
     */
    private function replace_unresolved_placeholders( string $prompt ) : array {
        $unresolved = [];
        $result = preg_replace_callback( $this->variable_pattern, function ( $matches ) use(&$unresolved) {
            // $matches[2] (optional default value) is intentionally ignored:
            // by the time we hit replace_unresolved_placeholders(), inject_variables()
            // has already applied any {{var|default}} fallback.
            $var_name = $matches[1];
            $parsed = $this->parse_variable_token( $var_name );
            $key = $parsed['key'];
            // Replace underscores with spaces for readability
            $readable_key = str_replace( '_', ' ', $key );
            $unresolved[] = $readable_key;
            return '[' . $readable_key . ' : ' . self::UNRESOLVED_LABEL . ']';
        }, $prompt );
        return [
            'prompt'     => $result,
            'unresolved' => $unresolved,
        ];
    }

    /**
     * Interpolates variables for learner-facing display text.
     *
     * Unlike build_system_prompt(), this path is for content rendered
     * directly to the learner (not to the LLM). Any {{var}} placeholder
     * that remains unresolved after inject_variables() is stripped (replaced
     * with an empty string), and the output is trimmed and whitespace-collapsed.
     *
     * @param string $text      The raw text with {{var}} or {{var|default}} placeholders
     * @param array  $variables Resolved variables (same shape as inject_variables)
     * @return string The display-ready text
     */
    public function interpolate_display( string $text, array $variables ) : string {
        if ( $text === '' ) {
            return '';
        }
        // Run the standard interpolation (applies |default where present)
        $result = $this->inject_variables( $text, $variables );
        // Strip any unresolved {{...}} placeholders (no default was given)
        $result = preg_replace( $this->variable_pattern, '', $result );
        // Clean up artifacts left by stripped placeholders:
        //  - space-then-punctuation: "Hello !" -> "Hello!"
        //  - runs of 2+ spaces collapsed to 1
        //  - leading/trailing whitespace trimmed
        $result = preg_replace( '/\\s+([!?.,;:])/u', '$1', $result );
        $result = preg_replace( '/[ \\t]{2,}/u', ' ', $result );
        return trim( $result );
    }

    /**
     * Generates the instruction for unresolved variables
     *
     * @return string The instruction to append to the system prompt
     */
    private function build_unresolved_variables_instruction() : string {
        $label = self::UNRESOLVED_LABEL;
        return "--- MISSING INFORMATION ---\n" . "Some learner information above is marked \"[... : {$label}]\" because it is not available for this session. " . 'Adapt your response naturally without mentioning, guessing at, or drawing attention to this missing information.';
    }

    /**
     * Extracts variables from a prompt
     *
     * @param string $prompt The prompt to analyze
     * @return array List of found variable names
     */
    public function extract_variables( $prompt ) {
        if ( empty( $prompt ) ) {
            return [];
        }
        preg_match_all( $this->variable_pattern, $prompt, $matches );
        // Return unique names
        return array_unique( $matches[1] );
    }

    /**
     * Validates that all required variables are provided
     *
     * @param string $prompt The prompt to validate
     * @param array $variables The provided variables
     * @return array ['valid' => bool, 'missing' => array]
     */
    public function validate_variables( $prompt, array $variables ) {
        $required = $this->extract_variables( $prompt );
        $provided = array_keys( $variables );
        $missing = array_diff( $required, $provided );
        return [
            'valid'   => empty( $missing ),
            'missing' => array_values( $missing ),
        ];
    }

    /**
     * Sanitizes variables to prevent injections
     *
     * Accepts keys with dots for namespaces (memory.key, profile.key)
     *
     * @param array $variables
     * @return array
     */
    private function sanitize_variables( array $variables ) {
        $safe = [];
        foreach ( $variables as $key => $value ) {
            // Validate the variable name (supports dots for namespaces)
            if ( !preg_match( '/^[\\p{L}_][\\p{L}0-9_]*(?:\\.[\\p{L}_][\\p{L}0-9_]*){0,2}$/u', $key ) ) {
                continue;
            }
            // Sanitize the value
            if ( is_string( $value ) ) {
                $safe[$key] = sanitize_textarea_field( $value );
            } elseif ( is_numeric( $value ) ) {
                $safe[$key] = strval( $value );
            } elseif ( is_bool( $value ) ) {
                $safe[$key] = ( $value ? 'true' : 'false' );
            } elseif ( is_array( $value ) ) {
                // Convert arrays to readable JSON
                $safe[$key] = wp_json_encode( $value, JSON_UNESCAPED_UNICODE );
            }
        }
        return $safe;
    }

    /**
     * Splits a variable token into its components
     *
     * Exemples:
     *   'prenom'              => ['namespace' => null, 'scope' => null, 'key' => 'prenom']
     *   'memory.prenom'       => ['namespace' => 'memory', 'scope' => null, 'key' => 'prenom']
     *   'memory.global.ville' => ['namespace' => 'memory', 'scope' => 'global', 'key' => 'ville']
     *   'profile.display_name'=> ['namespace' => 'profile', 'scope' => null, 'key' => 'display_name']
     *
     * @param string $token The full token (e.g. 'memory.prenom')
     * @return array ['namespace' => string|null, 'scope' => string|null, 'key' => string]
     */
    private function parse_variable_token( string $token ) : array {
        $parts = explode( '.', $token );
        if ( count( $parts ) === 1 ) {
            return [
                'namespace' => null,
                'scope'     => null,
                'key'       => $parts[0],
            ];
        }
        if ( count( $parts ) === 2 ) {
            return [
                'namespace' => $parts[0],
                'scope'     => null,
                'key'       => $parts[1],
            ];
        }
        // count >= 3
        return [
            'namespace' => $parts[0],
            'scope'     => $parts[1],
            'key'       => $parts[2],
        ];
    }

    /**
     * Resolves memory and profile variables from the prompt
     *
     * Extracts {{memory.*}} and {{profile.*}} variables from the prompt,
     * resolves them via LearnerMemory and the WordPress/BuddyBoss profile,
     * and returns a key => value array ready to merge.
     *
     * @param string         $prompt                The system prompt
     * @param int            $user_id               WordPress user ID
     * @param int|null       $module_id             Module ID (null if no context)
     * @param int|null       $course_id             Course ID (null if no context)
     * @param LearnerMemory  $learner_memory        LearnerMemory instance
     * @param array          $allowed_profile_fields Profile fields allowed by the admin
     * @return array Tableau ['memory.prenom' => 'Marie', 'profile.role' => 'stagiaire', ...]
     */
    public function resolve_learner_context(
        string $prompt,
        int $user_id,
        ?int $module_id,
        ?int $course_id,
        LearnerMemory $learner_memory,
        array $allowed_profile_fields = [],
        string $session_id = ''
    ) : array {
        $all_vars = $this->extract_variables( $prompt );
        if ( empty( $all_vars ) ) {
            return [];
        }
        // Separate variables by namespace
        $memory_tokens = [];
        $profile_tokens = [];
        foreach ( $all_vars as $var ) {
            $parsed = $this->parse_variable_token( $var );
            if ( $parsed['namespace'] === 'memory' ) {
                $memory_tokens[$var] = $parsed;
            } elseif ( $parsed['namespace'] === 'profile' ) {
                $profile_tokens[$var] = $parsed;
            }
        }
        $context = [];
        // Resolve memory variables
        if ( !empty( $memory_tokens ) ) {
            $context = array_merge( $context, $this->resolve_memory_variables(
                $memory_tokens,
                $user_id,
                $module_id,
                $course_id,
                $learner_memory,
                $session_id
            ) );
        }
        // Resolve profile variables (only for authenticated users)
        if ( !empty( $profile_tokens ) && $user_id > 0 ) {
            // Free: 5 WP native fields only. Premium with Memory axis: all configured fields.
            $filtered_profile_fields = $allowed_profile_fields;
            $is_native_only = true;
            if ( $is_native_only ) {
                $native_keys = [
                    'display_name',
                    'email',
                    'first_name',
                    'last_name',
                    'role'
                ];
                $filtered_profile_fields = array_values( array_intersect( $filtered_profile_fields, $native_keys ) );
            }
            $context = array_merge( $context, $this->resolve_profile_variables( $profile_tokens, $user_id, $filtered_profile_fields ) );
        }
        return $context;
    }

    /**
     * Resolves {{memory.*}} variables via LearnerMemory
     *
     * Two modes:
     * - Cascade (memory.key): uses get_multiple_with_cascade (module > course > global)
     * - Forced scope (memory.scope.key): uses get() with the specified scope
     *
     * @param array         $tokens         Array [full_token => parsed_info]
     * @param int           $user_id        User ID
     * @param int|null      $module_id      Module ID
     * @param int|null      $course_id      Course ID
     * @param LearnerMemory $learner_memory LearnerMemory instance
     * @return array Array [full_token => value]
     */
    private function resolve_memory_variables(
        array $tokens,
        int $user_id,
        ?int $module_id,
        ?int $course_id,
        LearnerMemory $learner_memory,
        string $session_id = ''
    ) : array {
        $resolved = [];
        // Separate cascade vs forced scope
        $cascade_keys = [];
        // key => full_token
        $scoped = [];
        // scope => [key => full_token]
        foreach ( $tokens as $full_token => $parsed ) {
            if ( $parsed['scope'] === null ) {
                // memory.prenom => cascade
                $cascade_keys[$parsed['key']] = $full_token;
            } else {
                // memory.global.ville => scope force
                $scope = $parsed['scope'];
                if ( in_array( $scope, ['global', 'course', 'module'], true ) ) {
                    if ( !isset( $scoped[$scope] ) ) {
                        $scoped[$scope] = [];
                    }
                    $scoped[$scope][$parsed['key']] = $full_token;
                } else {
                    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging
                    error_log( sprintf( '[SM PromptEngine] Scope memoire invalide "%s" dans {{%s}}. Scopes valides : global, course, module', $scope, $full_token ) );
                }
            }
        }
        // Resolve cascade variables (1 call, max 3 internal SQL queries)
        if ( !empty( $cascade_keys ) ) {
            $values = $learner_memory->get_multiple_with_cascade(
                $user_id,
                $module_id,
                $course_id,
                array_keys( $cascade_keys ),
                $session_id
            );
            foreach ( $values as $key => $value ) {
                if ( isset( $cascade_keys[$key] ) ) {
                    $resolved[$cascade_keys[$key]] = sanitize_textarea_field( $value );
                }
            }
        }
        // Resolve forced-scope variables (1 call per present scope)
        foreach ( $scoped as $scope => $keys ) {
            $scope_id = null;
            if ( $scope === 'module' ) {
                $scope_id = $module_id;
            } elseif ( $scope === 'course' ) {
                $scope_id = $course_id;
            }
            $values = $learner_memory->get(
                $user_id,
                $scope,
                $scope_id,
                array_keys( $keys ),
                $session_id
            );
            foreach ( $values as $key => $value ) {
                if ( isset( $keys[$key] ) ) {
                    $resolved[$keys[$key]] = sanitize_textarea_field( $value );
                }
            }
        }
        return $resolved;
    }

    /**
     * Resolves {{profile.*}} variables via WordPress/BuddyBoss
     *
     * Verifies that each field is in the admin whitelist before resolution.
     *
     * @param array $tokens               Array [full_token => parsed_info]
     * @param int   $user_id              User ID
     * @param array $allowed_fields       Fields allowed by the admin
     * @return array Array [full_token => value]
     */
    private function resolve_profile_variables( array $tokens, int $user_id, array $allowed_fields ) : array {
        if ( empty( $allowed_fields ) ) {
            return [];
        }
        // Load aliases (alias => original key)
        $settings = get_option( 'slidemind_settings', [] );
        $aliases_map = $settings['profile_field_aliases'] ?? [];
        // Invert: alias => original_key
        $alias_to_real = [];
        foreach ( $aliases_map as $original_key => $alias ) {
            if ( !empty( $alias ) ) {
                $alias_to_real[$alias] = $original_key;
            }
        }
        $resolved = [];
        foreach ( $tokens as $full_token => $parsed ) {
            // Profile variables do not support scope (profile.scope.key is invalid)
            if ( $parsed['scope'] !== null ) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging
                error_log( sprintf(
                    '[SM PromptEngine] Variable profil invalide "%s" : utilisez {{profile.%s}} au lieu de {{%s}}',
                    $full_token,
                    $parsed['key'],
                    $full_token
                ) );
                continue;
            }
            $field_key = $parsed['key'];
            // Resolve the alias to the original key if applicable
            if ( isset( $alias_to_real[$field_key] ) ) {
                $field_key = $alias_to_real[$field_key];
            }
            // Check the whitelist (always against the original key)
            if ( !in_array( $field_key, $allowed_fields, true ) ) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Intentional debug logging
                error_log( sprintf( '[SM PromptEngine] Profile field "%s" is not in the allowed set. Token: {{%s}}', $field_key, $full_token ) );
                continue;
            }
            $value = self::resolve_profile_field_value( $user_id, $field_key );
            if ( $value !== null && $value !== '' ) {
                $resolved[$full_token] = sanitize_textarea_field( (string) $value );
            }
        }
        return $resolved;
    }

    /**
     * Resolves the value of a profile field for a given user
     *
     * Public static method reusable by the REST endpoint /profile/get.
     *
     * @param int    $user_id   WordPress user ID
     * @param string $field_key Profile field key (e.g. 'display_name', 'email', 'ld_enrolled_courses')
     * @return string|null Resolved value or null if not available
     */
    public static function resolve_profile_field_value( int $user_id, string $field_key ) : ?string {
        static $user_cache = [];
        if ( !isset( $user_cache[$user_id] ) ) {
            $user_cache[$user_id] = get_userdata( $user_id );
        }
        $user_data = $user_cache[$user_id];
        $value = null;
        switch ( $field_key ) {
            case 'display_name':
            case 'first_name':
            case 'last_name':
                if ( $user_data ) {
                    $value = $user_data->{$field_key};
                }
                break;
            case 'email':
                if ( $user_data ) {
                    $value = $user_data->user_email;
                }
                break;
            case 'role':
                if ( $user_data && !empty( $user_data->roles ) ) {
                    $value = $user_data->roles[0];
                }
                break;
            default:
                break;
        }
        if ( $value !== null && $value !== '' ) {
            return (string) $value;
        }
        return null;
    }

    /**
     * Builds the complete system prompt
     *
     * Merges learner context (memory/profile) with variables.
     * Variables take priority in case of conflict.
     *
     * @param array $config           The configuration
     * @param array $variables        The variables
     * @param array $learner_context  Resolved memory/profile variables
     * @return string The system prompt ready to send
     */
    public function build_system_prompt( array $config, array $variables = [], array $learner_context = [] ) : string {
        $base_prompt = ( isset( $config['system_prompt'] ) ? $config['system_prompt'] : '' );
        if ( empty( $base_prompt ) ) {
            return '';
        }
        // Merge: learner_context first, variables override in case of conflict
        $merged = array_merge( $learner_context, $variables );
        // Inject the variables
        $prompt = $this->inject_variables( $base_prompt, $merged );
        // Replace unresolved placeholders with an explicit indicator
        $result = $this->replace_unresolved_placeholders( $prompt );
        $prompt = $result['prompt'];
        $has_unresolved = !empty( $result['unresolved'] );
        // Add the response example block if defined
        $response_example = $config['response_example'] ?? '';
        if ( !empty( $response_example ) ) {
            $example_vars = array_merge( $merged, [
                'course_response_example' => $config['course_response_example'] ?? '',
                'module_response_example' => $config['module_response_example'] ?? '',
            ] );
            $processed_example = $this->inject_variables( $response_example, $example_vars );
            $example_result = $this->replace_unresolved_placeholders( $processed_example );
            $processed_example = $example_result['prompt'];
            if ( !empty( $example_result['unresolved'] ) ) {
                $has_unresolved = true;
            }
            if ( !empty( trim( $processed_example ) ) ) {
                $prompt .= "\n\n" . $this->build_response_example_block( $processed_example );
            }
        }
        // Add the instruction for unresolved variables
        if ( $has_unresolved ) {
            $prompt .= "\n\n" . $this->build_unresolved_variables_instruction();
        }
        // Add JSON instructions if output_variables are defined
        $output_variables = ( isset( $config['output_variables'] ) ? $config['output_variables'] : [] );
        if ( !empty( $output_variables ) ) {
            $prompt .= "\n\n" . $this->build_json_instruction_block( $output_variables );
        }
        return $prompt;
    }

    /**
     * Generates the instruction block to force a structured JSON response
     *
     * @param array $output_variables The configured output variables
     * @return string The instruction block to append to the prompt
     */
    public function build_json_instruction_block( array $output_variables ) : string {
        $fields = [];
        foreach ( $output_variables as $var ) {
            $name = $var['name'];
            $type = $var['type'];
            $desc = ( !empty( $var['description'] ) ? $var['description'] : '' );
            switch ( $type ) {
                case 'nombre':
                    $type_hint = 'number';
                    break;
                case 'booleen':
                    $type_hint = 'boolean: true|false';
                    break;
                case 'enum':
                    $values = ( isset( $var['values'] ) ? $var['values'] : [] );
                    if ( is_string( $values ) ) {
                        $values = array_map( 'trim', explode( ',', $values ) );
                    }
                    $type_hint = 'enum: ' . implode( '|', $values );
                    break;
                default:
                    $type_hint = 'string';
                    break;
            }
            $field_desc = "({$type_hint})";
            if ( $desc ) {
                $field_desc .= " {$desc}";
            }
            $fields[] = "    \"{$name}\": \"{$field_desc}\"";
        }
        $json_example = "{\n" . implode( ",\n", $fields ) . "\n}";
        $block = "IMPORTANT : Tu DOIS repondre UNIQUEMENT en JSON valide avec cette structure exacte :\n";
        $block .= $json_example . "\n";
        $block .= "Ne retourne AUCUN texte en dehors du JSON. Pas de markdown, pas de commentaires, juste le JSON.";
        return $block;
    }

    /**
     * Generates the expected response format example block
     *
     * @param string $example The response example (already processed with variables)
     * @return string The block to append to the prompt
     */
    public function build_response_example_block( string $example ) : string {
        $block = "EXEMPLE DE FORMAT DE REPONSE ATTENDU :\n";
        $block .= "Tu DOIS suivre ce format, ce ton et cette structure dans tes reponses :\n\n";
        $block .= $example;
        return $block;
    }

    /**
     * Formats messages for the OpenRouter API
     *
     * @param array $messages The chat messages
     * @param string $system_prompt The system prompt (optional)
     * @return array The formatted messages
     */
    public function format_messages( array $messages, $system_prompt = '' ) {
        $formatted = [];
        // Add the system prompt first
        if ( !empty( $system_prompt ) ) {
            $formatted[] = [
                'role'    => 'system',
                'content' => $system_prompt,
            ];
        }
        // Add user/assistant messages
        foreach ( $messages as $message ) {
            if ( !isset( $message['role'] ) || !isset( $message['content'] ) ) {
                continue;
            }
            $role = sanitize_text_field( $message['role'] );
            // Validate the role
            if ( !in_array( $role, ['user', 'assistant', 'system'], true ) ) {
                continue;
            }
            $formatted[] = [
                'role'    => $role,
                'content' => sanitize_textarea_field( $message['content'] ),
            ];
        }
        return $formatted;
    }

    /**
     * Generates a preview of the prompt with highlighted variables
     *
     * Color-coded by namespace: green (memory.), pink (profile.), blue (others)
     *
     * @param string $prompt
     * @return string HTML avec les variables colorées
     */
    public function get_preview_html( $prompt ) {
        if ( empty( $prompt ) ) {
            return '';
        }
        $escaped = esc_html( $prompt );
        // Highlight variables with color by namespace
        $highlighted = preg_replace_callback( '/\\{\\{([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*){0,2})\\}\\}/', function ( $matches ) {
            $token = $matches[1];
            $parts = explode( '.', $token );
            if ( $parts[0] === 'memory' ) {
                // Green for memory
                return '<span class="sm-variable sm-variable-memory" style="background:#e8f5e9;padding:2px 4px;border-radius:3px;color:#2e7d32;">{{' . $token . '}}</span>';
            } elseif ( $parts[0] === 'profile' ) {
                // Pink for profile
                return '<span class="sm-variable sm-variable-profile" style="background:#fce4ec;padding:2px 4px;border-radius:3px;color:#c62828;">{{' . $token . '}}</span>';
            } else {
                // Blue for simple variables
                return '<span class="sm-variable" style="background:#e3f2fd;padding:2px 4px;border-radius:3px;color:#1565c0;">{{' . $token . '}}</span>';
            }
        }, $escaped );
        return nl2br( $highlighted );
    }

}
