<?php

class Validator
{
    private array $errors = [];

    public function required(string $field, mixed $value, string $label = ''): self
    {
        if (empty(trim((string)$value))) {
            $this->errors[$field] = ($label ?: $field) . ' is required.';
        }
        return $this;
    }

    public function minLength(string $field, mixed $value, int $min, string $label = ''): self
    {
        if (strlen((string)$value) < $min) {
            $this->errors[$field] = ($label ?: $field) . " must be at least {$min} characters.";
        }
        return $this;
    }

    public function maxLength(string $field, mixed $value, int $max, string $label = ''): self
    {
        if (strlen((string)$value) > $max) {
            $this->errors[$field] = ($label ?: $field) . " must be at most {$max} characters.";
        }
        return $this;
    }

    public function email(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->errors[$field] = ($label ?: $field) . ' must be a valid email.';
        }
        return $this;
    }

    public function match(string $field, mixed $value1, mixed $value2, string $label = ''): self
    {
        if ($value1 !== $value2) {
            $this->errors[$field] = ($label ?: $field) . ' does not match.';
        }
        return $this;
    }

    public function alphanumeric(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !preg_match('/^[a-zA-Z0-9]+$/', $value)) {
            $this->errors[$field] = ($label ?: $field) . ' must contain only letters and numbers.';
        }
        return $this;
    }

    public function alpha(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !preg_match('/^[a-zA-Z]+$/', $value)) {
            $this->errors[$field] = ($label ?: $field) . ' must contain only letters.';
        }
        return $this;
    }

    public function numeric(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !is_numeric($value)) {
            $this->errors[$field] = ($label ?: $field) . ' must be a number.';
        }
        return $this;
    }

    public function integer(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_INT)) {
            $this->errors[$field] = ($label ?: $field) . ' must be an integer.';
        }
        return $this;
    }

    public function float(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_FLOAT)) {
            $this->errors[$field] = ($label ?: $field) . ' must be a decimal number.';
        }
        return $this;
    }

    public function url(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
            $this->errors[$field] = ($label ?: $field) . ' must be a valid URL.';
        }
        return $this;
    }

    public function regex(string $field, mixed $value, string $pattern, string $label = '', string $message = ''): self
    {
        if (!empty($value) && !preg_match($pattern, $value)) {
            $this->errors[$field] = $message ?: ($label ?: $field) . ' format is invalid.';
        }
        return $this;
    }

    public function in(string $field, mixed $value, array $allowed, string $label = ''): self
    {
        if (!empty($value) && !in_array($value, $allowed, true)) {
            $this->errors[$field] = ($label ?: $field) . ' must be one of: ' . implode(', ', $allowed);
        }
        return $this;
    }

    public function notIn(string $field, mixed $value, array $forbidden, string $label = ''): self
    {
        if (!empty($value) && in_array($value, $forbidden, true)) {
            $this->errors[$field] = ($label ?: $field) . ' contains forbidden value.';
        }
        return $this;
    }

    public function date(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value)) {
            $date = DateTime::createFromFormat('Y-m-d', $value);
            if (!$date || $date->format('Y-m-d') !== $value) {
                $this->errors[$field] = ($label ?: $field) . ' must be a valid date (YYYY-MM-DD).';
            }
        }
        return $this;
    }

    public function datetime(string $field, mixed $value, string $format = 'Y-m-d H:i:s', string $label = ''): self
    {
        if (!empty($value)) {
            $date = DateTime::createFromFormat($format, $value);
            if (!$date || $date->format($format) !== $value) {
                $this->errors[$field] = ($label ?: $field) . " must be a valid datetime ({$format}).";
            }
        }
        return $this;
    }

    public function phone(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value) && !preg_match('/^[\d\s\-\+\(\)]+$/', $value)) {
            $this->errors[$field] = ($label ?: $field) . ' must be a valid phone number.';
        }
        return $this;
    }

    public function username(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value)) {
            if (strlen($value) < 3 || strlen($value) > 30) {
                $this->errors[$field] = ($label ?: $field) . ' must be between 3 and 30 characters.';
            } elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $value)) {
                $this->errors[$field] = ($label ?: $field) . ' may contain only letters, numbers, and underscores.';
            }
        }
        return $this;
    }

    public function password(string $field, mixed $value, string $label = ''): self
    {
        if (!empty($value)) {
            if (strlen($value) < 8) {
                $this->errors[$field] = ($label ?: $field) . ' must be at least 8 characters long.';
            } elseif (!preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/', $value)) {
                $this->errors[$field] = ($label ?: $field) . ' must contain at least one uppercase letter, one lowercase letter, and one number.';
            }
        }
        return $this;
    }

    public function sanitize(string $value): string
    {
        return htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8');
    }

    public function sanitizeAlpha(string $value): string
    {
        return preg_replace('/[^a-zA-Z]/', '', $value);
    }

    public function sanitizeAlphanumeric(string $value): string
    {
        return preg_replace('/[^a-zA-Z0-9]/', '', $value);
    }

    public function sanitizeNumeric(string $value): string
    {
        return preg_replace('/[^0-9]/', '', $value);
    }

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

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

    public function firstError(): string
    {
        return reset($this->errors) ?: '';
    }

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