<?php

namespace Icinga\Module\Director\Web\Table;

use Icinga\Authentication\Auth;
use Icinga\Module\Director\Db;
use Icinga\Module\Director\Db\DbSelectParenthesis;
use Icinga\Module\Director\Db\IcingaObjectFilterHelper;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Module\Director\Restriction\FilterByNameRestriction;
use Icinga\Module\Director\Restriction\HostgroupRestriction;
use Icinga\Module\Director\Restriction\ObjectRestriction;
use gipfl\IcingaWeb2\Link;
use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
use gipfl\IcingaWeb2\Url;
use Ramsey\Uuid\Uuid;
use Zend_Db_Adapter_Pdo_Pgsql;
use Zend_Db_Select as ZfSelect;

class ObjectsTable extends ZfQueryBasedTable
{
    use TableWithBranchSupport;

    /** @var ObjectRestriction[] */
    protected $objectRestrictions;

    protected $columns = [
        'object_name' => 'o.object_name',
        'object_type' => 'o.object_type',
        'disabled'    => 'o.disabled',
        'uuid'        => 'o.uuid',
    ];

    protected $searchColumns = ['o.object_name'];

    protected $showColumns = ['object_name' => 'Name'];

    protected $filterObjectType = 'object';

    protected $type;

    protected $baseObjectUrl;

    /** @var IcingaObject */
    protected $dummyObject;

    protected $leftSubQuery;

    protected $rightSubQuery;

    /** @var Auth */
    private $auth;

    public function __construct($db, Auth $auth)
    {
        $this->auth = $auth;
        parent::__construct($db);
    }

    /**
     * @param $type
     * @param Db $db
     * @return static
     */
    public static function create($type, Db $db, Auth $auth)
    {
        $class = __NAMESPACE__ . '\\ObjectsTable' . ucfirst($type);
        if (! class_exists($class)) {
            $class = __CLASS__;
        }

        /** @var static $table */
        $table = new $class($db, $auth);
        $table->type = $type;
        return $table;
    }

    public function getType()
    {
        return $this->type;
    }

    /**
     * @param string $url
     * @return $this
     */
    public function setBaseObjectUrl($url)
    {
        $this->baseObjectUrl = $url;

        return $this;
    }

    public function filterObjectType($type)
    {
        $this->filterObjectType = $type;
        return $this;
    }

    public function addObjectRestriction(ObjectRestriction $restriction)
    {
        $this->objectRestrictions[$restriction->getName()] = $restriction;
        return $this;
    }

    public function getColumns()
    {
        return $this->columns;
    }

    public function getColumnsToBeRendered()
    {
        return $this->showColumns;
    }

    public function filterTemplate(
        IcingaObject $template,
        $inheritance = Db\IcingaObjectFilterHelper::INHERIT_DIRECT
    ) {
        if ($this->branchUuid) {
            $tableAlias = 'u';
        } else {
            $tableAlias = 'o';
        }
        IcingaObjectFilterHelper::filterByTemplate(
            $this->getQuery(),
            $template,
            $tableAlias,
            $inheritance,
            $this->branchUuid
        );

        return $this;
    }

    protected function getMainLinkLabel($row)
    {
        return $row->object_name;
    }

    protected function renderObjectNameColumn($row)
    {
        $type = $this->baseObjectUrl;
        $url = Url::fromPath("director/{$type}", [
            'uuid' => Uuid::fromBytes($row->uuid)->toString()
        ]);

        return static::td(Link::create($this->getMainLinkLabel($row), $url));
    }

    protected function renderExtraColumns($row)
    {
        $columns = $this->getColumnsToBeRendered();
        unset($columns['object_name']);
        $cols = [];
        foreach ($columns as $key => & $label) {
            $cols[] = static::td($row->$key);
        }

        return $cols;
    }

    public function renderRow($row)
    {
        if (isset($row->uuid) && is_resource($row->uuid)) {
            $row->uuid = stream_get_contents($row->uuid);
        }
        $tr = static::tr([
            $this->renderObjectNameColumn($row),
            $this->renderExtraColumns($row)
        ]);

        $classes = $this->getRowClasses($row);
        if ($row->disabled === 'y') {
            $classes[] = 'disabled';
        }
        if (! empty($classes)) {
            $tr->getAttributes()->add('class', $classes);
        }

        return $tr;
    }

    protected function getRowClasses($row)
    {
        // TODO: remove isset, to figure out where it is missing
        if (isset($row->branch_uuid) && $row->branch_uuid !== null) {
            return ['branch_modified'];
        }
        return [];
    }

    protected function applyObjectTypeFilter(ZfSelect $query, ZfSelect $right = null)
    {
        if ($right) {
            $right->where(
                'bo.object_type = ?',
                $this->filterObjectType
            );
        }
        return $query->where(
            'o.object_type = ?',
            $this->filterObjectType
        );
    }

    protected function applyRestrictions(ZfSelect $query)
    {
        foreach ($this->getRestrictions() as $restriction) {
            $restriction->applyToQuery($query);
        }

        return $query;
    }

    protected function getRestrictions()
    {
        if ($this->objectRestrictions === null) {
            $this->objectRestrictions = $this->loadRestrictions();
        }

        return $this->objectRestrictions;
    }

    protected function loadRestrictions()
    {
        /** @var Db $db */
        $db = $this->connection();
        $dummyObject = $this->getDummyObject();
        $type = $dummyObject->getShortTableName();

        if ($dummyObject->isApplyRule()) {
            return [new FilterByNameRestriction($db, $this->auth, $type)];
        } else {
            return [new HostgroupRestriction($db, $this->auth)];
        }
    }

    /**
     * @return IcingaObject
     */
    protected function getDummyObject()
    {
        if ($this->dummyObject === null) {
            $type = $this->getType();
            $this->dummyObject = IcingaObject::createByType($type);
        }
        return $this->dummyObject;
    }

    protected function prepareQuery()
    {
        $table = $this->getDummyObject()->getTableName();
        if ($this->branchUuid) {
            $this->columns['branch_uuid'] = 'bo.branch_uuid';
        }

        $columns = $this->getColumns();
        if ($this->branchUuid) {
            $columns = $this->branchifyColumns($columns);
            $this->stripSearchColumnAliases();
        }
        $query = $this->db()->select()->from(['o' => $table], $columns);

        if ($this->branchUuid) {
            $right = clone($query);
            // Hint: Right part has only those with object = null
            //       This means that restrictions on $right would hide all
            //       new rows. Dedicated restriction logic for the branch-only
            //       part of thw union are not required, we assume that restrictions
            //       for new objects have been checked once they have been created
            $query = $this->applyRestrictions($query);
            /** @var Db $conn */
            $conn = $this->connection();
            $query->joinLeft(
                ['bo' => "branched_$table"],
                // TODO: PgHexFunc
                $this->db()->quoteInto(
                    'bo.uuid = o.uuid AND bo.branch_uuid = ?',
                    $conn->quoteBinary($this->branchUuid->getBytes())
                ),
                []
            );

            // keep the imported templates as columns
            $leftColumns = $columns;
            $rightColumns = $columns;

            if ($this->db() instanceof Zend_Db_Adapter_Pdo_Pgsql) {
                $leftColumns['imports'] = 'CONCAT(\'[\', ARRAY_TO_STRING(ARRAY_AGG'
                    . '(CONCAT(\'"\', sub_o.object_name, \'"\')), \',\'), \']\')';
            } else {
                $leftColumns['imports'] = 'CONCAT(\'[\', '
                    . 'GROUP_CONCAT(CONCAT(\'"\', sub_o.object_name, \'"\')), \']\')';
            }

            $query->reset('columns');

            $query->columns($leftColumns)
                ->joinLeft(
                    ['oi' => $table . '_inheritance'],
                    'o.id = oi.' . $this->getType() . '_id',
                    []
                )->joinLeft(
                    ['sub_o' => $table],
                    'sub_o.id = oi.parent_' . $this->getType() . '_id',
                    []
                )->group(['o.id', 'bo.uuid', 'bo.branch_uuid']);

            $rightColumns['imports'] = 'bo.imports';

            $right->reset('columns');
            $right->columns($rightColumns);

            $query->where("(bo.branch_deleted IS NULL OR bo.branch_deleted = 'n')");
            $this->applyObjectTypeFilter($query, $right);
            $right->joinRight(
                ['bo' => "branched_$table"],
                'bo.uuid = o.uuid',
                []
            )
            ->where('o.uuid IS NULL')
            ->where('bo.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes()));
            $this->leftSubQuery = $query;
            $this->rightSubQuery = $right;
            $query = $this->db()->select()->union([
                'l' => new DbSelectParenthesis($query),
                'r' => new DbSelectParenthesis($right),
            ]);
            $query = $this->db()->select()->from(['u' => $query]);
            $query->order('object_name')->limit(100);
        } else {
            $this->applyObjectTypeFilter($query);
            $query = $this->applyRestrictions($query);
            $query->order('o.object_name')->limit(100);
        }

        return $query;
    }

    public function removeQueryLimit()
    {
        $query = $this->getQuery();
        $query->reset($query::LIMIT_OFFSET);
        $query->reset($query::LIMIT_COUNT);

        return $this;
    }
}
