<?php

/**
 * BridgeMib.php
 *
 * -Description-
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * @link       https://www.librenms.org
 *
 * @copyright  2025 Tony Murray
 * @author     Tony Murray <murraytony@gmail.com>
 */

namespace LibreNMS\OS\Traits;

use App\Facades\PortCache;
use App\Models\PortStp;
use App\Models\Stp;
use Illuminate\Support\Collection;
use LibreNMS\Util\Mac;
use LibreNMS\Util\StringHelpers;
use SnmpQuery;

trait BridgeMib
{
    private ?array $ifIndexToBridgePort = null;

    public function discoverStpInstances(?string $vlan = null): Collection
    {
        $protocol = SnmpQuery::context("$vlan", 'vlan-')->get('BRIDGE-MIB::dot1dStpProtocolSpecification.0')->value();
        // 1 = unknown (mstp?), 3 = ieee8021d

        if ($protocol != 1 && $protocol != 3) {
            return new Collection;
        }

        $timeFactor = $this->stpTimeFactor ?? 0.01;

        // fetch STP config and store it
        $stp = SnmpQuery::context("$vlan", 'vlan-')->enumStrings()->get([
            'BRIDGE-MIB::dot1dBaseBridgeAddress.0',
            'BRIDGE-MIB::dot1dStpProtocolSpecification.0',
            'BRIDGE-MIB::dot1dStpPriority.0',
            'BRIDGE-MIB::dot1dStpTimeSinceTopologyChange.0',
            'BRIDGE-MIB::dot1dStpTopChanges.0',
            'BRIDGE-MIB::dot1dStpDesignatedRoot.0',
            'BRIDGE-MIB::dot1dStpRootCost.0',
            'BRIDGE-MIB::dot1dStpRootPort.0',
            'BRIDGE-MIB::dot1dStpMaxAge.0',
            'BRIDGE-MIB::dot1dStpHelloTime.0',
            'BRIDGE-MIB::dot1dStpHoldTime.0',
            'BRIDGE-MIB::dot1dStpForwardDelay.0',
            'BRIDGE-MIB::dot1dStpBridgeMaxAge.0',
            'BRIDGE-MIB::dot1dStpBridgeHelloTime.0',
            'BRIDGE-MIB::dot1dStpBridgeForwardDelay.0',
        ])->values();

        if (empty($stp)) {
            return new Collection;
        }

        $bridge = Mac::parseBridge($stp['BRIDGE-MIB::dot1dBaseBridgeAddress.0'] ?? '');
        $bridgeMac = $bridge->hex();
        $drBridge = Mac::parseBridge($stp['BRIDGE-MIB::dot1dStpDesignatedRoot.0'] ?? '');
        \Log::info(sprintf('VLAN: %s Bridge: %s DR: %s', $vlan ?: 1, $bridge->readable(), $drBridge->readable()));

        $instance = new Stp([
            'vlan' => $vlan,
            'rootBridge' => $bridgeMac == $drBridge->hex() ? 1 : 0,
            'bridgeAddress' => $bridgeMac,
            'protocolSpecification' => $stp['BRIDGE-MIB::dot1dStpProtocolSpecification.0'] ?? 'unknown',
            'priority' => $stp['BRIDGE-MIB::dot1dStpPriority.0'] ?? 0,
            'timeSinceTopologyChange' => substr($stp['BRIDGE-MIB::dot1dStpTimeSinceTopologyChange.0'] ?? '', 0, -2) ?: 0,
            'topChanges' => $stp['BRIDGE-MIB::dot1dStpTopChanges.0'] ?? 0,
            'designatedRoot' => $drBridge->hex(),
            'rootCost' => $stp['BRIDGE-MIB::dot1dStpRootCost.0'] ?? 0,
            'rootPort' => $stp['BRIDGE-MIB::dot1dStpRootPort.0'] ?? 0,
            'maxAge' => ($stp['BRIDGE-MIB::dot1dStpMaxAge.0'] ?? 0) * $timeFactor,
            'helloTime' => ($stp['BRIDGE-MIB::dot1dStpHelloTime.0'] ?? 0) * $timeFactor,
            'holdTime' => ($stp['BRIDGE-MIB::dot1dStpHoldTime.0'] ?? 0) * $timeFactor,
            'forwardDelay' => ($stp['BRIDGE-MIB::dot1dStpForwardDelay.0'] ?? 0) * $timeFactor,
            'bridgeMaxAge' => ($stp['BRIDGE-MIB::dot1dStpBridgeMaxAge.0'] ?? 0) * $timeFactor,
            'bridgeHelloTime' => ($stp['BRIDGE-MIB::dot1dStpBridgeHelloTime.0'] ?? 0) * $timeFactor,
            'bridgeForwardDelay' => ($stp['BRIDGE-MIB::dot1dStpBridgeForwardDelay.0'] ?? 0) * $timeFactor,
        ]);

        return (new Collection())->push($instance);
    }

    public function discoverStpPorts(Collection $stpInstances): Collection
    {
        $ports = new Collection;

        foreach ($stpInstances as $instance) {
            $vlanContext = $instance->vlan == 1 ? '' : (string) $instance->vlan;

            // prep base port to port_id map for this specific VLAN context
            $baseIfIndex = SnmpQuery::context($vlanContext, 'vlan-')->cache()->walk('BRIDGE-MIB::dot1dBasePortIfIndex')->pluck();
            $basePortIdMap = array_map(fn ($ifIndex) => PortCache::getIdFromIfIndex($ifIndex, $this->getDevice()), $baseIfIndex);

            $vlan_ports = SnmpQuery::context($vlanContext, 'vlan-')
                ->enumStrings()
                ->cache()
                ->walk('BRIDGE-MIB::dot1dStpPortTable')
                ->mapTable(fn ($data, $port) => new PortStp([
                    'vlan' => $instance->vlan,
                    'port_id' => $basePortIdMap[$port] ?? 0,
                    'port_index' => $port,
                    'priority' => $data['BRIDGE-MIB::dot1dStpPortPriority'] ?? 0,
                    'state' => $data['BRIDGE-MIB::dot1dStpPortState'] ?? 'unknown',
                    'enable' => $data['BRIDGE-MIB::dot1dStpPortEnable'] ?? 'unknown',
                    'pathCost' => $data['BRIDGE-MIB::dot1dStpPortPathCost32'] ?? $data['BRIDGE-MIB::dot1dStpPortPathCost'] ?? 0,
                    'designatedRoot' => Mac::parseBridge($data['BRIDGE-MIB::dot1dStpPortDesignatedRoot'] ?? '')->hex(),
                    'designatedCost' => $data['BRIDGE-MIB::dot1dStpPortDesignatedCost'] ?? 0,
                    'designatedBridge' => Mac::parseBridge($data['BRIDGE-MIB::dot1dStpPortDesignatedBridge'] ?? '')->hex(),
                    'designatedPort' => $this->designatedPort($data['BRIDGE-MIB::dot1dStpPortDesignatedPort'] ?? ''),
                    'forwardTransitions' => $data['BRIDGE-MIB::dot1dStpPortForwardTransitions'] ?? 0,
                ]))->filter(function (PortStp $port) {
                    if ($port->enable === 'disabled') {
                        d_echo("$port->port_index ($port->vlan) disabled skipping\n");

                        return false;
                    }
                    if ($port->state === 'disabled') {
                        d_echo("$port->port_index ($port->vlan) state disabled skipping\n");

                        return false;
                    }
                    if (! $port->port_id) {
                        d_echo("$port->port_index ($port->vlan) port not found skipping\n");

                        return false;
                    }
                    d_echo("Discovered STP port $port->port_index ($port->vlan): $port->port_id");

                    return true;
                });

            $ports = $ports->merge($vlan_ports);
        }

        return $ports;
    }

    public function pollStpInstances(Collection $stpInstances): Collection
    {
        return $stpInstances->each(function (Stp $instance): void {
            $data = SnmpQuery::context("$instance->vlan", 'vlan-')->enumStrings()->get([
                'BRIDGE-MIB::dot1dStpTimeSinceTopologyChange.0',
                'BRIDGE-MIB::dot1dStpTopChanges.0',
                'BRIDGE-MIB::dot1dStpDesignatedRoot.0',
            ])->values();

            $instance->timeSinceTopologyChange = substr($data['BRIDGE-MIB::dot1dStpTimeSinceTopologyChange.0'] ?? '', 0, -2) ?: 0;
            $instance->topChanges = $data['BRIDGE-MIB::dot1dStpTopChanges.0'] ?? 0;
            $instance->designatedRoot = Mac::parseBridge($data['BRIDGE-MIB::dot1dStpDesignatedRoot.0'] ?? '')->hex();
            $instance->rootBridge = $instance->bridgeAddress == $instance->designatedRoot; // dr might have changed
        });
    }

    public function pollStpPorts(Collection $stpPorts): Collection
    {
        foreach ($stpPorts->groupBy('vlan') as $vlan => $vlan_ports) {
            $vlan_ports = $vlan_ports->keyBy('port_index');
            $oids = $vlan_ports->keys()->sort()->reduce(function ($carry, $base_port) {
                $carry[] = 'BRIDGE-MIB::dot1dStpPortState.' . $base_port;
                $carry[] = 'BRIDGE-MIB::dot1dStpPortEnable.' . $base_port;
                $carry[] = 'BRIDGE-MIB::dot1dStpPortDesignatedRoot.' . $base_port;
                $carry[] = 'BRIDGE-MIB::dot1dStpPortDesignatedBridge.' . $base_port;

                return $carry;
            }, []);

            SnmpQuery::context("$vlan", 'vlan-')->enumStrings()->get($oids)
                ->mapTable(function ($data, $base_port) use ($vlan, $vlan_ports) {
                    $port = $vlan_ports->get($base_port);
                    $port->vlan = $vlan;
                    $port->state = $data['BRIDGE-MIB::dot1dStpPortState'] ?? 'unknown';
                    $port->enable = $data['BRIDGE-MIB::dot1dStpPortEnable'] ?? 'unknown';
                    $port->designatedRoot = Mac::parseBridge($data['BRIDGE-MIB::dot1dStpPortDesignatedRoot'] ?? '')->hex();
                    $port->designatedBridge = Mac::parseBridge($data['BRIDGE-MIB::dot1dStpPortDesignatedBridge'] ?? '')->hex();

                    return $port;
                });
        }

        return $stpPorts;
    }

    private function designatedPort(string $dp): int
    {
        if (preg_match('/-(\d+)/', $dp, $matches)) {
            // Syntax with "priority" dash "portID" like so : 32768-54, both in decimal
            return (int) $matches[1];
        }

        // Port saved in format priority+port (ieee 802.1d-1998: clause 8.5.5.1)
        $dp = substr($dp, -2); //discard the first octet (priority part)

        if (! is_numeric($dp) && ! StringHelpers::isHex($dp)) {
            return 0;
        }

        return (int) hexdec($dp);
    }

    public function bridgePortFromIfIndex(int|string|null $ifIndex): int
    {
        if (! $ifIndex) {
            return 0;
        }

        $this->ifIndexToBridgePort ??= SnmpQuery::walk('BRIDGE-MIB::dot1dBasePortIfIndex')->pluck();

        return (int) (array_flip($this->ifIndexToBridgePort)[$ifIndex] ?? 0);
    }

    public function ifIndexFromBridgePort(int|string|null $bridgePort): int
    {
        if (! $bridgePort) {
            return 0;
        }

        $this->ifIndexToBridgePort ??= SnmpQuery::walk('BRIDGE-MIB::dot1dBasePortIfIndex')->pluck();

        return (int) ($this->ifIndexToBridgePort[$bridgePort] ?? 0);
    }
}
