<?php

/**
 * LolaCore - SkillValidator
 *
 * Stage-2 semantic validation of the frontmatter array produced by
 * YamlParser. Required fields, value ranges, regex compilability and
 * semver shape are enforced here. Recoverable issues (long description,
 * out-of-range priority, name needing sanitization) are corrected and
 * surfaced via getWarnings().
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Skills;

use LolaCore\Skills\Exceptions\SkillValidationError;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Validator internals: exceptions carry field/path diagnostic data; messages are escaped at the presentation layer.
// phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- Used as a regex validation guard, not debug logging.

class SkillValidator
{
    public const DESCRIPTION_MAX_LENGTH = 200;
    public const PRIORITY_MIN           = 0;
    public const PRIORITY_MAX           = 100;

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

    /**
     * Validate and sanitize a parsed frontmatter array.
     *
     * @param array<string,mixed> $frontmatter
     * @param string              $filePath    For error reporting context.
     * @return array<string,mixed> Sanitized frontmatter ready for Skill::fromParsed().
     * @throws SkillValidationError On fatal validation failure.
     */
    public function validate(array $frontmatter, string $filePath = ''): array
    {
        $this->warnings = [];

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

        $rawName       = (string) $frontmatter['name'];
        $sanitizedName = $this->sanitizeName($rawName);
        if ($sanitizedName === '') {
            throw new SkillValidationError(
                "Skill 'name' is empty after sanitization",
                'name',
                $filePath
            );
        }
        if ($sanitizedName !== $rawName) {
            $this->warnings[] = sprintf(
                "Skill 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(
                "Skill 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;

        if (array_key_exists('priority', $frontmatter)) {
            if (!is_int($frontmatter['priority'])) {
                throw new SkillValidationError(
                    "Skill 'priority' must be an integer",
                    'priority',
                    $filePath
                );
            }
            $priority = $frontmatter['priority'];
            if ($priority < self::PRIORITY_MIN) {
                $this->warnings[] = sprintf("Priority %d clamped to %d", $priority, self::PRIORITY_MIN);
                $priority         = self::PRIORITY_MIN;
            } elseif ($priority > self::PRIORITY_MAX) {
                $this->warnings[] = sprintf("Priority %d clamped to %d", $priority, self::PRIORITY_MAX);
                $priority         = self::PRIORITY_MAX;
            }
            $frontmatter['priority'] = $priority;
        }

        if (array_key_exists('always', $frontmatter) && !is_bool($frontmatter['always'])) {
            throw new SkillValidationError(
                "Skill 'always' must be a boolean",
                'always',
                $filePath
            );
        }

        if (array_key_exists('writes_data', $frontmatter) && !is_bool($frontmatter['writes_data'])) {
            throw new SkillValidationError(
                "Skill 'writes_data' must be a boolean",
                'writes_data',
                $filePath
            );
        }

        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 SkillValidationError(
                    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 SkillValidationError(
                    sprintf("Invalid semver in 'version': '%s'", $ver),
                    'version',
                    $filePath
                );
            }
            $frontmatter['version'] = $ver;
        }

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

        if (isset($frontmatter['conditions'])) {
            if (!is_array($frontmatter['conditions'])) {
                throw new SkillValidationError(
                    "Skill 'conditions' must be a map",
                    'conditions',
                    $filePath
                );
            }
            $frontmatter['conditions'] = $this->validateConditions($frontmatter['conditions'], $filePath);
        }

        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 SkillValidationError(
                    "'triggers.keywords' must be an array",
                    'triggers.keywords',
                    $filePath
                );
            }
            foreach ($triggers['keywords'] as $kw) {
                if (!is_string($kw)) {
                    throw new SkillValidationError(
                        "'triggers.keywords' entries must be strings",
                        'triggers.keywords',
                        $filePath
                    );
                }
            }
        }

        if (isset($triggers['regex'])) {
            if (!is_array($triggers['regex'])) {
                throw new SkillValidationError(
                    "'triggers.regex' must be an array",
                    'triggers.regex',
                    $filePath
                );
            }
            foreach ($triggers['regex'] as $pattern) {
                if (!is_string($pattern)) {
                    throw new SkillValidationError(
                        "'triggers.regex' entries must be strings",
                        'triggers.regex',
                        $filePath
                    );
                }
                set_error_handler(static function (): bool {
                    return true;
                });
                $result = @preg_match($pattern, '');
                restore_error_handler();
                if ($result === false) {
                    throw new SkillValidationError(
                        sprintf("Invalid regex in 'triggers.regex': '%s'", $pattern),
                        'triggers.regex',
                        $filePath
                    );
                }
            }
        }

        if (isset($triggers['intents'])) {
            if (!is_array($triggers['intents'])) {
                throw new SkillValidationError(
                    "'triggers.intents' must be an array",
                    'triggers.intents',
                    $filePath
                );
            }
            foreach ($triggers['intents'] as $intent) {
                if (!is_string($intent)) {
                    throw new SkillValidationError(
                        "'triggers.intents' entries must be strings",
                        'triggers.intents',
                        $filePath
                    );
                }
            }
        }

        return $triggers;
    }

    /**
     * @param array<string,mixed> $conditions
     * @return array<string,mixed>
     */
    private function validateConditions(array $conditions, string $filePath): array
    {
        foreach (['requires_addons', 'requires_plugins'] as $field) {
            if (isset($conditions[$field])) {
                if (!is_array($conditions[$field])) {
                    throw new SkillValidationError(
                        sprintf("'conditions.%s' must be an array", $field),
                        'conditions.' . $field,
                        $filePath
                    );
                }
                foreach ($conditions[$field] as $entry) {
                    if (!is_string($entry) || $entry === '') {
                        throw new SkillValidationError(
                            sprintf("'conditions.%s' entries must be non-empty strings", $field),
                            'conditions.' . $field,
                            $filePath
                        );
                    }
                }
            }
        }

        if (isset($conditions['user_capability'])) {
            if (!is_string($conditions['user_capability']) || $conditions['user_capability'] === '') {
                throw new SkillValidationError(
                    "'conditions.user_capability' must be a non-empty string",
                    'conditions.user_capability',
                    $filePath
                );
            }
        }

        if (isset($conditions['locale'])) {
            if (!is_array($conditions['locale'])) {
                throw new SkillValidationError(
                    "'conditions.locale' must be an array",
                    'conditions.locale',
                    $filePath
                );
            }
            foreach ($conditions['locale'] as $entry) {
                if (!is_string($entry) || $entry === '') {
                    throw new SkillValidationError(
                        "'conditions.locale' entries must be non-empty strings",
                        'conditions.locale',
                        $filePath
                    );
                }
            }
        }

        return $conditions;
    }

    /**
     * Sanitize a skill name to kebab-case using sanitize_title() if available.
     */
    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, '-');
    }
}
