<?php

/*
 * Copyright (C) 2019 Michael Muenz <m.muenz@gmail.com>
 * Copyright (C) 2020 Deciso B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

namespace OPNsense\Unbound\Api;

use OPNsense\Base\ApiMutableModelControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;

class SettingsController extends ApiMutableModelControllerBase
{
    protected static $internalModelClass = '\OPNsense\Unbound\Unbound';
    protected static $internalModelName = 'unbound';

    private $type = 'forward';

    public function updateBlocklistAction()
    {
        $result = ["status" => "failed"];
        if ($this->request->isPost() && $this->request->hasPost('domain') && $this->request->hasPost('type') && $this->request->hasPost('uuid')) {
            Config::getInstance()->lock();
            $this->throwReadOnly();
            $domain = $this->request->getPost('domain');
            $type = $this->request->getPost('type');
            $uuid = $this->request->getPost('uuid');
            $action = 'block';
            $mdl = $this->getModel();

            $remove = function ($csv, $part) {
                while (($i = array_search($part, $csv)) !== false) {
                    unset($csv[$i]);
                }
                return implode(',', $csv);
            };

            $node = $mdl->getNodeByReference('dnsbl.blocklist.' . $uuid);
            if ($node == null) {
                return $result;
            }

            $modelType = $node->$type;
            // strip off any trailing dot
            $value = rtrim($domain, '.');

            $existing_domains = explode(',', (string)$modelType);
            if (in_array($value, $existing_domains)) {
                // value already in model
                return $result;
            }

            $wl = explode(',', (string)$node->allowlists);
            $bl = explode(',', (string)$node->blocklists);

            // Check if domains should be switched around in the model
            if ($type == 'allowlists' && in_array($value, $bl)) {
                $node->blocklists = $remove($bl, $value);
            } elseif ($type == 'blocklists' && in_array($value, $wl)) {
                $node->allowlists = $remove($wl, $value);
            }

            // update the model
            $list = array_filter($existing_domains); // removes all empty entries
            $list[] = $value;
            $node->$type = implode(',', $list);

            $mdl->serializeToConfig();
            Config::getInstance()->save();

            if ($type == 'allowlists') {
                $action = 'allow';
            }

            $result["status"] = trim((new Backend())->configdpRun('unbound dnsblquick', [$uuid, $domain, $action]));
            if ($result["status"] != "OK" && str_contains($result["status"], '99')) {
                /* locked */
                $result["status"] = "failed";
                throw new \OPNsense\Base\UserException(gettext(
                    "The action was successfully applied, but could not be loaded into Unbound " .
                    "because another update process is already active. " .
                    "Please re-apply the Unbound blocklists settings manually."
                ), gettext("Blocklist error"));
            }
        }

        return $result;
    }

    public function getNameserversAction()
    {
        if ($this->request->isGet()) {
            $backend = new Backend();
            $nameservers = json_decode(trim($backend->configdRun("system list nameservers")));

            if ($nameservers !== null) {
                return $nameservers;
            }
        }
        return array("message" => "Unable to run configd action");
    }

    /*
     * Catch all Dot API endpoints and redirect them to Forward for
     * backwards compatibility and infer the type from the request.
     * If no type is provided, default to forward (__call only triggers on non-existing methods).
     */
    public function __call($method, $args)
    {
        if (substr($method, -6) == 'Action') {
            $fn = preg_replace('/Dot/', 'Forward', $method);
            if (method_exists(get_class($this), $fn) && preg_match("/.*dot/i", $method)) {
                $this->type = "dot";
                return $this->$fn(...$args);
            }
        }
    }

    public function searchForwardAction()
    {
        $filter_fn = function ($record) {
            return $record->type == $this->type;
        };

        return $this->searchBase('dots.dot', null, null, $filter_fn);
    }

    public function getForwardAction($uuid = null)
    {
        return $this->getBase('dot', 'dots.dot', $uuid);
    }

    public function addForwardAction()
    {
        return $this->addBase(
            'dot',
            'dots.dot',
            [ "type" => $this->type ]
        );
    }

    public function delForwardAction($uuid)
    {
        return $this->delBase('dots.dot', $uuid);
    }

    public function setForwardAction($uuid)
    {
        return $this->setBase(
            'dot',
            'dots.dot',
            $uuid,
            [ "type" => $this->type ]
        );
    }

    public function toggleForwardAction($uuid, $enabled = null)
    {
        return $this->toggleBase('dots.dot', $uuid, $enabled);
    }

    /* Host overrides */

    public function searchHostOverrideAction()
    {
        $result = $this->searchBase('hosts.host', null);
        $mdl = $this->getModel();

        foreach ($result['rows'] as &$host) {
            $child = $host;
            $host['isAlias'] = false;
            $uuid = $host['uuid'];
            foreach ($mdl->getHostAliases($uuid) as $node) {
                $child['uuid'] = $node->getAttribute('uuid');
                $child['isAlias'] = true;
                foreach (['enabled', 'hostname', 'domain', 'description'] as $key) {
                    $child[$key] = $node->$key->getValue();
                }
                unset($child['aliases']);
                $host['_children'] ??= [];
                $host['_children'][] = $child;
            }
        }

        return $result;
    }

    public function getHostOverrideAction($uuid = null)
    {
        return $this->getBase('host', 'hosts.host', $uuid);
    }

    public function addHostOverrideAction()
    {
        return $this->addBase('host', 'hosts.host');
    }

    public function delHostOverrideAction($uuid)
    {
        /* Make sure the linked aliases are deleted as well. */
        Config::getInstance()->lock();
        $node = $this->getModel();
        foreach ($node->aliases->alias->iterateItems() as $alias_uuid => $alias) {
            if ($alias->host == $uuid) {
                $this->delBase('aliases.alias', $alias_uuid);
            }
        }

        return $this->delBase('hosts.host', $uuid);
    }

    public function setHostOverrideAction($uuid)
    {
        return $this->setBase('host', 'hosts.host', $uuid);
    }

    public function toggleHostOverrideAction($uuid, $enabled = null)
    {
        return $this->toggleBase('hosts.host', $uuid, $enabled);
    }

    /* Aliases for hosts */

    public function searchHostAliasAction()
    {
        $host = $this->request->get('host');
        $filter_func = null;
        if (!empty($host)) {
            $filter_func = function ($record) use ($host) {
                return $record->host == $host;
            };
        }
        return $this->searchBase('aliases.alias', null, null, $filter_func);
    }

    public function getHostAliasAction($uuid = null)
    {
        $host_uuid = $this->request->get('host');
        $result = $this->getBase('alias', 'aliases.alias', $uuid);
        if (empty($uuid) && !empty($host_uuid)) {
            foreach ($result['alias']['host'] as $key => &$value) {
                if ($key == $host_uuid) {
                    $value['selected'] = 1;
                } else {
                    $value['selected'] = 0;
                }
            }
        }
        return $result;
    }

    public function addHostAliasAction()
    {
        return $this->addBase('alias', 'aliases.alias');
    }

    public function delHostAliasAction($uuid)
    {
        return $this->delBase('aliases.alias', $uuid);
    }

    public function setHostAliasAction($uuid)
    {
        return $this->setBase('alias', 'aliases.alias', $uuid);
    }

    public function toggleHostAliasAction($uuid, $enabled = null)
    {
        return $this->toggleBase('aliases.alias', $uuid, $enabled);
    }

    /* ACLs */

    public function searchAclAction()
    {
        return $this->searchBase('acls.acl', null);
    }

    public function getAclAction($uuid = null)
    {
        return $this->getBase('acl', 'acls.acl', $uuid);
    }

    public function addAclAction()
    {
        return $this->addBase('acl', 'acls.acl');
    }

    public function delAclAction($uuid)
    {
        return $this->delBase('acls.acl', $uuid);
    }

    public function setAclAction($uuid)
    {
        return $this->setBase('acl', 'acls.acl', $uuid);
    }

    public function toggleAclAction($uuid, $enabled = null)
    {
        return $this->toggleBase('acls.acl', $uuid, $enabled);
    }

    /* DNSBL */

    public function searchDnsblAction()
    {
        return $this->searchBase('dnsbl.blocklist', ['enabled', 'list', 'source_net', 'description']);
    }

    public function getDnsblAction($uuid = null)
    {
        return $this->getBase('blocklist', 'dnsbl.blocklist', $uuid);
    }

    public function addDnsblAction()
    {
        return $this->addBase('blocklist', 'dnsbl.blocklist');
    }

    public function delDnsblAction($uuid)
    {
        return $this->delBase('dnsbl.blocklist', $uuid);
    }

    public function setDnsblAction($uuid)
    {
        return $this->setBase('blocklist', 'dnsbl.blocklist', $uuid);
    }

    public function toggleDnsblAction($uuid, $enabled = null)
    {
        return $this->toggleBase('dnsbl.blocklist', $uuid, $enabled);
    }
}
