<?php

/**
 * LolaCore - PlaybookValidator
 *
 * Stage-2 semantic validation of playbook frontmatter and body size.
 * Recoverable issues are corrected and surfaced via getWarnings().
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Playbooks;

use LolaCore\Playbooks\Exceptions\PlaybookValidationError;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Validator internals: exceptions carry field/path diagnostic data.

class PlaybookValidator
{
    public const DESCRIPTION_MAX_LENGTH = 200;
    public const BODY_MAX_TOKENS        = 1500;
    public const BODY_MAX_CHARS         = 6000;

    /** @var array<int,string> */
    private array $warnings = [];

    /**
     * @param array<string,mixed> $frontmatter
     * @return array<string,mixed>
     * @throws PlaybookValidationError
     */
    public function validate(array $frontmatter, string $body, string $filePath = ''): array
    {
        $this->warnings = [];

        if (!isset($frontmatter['name']) || !is_string($frontmatter['name']) || trim($frontmatter['name']) === '') {
            throw new PlaybookValidationError(
                "Playbook is missing required field 'name'",
                'name',
                $filePath
            );
        }
        if (!isset($frontmatter['description']) || !is_string($frontmatter['description']) || trim($frontmatter['description']) === '') {
            throw new PlaybookValidationError(
                "Playbook is missing required field 'description'",
                'description',
                $filePath
            );
        }
        if (!isset($frontmatter['domain']) || !is_string($frontmatter['domain']) || trim($frontmatter['domain']) === '') {
            throw new PlaybookValidationError(
                "Playbook is missing required field 'domain'",
                'domain',
                $filePath
            );
        }

        $rawName       = (string) $frontmatter['name'];
        $sanitizedName = $this->sanitizeName($rawName);
        if ($sanitizedName === '') {
            throw new PlaybookValidationError(
                "Playbook 'name' is empty after sanitization",
                'name',
                $filePath
            );
        }
        if ($sanitizedName !== $rawName) {
            $this->warnings[] = sprintf(
                "Playbook name '%s' sanitized to '%s'",
                $rawName,
                $sanitizedName
            );
        }
        $frontmatter['name'] = $sanitizedName;

        $description = (string) $frontmatter['description'];
        if (mb_strlen($description) > self::DESCRIPTION_MAX_LENGTH) {
            $this->warnings[] = sprintf(
                "Playbook description truncated from %d to %d chars",
                mb_strlen($description),
                self::DESCRIPTION_MAX_LENGTH
            );
            $description = mb_substr($description, 0, self::DESCRIPTION_MAX_LENGTH);
        }
        $frontmatter['description'] = $description;

        $domain = trim((string) $frontmatter['domain']);
        $frontmatter['domain'] = $domain;

        if (array_key_exists('type', $frontmatter) && $frontmatter['type'] !== null && $frontmatter['type'] !== '') {
            $type = (string) $frontmatter['type'];
            if (!in_array($type, [Playbook::TYPE_SYSTEM, Playbook::TYPE_PROJECT], true)) {
                throw new PlaybookValidationError(
                    sprintf("Playbook 'type' must be '%s' or '%s'", Playbook::TYPE_SYSTEM, Playbook::TYPE_PROJECT),
                    'type',
                    $filePath
                );
            }
            $frontmatter['type'] = $type;
        }

        if (isset($frontmatter['min_lolacore_version']) && $frontmatter['min_lolacore_version'] !== '') {
            $min = (string) $frontmatter['min_lolacore_version'];
            if (!preg_match('/^\d+\.\d+\.\d+$/', $min)) {
                throw new PlaybookValidationError(
                    sprintf("Invalid semver in 'min_lolacore_version': '%s'", $min),
                    'min_lolacore_version',
                    $filePath
                );
            }
            $frontmatter['min_lolacore_version'] = $min;
        }

        if (isset($frontmatter['version']) && $frontmatter['version'] !== null && $frontmatter['version'] !== '') {
            $ver = (string) $frontmatter['version'];
            if (!preg_match('/^\d+\.\d+\.\d+$/', $ver)) {
                throw new PlaybookValidationError(
                    sprintf("Invalid semver in 'version': '%s'", $ver),
                    'version',
                    $filePath
                );
            }
            $frontmatter['version'] = $ver;
        }

        if (isset($frontmatter['ability_prefixes'])) {
            if (!is_array($frontmatter['ability_prefixes'])) {
                throw new PlaybookValidationError(
                    "Playbook 'ability_prefixes' must be an array",
                    'ability_prefixes',
                    $filePath
                );
            }
            foreach ($frontmatter['ability_prefixes'] as $prefix) {
                if (!is_string($prefix) || $prefix === '') {
                    throw new PlaybookValidationError(
                        "'ability_prefixes' entries must be non-empty strings",
                        'ability_prefixes',
                        $filePath
                    );
                }
            }
        }

        if (isset($frontmatter['triggers'])) {
            if (!is_array($frontmatter['triggers'])) {
                throw new PlaybookValidationError(
                    "Playbook 'triggers' must be a map",
                    'triggers',
                    $filePath
                );
            }
            $frontmatter['triggers'] = $this->validateTriggers($frontmatter['triggers'], $filePath);
        }

        $this->validateBodySize($body, $filePath);
        $this->warnMissingSections($body);

        return $frontmatter;
    }

    /**
     * @return array<int,string>
     */
    public function getWarnings(): array
    {
        return $this->warnings;
    }

    /**
     * @param array<string,mixed> $triggers
     * @return array<string,mixed>
     */
    private function validateTriggers(array $triggers, string $filePath): array
    {
        if (isset($triggers['keywords'])) {
            if (!is_array($triggers['keywords'])) {
                throw new PlaybookValidationError(
                    "'triggers.keywords' must be an array",
                    'triggers.keywords',
                    $filePath
                );
            }
            foreach ($triggers['keywords'] as $kw) {
                if (!is_string($kw)) {
                    throw new PlaybookValidationError(
                        "'triggers.keywords' entries must be strings",
                        'triggers.keywords',
                        $filePath
                    );
                }
            }
        }

        return $triggers;
    }

    private function validateBodySize(string $body, string $filePath): void
    {
        $charCount = mb_strlen($body);
        if ($charCount > self::BODY_MAX_CHARS) {
            throw new PlaybookValidationError(
                sprintf(
                    'Playbook body exceeds maximum size (%d chars, limit %d)',
                    $charCount,
                    self::BODY_MAX_CHARS
                ),
                'body',
                $filePath
            );
        }

        $estimatedTokens = (int) ceil($charCount / 4);
        if ($estimatedTokens > self::BODY_MAX_TOKENS) {
            throw new PlaybookValidationError(
                sprintf(
                    'Playbook body exceeds maximum token estimate (%d tokens, limit %d)',
                    $estimatedTokens,
                    self::BODY_MAX_TOKENS
                ),
                'body',
                $filePath
            );
        }
    }

    private function warnMissingSections(string $body): void
    {
        $recommended = ['## Purpose', '## Key concepts', '## Rules'];
        foreach ($recommended as $heading) {
            if (!str_contains($body, $heading)) {
                $this->warnings[] = sprintf("Playbook body is missing recommended section '%s'", $heading);
            }
        }
    }

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

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