<?php
class Validator
{
    private array $data;
    private array $rules;
    private array $errors = [];
    private array $validated = [];

    public function __construct(array $data, array $rules)
    {
        $this->data = $data;
        $this->rules = $rules;
        $this->validate();
    }

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

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

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

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

    private function validate(): void
    {
        foreach ($this->rules as $field => $ruleString) {
            $rules = explode('|', $ruleString);
            $value = $this->data[$field] ?? null;
            $isNullable = in_array('nullable', $rules);
            $isFile = in_array('file', $rules);

            if ($isNullable && ($value === null || $value === '')) {
                if (array_key_exists($field, $this->data)) {
                    $this->validated[$field] = $value;
                }
                continue;
            }

            foreach ($rules as $rule) {
                if ($rule === 'nullable') continue;

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

                $error = $this->checkRule($field, $value, $rule, $param);
                if ($error) {
                    $this->errors[$field][] = $error;
                }
            }

            if (!isset($this->errors[$field])) {
                $this->validated[$field] = $value;
            }
        }
    }

    private function checkRule(string $field, $value, string $rule, ?string $param): ?string
    {
        $label = str_replace('_', ' ', $field);

        return match ($rule) {
            'required' => ($value === null || $value === '' || $value === [])
                ? "{$label} is required" : null,

            'string' => (!is_string($value) && $value !== null)
                ? "{$label} must be a string" : null,

            'integer' => ($value !== null && $value !== '' && !filter_var($value, FILTER_VALIDATE_INT) && $value !== '0' && $value !== 0)
                ? "{$label} must be an integer" : null,

            'decimal' => ($value !== null && $value !== '' && !is_numeric($value))
                ? "{$label} must be a number" : null,

            'email' => ($value !== null && $value !== '' && !filter_var($value, FILTER_VALIDATE_EMAIL))
                ? "{$label} must be a valid email" : null,

            'date' => ($value !== null && $value !== '' && !strtotime($value))
                ? "{$label} must be a valid date" : null,

            'datetime' => ($value !== null && $value !== '' && !strtotime($value))
                ? "{$label} must be a valid datetime" : null,

            'boolean' => ($value !== null && !in_array($value, [true, false, 0, 1, '0', '1', 'true', 'false'], true))
                ? "{$label} must be true or false" : null,

            'min' => $this->checkMin($label, $value, (int)$param),
            'max' => $this->checkMax($label, $value, (int)$param),

            'between' => $this->checkBetween($label, $value, $param),

            'in' => ($value !== null && $value !== '' && !in_array($value, explode(',', $param)))
                ? "{$label} must be one of: {$param}" : null,

            'not_in' => ($value !== null && $value !== '' && in_array($value, explode(',', $param)))
                ? "{$label} must not be one of: {$param}" : null,

            'exists' => $this->checkExists($label, $value, $param),
            'unique' => $this->checkUnique($label, $value, $param),

            'file' => null, // File presence checked by 'required'

            'mimes' => $this->checkMimes($label, $field, $param),

            'max_file' => $this->checkMaxFile($label, $field, (int)$param),

            'regex' => ($value !== null && $value !== '' && !preg_match($param, $value))
                ? "{$label} format is invalid" : null,

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

            'same' => (($this->data[$param] ?? null) !== $value)
                ? "{$label} must match {$param}" : null,

            'after' => ($value !== null && $value !== '' && strtotime($value) <= strtotime($param))
                ? "{$label} must be after {$param}" : null,

            'before' => ($value !== null && $value !== '' && strtotime($value) >= strtotime($param))
                ? "{$label} must be before {$param}" : null,

            'array' => ($value !== null && !is_array($value))
                ? "{$label} must be an array" : null,

            'json' => ($value !== null && $value !== '' && json_decode($value) === null && json_last_error() !== JSON_ERROR_NONE)
                ? "{$label} must be valid JSON" : null,

            default => null,
        };
    }

    private function checkMin(string $label, $value, int $min): ?string
    {
        if ($value === null || $value === '') return null;
        if (is_string($value) && mb_strlen($value) < $min) {
            return "{$label} must be at least {$min} characters";
        }
        if (is_numeric($value) && $value < $min) {
            return "{$label} must be at least {$min}";
        }
        return null;
    }

    private function checkMax(string $label, $value, int $max): ?string
    {
        if ($value === null || $value === '') return null;
        if (is_string($value) && mb_strlen($value) > $max) {
            return "{$label} must not exceed {$max} characters";
        }
        if (is_numeric($value) && $value > $max) {
            return "{$label} must not exceed {$max}";
        }
        return null;
    }

    private function checkBetween(string $label, $value, string $param): ?string
    {
        if ($value === null || $value === '') return null;
        [$min, $max] = explode(',', $param);
        if (is_numeric($value) && ($value < $min || $value > $max)) {
            return "{$label} must be between {$min} and {$max}";
        }
        return null;
    }

    private function checkExists(string $label, $value, string $param): ?string
    {
        if ($value === null || $value === '') return null;
        [$table, $column] = explode(',', $param);
        try {
            $db = Database::getInstance();
            $count = $db->fetchColumn("SELECT COUNT(*) FROM `{$table}` WHERE `{$column}` = ?", [$value]);
            if ($count == 0) {
                return "{$label} does not exist";
            }
        } catch (\Exception $e) {
            return null;
        }
        return null;
    }

    private function checkUnique(string $label, $value, string $param): ?string
    {
        if ($value === null || $value === '') return null;
        $parts = explode(',', $param);
        $table = $parts[0];
        $column = $parts[1];
        $exceptId = $parts[2] ?? null;
        try {
            $db = Database::getInstance();
            $sql = "SELECT COUNT(*) FROM `{$table}` WHERE `{$column}` = ?";
            $params = [$value];
            if ($exceptId) {
                $sql .= " AND id != ?";
                $params[] = $exceptId;
            }
            $count = $db->fetchColumn($sql, $params);
            if ($count > 0) {
                return "{$label} already exists";
            }
        } catch (\Exception $e) {
            return null;
        }
        return null;
    }

    private function checkMimes(string $label, string $field, string $param): ?string
    {
        $file = $_FILES[$field] ?? null;
        if (!$file || $file['error'] === UPLOAD_ERR_NO_FILE) return null;
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $allowed = explode(',', $param);
        if (!in_array($ext, $allowed)) {
            return "{$label} must be a file of type: {$param}";
        }
        return null;
    }

    private function checkMaxFile(string $label, string $field, int $maxKB): ?string
    {
        $file = $_FILES[$field] ?? null;
        if (!$file || $file['error'] === UPLOAD_ERR_NO_FILE) return null;
        if ($file['size'] > $maxKB * 1024) {
            return "{$label} must not exceed " . round($maxKB / 1024, 1) . "MB";
        }
        return null;
    }
}