<?php

/*
 * Copyright (C) 2024-2026 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\Core\Api;

use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\ACL;
use OPNsense\Core\Config;
use OPNsense\Auth\User;
use OPNsense\Base\UserException;

class DashboardController extends ApiControllerBase
{
    private $metadataFileLocation = "/usr/local/opnsense/www/js/widgets/Metadata";
    private $acl = null;
    private $usermdl = null;

    public function __construct()
    {
        $this->acl = new ACL();
        $this->usermdl = new User();
    }

    private function canAccessEndpoints($endpoints)
    {
        foreach ($endpoints as $endpoint) {
            if (!$this->acl->isPageAccessible($this->getUserName(), $endpoint)) {
                return false;
            }
        }

        return true;
    }

    private function getMetadata()
    {
        $combinedXml = new \DOMDocument('1.0');
        $root = $combinedXml->createElement('metadata');
        $combinedXml->appendChild($root);
        foreach (glob($this->metadataFileLocation . '/*.xml') as $file) {
            $metadataXml = simplexml_load_file($file);
            if ($metadataXml === false) {
                // not a valid xml file
                continue;
            } elseif ($metadataXml->getName() !== "metadata") {
                // wrong type
                continue;
            }

            $node = dom_import_simplexml($metadataXml);
            $node = $root->ownerDocument->importNode($node, true);
            $root->appendChild($node);
        }

        return simplexml_import_dom($combinedXml);
    }

    private function getDefaultDashboard()
    {
        return [
            'options' => [],
            'widgets' => [
                ['id' => 'systeminformation', 'x' => 0, 'y' => 0, 'w' => 2],
                ['id' => 'memory', 'x' => 2, 'y' => 0],
                ['id' => 'disk', 'x' => 3, 'y' => 0],
                ['id' => 'interfacestatistics', 'x' => 4, 'y' => 0, 'w' => 4],
                ['id' => 'firewall', 'x' => 8, 'y' => 0, 'w' => 4],
                ['id' => 'gateways', 'x' => 2, 'y' => 1, 'w' => 2],
                ['id' => 'interfaces', 'x' => 4, 'y' => 1, 'w' => 4],
                ['id' => 'services', 'x' => 4, 'y' => 2, 'w' => '4'],
                ['id' => 'traffic', 'x' => 8, 'y' => 1, 'w' => 4],
                ['id' => 'cpu', 'x' => 0, 'y' => 1, 'w' => 2],
                ['id' => 'announcements', 'x' => 2, 'y' => 2, 'w' => 2],
            ]
        ];
    }

    public function getDashboardAction()
    {
        $result = [];
        $dashboard = null;

        if (($node = $this->usermdl->getUserByName($this->getUserName())) !== null) {
            $dashboard = $node->dashboard->deserialize();
        }

        if (empty($dashboard)) {
            $dashboard = $this->getDefaultDashboard();
        }

        $result['modules'] = [];
        $metadata = $this->getMetadata();
        foreach ($metadata as $md) {
            foreach ($md as $widgetId => $metadataAttributes) {
                $widgetId = (string)$widgetId;
                $fname = (string)$metadataAttributes->filename;
                $link = (string)$metadataAttributes->link;
                $endpoints = (array)($metadataAttributes->endpoints->endpoint ?? []);
                $translations = (array)($metadataAttributes->translations ?? []);

                if (!empty($link) && !$this->canAccessEndpoints([$link])) {
                    $link = '';
                }

                if (!$this->canAccessEndpoints($endpoints)) {
                    continue;
                } elseif (!file_exists('/usr/local/opnsense/www/js/widgets/' . $fname)) {
                    continue;
                }

                foreach ($translations as $key => $value) {
                    $translations[$key] = gettext($value);
                }

                $result['modules'][] = [
                    'id' => $widgetId,
                    'module' => $fname,
                    'link' => $link,
                    'translations' => $translations
                ];
            }
        }

        // filter widgets according to metadata
        $moduleIds = array_column($result['modules'], 'id');
        $filteredWidgets = array_filter($dashboard['widgets'], function ($widget) use ($moduleIds) {
            return in_array($widget['id'], $moduleIds);
        });
        $dashboard['widgets'] = array_values($filteredWidgets);
        $result['dashboard'] = $dashboard;

        return $result;
    }

    public function saveWidgetsAction()
    {
        $result = ['result' => 'failed'];
        if ($this->request->isPost() && $this->request->hasPost('widgets')) {
            if (($node = $this->usermdl->getUserByName($this->getUserName())) !== null) {
                if (!$node->dashboard->serialize($this->request->getPost())) {
                    throw new UserException(gettext("Dashboard size limit reached"));
                }
                if ($this->usermdl->serializeToConfig(false, true)) {
                    /* selectively save dashboard property, ignoring user-config-readonly when set */
                    Config::getInstance()->save();
                    return ['result' => 'saved'];
                }
            }
        }

        return $result;
    }

    public function restoreDefaultsAction()
    {
        $result = ['result' => 'failed'];

        if ($this->request->isPost() && ($node = $this->usermdl->getUserByName($this->getUserName())) !== null) {
            $node->dashboard = '';
            $config = Config::getInstance()->object();
            if ($this->usermdl->serializeToConfig(false, true)) {
                /* selectively reset dashboard property, ignoring user-config-readonly when set */
                Config::getInstance()->save();
                $result['result'] = 'saved';
            }
        }

        return $result;
    }

    public function productInfoFeedAction()
    {
        $result = ['items' => []];
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://forum.opnsense.org/index.php?board=11.0&action=.xml;limit=5;type=rss2');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $output = curl_exec($ch);
        $payload = @simplexml_load_string($output);
        if (empty($payload)) {
            return $result;
        }
        foreach ($payload->channel->children() as $key => $node) {
            if ($key == 'item') {
                $result['items'][] = [
                    'title' => (string)$node->title,
                    'description' => (string)$node->description,
                    'link' => (string)$node->link,
                    'pubDate' => (string)$node->pubDate,
                    'guid' => (string)$node->guid
                ];
            }
        }
        return $result;
    }

    public function pictureAction()
    {
        $result = ['result' => 'failed'];
        $config = Config::getInstance()->object();
        if (!empty($config->system->picture) && !empty($config->system->picture_filename)) {
            $ext = pathinfo((string)$config->system->picture_filename, PATHINFO_EXTENSION);
            if (empty($ext)) {
                return $result;
            }
            return [
                'result' => 'ok',
                'mime' => 'image/' . $ext,
                'picture' => (string)$config->system->picture,
            ];
        }

        return $result;
    }
}
