<?php

/**
 * LolaCore - PlaybookStore
 *
 * Single write sink for all playbook origins. Every create/update passes
 * through PlaybookValidator before touching the database.
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Playbooks;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Store internals: exceptions carry slug/status diagnostic data; messages are escaped at the presentation layer.

use LolaCore\Playbooks\Exceptions\PlaybookParseError;
use LolaCore\Playbooks\Exceptions\PlaybookValidationError;
use LolaCore\Skills\YamlParser;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

class PlaybookStore
{
    private PlaybookLoader $loader;

    public function __construct(?PlaybookLoader $loader = null)
    {
        $this->loader = $loader ?? new PlaybookLoader(new YamlParser(), new PlaybookValidator());
    }

    /**
     * @throws PlaybookParseError
     * @throws PlaybookValidationError
     * @throws \RuntimeException When the slug already exists or mismatches.
     */
    public function create(string $playbookSlug, string $content): int
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_playbooks');

        $playbook = $this->validateWritableContent($content, 'store://create');
        $slug     = $this->sanitizeSlug($playbookSlug);

        if ($slug !== $playbook->getSlug()) {
            throw new PlaybookValidationError(
                sprintf(
                    "Playbook slug '%s' does not match frontmatter name '%s'",
                    $slug,
                    $playbook->getSlug()
                ),
                'playbook_slug',
                'store://create'
            );
        }

        if ($this->getRowBySlug($slug) !== null) {
            throw new \RuntimeException(sprintf("Playbook slug '%s' already exists.", $slug));
        }

        $hash = hash('sha256', $content);

        $wpdb->insert(
            $table,
            [
                'playbook_slug' => $slug,
                'content'       => $content,
                'status'        => Playbook::STATUS_INACTIVE,
                'origin'        => Playbook::ORIGIN_DB,
                'content_hash'  => $hash,
            ],
            ['%s', '%s', '%s', '%s', '%s']
        );

        $this->bustObjectCache();

        return (int) $wpdb->insert_id;
    }

    /**
     * @throws PlaybookParseError
     * @throws PlaybookValidationError
     * @throws \RuntimeException
     */
    public function update(int $id, string $content): void
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_playbooks');

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

        if (!is_array($row)) {
            throw new \RuntimeException('Playbook not found.');
        }

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

        $this->validateWritableContent($content, sprintf('store://update/%d', $id));

        $newHash = hash('sha256', $content);
        if (($row['content_hash'] ?? '') === $newHash) {
            return;
        }

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

        $this->bustObjectCache();
    }

    public function delete(int $id): void
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_playbooks');

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

        $this->bustObjectCache();
    }

    /**
     * @throws \RuntimeException
     */
    public function setStatus(int $id, string $status): void
    {
        if (!in_array($status, [Playbook::STATUS_ACTIVE, Playbook::STATUS_INACTIVE, Playbook::STATUS_ARCHIVED], true)) {
            throw new \RuntimeException(sprintf("Invalid playbook status '%s'.", $status));
        }

        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_playbooks');

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

        if ($updated === false) {
            throw new \RuntimeException('Playbook not found.');
        }

        $this->bustObjectCache();
    }

    /**
     * @return array<string,mixed>|null
     */
    public function getRowBySlug(string $slug): ?array
    {
        global $wpdb;
        $table = sanitize_key($wpdb->prefix . 'lolacore_playbooks');

        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$table} WHERE playbook_slug = %s", $this->sanitizeSlug($slug)),
            ARRAY_A
        );

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

    /**
     * @throws PlaybookParseError
     * @throws PlaybookValidationError
     */
    private function validateWritableContent(string $content, string $sourcePath): Playbook
    {
        $result = $this->loader->parsePlaybookContent($content, Playbook::ORIGIN_DB, $sourcePath);
        if ($result instanceof PlaybookParseError || $result instanceof PlaybookValidationError) {
            throw $result;
        }
        return $result;
    }

    private function sanitizeSlug(string $slug): string
    {
        if (function_exists('sanitize_title')) {
            $sanitized = sanitize_title($slug);
            if (is_string($sanitized) && $sanitized !== '') {
                return $sanitized;
            }
        }

        $slug = mb_strtolower($slug);
        $slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';
        return trim($slug, '-');
    }

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