<?php

namespace Velis\Bpm\Ticket;

use Exception;
use InvalidArgumentException;
use Velis\App;
use Velis\Arrays;
use Velis\Bpm\Ticket\Mixin\FilesDataTrait;
use Velis\Bpm\Ticket\Service;
use Velis\Bpm\Ticket\Service\ActionResult;
use Velis\Bpm\Ticket\Service\ContextInterface;
use Velis\DuplicateRequestException;
use Velis\Exception\BusinessLogicException;
use Velis\Http\Request;
use Velis\Lang;
use Velis\Mvc\Controller\AccessException;
use Velis\Mvc\Controller\BaseController;
use Velis\Notification\Recipient;
use Velis\Output;

/**
 * Abstract base for ticket controllers
 * @author Olek Procki <olo@velis.pl>
 *
 * @property Request $request
 */
abstract class AbstractController extends BaseController implements ContextInterface
{
    use FilesDataTrait;

    /**
     * Ticket instance
     * @var Ticket
     */
    protected $_ticket;


    /**
     * Current ticket instance
     * @var Ticket
     */
    protected $_currTicket;


    /**
     * Grouped logs for single action
     * @var array
     */
    protected $_connectedLogs = [];


    /**
     * Additional notification recipients
     * @var Recipient[]
     */
    protected $_additionalRecipients = [];


    /**
     * Action log entry
     * @var Log
     */
    protected $_log;


    /**
     * Ticket post
     * @var Post
     */
    protected $_post;


    /**
     * Ticket changes
     * @var array
     */
    protected $_changes = [];


    /**
     * A flag for extended ticket details view to be used as data source
     * @var bool
     */
    protected $_useDetailsView = false;


    /**
     * Constructor
     */
    public function onConstruct()
    {
        parent::onConstruct();
        $this->view->ticketTypes = Ticket::getTypes();
    }


    /**
     * Should return ticket type id
     */
    protected function _getTicketType()
    {
        return Arrays::getFirst(explode('\\', get_class($this)));
    }


    /**
     * Pre create handler
     * @return AbstractController
     */
    protected function _preCreate()
    {
        return $this;
    }


    /**
     * Post create handler
     * @return AbstractController
     */
    protected function _postCreate()
    {
        return $this;
    }


    /**
     * Pre save handler
     * @return AbstractController
     */
    protected function _preSave()
    {
        return $this;
    }


    /**
     * Post save handler
     * @return AbstractController
     */
    protected function _postSave()
    {
        return $this;
    }


    /**
     * Pre notify handler
     * @return AbstractController
     */
    protected function _preNotify()
    {
        return $this;
    }


    /**
     * Initialize ticket object
     *
     * @param Ticket|null $ticket
     * @return $this
     * @throws InvalidArgumentException
     */
    protected function _setTicket(Ticket $ticket = null)
    {
        if (!isset($ticket)) {
            if (!is_array($this->_filter['ticket'])) {
                throw new InvalidArgumentException(Lang::get('GENERAL_NO_PARAMETERES'));
            } else {
                /** @var Service */
                $service = App::getService('ticketService');
                $this->_ticket = Factory::create(
                    $this->_filter->get('ticket', $service->getUnfilteredTicketFields()),
                    $this->_getTicketType()
                );
                $this->_filter['ticketFormData'] = $this->_filter['ticket'];
                // place ticket object in filter - it will be passed to ticketService
                // this way ticketService won't have to create ticket itself
                $this->_filter['ticket'] = $this->_ticket;
            }
        } else {
            $this->_ticket = $ticket;
        }

        return $this;
    }


    /**
     * Loads ticket data
     * @return Ticket|void
     * @throws AccessException
     */
    protected function _loadTicket()
    {
        if ($this->_filter->getInt('ticket_id')) {
            if (!$this->_ticket || $this->_useDetailsView) {
                $specs = Factory::getRegisteredSpecs();
                $class = $specs[$this->_getTicketType()];

                $this->_ticket = $class::instance(
                    $this->_filter->getInt('ticket_id'),
                    'index',
                    $this->_useDetailsView
                );

                if (!($this->_ticket instanceof Ticket)) {
                    $this->ticketNotFound();
                    return;
                }

                if (!$this->_ticket->checkAccess()) {
                    $this->ticketNotFound();
                    return;
                }

                if ($this->_ticket->ticket_type_id != $this->_getTicketType()) {
                    $this->_redirect($this->_ticket->url());
                    return;
                }
            }
        } elseif (!$this->_ticket) {
            $this->error(Lang::get('TICKET_NO_ID'), $this->url('ticket-list'));
        }
        $this->view->ticket = $this->_ticket;

        return $this->_ticket;
    }


    /**
     * Creates new ticket
     */
    public function createAction()
    {
        $this->noRender();

        if (App::$user->isLogged()) {
            $this->checkPriv('Ticket', 'Create');
        }

        try {
            // set ticket instance
            $this->_setTicket();

            $this->_executeCreateCommand();

            if (App::$user->isLogged()) {
                if ($this->isAjax()) {
                    if ($this->request->isInstant()) {
                        $this->noRender(false);
                        $this->_forward([
                            'action' => 'index',
                            'url' => $this->_ticket->url(),
                            'params' => [
                                'ticket_id' => $this->_ticket->id()
                            ],
                        ]);
                    } else {
                        $this->_ticket['url'] = $this->_ticket->url();

                        return $this->response
                            ->setJsonContent($this->_ticket->getArrayCopy())
                        ;
                    }
                } else {
                    return $this->_redirect($this->_ticket->url());
                }
            } elseif (!$this->isAjax()) {
                if (App::$registry['redirectUrl']) {
                    return $this->_redirect(App::$registry['redirectUrl']);
                }
            }
        } catch (Exception $e) {
            $message = Lang::get('GENERAL_SAVING_ERROR');

            if ($this->isAjax(true)) {
                return $this->response
                    ->setStatusCode(500, 'Error')
                    ->setContent($message)
                ;
            } else {
                $this->error($message);
                if ($this->request->isInstant()) {
                    $this->noRender(false);
                    $this->_rewind();
                } else {
                    return $this->back();
                }
            }
        }
    }


    /**
     * Executes ticket create command - to be implemented in derived controller
     */
    protected function _executeCreateCommand()
    {
        $params = $this->_filter;
        $params['files'] = $this->_getFilesData();

        // transitional solution: call service directly from controller
        // before command bus is fully implemented in lib/Velis based apps
        $this->_handleServiceResult(
            $this->ticketService->create($params, $this)
        );
    }


    /**
     * Saves ticket changes
     */
    public function saveAction()
    {
        $this->noRender();
        $this->checkPriv('Ticket', 'Edit');

        $response = [];

        try {
            if (is_array($this->_filter['ticket'])) {
                $this->_setTicket();
                if (!isset($this->_currTicket)) {
                    $this->_currTicket = $this->_ticket->getCurrentInstance();
                }

                // check access for logged user
                if (!$this->_currTicket->checkAccess()) {
                    $this->deny();
                }

                if (
                    !$this->validateTicketResponsibility(
                        $this->_filter['ticket']->getArrayCopy() + $this->_currTicket->getArrayCopy(),
                        $this->_currTicket['ticket_type_id']
                    )
                ) {
                    throw new BusinessLogicException(Lang::get('TICKET_DEPARTMENT_OR_USER'));
                }

                $this->_executeUpdateCommand();
            }
        } catch (Exception $e) {
            if (!$e instanceof DuplicateRequestException) {
                $message = Lang::get('GENERAL_SAVING_ERROR') . ': ' . $e->getMessage();

                if ($this->isAjax(true)) {
                    $response['error'] = $message;
                } else {
                    $this->error($message);
                }
            }
        }

        if ($this->isAjax()) {
            if ($this->request->isInstant()) {
                $this->noRender(false);

                if ($this->_ticket) {
                    $this->_ticket->load(true, true);
                    $this->_forward([
                        'action' => 'index',
                        'params' => [
                            'ticket_id' => $this->_ticket->id(),
                            'editAction' => 1,
                        ],
                    ]);
                } else {
                    // most probably some error occurred
                    $this->_rewind();
                }
            } else {
                $response['saved'] = !array_key_exists('error', $response);
                echo Output::jsonEncode($response);
            }
        } elseif (
            !empty($this->_filter['redirectLink'])
            && strpos($this->_filter['redirectLink'], '//') === false
            && strpos($this->_filter['redirectLink'], 'www') !== 0
        ) {
            $this->_redirect($this->_filter['redirectLink']);
        } else {
            $this->back();
        }
    }

    /**
     * Validates ticket responsibility assignment rules
     */
    protected function validateTicketResponsibility($ticketData, string $ticketTypeId): bool
    {
        $hasUser = !empty($ticketData['responsible_user_id']);
        $hasDepartment = !empty($ticketData['responsible_department_id']);
        $isProjectType = isset($ticketData['ticket_type_id']) &&
            $ticketTypeId === 'Project';

        // Case 1: Has user but no department
        $case1 = $hasUser && !$hasDepartment;

        // Case 2: Has department but no user
        $case2 = !$hasUser && $hasDepartment;

        // Case 3: Is a project type ticket
        $case3 = $isProjectType;

        // Validation succeeds if any of the cases is true
        $isValid = $case1 || $case2 || $case3;

        if (!$isValid) {
            return false;
        }

        return true;
    }


    /**
     * Executes ticket update command - to implemented in derived controller
     */
    protected function _executeUpdateCommand()
    {
        $params = $this->_filter;
        $params['files'] = $this->_getFilesData();

        // transitional solution: call service directly from controller
        // before command bus is fully implemented in lib/Velis based apps
        $this->_handleServiceResult(
            $this->ticketService->update($params, $this)
        );
    }


    /**
     * Handle ticket service result
     * @param ActionResult $result
     */
    protected function _handleServiceResult(ActionResult $result)
    {
        // Did we have any non interrupting errors?
        if ($result->didHaveErrors()) {
            foreach ($result->errors as $msg) {
                $this->warning($msg);
            }
        }
    }


    /**
     * Authorization obfuscate method
     * @throws AccessException
     */
    public function ticketNotFound()
    {
        $this->error(
            Lang::get('TICKET_NOT_FOUND') . ' ' . $this->_filter->getInt('ticket_id'),
            $this->url('ticket-list')
        );

        $e = new AccessException();

        throw $e->silent(true);
    }


    /**
     * Ticket service context pre action handler
     * @param Ticket $ticket
     */
    public function preAction(Ticket $ticket)
    {
        if ($this->_actionName == 'create') {
            $this->_preCreate();
        } else {
            $this->_preSave();
        }
    }


    /**
     * Ticket service context post action handler
     *
     * @param Ticket $ticket
     * @param Log $log
     * @param array $connectedLogs
     * @param array $changes
     */
    public function postAction(Ticket $ticket, Log $log, array &$connectedLogs, array &$changes)
    {
        $this->_log = $log;
        if ($this->_actionName == 'create') {
            $this->_postCreate();
        } else {
            $this->_postSave();
        }
        $this->_post = $ticket->getCurrentPost();

        // join controller logs/changes with service's ones
        $connectedLogs = array_merge($connectedLogs, $this->_connectedLogs);
        $changes = array_merge($changes, $this->_changes);
    }


    /**
     * Ticket service context pre notify handler
     *
     * @param Ticket $ticket
     * @param Log $log
     */
    public function preNotify(Ticket $ticket, Log $log)
    {
        $this->_preNotify();
        // variable name typo intentionally kept due to existing code base
        if ($this->_additionalRecipients) {
            $log->addRecipients($this->_additionalRecipients);
        }
    }
}
