<?php

/*
 * Copyright (C) 2015-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\Base\FieldTypes;

use OPNsense\Core\Config;

/**
 * Class InterfaceField field type to select usable interfaces,
 * currently this is kind of a backward compatibility
 * package to glue legacy interfaces into the model.
 * @package OPNsense\Base\FieldTypes
 */
class InterfaceField extends BaseListField
{
    /**
     * @var array filters to use on the interface list
     */
    private $internalFilters = [];

    /**
     * @var string key to use for option selections, to prevent excessive reloading
     */
    private $internalCacheKey = '*';

    /**
     * @var bool add physical interfaces to selection (collected from lagg, vlan)
     */
    private $internalAddParentDevices = false;

    /**
     * @var bool allow dynamic interfaces
     */
    private $internalAllowDynamic = 0;

    /**
     *  collect parents for lagg interfaces
     *  @return array named array containing device and lagg interface
     */
    private function getConfigLaggInterfaces()
    {
        $physicalInterfaces = [];
        $configObj = Config::getInstance()->object();
        if (!empty($configObj->laggs)) {
            foreach ($configObj->laggs->children() as $key => $lagg) {
                if (!empty($lagg->members)) {
                    foreach (explode(',', $lagg->members) as $interface) {
                        if (!isset($physicalInterfaces[$interface])) {
                            $physicalInterfaces[$interface] = [];
                        }
                        $physicalInterfaces[$interface][] = (string)$lagg->laggif;
                    }
                }
            }
        }
        return $physicalInterfaces;
    }

    /**
     *  collect parents for VLAN interfaces
     *  @return array named array containing device and VLAN interfaces
     */
    private function getConfigVLANInterfaces()
    {
        $physicalInterfaces = [];
        $configObj = Config::getInstance()->object();
        if (!empty($configObj->vlans)) {
            foreach ($configObj->vlans->children() as $key => $vlan) {
                if (!isset($physicalInterfaces[(string)$vlan->if])) {
                    $physicalInterfaces[(string)$vlan->if] = [];
                }
                $physicalInterfaces[(string)$vlan->if][] = (string)$vlan->vlanif;
            }
        }
        return $physicalInterfaces;
    }

    /**
     * generate validation data (list of interfaces and well know ports)
     */
    protected function actionPostLoadingEvent()
    {
        if ($this->hasStaticOptions($this->internalCacheKey)) {
            $this->internalOptionList = $this->getStaticOptions($this->internalCacheKey);
            return;
        }

        $configObj = Config::getInstance()->object();

        $allInterfacesDevices = []; // mapping device -> interface handle (lan/wan/optX)
        $allInterfaces = [];
        $data = [];

        // Iterate over all interfaces configuration and collect data
        if (isset($configObj->interfaces) && $configObj->interfaces->count() > 0) {
            foreach ($configObj->interfaces->children() as $key => $value) {
                if (!$this->internalAllowDynamic && !empty($value->internal_dynamic)) {
                    continue;
                } elseif ($this->internalAllowDynamic == 2 && !empty($value->internal_dynamic)) {
                    if (empty($value->ipaddr) && empty($value->ipaddrv6)) {
                        continue;
                    }
                }
                $allInterfaces[$key] = $value;
                if (!empty($value->if)) {
                    $allInterfacesDevices[(string)$value->if] = $key;
                }
            }
        }

        if ($this->internalAddParentDevices) {
            // collect parents for lagg/vlan interfaces
            $physicalInterfaces = $this->getConfigLaggInterfaces();
            $physicalInterfaces = array_merge($physicalInterfaces, $this->getConfigVLANInterfaces());

            // add unique devices
            foreach ($physicalInterfaces as $interface => $devices) {
                // construct interface node
                $interfaceNode = new \stdClass();
                $interfaceNode->enable = 0;
                $interfaceNode->descr = "[{$interface}]";
                $interfaceNode->if = $interface;
                foreach ($devices as $device) {
                    if (!empty($allInterfacesDevices[$device])) {
                        $configuredInterface = $allInterfaces[$allInterfacesDevices[$device]];
                        if (!empty($configuredInterface->enable)) {
                            // set device enabled if any member is
                            $interfaceNode->enable = (string)$configuredInterface->enable;
                        }
                    }
                }
                // only add unconfigured devices
                if (empty($allInterfacesDevices[$interface])) {
                    $allInterfaces[$interface] = $interfaceNode;
                }
            }
        }

        // collect this items options
        foreach ($allInterfaces as $key => $value) {
            // use filters to determine relevance
            $isMatched = true;
            foreach ($this->internalFilters as $filterKey => $filterData) {
                if (isset($value->$filterKey)) {
                    $fieldData = $value->$filterKey;
                } else {
                    // not found, might be a boolean.
                    $fieldData = "0";
                }

                if (!preg_match($filterData, $fieldData)) {
                    $isMatched = false;
                }
            }
            if ($isMatched) {
                $data[$key] = !empty($value->descr) ? (string)$value->descr : strtoupper($key);
            }
        }

        natcasesort($data);

        $this->internalOptionList = $this->setStaticOptions($data, $this->internalCacheKey);
    }

    private function updateInternalCacheKey()
    {
        $tmp  = serialize($this->internalFilters);
        $tmp .= (string)$this->internalAllowDynamic;
        $tmp .= $this->internalAddParentDevices ? "Y" : "N";
        $this->internalCacheKey = md5($tmp);
    }
    /**
     * set filters to use (in regex) per field, all tags are combined
     * and cached for the next object using the same filters
     * @param $filters filters to use
     */
    public function setFilters($filters)
    {
        if (is_array($filters)) {
            $this->internalFilters = $filters;
            $this->updateInternalCacheKey();
        }
    }

    /**
     * add parent devices to the selection in case the parent has no configuration
     * @param $value boolean value 0/1
     */
    public function setAddParentDevices($value)
    {
        if (trim(strtoupper($value)) == "Y") {
            $this->internalAddParentDevices = true;
        } else {
            $this->internalAddParentDevices = false;
        }
        $this->updateInternalCacheKey();
    }

    /**
     * select if dynamic (hotplug) interfaces maybe selectable
     * @param $value Y/N/S (Yes, No, Static)
     */
    public function setAllowDynamic($value)
    {
        if (trim(strtoupper($value)) == "Y") {
            $this->internalAllowDynamic = 1;
        } elseif (trim(strtoupper($value)) == "S") {
            $this->internalAllowDynamic = 2;
        } else {
            $this->internalAllowDynamic = 0;
        }
        $this->updateInternalCacheKey();
    }
}
