<?php

namespace Icinga\Module\Director\Web\Controller;

use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Exception\NotFoundError;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Director\Data\Db\DbObjectStore;
use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry;
use Icinga\Module\Director\Forms\IcingaMultiEditForm;
use Icinga\Module\Director\Objects\IcingaCommand;
use Icinga\Module\Director\Objects\IcingaHost;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Module\Director\Objects\IcingaService;
use Icinga\Module\Director\RestApi\IcingaObjectsHandler;
use Icinga\Module\Director\Web\ActionBar\ObjectsActionBar;
use Icinga\Module\Director\Web\ActionBar\TemplateActionBar;
use Icinga\Module\Director\Web\Controller\Extension\ObjectRestrictions;
use Icinga\Module\Director\Web\Form\FormLoader;
use Icinga\Module\Director\Web\Table\ApplyRulesTable;
use Icinga\Module\Director\Web\Table\ObjectSetTable;
use Icinga\Module\Director\Web\Table\ObjectsTable;
use Icinga\Module\Director\Web\Table\TemplatesTable;
use Icinga\Module\Director\Web\Tabs\ObjectsTabs;
use Icinga\Module\Director\Web\Tree\TemplateTreeRenderer;
use gipfl\IcingaWeb2\Link;
use Icinga\Module\Director\Web\Widget\AdditionalTableActions;
use Icinga\Module\Director\Web\Widget\BranchedObjectsHint;
use InvalidArgumentException;
use Ramsey\Uuid\Uuid;

abstract class ObjectsController extends ActionController
{
    use BranchHelper;
    use ObjectRestrictions;

    protected $isApified = true;

    /** @var ObjectsTable */
    protected $table;

    protected function checkDirectorPermissions()
    {
        if ($this->getRequest()->getActionName() !== 'sets') {
            $this->assertPermission('director/' . $this->getPluralBaseType());
        }
    }

    /**
     * @return $this
     * @throws \Icinga\Exception\Http\HttpNotFoundException
     */
    protected function addObjectsTabs()
    {
        $tabName = $this->getRequest()->getActionName();
        if (substr($this->getType(), -5) === 'Group') {
            $tabName = 'groups';
        }
        $this->tabs(new ObjectsTabs(
            $this->getBaseType(),
            $this->Auth(),
            $this->getBaseObjectUrl()
        ))->activate($tabName);

        return $this;
    }

    /**
     * @return IcingaObjectsHandler
     * @throws NotFoundError
     */
    protected function apiRequestHandler()
    {
        $request = $this->getRequest();
        $table = $this->getTable();
        if (
            $request->getControllerName() === 'services'
            && $hostName = $this->params->get('host')
        ) {
            $host = IcingaHost::load($hostName, $this->db());
            if (! $this->allowsObject($host)) {
                throw new NotFoundError(sprintf('Failed to load %s "%s"', $host->getTableName(), $hostName));
            }

            $table->getQuery()->where('o.host_id = ?', $host->get('id'));
        }

        if ($request->getActionName() === 'templates') {
            $table->filterObjectType('template');
        } elseif ($request->getActionName() === 'applyrules') {
            $table->filterObjectType('apply');
        }
        /** @var ?string $search */
        $search = $this->params->get('q');
        if ($search) {
            $table->search($search);
        }

        return (new IcingaObjectsHandler(
            $request,
            $this->getResponse(),
            $this->db()
        ))->setTable($table);
    }

    /**
     * @throws \Icinga\Exception\Http\HttpNotFoundException
     * @throws NotFoundError
     */
    public function indexAction()
    {
        if ($this->getRequest()->isApiRequest()) {
            $this->apiRequestHandler()->dispatch();
            return;
        }

        $type = $this->getType();
        if ($this->params->get('format') === 'json') {
            $filename = sprintf(
                "director-{$type}_%s.json",
                date('YmdHis')
            );
            $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
            $this->apiRequestHandler()->dispatch();
            return;
        }

        $this
            ->addObjectsTabs()
            ->setAutorefreshInterval(10)
            ->addTitle($this->translate(ucfirst($this->getPluralType())))
            ->actions(new ObjectsActionBar($this->getBaseObjectUrl(), $this->url()));

        $this->content()->add(new BranchedObjectsHint($this->getBranch(), $this->Auth(), $this->hasPreferredBranch()));

        if ($type === 'command' && $this->params->get('type') === 'external_object') {
            $this->tabs()->activate('external');
        }

        // Hint: might be used in controllers extending this
        $this->table = $this->eventuallyFilterCommand($this->getTable());

        $this->table->renderTo($this);
        (new AdditionalTableActions($this->getAuth(), $this->url(), $this->table))
            ->appendTo($this->actions());
    }

    /**
     * @return ObjectsTable
     */
    protected function getTable()
    {
        $table = ObjectsTable::create($this->getType(), $this->db(), $this->getAuth())
            ->setBranchUuid($this->getBranchUuid())
            ->setBaseObjectUrl($this->getBaseObjectUrl());

        return $table;
    }

    /**
     * @return ApplyRulesTable
     * @throws NotFoundError
     */
    protected function getApplyRulesTable()
    {
        $table = (new ApplyRulesTable($this->db()))->setBranch($this->getBranch());
        $table->setType($this->getType())
            ->setBaseObjectUrl($this->getBaseObjectUrl());
        $this->eventuallyFilterCommand($table);

        return $table;
    }

    /**
     * @throws NotFoundError
     */
    public function edittemplatesAction()
    {
        $this->commonForEdit();
    }

    /**
     * @throws NotFoundError
     */
    public function editAction()
    {
        $this->commonForEdit();
    }

    /**
     * @throws NotFoundError
     */
    public function commonForEdit()
    {
        $type = ucfirst($this->getType());

        if (empty($this->multiEdit)) {
            throw new NotFoundError('Cannot edit multiple "%s" instances', $type);
        }

        $objects = $this->loadMultiObjectsFromParams();
        if (empty($objects)) {
            throw new NotFoundError('No "%s" instances have been loaded', $type);
        }
        $formName = 'icinga' . $type;
        $form = IcingaMultiEditForm::load()
            ->setBranch($this->getBranch())
            ->setObjects($objects)
            ->pickElementsFrom($this->loadForm($formName), $this->multiEdit);
        if ($type === 'Service') {
            $form->setListUrl('director/services');
        } elseif ($type === 'Host') {
            $form->setListUrl('director/hosts');
        }

        $form->handleRequest();

        $this
            ->addSingleTab($this->translate('Multiple objects'))
            ->addTitle(
                $this->translate('Modify %d objects'),
                count($objects)
            )->content()->add($form);
    }

    /**
     * Loads the TemplatesTable or the TemplateTreeRenderer
     *
     * Passing render=tree switches to the tree view.
     * @throws \Icinga\Exception\Http\HttpNotFoundException
     * @throws \Icinga\Security\SecurityException
     * @throws NotFoundError
     */
    public function templatesAction()
    {
        if ($this->getRequest()->isApiRequest()) {
            $this->apiRequestHandler()->dispatch();
            return;
        }
        $type = $this->getType();

        if ($this->params->get('format') === 'json') {
            $filename = sprintf(
                "director-{$type}-templates_%s.json",
                date('YmdHis')
            );
            $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
            $this->apiRequestHandler()->dispatch();
            return;
        }

        $shortType = IcingaObject::createByType($type)->getShortTableName();
        $this
            ->assertPermission('director/admin')
            ->addObjectsTabs()
            ->setAutorefreshInterval(10)
            ->addTitle(
                $this->translate('All your %s Templates'),
                $this->translate(ucfirst($type))
            )
            ->actions(new TemplateActionBar($shortType, $this->url()));

        if ($this->params->get('render') === 'tree') {
            TemplateTreeRenderer::showType($shortType, $this, $this->db());
        } else {
            $table = TemplatesTable::create($shortType, $this->db());
            $this->eventuallyFilterCommand($table);
            $table->renderTo($this);
            (new AdditionalTableActions($this->getAuth(), $this->url(), $table))
                ->appendTo($this->actions());
        }
    }

    /**
     * @return $this
     * @throws \Icinga\Security\SecurityException
     */
    protected function assertApplyRulePermission()
    {
        return $this->assertPermission('director/admin');
    }

    /**
     * @throws \Icinga\Exception\Http\HttpNotFoundException
     * @throws \Icinga\Security\SecurityException
     * @throws NotFoundError
     */
    public function applyrulesAction()
    {
        if ($this->getRequest()->isApiRequest()) {
            $this->apiRequestHandler()->dispatch();
            return;
        }

        $type = $this->getType();

        if ($this->params->get('format') === 'json') {
            $filename = sprintf(
                "director-{$type}-applyrules_%s.json",
                date('YmdHis')
            );
            $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true);
            $this->apiRequestHandler()->dispatch();
            return;
        }

        $tType = $this->translate(ucfirst($type));
        $this
            ->assertApplyRulePermission()
            ->addObjectsTabs()
            ->setAutorefreshInterval(10)
            ->addTitle(
                $this->translate('All your %s Apply Rules'),
                $tType
            );
        $baseUrl = 'director/' . $this->getBaseObjectUrl();
        $this->actions()
            //->add($this->getBackToDashboardLink())
            ->add(
                Link::create(
                    $this->translate('Add'),
                    "{$baseUrl}/add",
                    ['type' => 'apply'],
                    [
                        'title' => sprintf(
                            $this->translate('Create a new %s Apply Rule'),
                            $tType
                        ),
                        'class' => 'icon-plus',
                        'data-base-target' => '_next'
                    ]
                )
            );

        $table = $this->getApplyRulesTable();
        $table->renderTo($this);
        (new AdditionalTableActions($this->getAuth(), $this->url(), $table))
            ->appendTo($this->actions());
    }

    /**
     * @throws NotFoundError
     * @throws \Icinga\Exception\Http\HttpNotFoundException
     * @throws \Icinga\Security\SecurityException
     */
    public function setsAction()
    {
        $type = $this->getType();
        $tType = $this->translate(ucfirst($type));
        $this
            ->assertPermission('director/' . $this->getBaseType() . 'sets')
            ->addObjectsTabs()
            ->requireSupportFor('Sets')
            ->setAutorefreshInterval(10)
            ->addTitle(
                $this->translate('Icinga %s Sets'),
                $tType
            );

        $this->actions()->add(
            Link::create(
                $this->translate('Add'),
                "director/{$type}set/add",
                null,
                [
                    'title' => sprintf(
                        $this->translate('Create a new %s Set'),
                        $tType
                    ),
                    'class' => 'icon-plus',
                    'data-base-target' => '_next'
                ]
            )
        );

        ObjectSetTable::create($type, $this->db(), $this->getAuth())
            ->setBranch($this->getBranch())
            ->renderTo($this);
    }

    /**
     * @return array
     * @throws NotFoundError
     */
    protected function loadMultiObjectsFromParams()
    {
        $filter = Filter::fromQueryString($this->params->toString());
        $type = $this->getType();
        $objects = array();
        $db = $this->db();
        $class = DbObjectTypeRegistry::classByType($type);
        $table = DbObjectTypeRegistry::tableNameByType($type);
        $store = new DbObjectStore($db, $this->getBranch());

        /** @var $filter FilterChain */
        foreach ($filter->filters() as $sub) {
            /** @var $sub FilterChain */
            foreach ($sub->filters() as $ex) {
                /** @var $ex FilterChain|FilterExpression */
                $col = $ex->getColumn();
                if ($ex->isExpression()) {
                    if ($col === 'name') {
                        $name = $ex->getExpression();
                        if ($type === 'service') {
                            $key = [
                                'object_type' => 'template',
                                'object_name' => $name
                            ];
                        } else {
                            $key = $name;
                        }
                        $objects[$name] = $class::load($key, $db);
                    } elseif ($col === 'id') {
                        $name = $ex->getExpression();
                        $objects[$name] = $class::load($name, $db);
                    } elseif ($col === 'uuid') {
                        $object = $store->load($table, Uuid::fromString($ex->getExpression()));
                        if ($object instanceof IcingaService) {
                            $host = $object->getRelated('host');
                            $objects[$host->getObjectName() . ': ' . $object->getObjectName()] = $object;
                        } else {
                            $objects[$object->getObjectName()] = $object;
                        }
                    } else {
                        throw new InvalidArgumentException("'$col' is no a valid key component for '$type'");
                    }
                }
            }
        }

        return $objects;
    }

    /**
     * @param string $name
     *
     * @return \Icinga\Module\Director\Web\Form\QuickForm
     */
    public function loadForm($name)
    {
        $form = FormLoader::load($name, $this->Module());
        if ($this->getRequest()->isApiRequest()) {
            // TODO: Ask form for API support?
            $form->setApiRequest();
        }

        return $form;
    }

    /**
     * @param ZfQueryBasedTable $table
     * @return ZfQueryBasedTable
     * @throws NotFoundError
     */
    protected function eventuallyFilterCommand(ZfQueryBasedTable $table)
    {
        if ($this->params->get('command')) {
            $command = IcingaCommand::load($this->params->get('command'), $this->db());
            switch ($this->getBaseType()) {
                case 'host':
                case 'service':
                    $table->getQuery()->where(
                        $this->db()->getDbAdapter()->quoteInto(
                            '(o.check_command_id = ? OR o.event_command_id = ?)',
                            $command->getAutoincId()
                        )
                    );
                    break;
                case 'notification':
                    $table->getQuery()->where(
                        'o.command_id = ?',
                        $command->getAutoincId()
                    );
                    break;
            }
        }

        return $table;
    }

    /**
     * @param $feature
     * @return $this
     * @throws NotFoundError
     */
    protected function requireSupportFor($feature)
    {
        if ($this->supports($feature) !== true) {
            throw new NotFoundError(
                '%s does not support %s',
                $this->getType(),
                $feature
            );
        }

        return $this;
    }

    /**
     * @param $feature
     * @return bool
     */
    protected function supports($feature)
    {
        $func = "supports$feature";
        return IcingaObject::createByType($this->getType())->$func();
    }

    /**
     * @return string
     */
    protected function getBaseType()
    {
        $type = $this->getType();
        if (substr($type, -5) === 'Group') {
            return substr($type, 0, -5);
        } else {
            return $type;
        }
    }

    protected function getBaseObjectUrl()
    {
        return $this->getType();
    }

    /**
     * @return string
     */
    protected function getType()
    {
        // Strip final 's' and upcase an eventual 'group'
        return preg_replace(
            array('/group$/', '/period$/', '/argument$/', '/apiuser$/', '/dependencie$/', '/set$/'),
            array('Group', 'Period', 'Argument', 'ApiUser', 'dependency', 'Set'),
            str_replace(
                'template',
                '',
                substr($this->getRequest()->getControllerName(), 0, -1)
            )
        );
    }

    /**
     * @return string
     */
    protected function getPluralType()
    {
        return preg_replace('/cys$/', 'cies', $this->getType() . 's');
    }

    /**
     * @return string
     */
    protected function getPluralBaseType()
    {
        return preg_replace('/cys$/', 'cies', $this->getBaseType() . 's');
    }
}
