<?php

declare(strict_types=1);

namespace BotGuard\Core;

/**
 * Small rule-based input validator.
 *
 * Rules are expressed as `field => 'rule1|rule2:param'`. Supported rules cover
 * the common needs of this application (required, email, url, numeric ranges,
 * lengths, in-list, confirmed). Returns an error map keyed by field.
 */
final class Validator
{
    /** @var array<string, string[]> */
    private array $errors = [];

    /**
     * @param array<string, mixed>  $data
     * @param array<string, string> $rules
     */
    public function __construct(private array $data, private array $rules)
    {
    }

    /**
     * @param array<string, mixed>  $data
     * @param array<string, string> $rules
     */
    public static function make(array $data, array $rules): self
    {
        $validator = new self($data, $rules);
        $validator->validate();

        return $validator;
    }

    public function validate(): bool
    {
        foreach ($this->rules as $field => $ruleString) {
            $value = $this->data[$field] ?? null;

            foreach (explode('|', $ruleString) as $rule) {
                [$name, $param] = array_pad(explode(':', $rule, 2), 2, null);
                $this->applyRule($field, $name, $value, $param);
            }
        }

        return $this->passes();
    }

    public function passes(): bool
    {
        return $this->errors === [];
    }

    public function fails(): bool
    {
        return ! $this->passes();
    }

    /**
     * @return array<string, string[]>
     */
    public function errors(): array
    {
        return $this->errors;
    }

    public function firstError(): ?string
    {
        foreach ($this->errors as $messages) {
            return $messages[0] ?? null;
        }

        return null;
    }

    private function applyRule(string $field, string $rule, mixed $value, ?string $param): void
    {
        $filled = ! ($value === null || $value === '');
        $label = ucfirst(str_replace('_', ' ', $field));

        switch ($rule) {
            case 'required':
                if (! $filled) {
                    $this->addError($field, "{$label} is required.");
                }
                break;

            case 'email':
                if ($filled && ! filter_var($value, FILTER_VALIDATE_EMAIL)) {
                    $this->addError($field, "{$label} must be a valid email address.");
                }
                break;

            case 'url':
                if ($filled && ! filter_var($value, FILTER_VALIDATE_URL)) {
                    $this->addError($field, "{$label} must be a valid URL.");
                }
                break;

            case 'numeric':
                if ($filled && ! is_numeric($value)) {
                    $this->addError($field, "{$label} must be numeric.");
                }
                break;

            case 'integer':
                if ($filled && filter_var($value, FILTER_VALIDATE_INT) === false) {
                    $this->addError($field, "{$label} must be an integer.");
                }
                break;

            case 'min':
                if ($filled && (float) $value < (float) $param) {
                    $this->addError($field, "{$label} must be at least {$param}.");
                }
                break;

            case 'max':
                if ($filled && (float) $value > (float) $param) {
                    $this->addError($field, "{$label} must not exceed {$param}.");
                }
                break;

            case 'minlen':
                if ($filled && mb_strlen((string) $value) < (int) $param) {
                    $this->addError($field, "{$label} must be at least {$param} characters.");
                }
                break;

            case 'maxlen':
                if ($filled && mb_strlen((string) $value) > (int) $param) {
                    $this->addError($field, "{$label} must not exceed {$param} characters.");
                }
                break;

            case 'in':
                $allowed = explode(',', (string) $param);
                if ($filled && ! in_array((string) $value, $allowed, true)) {
                    $this->addError($field, "{$label} is invalid.");
                }
                break;

            case 'confirmed':
                if ($value !== ($this->data[$field . '_confirmation'] ?? null)) {
                    $this->addError($field, "{$label} confirmation does not match.");
                }
                break;
        }
    }

    private function addError(string $field, string $message): void
    {
        $this->errors[$field][] = $message;
    }
}
