<?php
declare(strict_types=1);

namespace App\Core;

use App\Core\Registries\ValidatorRegistry;

final class Validator
{
    private array $messages = [
        'required'         => 'حقل :field مطلوب',
        'string'           => 'حقل :field يجب أن يكون نصاً',
        'integer'          => 'حقل :field يجب أن يكون رقماً صحيحاً',
        'numeric'          => 'حقل :field يجب أن يكون رقماً',
        'email'            => 'حقل :field يجب أن يكون بريد إلكتروني صالح',
        'date'             => 'حقل :field يجب أن يكون تاريخاً صالحاً',
        'min'              => 'حقل :field يجب أن يكون على الأقل :param',
        'max'              => 'حقل :field يجب ألا يزيد عن :param',
        'in'               => 'قيمة حقل :field غير صالحة',
        'unique'           => 'قيمة حقل :field مستخدمة بالفعل',
        'exists'           => 'قيمة حقل :field غير موجودة',
        'digits'           => 'حقل :field يجب أن يكون :param رقم',
        'phone_eg'         => 'حقل :field يجب أن يكون رقم هاتف مصري صالح',
        'national_id'      => 'حقل :field يجب أن يكون رقم قومي مصري صالح',
        'confirmed'        => 'حقل :field غير متطابق مع التأكيد',
        'regex'            => 'صيغة حقل :field غير صالحة',
        'between'          => 'حقل :field يجب أن يكون بين :param',
        'file'             => 'حقل :field يجب أن يكون ملفاً',
        'file_max'         => 'حجم الملف يجب ألا يتجاوز :param كيلوبايت',
        'file_types'       => 'نوع الملف غير مسموح به',
        'arabic_text'      => 'حقل :field يجب أن يحتوي على نص عربي فقط',
        'english_text'     => 'حقل :field يجب أن يحتوي على نص إنجليزي فقط',
        'date_before'      => 'حقل :field يجب أن يكون قبل :param',
        'date_after'       => 'حقل :field يجب أن يكون بعد :param',
        'date_before_today' => 'حقل :field يجب أن يكون قبل اليوم',
        'date_after_today' => 'حقل :field يجب أن يكون بعد اليوم',
    ];

    public function validate(array $data, array $rules): ValidationResult
    {
        $errors = [];
        $validated = [];

        foreach ($rules as $field => $ruleString) {
            $ruleList = is_array($ruleString) ? $ruleString : explode('|', $ruleString);
            $value = $data[$field] ?? null;
            $fieldErrors = [];

            $isNullable = in_array('nullable', $ruleList);

            if ($isNullable && ($value === null || $value === '')) {
                $validated[$field] = $value;
                continue;
            }

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

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

                $error = $this->checkRule($rule, $field, $value, $param, $data);
                if ($error !== null) {
                    $fieldErrors[] = $error;
                    if ($rule === 'required') break;
                }
            }

            if (empty($fieldErrors)) {
                $validated[$field] = $value;
            } else {
                $errors[$field] = $fieldErrors;
            }
        }

        return new ValidationResult($errors, $validated);
    }

    private function checkRule(string $rule, string $field, $value, ?string $param, array $allData): ?string
    {
        $customCheck = ValidatorRegistry::get($rule);
        if ($customCheck !== null) {
            $result = ($customCheck['handler'])($value, $param, $allData, $field);
            if ($result !== true) {
                return is_string($result) ? $result : $this->msg($rule, $field, $param);
            }
            return null;
        }

        return match ($rule) {
            'required' => ($value === null || $value === '' || $value === []) ? $this->msg('required', $field) : null,
            'string' => is_string($value) ? null : $this->msg('string', $field),
            'integer' => (is_numeric($value) && (int) $value == $value) ? null : $this->msg('integer', $field),
            'numeric' => is_numeric($value) ? null : $this->msg('numeric', $field),
            'email' => filter_var($value, FILTER_VALIDATE_EMAIL) !== false ? null : $this->msg('email', $field),
            'date' => $this->isValidDate($value) ? null : $this->msg('date', $field),
            'min' => $this->checkMin($value, $param, $field),
            'max' => $this->checkMax($value, $param, $field),
            'between' => $this->checkBetween($value, $param, $field),
            'in' => in_array($value, explode(',', $param ?? '')) ? null : $this->msg('in', $field),
            'not_in' => !in_array($value, explode(',', $param ?? '')) ? null : $this->msg('in', $field),
            'digits' => (is_string($value) && preg_match('/^\d{' . $param . '}$/', $value)) ? null : $this->msg('digits', $field, $param),
            'digits_between' => $this->checkDigitsBetween($value, $param, $field),
            'regex' => preg_match('/' . $param . '/', (string) $value) ? null : $this->msg('regex', $field),
            'confirmed' => ($value === ($allData[$field . '_confirmation'] ?? null)) ? null : $this->msg('confirmed', $field),
            'unique' => $this->checkUnique($value, $param, $allData, $field),
            'exists' => $this->checkExists($value, $param, $field),
            'phone_eg' => preg_match('/^01[0-9]{9}$/', (string) $value) ? null : $this->msg('phone_eg', $field),
            'national_id' => $this->checkNationalId($value, $field),
            'arabic_text' => preg_match('/^[\p{Arabic}\s\d\-\.]+$/u', (string) $value) ? null : $this->msg('arabic_text', $field),
            'english_text' => preg_match('/^[a-zA-Z\s\d\-\.]+$/', (string) $value) ? null : $this->msg('english_text', $field),
            'date_before_today' => ($this->isValidDate($value) && $value < date('Y-m-d')) ? null : $this->msg('date_before_today', $field),
            'date_after_today' => ($this->isValidDate($value) && $value > date('Y-m-d')) ? null : $this->msg('date_after_today', $field),
            'date_before' => ($this->isValidDate($value) && isset($allData[$param]) && $value < $allData[$param]) ? null : $this->msg('date_before', $field, $param),
            'date_after' => ($this->isValidDate($value) && isset($allData[$param]) && $value > $allData[$param]) ? null : $this->msg('date_after', $field, $param),
            'array' => is_array($value) ? null : 'حقل ' . $field . ' يجب أن يكون مصفوفة',
            'json' => (json_decode((string) $value) !== null) ? null : 'حقل ' . $field . ' يجب أن يكون JSON صالح',
            'file' => (is_array($value) && isset($value['tmp_name'])) ? null : $this->msg('file', $field),
            'file_max' => $this->checkFileMax($value, $param, $field),
            'file_types' => $this->checkFileTypes($value, $param, $field),
            'decimal' => $this->checkDecimal($value, $param, $field),
            default => null,
        };
    }

    private function msg(string $rule, string $field, ?string $param = null): string
    {
        $msg = $this->messages[$rule] ?? 'حقل :field غير صالح';
        $msg = str_replace(':field', $field, $msg);
        if ($param !== null) {
            $msg = str_replace(':param', $param, $msg);
        }
        return $msg;
    }

    private function isValidDate($value): bool
    {
        if (!is_string($value)) return false;
        $d = \DateTime::createFromFormat('Y-m-d', $value);
        return $d && $d->format('Y-m-d') === $value;
    }

    private function checkMin($value, ?string $param, string $field): ?string
    {
        $min = (int) $param;
        if (is_string($value) && mb_strlen($value) < $min) {
            return $this->msg('min', $field, $param);
        }
        if (is_numeric($value) && $value < $min) {
            return $this->msg('min', $field, $param);
        }
        return null;
    }

    private function checkMax($value, ?string $param, string $field): ?string
    {
        $max = (int) $param;
        if (is_string($value) && mb_strlen($value) > $max) {
            return $this->msg('max', $field, $param);
        }
        if (is_numeric($value) && $value > $max) {
            return $this->msg('max', $field, $param);
        }
        return null;
    }

    private function checkBetween($value, ?string $param, string $field): ?string
    {
        [$min, $max] = explode(',', $param ?? '0,0');
        if (is_numeric($value) && ($value < (int)$min || $value > (int)$max)) {
            return $this->msg('between', $field, $param);
        }
        return null;
    }

    private function checkDigitsBetween($value, ?string $param, string $field): ?string
    {
        [$min, $max] = explode(',', $param ?? '0,0');
        $len = strlen((string) $value);
        if (!ctype_digit((string) $value) || $len < (int) $min || $len > (int) $max) {
            return $this->msg('digits', $field, $param);
        }
        return null;
    }

    private function checkUnique($value, ?string $param, array $allData, string $field): ?string
    {
        $parts = explode(',', $param ?? '');
        $table = $parts[0] ?? '';
        $column = $parts[1] ?? $field;
        $exceptId = $parts[2] ?? ($allData['id'] ?? null);

        $db = App::getInstance()->db();
        $sql = "SELECT COUNT(*) as cnt FROM `{$table}` WHERE `{$column}` = ?";
        $params = [$value];

        if ($exceptId) {
            $sql .= " AND `id` != ?";
            $params[] = $exceptId;
        }

        $result = $db->selectOne($sql, $params);
        return ($result['cnt'] ?? 0) > 0 ? $this->msg('unique', $field) : null;
    }

    private function checkExists($value, ?string $param, string $field): ?string
    {
        [$table, $column] = explode(',', $param ?? ',');
        $column = $column ?: $field;
        $db = App::getInstance()->db();
        $result = $db->selectOne("SELECT COUNT(*) as cnt FROM `{$table}` WHERE `{$column}` = ?", [$value]);
        return ($result['cnt'] ?? 0) === 0 ? $this->msg('exists', $field) : null;
    }

    private function checkNationalId($value, string $field): ?string
    {
        if (!is_string($value) || !preg_match('/^\d{14}$/', $value)) {
            return $this->msg('national_id', $field);
        }

        $century = (int) $value[0];
        if (!in_array($century, [2, 3])) {
            return $this->msg('national_id', $field);
        }

        $month = (int) substr($value, 3, 2);
        $day = (int) substr($value, 5, 2);
        $year = ($century === 2 ? 1900 : 2000) + (int) substr($value, 1, 2);

        if (!checkdate($month, $day, $year)) {
            return $this->msg('national_id', $field);
        }

        return null;
    }

    private function checkFileMax($value, ?string $param, string $field): ?string
    {
        if (!is_array($value) || !isset($value['size'])) return null;
        $maxKb = (int) $param;
        if ($value['size'] > $maxKb * 1024) {
            return $this->msg('file_max', $field, $param);
        }
        return null;
    }

    private function checkFileTypes($value, ?string $param, string $field): ?string
    {
        if (!is_array($value) || !isset($value['name'])) return null;
        $allowedTypes = explode(',', $param ?? '');
        $ext = strtolower(pathinfo($value['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, $allowedTypes)) {
            return $this->msg('file_types', $field);
        }
        return null;
    }

    private function checkDecimal($value, ?string $param, string $field): ?string
    {
        $places = (int) ($param ?? 2);
        $pattern = '/^\d+(\.\d{1,' . $places . '})?$/';
        if (!preg_match($pattern, (string) $value)) {
            return 'حقل ' . $field . ' يجب أن يكون رقماً عشرياً بحد أقصى ' . $places . ' خانات';
        }
        return null;
    }
}

final class ValidationResult
{
    private array $errors;
    private array $validated;

    public function __construct(array $errors, array $validated)
    {
        $this->errors = $errors;
        $this->validated = $validated;
    }

    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 validated(): array
    {
        return $this->validated;
    }
}