<?php

namespace ZenoChatbot\Framework\Validation;

use ZenoChatbot\Framework\Support\Arr;

class Validator
{
    /**
     * Original data.
     * @var array<string, mixed>
     */
    protected array $data;

    /**
     * Rules per attribute.
     * @var array<string, array<int, array{rule:string, params:array<int, string>}>>
     */
    protected array $parsedRules = [];

    /**
     * Custom messages.
     * @var array<string, string>
     */
    protected array $messages = [];

    /**
     * Errors per attribute.
     * @var array<string, array<int, string>>
     */
    protected array $errors = [];

    /**
     * Validated data output.
     * @var array<string, mixed>
     */
    protected array $validated = [];

    /**
     * Default rule messages
     * @var array<string, string>
     */
    protected array $defaultMessages = [
        'required'   => 'The :attribute field is required.',
        'string'     => 'The :attribute must be a string.',
        'integer'    => 'The :attribute must be an integer.',
        'numeric'    => 'The :attribute must be a number.',
        'boolean'    => 'The :attribute field must be true or false.',
        'array'      => 'The :attribute must be an array.',
        'email'      => 'The :attribute must be a valid email address.',
        'url'        => 'The :attribute must be a valid URL.',
        'min.string' => 'The :attribute must be at least :min characters.',
        'min.numeric'=> 'The :attribute must be at least :min.',
        'max.string' => 'The :attribute may not be greater than :max characters.',
        'max.numeric'=> 'The :attribute may not be greater than :max.',
        'between.string' => 'The :attribute must be between :min and :max characters.',
        'between.numeric'=> 'The :attribute must be between :min and :max.',
        'in'         => 'The selected :attribute is invalid.',
        'not_in'     => 'The selected :attribute is invalid.',
        'date'       => 'The :attribute is not a valid date.',
        'before'     => 'The :attribute must be a date before :date.',
        'after'      => 'The :attribute must be a date after :date.',
        'size.string'=> 'The :attribute must be :size characters.',
        'size.numeric'=> 'The :attribute must be :size.',
        'confirmed'  => 'The :attribute confirmation does not match.',
        'same'       => 'The :attribute and :other must match.',
        'different'  => 'The :attribute and :other must be different.',
    ];

    /**
     * Create a validator instance.
     *
     * @param array<string, mixed> $data
     * @param array<string, string|array<int, string>> $rules
     * @param array<string, string> $messages
     */
    public function __construct(array $data, array $rules, array $messages = [])
    {
        $this->data = $data;
        $this->messages = $messages;
        $this->parsedRules = $this->parseRules($rules);
        $this->validateInternal();
    }

    /**
     * Static factory compatible with Validator::make(...)
     * @param array<string, mixed> $data
     * @param array<string, string|array<int, string>> $rules
     * @param array<string, string> $messages
     */
    public static function make(array $data, array $rules, array $messages = []): self
    {
        return new self($data, $rules, $messages);
    }

    /**
     * Whether validation failed.
     */
    public function fails(): bool
    {
        return !empty($this->errors);
    }

    /**
     * Whether validation passed.
     */
    public function passes(): bool
    {
        return empty($this->errors);
    }

    /**
     * Errors array getter
     * @return array<string, array<int, string>>
     */
    public function errors(): array
    {
        return $this->errors;
    }

    /**
     * Get first error for a key.
     */
    public function first(string $key): ?string
    {
        return $this->errors[$key][0] ?? null;
    }

    /**
     * Get validated data.
     * @return array<string, mixed>
     */
    public function validated(): array
    {
        return $this->validated;
    }

    /**
     * Parse rules into normalized structure.
     * @param array<string, string|array<int, string>> $rules
     * @return array<string, array<int, array{rule:string, params:array<int, string>}>>
     */
    protected function parseRules(array $rules): array
    {
        $parsed = [];
        foreach ($rules as $attribute => $ruleSet) {
            $list = [];
            if (is_string($ruleSet)) {
                $parts = array_filter(explode('|', $ruleSet), fn($v) => $v !== '');
            } elseif (is_array($ruleSet)) {
                $parts = $ruleSet;
            } else {
                $parts = [];
            }
            foreach ($parts as $part) {
                if (is_string($part)) {
                    [$name, $params] = $this->extractNameAndParams($part);
                    $list[] = ['rule' => strtolower($name), 'params' => $params];
                }
            }
            $parsed[$attribute] = $list;
        }
        return $parsed;
    }

    /**
     * Extract rule name and params from a rule string, e.g. min:3 or in:a,b
     * @return array{0:string, 1:array<int, string>}
     */
    protected function extractNameAndParams(string $rule): array
    {
        $name = $rule;
        $params = [];
        if (strpos($rule, ':') !== false) {
            [$name, $paramStr] = explode(':', $rule, 2);
            // split by comma not respecting quotes for simplicity
            $params = $paramStr === '' ? [] : explode(',', $paramStr);
        }
        return [$name, $params];
    }

    /**
     * Perform validation pass populating errors and validated data.
     */
    protected function validateInternal(): void
    {
        $this->errors = [];
        $this->validated = [];

        foreach ($this->parsedRules as $attribute => $rules) {
            $valuePresent = Arr::has($this->data, $attribute);
            $value = Arr::get($this->data, $attribute);

            $hasSometimes = $this->hasRule($rules, 'sometimes');
            if ($hasSometimes && !$valuePresent) {
                // Skip validation entirely if field not present
                continue;
            }

            $isNullable = $this->hasRule($rules, 'nullable');
            $isRequired = $this->hasRule($rules, 'required');

            if ($isRequired && (!$valuePresent || $this->isEmpty($value))) {
                $this->addError($attribute, 'required', $value, []);
                continue;
            }

            if (!$valuePresent) {
                // Not required and not present: nothing to validate and not included in validated()
                continue;
            }

            if ($isNullable && $this->isNullEquivalent($value)) {
                // Accept null/empty and include as-is
                $this->validatedSet($attribute, $value);
                continue;
            }

            // Validate other rules
            $failed = false;
            foreach ($rules as $rule) {
                $name = $rule['rule'];
                $params = $rule['params'];

                if (in_array($name, ['required', 'nullable', 'sometimes'], true)) {
                    continue; // already handled
                }

                $result = $this->validateRule($name, $attribute, $value, $params);
                if ($result !== true) {
                    $this->addError($attribute, $name, $value, $params, $result);
                    $failed = true;
                }
            }

            if (!$failed) {
                $this->validatedSet($attribute, $value);
            }
        }
    }

    /**
     * Check if a rule is present.
     * @param array<int, array{rule:string, params:array<int, string>}> $rules
     */
    protected function hasRule(array $rules, string $name): bool
    {
        foreach ($rules as $r) {
            if ($r['rule'] === $name) return true;
        }
        return false;
    }

    /**
     * Empty detection similar to Laravel (but simplified)
     */
    protected function isEmpty($value): bool
    {
        if ($value === null) return true;
        if (is_string($value)) return trim($value) === '';
        if (is_array($value)) return count($value) === 0;
        return false;
    }

    protected function isNullEquivalent($value): bool
    {
        if ($value === null) return true;
        if (is_string($value) && trim($value) === '') return true;
        return false;
    }

    /**
     * Validate a single rule and return true or an override message string.
     * @param mixed $value
     * @param array<int, string> $params
     * @return bool|string
     */
    protected function validateRule(string $name, string $attribute, $value, array $params)
    {
        switch ($name) {
            case 'string':
                return is_string($value);
            case 'integer':
                return is_int($value) || (is_string($value) && ctype_digit($value));
            case 'numeric':
                return is_numeric($value);
            case 'boolean':
                return $this->isBoolean($value);
            case 'array':
                return is_array($value);
            case 'email':
                return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
            case 'url':
                return filter_var($value, FILTER_VALIDATE_URL) !== false;
            case 'min':
                $min = isset($params[0]) ? (float)$params[0] : 0;
                return $this->checkMin($value, $min);
            case 'max':
                $max = isset($params[0]) ? (float)$params[0] : 0;
                return $this->checkMax($value, $max);
            case 'between':
                $min = isset($params[0]) ? (float)$params[0] : 0;
                $max = isset($params[1]) ? (float)$params[1] : 0;
                return $this->checkBetween($value, $min, $max);
            case 'in':
                return in_array((string)$value, array_map('strval', $params), true);
            case 'not_in':
                return !in_array((string)$value, array_map('strval', $params), true);
            case 'date':
                return $this->parseDate($value) !== null;
            case 'before':
                return $this->compareDate($value, $params[0] ?? null, '<');
            case 'after':
                return $this->compareDate($value, $params[0] ?? null, '>');
            case 'size':
                $size = isset($params[0]) ? (float)$params[0] : 0;
                return $this->checkSize($value, $size);
            case 'confirmed':
                $other = $attribute . '_confirmation';
                $otherVal = Arr::get($this->data, $other);
                return $this->valuesEqual($value, $otherVal);
            case 'same':
                $other = $params[0] ?? '';
                $otherVal = Arr::get($this->data, $other);
                return $this->valuesEqual($value, $otherVal);
            case 'different':
                $other = $params[0] ?? '';
                $otherVal = Arr::get($this->data, $other);
                return !$this->valuesEqual($value, $otherVal);
            default:
                // Unknown rule: treat as pass to avoid breaking
                return true;
        }
    }

    protected function isBoolean($value): bool
    {
        if (is_bool($value)) return true;
        if (is_int($value)) return $value === 0 || $value === 1;
        if (is_string($value)) {
            $v = strtolower($value);
            return in_array($v, ['1','0','true','false','on','off','yes','no'], true);
        }
        return false;
    }

    protected function checkMin($value, float $min): bool
    {
        if (is_numeric($value)) return (float)$value >= $min;
        if (is_string($value)) return mb_strlen($value) >= $min;
        if (is_array($value)) return count($value) >= $min;
        return false;
    }

    protected function checkMax($value, float $max): bool
    {
        if (is_numeric($value)) return (float)$value <= $max;
        if (is_string($value)) return mb_strlen($value) <= $max;
        if (is_array($value)) return count($value) <= $max;
        return false;
    }

    protected function checkBetween($value, float $min, float $max): bool
    {
        if (is_numeric($value)) {
            $v = (float)$value; return $v >= $min && $v <= $max;
        }
        if (is_string($value)) {
            $len = mb_strlen($value); return $len >= $min && $len <= $max;
        }
        if (is_array($value)) {
            $len = count($value); return $len >= $min && $len <= $max;
        }
        return false;
    }

    protected function checkSize($value, float $size): bool
    {
        if (is_numeric($value)) return (float)$value === $size;
        if (is_string($value)) return mb_strlen($value) === (int)$size;
        if (is_array($value)) return count($value) === (int)$size;
        return false;
    }

    protected function parseDate($value): ?int
    {
        if ($value === 'now') return time();
        if (is_numeric($value)) return (int)$value;
        $ts = strtotime((string)$value);
        return $ts === false ? null : $ts;
    }

    protected function compareDate($value, ?string $param, string $op): bool
    {
        $a = $this->parseDate($value);
        $b = $this->parseDate($param ?? 'now');
        if ($a === null || $b === null) return false;
        return $op === '<' ? $a < $b : $a > $b;
    }

    protected function valuesEqual($a, $b): bool
    {
        if (is_numeric($a) && is_numeric($b)) return (float)$a === (float)$b;
        return (string)$a === (string)$b;
    }

    /**
     * Add an error message for rule failure.
     * @param mixed $value
     * @param array<int, string> $params
     * @param string|bool|null $overrideMessage
     */
    protected function addError(string $attribute, string $rule, $value, array $params, $overrideMessage = null): void
    {
        $message = $this->resolveMessage($attribute, $rule, $value, $params, $overrideMessage);
        $this->errors[$attribute][] = $message;
    }

    /**
     * Resolve error message from custom or default templates and replace placeholders.
     * @param mixed $value
     * @param array<int, string> $params
     */
    protected function resolveMessage(string $attribute, string $rule, $value, array $params, $overrideMessage = null): string
    {
        // 1) Specific custom message: "attribute.rule"
        $keySpecific = $attribute . '.' . $rule;
        if (isset($this->messages[$keySpecific])) {
            $template = $this->messages[$keySpecific];
        }
        // 2) Generic custom for attribute (as per provided example)
        elseif (isset($this->messages[$attribute])) {
            $template = $this->messages[$attribute];
        }
        // 3) Override message passed from rule validator (rarely used here)
        elseif (is_string($overrideMessage)) {
            $template = $overrideMessage;
        }
        // 4) Default messages with type differentiation for string/numeric
        else {
            $typeKey = $rule;
            $type = $this->inferTypeForMessage($value);
            if (in_array($rule, ['min','max','between','size'], true)) {
                $typeKey = $rule . '.' . $type;
            }
            $template = $this->defaultMessages[$typeKey] ?? ('The ' . $attribute . ' field has invalid value.');
        }

        $replacements = [
            ':attribute' => str_replace('_', ' ', $attribute),
            ':min' => (string)($params[0] ?? ''),
            ':max' => (string)($params[1] ?? ($params[0] ?? '')),
            ':size' => (string)($params[0] ?? ''),
            ':other' => (string)($params[0] ?? ''),
            ':value' => is_scalar($value) ? (string)$value : '[value]',
            ':date' => (string)($params[0] ?? ''),
        ];

        return strtr($template, $replacements);
    }

    protected function inferTypeForMessage($value): string
    {
        if (is_numeric($value)) return 'numeric';
        if (is_array($value)) return 'string'; // arrays use string messages for now
        return 'string';
    }

    /**
     * Set validated value at attribute path
     */
    protected function validatedSet(string $attribute, $value): void
    {
        Arr::set($this->validated, $attribute, $value);
    }
}
