<?php

namespace Velis\Bpm;

use Exception;
use Psr\SimpleCache\InvalidArgumentException;
use ReflectionException;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Workflow\Action;
use Velis\Bpm\Workflow\Event;
use Velis\Bpm\Workflow\Handler;
use Velis\Bpm\Workflow\Subject;
use Velis\Bpm\Workflow\Validator;
use Velis\Model\Cacheable;
use Velis\Model\DataObject;
use Velis\Model\Routable;
use Velis\Output;
use Velis\ParameterBag;

/**
 * Workflow rule model
 * @author Olek Procki <olo@velis.pl>
 */
class Workflow extends DataObject implements Cacheable, Routable
{
    public const PARAM_USER = 'user';
    public const PARAM_COMPANY = 'company';
    public const PARAM_NUMERIC = 'numeric';
    public const PARAM_INT = 'int';
    public const PARAM_DATE = 'date';
    public const PARAM_STRING = 'string';
    public const PARAM_TEXT = 'text';
    public const PARAM_ROLE = 'role';


    /**
     * Default order for workflow list
     * @var string
     */
    protected static $_listDefaultOrder = 'sort_order, name';


    /**
     * Filter list params by default
     * @var bool
     */
    protected static $_filterListParams = true;

    /**
     * Indicates whether the workflow is currently executing
     * @var bool
     */
    protected static bool $isRunning = false;

    /**
     * Actions to be triggered
     * @var Action[]
     */
    protected $_actions;


    /**
     * Handler objects
     * @var Handler[]
     */
    protected $_handlers;


    /**
     * Workflow conditions
     * @var array
     */
    protected $_conditions;


    /**
     * Event invoked by EventsManager
     * @var \Phalcon\Events\Event;
     */
    protected $_invokedEvent;


    /**
     * Is invoked before or after event
     * @var string
     */
    protected $_invocationTime;


    /**
     * Returns related table name
     * @return string
     */
    protected function _getTableName()
    {
        return 'app.workflow_tab';
    }


    /**
     * Return workflow rule name
     * @return string
     */
    public function getName()
    {
        return $this['name'];
    }


    /**
     * Returns workflow rule route name
     * @return string
     */
    public function getRouteName()
    {
        return 'workflow';
    }


    /**
     * Returns standard url without rewrite route
     * @return string
     */
    public function getStandardUrl()
    {
        return '/workflow/index?workflow_id=' . $this->id();
    }


    /**
     * {@inheritDoc}
     */
    public static function getList($page = 1, $params = null, $order = null, $limit = self::ITEMS_PER_PAGE, $fields = null)
    {
        $params = new ParameterBag($params);

        if ($params['search']) {
            self::$_listConditions[] = 'name ILIKE :search';
            self::$_listParams['search'] = '%' . $params['search'] . '%';
        }

        return parent::getList($page, $params, $order, $limit, $fields);
    }


    /**
     * {@inheritDoc}
     */
    public function add($updateObjectId = true)
    {
        if (!isset($this['added_by_user_id'])) {
            $this['added_by_user_id'] = App::$user->id();
        }
        $this['sort_order'] = self::getMaxSortOrder() + 1;

        return parent::add($updateObjectId);
    }


    /**
     * Gets the highest sort order value
     *
     * @return int
     */
    private function getMaxSortOrder()
    {
        return self::$_db->getOne('SELECT MAX(sort_order) FROM app.workflow_tab');
    }


    /**
     * {@inheritDoc}
     * @param array|null $params
     */
    public function save($params = null)
    {
        if (
            is_array($params) &&
            array_key_exists('conditions', $params) &&
            array_key_exists('actions', $params)
        ) {
            $conditions = $params['conditions'];
            $actions = $params['actions'];
        } elseif (func_num_args() > 1) {
            list($conditions, $actions) = func_get_args();
        }

        try {
            self::$_db->startTrans();

            if (is_array($conditions)) {
                foreach ($conditions as $field => $values) {
                    if (Arrays::countDimensions($values) == 1) {
                        $conditionsFiltered = array_unique(Arrays::filterKeepZeros($values));
                    } else {
                        $conditionsFiltered = array(
                            array_unique(Arrays::filterKeepZeros($values[0])),
                            array_unique(Arrays::filterKeepZeros($values[1]))
                        );
                    }
                    if (!$conditionsFiltered) {
                        unset($conditions[$field]);
                    } else {
                        $conditions[$field] = $conditionsFiltered;
                    }
                }

                $this->_conditions = $conditions;
                $this['conditions'] = Output::jsonEncode($conditions);
            }

            parent::save(true);

            if (is_array($actions)) {
                $this->saveActions($actions);
            }

            self::$_db->commit();
        } catch (Exception $e) {
            self::$_db->rollback();
            throw $e;
        }

        return $this;
    }


    /**
     * Saves workflow attached actions
     *
     * @param array $actions
     * @throws DataObject\NoColumnsException
     */
    public function saveActions($actions)
    {
        $commit = self::$_db->startTrans();

        try {
            self::$_db->execDML(
                "DELETE FROM app.workflow_action_tab WHERE workflow_id=:workflow_id",
                $this->_getPrimaryKeyParam()
            );

            if ($actions) {
                foreach ($actions as $actionData) {
                    $action = new Action($actionData);
                    $action->setParams($actionData['params']);
                    $action['workflow_id'] = $this->id();

                    $action->add();
                }
            }

            if ($commit) {
                self::$_db->commit();
            }
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }
    }


    /**
     * Removes workflow rule with its actions
     *
     * @return bool
     * @throws Exception
     */
    public function remove()
    {
        $commit = self::$_db->startTrans();
        try {
            $sortOrder = $this->sort_order;
            self::$_db->execDML(
                "DELETE FROM app.workflow_action_tab WHERE workflow_id=:workflow_id",
                $this->_getPrimaryKeyParam()
            );
            parent::_remove();

            self::$_db->execDML(
                'UPDATE app.workflow_tab SET sort_order = sort_order - 1 WHERE sort_order > :to_delete',
                ['to_delete' => $sortOrder]
            );

            if ($commit) {
                self::$_db->commit();
            }
        } catch (Exception $e) {
            if ($commit) {
                self::$_db->rollback();
            }
            throw $e;
        }

        return true;
    }


    /**
     * Creates a rule duplicate
     *
     * @param string $name
     * @throws ReflectionException
     */
    public function duplicate($name)
    {
        self::$_db->startTrans();

        try {
            $duplicate = self::instance($this->id());
            $actions   = $this->getActions();

            unset(
                $duplicate['workflow_id'],
                $duplicate['date_added']
            );
            $duplicate['name'] = $name;
            $duplicate->add();

            foreach ($actions as $action) {
                $action['workflow_id'] = $duplicate->id();
                unset($action['workflow_action_id']);

                $action->add();
            }

            self::$_db->commit();

            return $duplicate;
        } catch (Exception $e) {
            self::$_db->rollback();
            throw $e;
        }
    }


    /**
     * Returns event invoked by EventsManager
     * @retury \Phalcon\Events\Event
     */
    public function getInvokedEvent()
    {
        return $this->_invokedEvent;
    }


    /**
     * Sets event invoked by EventsManager
     *
     * @param \Phalcon\Events\Event $event
     * @param string $beforeOrAfter
     */
    public function setInvokedEvent($event, $beforeOrAfter)
    {
        $this->_invokedEvent   = $event;
        $this->_invocationTime = $beforeOrAfter;

        return $this;
    }


    /**
     * Returns true if is before event execution
     * @return bool
     */
    public function isInvokedBeforeEvent()
    {
        if ($this->_invocationTime) {
            return $this->_invocationTime == Event::BEFORE;
        } elseif ($this->getInvokedEvent()) {
            return stripos($this->getInvokedEvent()->getType(), Event::BEFORE) === 0;
        }
        return false;
    }


    /**
     * Returns true if is after event execution
     * @return bool
     */
    public function isInvokedAfterEvent()
    {
        if ($this->_invocationTime) {
            return $this->_invocationTime == Event::AFTER;
        } elseif ($this->getInvokedEvent()) {
            return stripos($this->getInvokedEvent()->getType(), Event::AFTER) === 0;
        }
        return false;
    }


    /**
     * Returns event
     * @return Event
     * @throws InvalidArgumentException
     */
    public function getEvent()
    {
        return Event::get($this->workflow_event_id);
    }


    /**
     * Returns action
     * @return Action[]
     * @throws ReflectionException
     */
    public function getActions()
    {
        if (!isset($this->_actions)) {
            $this->_actions = array();
            $i = 1;
            foreach (Action::listAll($this->_getPrimaryKeyParam()) as $action) {
                $action->setWorkflow($this);
                $action['action_no'] = $i++;

                $this->_actions[] = $action;
            }
        }

        return $this->_actions;
    }


    /**
     * Returns attached actions handlers
     *
     * @param string $beforeOrAfter
     * @return Handler[]
     * @throws ReflectionException
     */
    public function getHandlers($beforeOrAfter = null)
    {
        $this->_handlers = [];

        foreach ($this->getActions() as $action) {
            $handler = $action->getHandler();
            if ($handler instanceof Handler) {
                $this->_handlers[] = $handler;
            }
        }

        if (!$beforeOrAfter) {
            return $this->_handlers;
        } else {
            $result = [];
            foreach ($this->_handlers as $handler) {
                $action = $handler->getAction();

                if ($beforeOrAfter == Event::BEFORE && $action['is_before']) {
                    $result[] = $handler;
                } elseif ($beforeOrAfter == Event::AFTER && !$action['is_before']) {
                    $result[] = $handler;
                }
            }
            return $result;
        }
    }

    /**
     * Returns decoded conditions
     * @return array|null
     */
    public function getConditions(): ?array
    {
        if (!isset($this->_conditions)) {
            $this->_conditions = Output::jsonDecode($this->conditions);
        }

        return $this->_conditions;
    }

    /**
     * Returns true if $field present in conditions
     *
     * @param string $field
     * @return bool
     */
    public function hasCondition($field): bool
    {
        $conditions = $this->getConditions() ?? [];

        return array_key_exists($field, $conditions);
    }


    /**
     * Returns enabled handlers
     * @return Workflow[]
     */
    public static function listEnabled()
    {
        $cached = self::listCached();

        return Arrays::byValue($cached, 'enabled', 1);
    }


    /**
     * Returns workflow item grouped by event
     * @return array
     */
    public static function listGroupped()
    {
        $result = array();
        foreach (self::listCached() as $workflow) {
            $result[$workflow['workflow_event_id']][$workflow->id()] = $workflow;
        }

        return $result;
    }


    /**
     * Attaches enabled event handlers
     * @throws InvalidArgumentException
     */
    public static function setup()
    {
        foreach (static::listEnabled() as $workflow) {
            foreach ([Event::BEFORE, Event::AFTER] as $beforeOrAfter) {
                App::getEvents()->attach(
                    $workflow->getEvent()->identifier($beforeOrAfter),
                    function ($event, $subject) use ($workflow, $beforeOrAfter) {

                        $workflow->setInvokedEvent($event, $beforeOrAfter);

                        $additionalData = $event->getData();
                        $previous = $workflow->getEvent()->is_change ? $additionalData : null;

                        if ($workflow->validate($subject, $previous)) {
                            $workflow->executeHandlers($subject, $additionalData, $beforeOrAfter);
                        }
                    }
                );
            }
        }
    }


    /**
     * Executes all attached action triggers
     *
     * @param object $subject
     * @param mixed $additionalData
     * @param string $beforeOrAfter
     * @throws ReflectionException
     */
    public function executeHandlers($subject, $additionalData, $beforeOrAfter = Event::AFTER)
    {
        $restore = App::$di['db']->checkDuplicatedQueries(false);
        foreach ($this->getHandlers($beforeOrAfter) as $handler) {
            self::$isRunning = true;
            $handler->run($subject, $additionalData);
            self::$isRunning = false;
        }
        App::$di['db']->checkDuplicatedQueries($restore);
    }


    /**
     * Validates conditions compliance
     *
     * @param Subject $subject
     * @param object $previous
     *
     * @return bool
     */
    public function validate($subject, $previous = null)
    {
        $validator = new Validator($this, $subject, $previous);

        return $validator->validate();
    }


    public static function isRunning(): bool
    {
        return self::$isRunning;
    }


    /**
     * Sorts conditions by name (or validation cost if $subject provided)
     *
     * @param array $conditions
     * @param Subject|null $subject
     */
    public static function sortConditions(array &$conditions, Subject $subject = null)
    {
        if ($subject) {
            // sort conditions by validation cost
            // (put conditions validated by method at the end of validation chain)
            $conditionFields = array_keys($conditions);

            usort(
                $conditionFields,
                function ($a, $b) use ($subject) {
                    $aMethod = method_exists($subject, 'validate' . Output::toPascalCase($a));
                    $bMethod = method_exists($subject, 'validate' . Output::toPascalCase($b));

                    if ($aMethod == $bMethod) {
                        return 0;
                    }
                    return ($aMethod < $bMethod) ? -1 : 1;
                }
            );
            $conditionsSorted = [];
            foreach ($conditionFields as $field) {
                $conditionsSorted[$field] = $conditions[$field];
            }
            $conditions = $conditionsSorted;
        } else {
            uasort(
                $conditions,
                function ($a, $b) {
                    return strcasecmp($a['label'], $b['label']);
                }
            );
        }
    }


    /**
     * Enables workflow rule
     * @return Workflow
     * @throws Exception
     */
    public function enable()
    {
        $this['enabled'] = 1;
        return $this->modify();
    }


    /**
     * Enables workflow rule
     * @return Workflow
     * @throws Exception
     */
    public function disable()
    {
        $this['enabled'] = 0;
        return $this->modify();
    }


    /**
     * Moves workflow (changes sort order)
     * @param Workflow|int $previous
     * @param Workflow|int $following
     * @throws InvalidArgumentException
     */
    public function move($previous, $following)
    {
        self::$_db->startTrans();

        try {
            if ($previous && !($previous instanceof self)) {
                $previous = self::get($previous);
            }

            if ($following && !($following instanceof self)) {
                $following = self::get($following);
            }

            $initialSortOrder = $this->sort_order;
            $followingSortOrder = $following ? $following->sort_order : ($previous->sort_order + 1);

            if ($initialSortOrder >= $followingSortOrder) {
                $sign = '+';
            } else {
                $sign = '-';
                $followingSortOrder--;
            }

            self::$_db->execDML(
                'UPDATE app.workflow_tab SET sort_order = sort_order' . $sign . '1
                WHERE (sort_order BETWEEN :from AND :to) OR (sort_order BETWEEN :to AND :from)',
                ['from' => $this->sort_order, 'to' => $followingSortOrder]
            );

            $this->sort_order = $followingSortOrder;
            $this->modify();

            self::$_db->commit();
            return $this;
        } catch (Exception $e) {
            self::$_db->rollback();
            throw $e;
        }
    }
}
