<?php

/**
 * LolaCore - SkillDraftManager
 *
 * CRUD for the `wp_lolacore_skills` table. Handles insertion with
 * validation, status transitions (draft → active → archived), and
 * bulk archival of stale drafts.
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Skills;

use LolaCore\Skills\Exceptions\SkillValidationError;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom LolaCore table queries.

class SkillDraftManager
{
    private YamlParser $parser;
    private SkillValidator $validator;

    public function __construct(?YamlParser $parser = null, ?SkillValidator $validator = null)
    {
        $this->parser    = $parser    ?? new YamlParser();
        $this->validator = $validator ?? new SkillValidator();
    }

    /**
     * Insert a new skill row with status='inactive'.
     *
     * Skills are created off so the user has a chance to review the source
     * before they affect responses. The user explicitly activates them via
     * the admin toggle or via Lola's activateSkill ability.
     *
     * The content is parsed and validated before insertion.
     * On validation failure, throws SkillValidationError.
     *
     * @param string $skillName Unique skill name.
     * @param string $content   Full markdown content (frontmatter + body).
     * @param string $origin    e.g. 'user_imported', 'lola_authored'.
     * @return int              Inserted row ID.
     *
     * @throws SkillValidationError
     * @throws Exceptions\SkillParseError
     */
    public function insert(string $skillName, string $content, string $origin): int
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $wpdb->insert(
            $table,
            [
                'skill_name'   => $skillName,
                'content'      => $content,
                'status'       => 'inactive',
                'origin'       => $origin,
                'content_hash' => hash('sha256', $content),
            ],
            ['%s', '%s', '%s', '%s', '%s']
        );

        $id = (int) $wpdb->insert_id;

        $this->bustObjectCache();
        // Without this, the SkillRegistry's hot transient and cold snapshot serve
        // stale data — the draft is in the DB but the admin UI does not render it
        // until either cache expires or the filesystem hash changes (neither of
        // which happens just because we inserted a row).
        $this->invalidateRegistry();

        return $id;
    }

    /**
     * Update the content of an existing skill row.
     *
     * Rejects updates to archived skills.
     *
     * @throws \RuntimeException If the skill is archived.
     * @throws SkillValidationError
     */
    public function update(int $id, string $content): void
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT status FROM {$table} WHERE id = %d", $id),
            ARRAY_A
        );

        if ($row === null) {
            throw new \RuntimeException('Skill not found.');
        }

        if (($row['status'] ?? '') === 'archived') {
            throw new \RuntimeException('Cannot update an archived skill.');
        }

        $wpdb->update(
            $table,
            [
                'content'      => $content,
                'content_hash' => hash('sha256', $content),
            ],
            ['id' => $id],
            ['%s', '%s'],
            ['%d']
        );

        $this->bustObjectCache();
        $this->invalidateRegistry();
    }

    /**
     * Activate a draft skill — sets status to 'active'.
     */
    public function activate(int $id): void
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $wpdb->update(
            $table,
            ['status' => 'active'],
            ['id' => $id],
            ['%s'],
            ['%d']
        );

        $this->bustObjectCache();
        $this->invalidateRegistry();
    }

    /**
     * Deactivate an active skill — sets status to 'inactive'.
     *
     * The skill stops firing in the system prompt but stays visible in the
     * admin panel with its toggle in the OFF position, so the user can
     * re-enable it later without losing the content.
     */
    public function deactivate(int $id): void
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $wpdb->update(
            $table,
            ['status' => 'inactive'],
            ['id' => $id],
            ['%s'],
            ['%d']
        );

        $this->bustObjectCache();
        $this->invalidateRegistry();
    }

    /**
     * Archive a skill — sets status to 'archived'.
     */
    public function archive(int $id): void
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $wpdb->update(
            $table,
            ['status' => 'archived'],
            ['id' => $id],
            ['%s'],
            ['%d']
        );

        $this->bustObjectCache();
        $this->invalidateRegistry();
    }

    /**
     * Permanently delete a skill row.
     */
    public function delete(int $id): void
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $wpdb->delete($table, ['id' => $id], ['%d']);

        $this->bustObjectCache();
        // Without this, the hot transient and cold snapshot keep serving the
        // deleted skill to the admin panel and to listSkills — the row is gone
        // from the DB but every reader still sees it as if nothing happened.
        $this->invalidateRegistry();
    }

    /**
     * Retrieve a single skill row by ID.
     *
     * @return array<string,mixed>|null
     */
    public function getById(int $id): ?array
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id),
            ARRAY_A
        );

        return is_array($row) ? $row : null;
    }

    /**
     * Retrieve a single skill row by its unique skill_name.
     *
     * @return array<string,mixed>|null
     */
    public function getBySkillName(string $skillName): ?array
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_skills');

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$table} WHERE skill_name = %s", $skillName),
            ARRAY_A
        );

        return is_array($row) ? $row : null;
    }

    /**
     * Force both cache layers (hot transient + cold snapshot) to be dropped.
     *
     * Use this when the DB and the cache are known to disagree — e.g. the
     * admin panel just attempted to delete a row that the cache claimed
     * existed but the DB had already removed. Without busting the cold
     * snapshot too, the next page load resurrects the ghost row from it.
     */
    public function invalidateCaches(): void
    {
        $this->bustObjectCache();
        $this->invalidateRegistry();
    }

    // -----------------------------------------------------------------------
    // Internal helpers
    // -----------------------------------------------------------------------

    private function bustObjectCache(): void
    {
        if (function_exists('wp_cache_delete')) {
            \wp_cache_delete('lolacore_skills_db_active', 'lolacore');
        }
    }

    private function invalidateRegistry(): void
    {
        if (function_exists('delete_transient')) {
            \delete_transient(SkillRegistry::TRANSIENT_KEY);
        }

        // Drop the cold snapshot too. It is keyed by filesystem hash, so a DB-only
        // change (insert / update / activate of a row in wp_lolacore_skills) would
        // otherwise keep returning the stale serialized registry.
        global $wpdb;
        if (isset($wpdb) && is_object($wpdb) && method_exists($wpdb, 'delete')) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom LolaCore table; cache invalidation.
            $wpdb->delete(
                sanitize_key($wpdb->prefix . 'lolacore_config'),
                ['config_key' => SkillRegistry::SNAPSHOT_CONFIG_KEY],
                ['%s']
            );
        }
    }
}
