<?php

/**
 * LolaCore � ContextBuilder
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Memory;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required for core functionality, dynamic queries, and performance. Validated inputs.

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class ContextBuilder
 *
 * Selects and formats the most relevant memory facts for injection
 * into the LLM system prompt. This is the bridge between the Memory
 * Engine and the Chat layer.
 *
 * Output format example:
 *   MEMORY (12 facts):
 *   - [site_state] Plugin WP Rocket v3.15, cache TTL 2h (importance: 7, 2d ago)
 *   - [user_preference] Prefers direct style (importance: 8, 1d ago)
 */
class ContextBuilder
{
    private MemoryManager $memoryManager;
    private int $maxEntries;

    /**
     * @param MemoryManager|null $memoryManager Optional injectable manager (for testing).
     * @param int|null           $maxEntries    Max facts to include. If null, reads from config.
     */
    public function __construct(?MemoryManager $memoryManager = null, ?int $maxEntries = null)
    {
        $this->memoryManager = $memoryManager ?? new MemoryManager();
        $this->maxEntries    = $maxEntries ?? $this->getMaxEntriesFromConfig();
    }

    /**
     * Builds the full memory context string for the system prompt.
     *
     * When a user message is provided, facts matching the user's query are
     * boosted in the ranking so contextually relevant memories surface first.
     *
     * @param string $userMessage Optional current user message for contextual boosting.
     * @return string Formatted context block, or empty string if no facts.
     */
    public function buildContext(string $userMessage = ''): string
    {
        $facts = $this->getBudgetedFacts($this->resolveAdaptiveBudget($userMessage));

        if (empty($facts)) {
            return '';
        }

        // If we have a user message, boost facts whose content matches keywords
        if (!empty($userMessage)) {
            $facts = $this->boostByRelevance($facts, $userMessage);
        }

        return $this->formatFacts($facts);
    }

    /**
     * Builds context filtered by specific fact types.
     *
     * @param array $factTypes Array of fact type strings to include.
     * @return string Formatted context block.
     */
    public function buildContextByTypes(array $factTypes): string
    {
        $allFacts = [];

        foreach ($factTypes as $type) {
            $typeFacts = $this->memoryManager->getFactsByType($type, $this->maxEntries);
            $allFacts  = array_merge($allFacts, $typeFacts);
        }

        if (empty($allFacts)) {
            return '';
        }

        // Re-sort merged results by composite relevance score
        usort($allFacts, function (array $a, array $b): int {
            $scoreA = (int) $a['importance'] * (float) $a['decay_score'];
            $scoreB = (int) $b['importance'] * (float) $b['decay_score'];
            return $scoreB <=> $scoreA;
        });

        // Trim to max entries
        $allFacts = array_slice($allFacts, 0, $this->maxEntries);

        return $this->formatFacts($allFacts);
    }

    /**
     * Retrieves the last session summary from config.
     *
     * @return string The summary, or empty string if none.
     */
    public function getLastSessionSummary(): string
    {
        global $wpdb;
        $table = $wpdb->prefix . 'lolacore_config';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Custom LolaCore table query.
        $summary = $wpdb->get_var(
            $wpdb->prepare("SELECT config_value FROM {$table} WHERE config_key = %s", 'last_session_summary'),
        );

        return $summary ?? '';
    }

    /**
     * Retrieves facts with a per-category budget to prevent any single
     * fact type from dominating the context window.
     *
     * Scanner-generated site_state facts are capped at 10 entries,
     * leaving the rest of the budget for conversational facts
     * (user_preference, business_context, decision_history, session_summary).
     *
     * @param int $limit Total max facts to return.
     * @return array Budgeted fact rows sorted by composite relevance.
     */
    private function getBudgetedFacts(int $limit): array
    {
        // Fetch more than needed to allow for budget trimming
        $allFacts = $this->memoryManager->getRelevantFacts($limit * 2);

        if (empty($allFacts)) {
            return [];
        }

        $siteStateBudget   = (int) floor($limit * 0.35); // ~10 of 30
        $bootstrapBudget   = (int) floor($limit * 0.40); // ~12 of 30
        $siteStateCount    = 0;
        $bootstrapCount    = 0;
        $budgeted          = [];

        foreach ($allFacts as $fact) {
            $type   = $fact['fact_type'] ?? '';
            $source = $fact['source'] ?? '';

            // Cap bootstrap facts at 40% of the budget to preserve
            // room for conversational memory (user preferences, decisions, etc.)
            if ($source === 'bootstrap') {
                if ($bootstrapCount >= $bootstrapBudget) {
                    continue;
                }
                $bootstrapCount++;
            }

            if ($type === 'site_state' && $source !== 'bootstrap') {
                if ($siteStateCount >= $siteStateBudget) {
                    continue;
                }
                $siteStateCount++;
            }

            $budgeted[] = $fact;

            if (count($budgeted) >= $limit) {
                break;
            }
        }

        return $budgeted;
    }

    /**
     * Re-ranks facts by boosting those whose content matches keywords
     * from the user's current message.
     *
     * Uses simple keyword overlap: each matching word adds a boost to
     * the fact's effective score, bringing contextually relevant facts
     * to the top of the context window.
     *
     * @param array  $facts       Array of fact rows.
     * @param string $userMessage The current user message.
     * @return array Re-ranked fact rows.
     */
    private function boostByRelevance(array $facts, string $userMessage): array
    {
        // Extract meaningful keywords (3+ chars, lowercase)
        $words = preg_split('/[\s\p{P}]+/u', mb_strtolower($userMessage));
        $keywords = array_filter($words, function (string $w): bool {
            return mb_strlen($w) >= 3;
        });

        if (empty($keywords)) {
            return $facts;
        }

        // Score each fact by keyword overlap
        $scored = [];
        foreach ($facts as $fact) {
            $contentLower = mb_strtolower($fact['content'] ?? '');
            $matchCount   = 0;
            foreach ($keywords as $kw) {
                if (mb_strpos($contentLower, $kw) !== false) {
                    $matchCount++;
                }
            }

            // Composite: original relevance + keyword boost
            $baseScore = (int) ($fact['importance'] ?? 5) * (float) ($fact['decay_score'] ?? 1);
            $boostedScore = $baseScore + ($matchCount * 3);

            $scored[] = [
                'fact'  => $fact,
                'score' => $boostedScore,
            ];
        }

        // Sort by boosted score descending
        usort($scored, function (array $a, array $b): int {
            return $b['score'] <=> $a['score'];
        });

        return array_column($scored, 'fact');
    }

    /**
     * Formats an array of fact rows into the memory context block.
     *
     * @param array $facts Array of fact rows from the database.
     * @return string Formatted context string.
     */
    private function formatFacts(array $facts): string
    {
        $count = count($facts);
        $lines = ["MEMORY ({$count} facts):"];

        foreach ($facts as $fact) {
            $type       = $fact['fact_type'] ?? 'unknown';
            $content    = $fact['content'] ?? '';
            $importance = (int) ($fact['importance'] ?? 5);
            $timeAgo    = $this->getTimeAgo($fact['last_accessed'] ?? $fact['created_at'] ?? '');

            $lines[] = "- [{$type}] {$content} (importance: {$importance}, {$timeAgo})";
        }

        return implode("\n", $lines);
    }

    /**
     * Calculates a human-readable "time ago" string from a datetime.
     *
     * @param string $datetime MySQL datetime string.
     * @return string e.g. "2d ago", "5h ago", "just now".
     */
    private function getTimeAgo(string $datetime): string
    {
        if ($datetime === '') {
            return 'unknown';
        }

        $timestamp = strtotime($datetime);
        if ($timestamp === false) {
            return 'unknown';
        }

        $now  = current_time('timestamp');
        $diff = $now - $timestamp;

        if ($diff < 60) {
            return 'just now';
        }

        if ($diff < 3600) {
            $minutes = (int) floor($diff / 60);
            return "{$minutes}m ago";
        }

        if ($diff < 86400) {
            $hours = (int) floor($diff / 3600);
            return "{$hours}h ago";
        }

        $days = (int) floor($diff / 86400);
        return "{$days}d ago";
    }

    /**
     * Returns an adaptive memory budget based on message length.
     *
     * Short queries (< 80 chars) need less context — most memory facts
     * are noise for a follow-up like "yes" or "and the price?". Longer,
     * complex queries justify the full budget.
     *
     * Thresholds (chars → max facts):
     *  < 80   → 10 (brief follow-up or greeting)
     *  80-199 → 20 (focused question)
     * ≥ 200   → $this->maxEntries (complex, multi-step request)
     *
     * @param string $userMessage Current user message.
     * @return int Effective fact budget for this request.
     */
    private function resolveAdaptiveBudget(string $userMessage): int
    {
        $len = mb_strlen($userMessage);
        if ($len < 80) {
            return min(10, $this->maxEntries);
        }
        if ($len < 200) {
            return min(20, $this->maxEntries);
        }
        return $this->maxEntries;
    }

    /**
     * Reads the max context entries from config table.
     *
     * @return int Default: 30.
     */
    private function getMaxEntriesFromConfig(): int
    {
        global $wpdb;
        $table = $wpdb->prefix . 'lolacore_config';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Custom LolaCore table query.
        $value = $wpdb->get_var(
            $wpdb->prepare("SELECT config_value FROM {$table} WHERE config_key = %s", 'memory_max_context_entries'),
        );

        return $value !== null ? (int) $value : 30;
    }
}