<?php

/**
 * LolaCore � DecayService
 *
 * @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 DecayService
 *
 * Handles temporal decay of memory facts. Each fact's decay_score decreases
 * over time if not accessed. When score drops below the archive threshold,
 * the fact is archived and excluded from LLM context injection.
 *
 * Called daily by WP-Cron hook 'lolacore_daily_decay'.
 * Decay formula: max(0, currentScore - rate × daysSinceAccess)
 */
class DecayService
{
    private const MAX_SCORE = 1.000;

    /**
     * @param float $decayRate        Score reduction per day since last access (default: 0.02).
     * @param float $archiveThreshold Score below which a fact is archived (default: 0.10).
     */
    public function __construct(
        private readonly float $decayRate = 0.02,
        private readonly float $archiveThreshold = 0.10,
    ) {
    }

    /**
     * Calculates the new decay score for a fact.
     *
     * Higher-importance facts decay slower. The effective decay rate is
     * reduced by the importance level: adjustedRate = baseRate / (1 + importance × 0.1).
     * A fact with importance 10 decays ~2× slower than importance 1.
     *
     * @param float $currentScore    Current decay score (0.000–1.000).
     * @param int   $daysSinceAccess Number of days since the fact was last accessed.
     * @param int   $importance      Fact importance (1–10). Higher = slower decay.
     * @return float The new score, floored at 0.000.
     */
    public function calculateDecay(float $currentScore, int $daysSinceAccess, int $importance = 5): float
    {
        // Adjust rate: importance 10 → rate/2.0, importance 1 → rate/1.1
        $adjustedRate = $this->decayRate / (1 + $importance * 0.1);
        $newScore = $currentScore - ($adjustedRate * $daysSinceAccess);

        return round(max(0.000, $newScore), 3);
    }

    /**
     * Determines if a fact should be archived based on its score.
     *
     * @param float $score The current decay score.
     * @return bool True if the score is strictly below the archive threshold.
     */
    public function shouldArchive(float $score): bool
    {
        return $score < $this->archiveThreshold;
    }

    /**
     * Returns the maximum decay score (used when a fact is accessed).
     *
     * @return float Always 1.000.
     */
    public function resetOnAccess(): float
    {
        return self::MAX_SCORE;
    }

    /**
     * Processes all active facts: recalculates decay scores and archives those below threshold.
     *
     * This is the batch operation called by the 'lolacore_daily_decay' WP-Cron hook.
     * Reads decay rate and threshold from wp_lolacore_config if not overridden in constructor.
     *
     * @return array{updated: int, archived: int} Counts of updated and archived facts.
     */
    public function processAll(): array
    {
        global $wpdb;
        $table = $wpdb->prefix . 'lolacore_memory';

        $updated  = 0;
        $archived = 0;

        // Fetch all active (non-archived) facts with their last_accessed timestamp.
        // Bootstrap facts (source = 'bootstrap') are excluded — they don't decay,
        // they are refreshed by MemoryBootstrapRunner instead.
        // 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.
        $facts = $wpdb->get_results(
            "SELECT id, decay_score, importance, last_accessed, created_at, is_pinned, source
             FROM {$table}
             WHERE is_archived = 0 AND source != 'bootstrap'",
            ARRAY_A,
        );

        if (empty($facts)) {
            return ['updated' => 0, 'archived' => 0];
        }

        $now = current_time('timestamp');

        foreach ($facts as $fact) {
            // Pinned facts never decay
            if (!empty($fact['is_pinned'])) {
                continue;
            }

            // Calculate days since last access (or since creation if never accessed)
            $referenceDate = $fact['last_accessed'] ?? $fact['created_at'];
            $referenceTimestamp = strtotime($referenceDate);

            if ($referenceTimestamp === false) {
                continue;
            }

            $daysSinceAccess = (int) floor(($now - $referenceTimestamp) / 86400);

            if ($daysSinceAccess <= 0) {
                continue;
            }

            $currentScore = (float) $fact['decay_score'];
            $importance   = (int) ($fact['importance'] ?? 5);
            $newScore     = $this->calculateDecay($currentScore, $daysSinceAccess, $importance);

            if ($this->shouldArchive($newScore)) {
                // Archive the fact
                // 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.
                $wpdb->update(
                    $table,
                    ['decay_score' => $newScore, 'is_archived' => 1],
                    ['id' => $fact['id']],
                    ['%f', '%d'],
                    ['%d'],
                );
                $archived++;
            } else {
                // Update the decay score
                // 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.
                $wpdb->update(
                    $table,
                    ['decay_score' => $newScore],
                    ['id' => $fact['id']],
                    ['%f'],
                    ['%d'],
                );
            }

            $updated++;
        }

        return ['updated' => $updated, 'archived' => $archived];
    }

    /**
     * Creates a DecayService instance with rates read from wp_lolacore_config.
     *
     * @return self
     */
    public static function fromConfig(): self
    {
        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.
        $rate = $wpdb->get_var(
            $wpdb->prepare("SELECT config_value FROM {$table} WHERE config_key = %s", 'memory_decay_rate'),
        );

        // 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.
        $threshold = $wpdb->get_var(
            $wpdb->prepare("SELECT config_value FROM {$table} WHERE config_key = %s", 'memory_decay_threshold'),
        );

        return new self(
            decayRate: $rate !== null ? (float) $rate : 0.02,
            archiveThreshold: $threshold !== null ? (float) $threshold : 0.10,
        );
    }
}