<?php
/**
 * SalonPro X - Input Validation Engine
 */

namespace core;

class Validator
{
    private array $errors = [];
    private array $data = [];
    private array $rules = [];
    private array $customMessages = [];
    private array $ruleRegistry = [];
    private mixed $lastParam = null;

    public function __construct(array $data, array $rules, array $messages = [])
    {
        $this->data = $data;
        $this->rules = $rules;
        $this->customMessages = $messages;
        $this->registerBuiltinRules();
        $this->validate();
    }

    public static function make(array $data, array $rules, array $messages = []): self
    {
        return new self($data, $rules, $messages);
    }

    private function registerBuiltinRules(): void
    {
        $this->ruleRegistry = [
            'required' => fn($field, $value) => $this->validateRequired($field, $value),
            'email' => fn($field, $value) => $this->validateEmail($field, $value),
            'numeric' => fn($field, $value) => $this->validateNumeric($field, $value),
            'integer' => fn($field, $value) => $this->validateInteger($field, $value),
            'min' => fn($field, $value, $param) => $this->validateMin($field, $value, $param),
            'max' => fn($field, $value, $param) => $this->validateMax($field, $value, $param),
            'min_length' => fn($field, $value, $param) => $this->validateMinLength($field, $value, $param),
            'max_length' => fn($field, $value, $param) => $this->validateMaxLength($field, $value, $param),
            'exact_length' => fn($field, $value, $param) => $this->validateExactLength($field, $value, $param),
            'alpha' => fn($field, $value) => $this->validateAlpha($field, $value),
            'alpha_numeric' => fn($field, $value) => $this->validateAlphaNumeric($field, $value),
            'alpha_dash' => fn($field, $value) => $this->validateAlphaDash($field, $value),
            'url' => fn($field, $value) => $this->validateUrl($field, $value),
            'phone' => fn($field, $value) => $this->validatePhone($field, $value),
            'date' => fn($field, $value) => $this->validateDate($field, $value),
            'date_format' => fn($field, $value, $param) => $this->validateDateFormat($field, $value, $param),
            'in' => fn($field, $value, $param) => $this->validateIn($field, $value, $param),
            'not_in' => fn($field, $value, $param) => $this->validateNotIn($field, $value, $param),
            'matches' => fn($field, $value, $param) => $this->validateMatches($field, $value, $param),
            'different' => fn($field, $value, $param) => $this->validateDifferent($field, $value, $param),
            'boolean' => fn($field, $value) => $this->validateBoolean($field, $value),
            'array' => fn($field, $value) => $this->validateArray($field, $value),
            'regex' => fn($field, $value, $param) => $this->validateRegex($field, $value, $param),
            'unique' => fn($field, $value, $param) => $this->validateUnique($field, $value, $param),
            'exists' => fn($field, $value, $param) => $this->validateExists($field, $value, $param),
            'image' => fn($field, $value) => $this->validateImage($field, $value),
            'mimes' => fn($field, $value, $param) => $this->validateMimes($field, $value, $param),
            'max_size' => fn($field, $value, $param) => $this->validateMaxSize($field, $value, $param),
        ];
    }

    private function validate(): void
    {
        foreach ($this->rules as $field => $ruleString) {
            if (is_string($ruleString)) {
                $rules = explode('|', $ruleString);
            } elseif (is_array($ruleString)) {
                $rules = $ruleString;
            } else {
                continue;
            }

            $value = $this->data[$field] ?? null;
            $isFileUpload = isset($_FILES[$field]);

            foreach ($rules as $rule) {
                $param = null;
                if (str_contains($rule, ':')) {
                    [$rule, $param] = explode(':', $rule, 2);
                }

                if (!isset($this->ruleRegistry[$rule])) {
                    continue;
                }

                $this->ruleRegistry[$rule]($field, $value, $param);

                if ($this->hasError($field)) {
                    break;
                }
            }
        }
    }

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

    private function getDefaultMessage(string $field, string $rule): string
    {
        $fieldName = str_replace(['_', '-'], ' ', $field);
        $fieldName = ucfirst($fieldName);

        $messages = [
            'required' => "{$fieldName} is required.",
            'email' => "{$fieldName} must be a valid email address.",
            'numeric' => "{$fieldName} must be a number.",
            'integer' => "{$fieldName} must be an integer.",
            'min' => "{$fieldName} must be at least {$this->lastParam}.",
            'max' => "{$fieldName} must not exceed {$this->lastParam}.",
            'min_length' => "{$fieldName} must be at least {$this->lastParam} characters.",
            'max_length' => "{$fieldName} must not exceed {$this->lastParam} characters.",
            'exact_length' => "{$fieldName} must be exactly {$this->lastParam} characters.",
            'alpha' => "{$fieldName} may only contain letters.",
            'alpha_numeric' => "{$fieldName} may only contain letters and numbers.",
            'alpha_dash' => "{$fieldName} may only contain letters, numbers, dashes and underscores.",
            'url' => "{$fieldName} must be a valid URL.",
            'phone' => "{$fieldName} must be a valid phone number.",
            'date' => "{$fieldName} must be a valid date.",
            'date_format' => "{$fieldName} must match the format {$this->lastParam}.",
            'in' => "The selected {$fieldName} is invalid.",
            'not_in' => "The selected {$fieldName} is invalid.",
            'matches' => "{$fieldName} does not match {$this->lastParam}.",
            'different' => "{$fieldName} must be different from {$this->lastParam}.",
            'boolean' => "{$fieldName} must be true or false.",
            'array' => "{$fieldName} must be an array.",
            'regex' => "{$fieldName} format is invalid.",
            'unique' => "{$fieldName} already exists.",
            'exists' => "The selected {$fieldName} does not exist.",
            'image' => "{$fieldName} must be an image.",
            'mimes' => "{$fieldName} must be a file of type: {$this->lastParam}.",
            'max_size' => "{$fieldName} must not exceed {$this->lastParam} KB.",
        ];

        if (isset($this->customMessages[$field . '.' . $rule])) {
            return $this->customMessages[$field . '.' . $rule];
        }

        return $messages[$rule] ?? "{$fieldName} is invalid.";
    }

    private function setLastParam($param): void
    {
        $this->lastParam = $param;
    }

    private function validateRequired(string $field, mixed $value): void
    {
        if (is_null($value) || (is_string($value) && trim($value) === '') || (is_array($value) && empty($value))) {
            $this->addError($field, 'required');
        }
    }

    private function validateEmail(string $field, mixed $value): void
    {
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->addError($field, 'email');
        }
    }

    private function validateNumeric(string $field, mixed $value): void
    {
        if (!empty($value) && !is_numeric($value)) {
            $this->addError($field, 'numeric');
        }
    }

    private function validateInteger(string $field, mixed $value): void
    {
        if (!empty($value) && filter_var($value, FILTER_VALIDATE_INT) === false) {
            $this->addError($field, 'integer');
        }
    }

    private function validateMin(string $field, mixed $value, $param): void
    {
        $this->setLastParam($param);
        if (is_numeric($value) && $value < $param) {
            $this->addError($field, 'min');
        }
    }

    private function validateMax(string $field, mixed $value, $param): void
    {
        $this->setLastParam($param);
        if (is_numeric($value) && $value > $param) {
            $this->addError($field, 'max');
        }
    }

    private function validateMinLength(string $field, mixed $value, $param): void
    {
        $this->setLastParam($param);
        if (is_string($value) && mb_strlen($value) < (int)$param) {
            $this->addError($field, 'min_length');
        }
    }

    private function validateMaxLength(string $field, mixed $value, $param): void
    {
        $this->setLastParam($param);
        if (is_string($value) && mb_strlen($value) > (int)$param) {
            $this->addError($field, 'max_length');
        }
    }

    private function validateExactLength(string $field, mixed $value, $param): void
    {
        $this->setLastParam($param);
        if (is_string($value) && mb_strlen($value) !== (int)$param) {
            $this->addError($field, 'exact_length');
        }
    }

    private function validateAlpha(string $field, mixed $value): void
    {
        if (!empty($value) && !ctype_alpha(str_replace(' ', '', $value))) {
            $this->addError($field, 'alpha');
        }
    }

    private function validateAlphaNumeric(string $field, mixed $value): void
    {
        if (!empty($value) && !ctype_alnum(str_replace(' ', '', $value))) {
            $this->addError($field, 'alpha_numeric');
        }
    }

    private function validateAlphaDash(string $field, mixed $value): void
    {
        if (!empty($value) && !preg_match('/^[a-zA-Z0-9_\- ]+$/', $value)) {
            $this->addError($field, 'alpha_dash');
        }
    }

    private function validateUrl(string $field, mixed $value): void
    {
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
            $this->addError($field, 'url');
        }
    }

    private function validatePhone(string $field, mixed $value): void
    {
        if (!empty($value) && !preg_match('/^[\+\d\s\-\(\)]{7,20}$/', $value)) {
            $this->addError($field, 'phone');
        }
    }

    private function validateDate(string $field, mixed $value): void
    {
        if (!empty($value) && !strtotime($value)) {
            $this->addError($field, 'date');
        }
    }

    private function validateDateFormat(string $field, mixed $value, $param): void
    {
        $this->setLastParam($param);
        if (!empty($value)) {
            $date = \DateTime::createFromFormat($param, $value);
            if (!$date || $date->format($param) !== $value) {
                $this->addError($field, 'date_format');
            }
        }
    }

    private function validateIn(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if (!empty($value) && $param !== null) {
            $allowed = explode(',', $param);
            if (!in_array($value, $allowed)) {
                $this->addError($field, 'in');
            }
        }
    }

    private function validateNotIn(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if (!empty($value) && $param !== null) {
            $disallowed = explode(',', $param);
            if (in_array($value, $disallowed)) {
                $this->addError($field, 'not_in');
            }
        }
    }

    private function validateMatches(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if ($param !== null && $value !== ($this->data[$param] ?? null)) {
            $this->addError($field, 'matches');
        }
    }

    private function validateDifferent(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if ($param !== null && $value === ($this->data[$param] ?? null)) {
            $this->addError($field, 'different');
        }
    }

    private function validateBoolean(string $field, mixed $value): void
    {
        if (!empty($value) && !in_array($value, [true, false, 1, 0, '1', '0', 'true', 'false', 'on', 'off'], true)) {
            $this->addError($field, 'boolean');
        }
    }

    private function validateArray(string $field, mixed $value): void
    {
        if (!empty($value) && !is_array($value)) {
            $this->addError($field, 'array');
        }
    }

    private function validateRegex(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if (!empty($value) && $param !== null && !preg_match($param, $value)) {
            $this->addError($field, 'regex');
        }
    }

    private function validateUnique(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if (!empty($value) && $param !== null) {
            $parts = explode(',', $param);
            $table = $parts[0];
            $column = $parts[1] ?? $field;
            $exceptId = $parts[2] ?? null;
            $exceptColumn = $parts[3] ?? 'id';

            $db = Database::getInstance();
            $db->table($table)->where($column, $value);
            if ($exceptId !== null) {
                $db->where($exceptColumn, '!=', $exceptId);
            }
            if ($db->exists()) {
                $this->addError($field, 'unique');
            }
        }
    }

    private function validateExists(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if (!empty($value) && $param !== null) {
            $parts = explode(',', $param);
            $table = $parts[0];
            $column = $parts[1] ?? $field;

            $db = Database::getInstance();
            $exists = $db->table($table)->where($column, $value)->exists();
            if (!$exists) {
                $this->addError($field, 'exists');
            }
        }
    }

    private function validateImage(string $field, mixed $value): void
    {
        if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) {
            return;
        }
        $file = $_FILES[$field];
        $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        if (!in_array($mimeType, $allowedTypes)) {
            $this->addError($field, 'image');
        }
    }

    private function validateMimes(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) {
            return;
        }
        $allowedExtensions = explode(',', $param);
        $extension = strtolower(pathinfo($_FILES[$field]['name'], PATHINFO_EXTENSION));
        if (!in_array($extension, $allowedExtensions)) {
            $this->addError($field, 'mimes');
        }
    }

    private function validateMaxSize(string $field, mixed $value, ?string $param): void
    {
        $this->setLastParam($param);
        if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) {
            return;
        }
        $maxBytes = (int)$param * 1024;
        if ($_FILES[$field]['size'] > $maxBytes) {
            $this->addError($field, 'max_size');
        }
    }

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

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

    public function errors(): array
    {
        return $this->errors;
    }

    public function firstError(): ?string
    {
        $first = reset($this->errors);
        return $first[0] ?? null;
    }

    public function error(string $field): ?string
    {
        return $this->errors[$field][0] ?? null;
    }

    public function hasError(string $field): bool
    {
        return isset($this->errors[$field]);
    }

    public function validated(): array
    {
        $validated = [];
        foreach ($this->rules as $field => $rules) {
            if (!isset($this->errors[$field]) && isset($this->data[$field])) {
                $validated[$field] = $this->data[$field];
            }
        }
        return $validated;
    }
}
