<?php
namespace App\Core;

/** Validator sederhana berbasis aturan, dengan pesan Bahasa Indonesia. */
class Validator
{
    private array $data;
    private array $errors = [];
    private array $labels = [];

    public function __construct(array $data, array $labels = [])
    {
        $this->data = $data;
        $this->labels = $labels;
    }

    public static function make(array $data, array $rules, array $labels = []): self
    {
        $v = new self($data, $labels);
        foreach ($rules as $field => $rule) $v->check($field, $rule);
        return $v;
    }

    private function label(string $f): string
    {
        return $this->labels[$f] ?? ucfirst(str_replace('_', ' ', $f));
    }

    public function value(string $f)
    {
        $v = $this->data[$f] ?? null;
        return is_string($v) ? trim($v) : $v;
    }

    public function check(string $field, string $ruleStr): void
    {
        $value = $this->value($field);
        foreach (explode('|', $ruleStr) as $rule) {
            if ($rule === '') continue;
            [$name, $param] = array_pad(explode(':', $rule, 2), 2, null);
            $L = $this->label($field);
            switch ($name) {
                case 'required':
                    if ($value === null || $value === '' || (is_array($value) && !$value)) $this->add($field, "$L wajib diisi.");
                    break;
                case 'nullable':
                    if ($value === null || $value === '') return;
                    break;
                case 'numeric':
                    if ($value !== null && $value !== '' && !is_numeric(is_string($value) ? str_replace([' '], '', $value) : $value)
                        && !Num::looksNumeric($value)) $this->add($field, "$L harus berupa angka.");
                    break;
                case 'integer':
                    if ($value !== null && $value !== '' && !preg_match('/^-?\d+$/', (string)$value)) $this->add($field, "$L harus berupa bilangan bulat.");
                    break;
                case 'min':
                    if ($value !== null && $value !== '' && Num::parse($value) < (float)$param) $this->add($field, "$L minimal " . Num::fmt($param, 0) . '.');
                    break;
                case 'max':
                    if ($value !== null && $value !== '' && Num::parse($value) > (float)$param) $this->add($field, "$L maksimal " . Num::fmt($param, 0) . '.');
                    break;
                case 'minlen':
                    if ($value !== null && mb_strlen((string)$value) < (int)$param) $this->add($field, "$L minimal $param karakter.");
                    break;
                case 'maxlen':
                    if ($value !== null && mb_strlen((string)$value) > (int)$param) $this->add($field, "$L maksimal $param karakter.");
                    break;
                case 'email':
                    if ($value !== null && $value !== '' && !filter_var($value, FILTER_VALIDATE_EMAIL)) $this->add($field, "$L harus berupa alamat email yang valid.");
                    break;
                case 'date':
                    if ($value !== null && $value !== '' && !Dates::parse($value)) $this->add($field, "$L harus berupa tanggal yang valid (format DD-MM-YYYY).");
                    break;
                case 'in':
                    $opts = explode(',', (string)$param);
                    if ($value !== null && $value !== '' && !in_array((string)$value, $opts, true)) $this->add($field, "$L tidak valid.");
                    break;
                case 'same':
                    if ((string)$value !== (string)$this->value((string)$param)) $this->add($field, "$L harus sama dengan " . $this->label((string)$param) . '.');
                    break;
                case 'unique':
                    // unique:tabel,kolom[,kecualiId]
                    $p = explode(',', (string)$param);
                    $table = $p[0]; $col = $p[1] ?? $field; $exceptId = $p[2] ?? null;
                    if ($value !== null && $value !== '') {
                        $sql = "SELECT COUNT(*) FROM `$table` WHERE `$col` = :v AND deleted_at IS NULL";
                        $bind = ['v' => $value];
                        if ($exceptId) { $sql .= ' AND id <> :ex'; $bind['ex'] = (int)$exceptId; }
                        if ((int)Database::i()->value($sql, $bind) > 0) $this->add($field, "$L sudah digunakan.");
                    }
                    break;
                case 'exists':
                    $p = explode(',', (string)$param);
                    $table = $p[0]; $col = $p[1] ?? 'id';
                    if ($value !== null && $value !== '') {
                        if ((int)Database::i()->value("SELECT COUNT(*) FROM `$table` WHERE `$col` = :v", ['v' => $value]) === 0) {
                            $this->add($field, "$L tidak ditemukan.");
                        }
                    }
                    break;
                case 'date_after_or_equal':
                    $other = $this->value((string)$param);
                    if ($value && $other) {
                        $a = Dates::parse($value); $b = Dates::parse($other);
                        if ($a && $b && $a < $b) $this->add($field, "$L tidak boleh lebih awal dari " . $this->label((string)$param) . '.');
                    }
                    break;
            }
        }
    }

    public function add(string $field, string $message): void { $this->errors[$field][] = $message; }
    public function fails(): bool  { return !empty($this->errors); }
    public function passes(): bool { return empty($this->errors); }
    public function errors(): array { return $this->errors; }

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

    public function allMessages(): array
    {
        $out = [];
        foreach ($this->errors as $msgs) foreach ($msgs as $m) $out[] = $m;
        return $out;
    }
}
