<?php

namespace Velis\Bpm\Workflow;

use DateTime;
use Exception;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Workflow;
use Velis\Filter;
use Velis\Output;

/**
 * Workflow conditions validator
 * @author Olek Procki <olo@velis.pl>
 */
class Validator
{
    /**
     * Workflow object
     * @var Workflow
     */
    protected $_workflow;

    /**
     * Validation subject
     * @var Subject
     */
    protected $_subject;

    /**
     * Validation subject
     * @var Subject
     */
    protected $_previous;

    /**
     * Constructor
     * @param Workflow $workflow
     * @param Subject $subject
     * @param Subject $previous
     */
    public function __construct($workflow, $subject, $previous = null)
    {
        $this->_workflow = $workflow;
        $this->_subject  = $subject;
        $this->_previous = $previous;
    }


    /**
     * Validates conditions compliance
     * @return bool
     */
    public function validate()
    {
        $conditions      = $this->_workflow->getConditions();
        $conditionsValid = 0;

        if (!$conditions) {
            return true;
        }

        // sort conditions by validation cost
        Workflow::sortConditions($conditions, $this->_subject);

        foreach ($conditions as $field => $condition) {
            if ($this->_validateField($field, $condition)) {
                if (!$this->_workflow->require_all_conditions) {
                    return true;
                }
                $conditionsValid++;
            } else {
                if ($this->_workflow->require_all_conditions) {
                    return false;
                }
            }
        }

        return $conditionsValid === count($conditions);
    }


    /**
     * Checks if change validation is required
     *
     * @param mixed $condition
     * @return bool
     */
    protected function _isChangeValidationRequired($condition)
    {
        $event = $this->_workflow->getEvent();

        if (!$event->is_change) {
            return false;
        }

        if (
            is_array($condition)
            && Arrays::countDimensions($condition) > 1
            && is_object($this->_previous)
        ) {
            return true;
        }

        return false;
    }


    /**
     * Validates subject field
     *
     * @param string $field
     * @param mixed $condition
     *
     * @return bool
     */
    protected function _validateField($field, $condition)
    {
        if ($field == 'week_day_and_time') {
            foreach ($condition as $c) {
                $search = ['“', '”', '‘', '’', '`'];
                $replace = ['"', '"', "'", "'", "'"];
                $json = str_replace($search, $replace, $c);
                $data = json_decode($json, true);
                if (self::validateDateOrTimeCondition($data)) {
                    return true;
                }
            }
            return false;
        } else if ($this->_isChangeValidationRequired($condition)) {
            return $this->_validateFieldChange($field, $condition);
        } else {
            return $this->_validateFieldValue($this->_subject, $field, $condition);
        }
    }


    /**
     * Validates field change
     *
     * @param type $field
     * @param type $condition
     *
     * @return bool
     */
    protected function _validateFieldChange($field, $condition)
    {
        $method = 'get' . Output::toPascalCase($field);

        if (method_exists($this->_subject, $method) && method_exists($this->_previous, $method)) {
             // check if field value has changed
            if ($this->_subject->$method() == $this->_previous->$method()) {
                return false;
            }
        } else if ($this->_subject->$field == $this->_previous->$field) {
            return false;
        }

        if (!(empty($condition[0]) || $this->_validateFieldValue($this->_previous, $field, $condition[0]))) {
            return false;
        }

        return empty($condition[1]) || $this->_validateFieldValue($this->_subject, $field, $condition[1]);
    }


    /**
     * Validates subject field
     *
     * @param object $object
     * @param string $field
     * @param mixed $condition
     *
     * @return bool
     */
    protected function _validateFieldValue($object, $field, $condition)
    {
        $validationMethod = 'validate' . Output::toPascalCase($field);

        if (method_exists($object, $validationMethod)) {
            return $object->$validationMethod($condition, $this->_workflow);
        } elseif (isset($object->$field)) {
            return self::validateValue($object->$field, $condition);
        } else {
            return false;
        }
    }


    /**
     * Validates single field condition
     *
     * @param mixed $value
     * @param mixed $condition
     *
     * @return bool
     */
    public static function validateValue($value, $condition)
    {
        if (is_array($condition)) {
            if (in_array($value, $condition)) {
                return true;
            } else {
                return self::validateStringValue($value, $condition);
            }
        } elseif (strpos($condition, '%') !== false) {
            return self::validateStringValue($value, $condition);
        } else {
            return (string)$value === (string)$condition;
        }
    }


    /**
     * Validates text value
     *
     * @param string $value
     * @param string $condition
     *
     * @return bool
     */
    public static function validateStringValue($value, $condition)
    {
        if (!is_array($condition)) {
            $condition = array($condition);
        }

        foreach ($condition as $c) {
            if (!is_string($c)) {
                return false;
            }

            if ($c == $value && strpos($c, '%') === false) {
                return true;
            }

            $rawCondition = trim($c, '%');

            if (Filter::startsWith($c, '%') && Filter::endsWith($c, '%')) {
                if (stripos($value, $rawCondition) !== false) {
                    return true;
                }
            } elseif (Filter::startsWith($c, '%') && Filter::endsWith($value, $rawCondition)) {
                return true;
            } elseif (Filter::endsWith($c, '%') && Filter::startsWith($value, $rawCondition)) {
                return true;
            }
        }

        return false;
    }


    /**
     * Validates day of the week
     *
     * @param int|array $value Day of the week (0-6, where 0 = Sunday, 6 = Saturday) or array of days
     * @return bool
     */
    public static function validateWeekDays($value)
    {
        // Convert single value to array for uniform processing
        $days = is_array($value) ? $value : [$value];

        // Validate all days in the array (0-6 range)
        $validDays = array_filter($days, function ($day) {
            return is_numeric($day) && $day >= 0 && $day <= 6;
        });

        // If no valid days found, return false
        if (empty($validDays)) {
            return false;
        }

        // Get timezone from App
        $timezone = App::getTimezone();

        try {
            // Create DateTime object with the app's timezone
            $dateTime = new DateTime('now', $timezone);

            // Get current day of week (0 = Sunday, 6 = Saturday)
            $currentDayOfWeek = (int)$dateTime->format('w');

            // Check if current day matches any of the provided days
            return in_array($currentDayOfWeek, array_map('intval', $validDays));

        } catch (Exception $e) {
            // If timezone is invalid or other error occurs, return false
            return false;
        }
    }

    /**
     * Validates time window
     *
     * @param array $value Array with 'timeFrom' and 'timeTo' keys in "HH:MM" format
     * @return bool
     */
    public static function validateTimeWindow($value)
    {
        // Validate input structure
        if (!is_array($value) || !isset($value['timeFrom']) || !isset($value['timeTo'])) {
            return true;
        }

        $timeFrom = $value['timeFrom'];
        $timeTo = $value['timeTo'];

        // Validate time format
        if (!preg_match('/^(\d{1,2}):(\d{2})$/', $timeFrom, $fromMatches) ||
            !preg_match('/^(\d{1,2}):(\d{2})$/', $timeTo, $toMatches)) {
            return false;
        }

        $startHour = (int)$fromMatches[1];
        $startMinute = (int)$fromMatches[2];
        $endHour = (int)$toMatches[1];
        $endMinute = (int)$toMatches[2];

        // Get timezone from App
        $timezone = App::getTimezone();
        // Create DateTime object with the app's timezone
        $dateTime = new DateTime('now', $timezone);
        // Validate time values
        try {
            $startTimeObj = new DateTime($dateTime->format('Y-m-d') . ' ' . $timeFrom, $timezone);
            $endTimeObj = new DateTime($dateTime->format('Y-m-d') . ' ' . $timeTo, $timezone);
        } catch (Exception $e) {
            return false;
        }

        try {
            $currentTime = $dateTime->format('H:i');
            $startTime = sprintf('%02d:%02d', $startHour, $startMinute);
            $endTime = sprintf('%02d:%02d', $endHour, $endMinute);

            return ($currentTime >= $startTime && $currentTime <= $endTime);

        } catch (Exception $e) {
            // If timezone is invalid or other error occurs, return false
            return false;
        }
    }

    /**
     * Checks if time window fields are present
     *
     * @param array $data
     * @return bool
     */
    private static function hasRequiredFields(array $data): bool
    {
        return isset($data['weekDays']);
    }


    /**
     * Validates date or time condition data
     *
     * @param array $data
     * @return bool
     */
    private static function validateDateOrTimeCondition(array $data): bool
    {
        return self::hasRequiredFields($data)
            && self::validateTimeWindow($data)
            && self::validateWeekDays($data['weekDays']);
    }

}
