<?php

namespace App\Services;

use App\Models\Mikrotik;
use App\Models\Olt;
use Illuminate\Support\Facades\Log;

class OltPortForwardService
{
    /**
     * Ensure NAT port-forward rules exist on the given MikroTik for the OLT.
     * Returns true on success (either existing or created), false on failure.
     */
    public function ensurePortForward(Olt $olt, Mikrotik $mikrotik): bool
    {
        if (! $olt->auto_port_forward || ! filter_var($olt->management_source_ip, FILTER_VALIDATE_IP)) {
            Log::warning('OLT port forward refused: trusted management source IP is missing.', ['olt_id' => $olt->id]);

            return false;
        }
        // We will connect to RouterOS API using the same TCP-based protocol as MikrotikApiService.
        // Duplicate minimal socket/login/write/read helpers here so we don't modify existing MikroTik module.

        $config = [
            'host' => $mikrotik->ip_address,
            'port' => $mikrotik->api_port ?? 8728,
            'username' => $mikrotik->username,
            'password' => $mikrotik->password,
            'ssl' => ! empty($mikrotik->ssl),
        ];

        try {
            $socket = $this->openSocket($config);
            $this->apiLogin($socket, $config['username'], $config['password']);

            // Check existing NAT rules with our comment
            $createdOrFound = false;
            if (! empty($olt->telnet_port) && ! empty($olt->ip_address)) {
                $comment = 'ISP-Billing OLT '.$olt->id.' Telnet';
                if (! $this->natRuleExists($socket, $comment)) {
                    $this->writeSentence($socket, ['/ip/firewall/nat/add', '=chain=dstnat', '=src-address='.$olt->management_source_ip, '=dst-address='.$mikrotik->ip_address, '=dst-port='.$olt->external_telnet_port, '=protocol=tcp', '=action=dst-nat', '=to-addresses='.$olt->ip_address, '=to-ports='.$olt->telnet_port, '=comment='.$comment]);
                    $this->consumeResponse($socket);
                }
                $createdOrFound = true;
            }

            if (! empty($olt->snmp_port) && ! empty($olt->ip_address)) {
                $comment = 'ISP-Billing OLT '.$olt->id.' SNMP';
                if (! $this->natRuleExists($socket, $comment)) {
                    $this->writeSentence($socket, ['/ip/firewall/nat/add', '=chain=dstnat', '=src-address='.$olt->management_source_ip, '=dst-address='.$mikrotik->ip_address, '=dst-port='.$olt->external_snmp_port, '=protocol=udp', '=action=dst-nat', '=to-addresses='.$olt->ip_address, '=to-ports='.$olt->snmp_port, '=comment='.$comment]);
                    $this->consumeResponse($socket);
                }
                $createdOrFound = true;
            }

            fclose($socket);

            return $createdOrFound;
        } catch (\Throwable $e) {
            Log::warning('Failed to ensure port forward for OLT '.$olt->id.': '.$e->getMessage());

            return false;
        }
    }

    public function removePortForward(Olt $olt, Mikrotik $mikrotik): bool
    {
        $config = ['host' => $mikrotik->ip_address, 'port' => $mikrotik->api_port ?? 8728, 'username' => $mikrotik->username, 'password' => $mikrotik->password];
        try {
            $socket = $this->openSocket($config);
            $this->apiLogin($socket, $config['username'], $config['password']);
            foreach (['ISP-Billing OLT '.$olt->id.' Telnet', 'ISP-Billing OLT '.$olt->id.' SNMP'] as $comment) {
                foreach ($this->findNatRuleIds($socket, $comment) as $id) {
                    $this->writeSentence($socket, ['/ip/firewall/nat/remove', '=.id='.$id]);
                    $this->consumeResponse($socket);
                }
            }
            fclose($socket);

            return true;
        } catch (\Throwable $exception) {
            Log::warning('OLT NAT cleanup failed.', ['olt_id' => $olt->id, 'error' => $exception->getMessage()]);

            return false;
        }
    }

    private function findNatRuleIds($socket, string $comment): array
    {
        $this->writeSentence($socket, ['/ip/firewall/nat/print', '?comment='.$comment]);
        $ids = [];
        while (true) {
            $sentence = $this->readSentence($socket);
            if (empty($sentence) || ($sentence[0] ?? '') === '!done') {
                return $ids;
            }
            if (($sentence[0] ?? '') === '!re') {
                $id = $this->wordsToAssoc($sentence)['.id'] ?? null;
                if ($id) {
                    $ids[] = $id;
                }
            }
        }
    }

    private function natRuleExists($socket, string $comment): bool
    {
        $this->writeSentence($socket, ['/ip/firewall/nat/print', '?comment='.$comment]);
        $found = false;
        while (true) {
            $sentence = $this->readSentence($socket);
            if (empty($sentence) || ($sentence[0] ?? '') === '!done') {
                return $found;
            }
            if (($sentence[0] ?? '') === '!re') {
                $found = true;
            }
            if (($sentence[0] ?? '') === '!trap') {
                throw new \RuntimeException('RouterOS failed while checking OLT NAT rule.');
            }
        }
    }

    private function openSocket(array $config)
    {
        $host = $config['host'];
        $port = $config['port'] ?? 8728;
        $timeout = 5;
        $errno = 0;
        $errstr = '';
        $fp = @fsockopen($host, $port, $errno, $errstr, $timeout);
        if (! $fp) {
            throw new \RuntimeException('Failed to open socket to '.$host.':'.$port.' '.$errstr);
        }
        stream_set_timeout($fp, $timeout);

        return $fp;
    }

    private function apiLogin($socket, string $username, string $password): void
    {
        // simplified plain login attempt
        $this->writeSentence($socket, ['/login', '=name='.$username, '=password='.$password]);
        $first = $this->readSentence($socket);
        if (empty($first)) {
            throw new \RuntimeException('Empty login response');
        }

        // if first is a challenge (with 'ret'), perform challenge response
        $ret = $this->wordsToAssoc($first)['ret'] ?? null;
        if ($ret) {
            $challenge = pack('H*', $ret);
            $md = md5(chr(0).$password.$challenge);
            $response = '00'.bin2hex($md);
            $this->writeSentence($socket, ['/login', '=name='.$username, '=response='.$response]);
            $this->consumeResponse($socket);
        }
    }

    private function writeSentence($socket, array $words): void
    {
        $data = '';
        foreach ($words as $w) {
            $len = strlen($w);
            $data .= $this->encodeLength($len).$w;
        }
        $data .= $this->encodeLength(0);
        fwrite($socket, $data, strlen($data));
    }

    private function readSentence($socket): array
    {
        $return = [];
        while (true) {
            $len = $this->readLength($socket);
            if ($len === 0) {
                break;
            }
            $word = '';
            if ($len > 0) {
                $word = fread($socket, $len);
            }
            $return[] = $word;
        }

        return $return;
    }

    private function consumeResponse($socket): void
    {
        while (true) {
            $sentence = $this->readSentence($socket);
            if (empty($sentence)) {
                break;
            }
            if (($sentence[0] ?? '') === '!done') {
                break;
            }
        }
    }

    private function encodeLength($len)
    {
        if ($len < 0x80) {
            return chr($len);
        }
        if ($len < 0x4000) {
            return chr(($len >> 8) | 0x80).chr($len & 0xFF);
        }
        if ($len < 0x200000) {
            return chr(($len >> 16) | 0xC0).chr(($len >> 8) & 0xFF).chr($len & 0xFF);
        }
        if ($len < 0x10000000) {
            return chr(($len >> 24) | 0xE0).chr(($len >> 16) & 0xFF).chr(($len >> 8) & 0xFF).chr($len & 0xFF);
        }

        return chr(0xF0).chr(($len >> 24) & 0xFF).chr(($len >> 16) & 0xFF).chr(($len >> 8) & 0xFF).chr($len & 0xFF);
    }

    private function readLength($socket)
    {
        $c = ord(fread($socket, 1));
        if ($c < 0x80) {
            return $c;
        }
        if (($c & 0xC0) === 0x80) {
            $c2 = ord(fread($socket, 1));

            return (($c & ~0xC0) << 8) + $c2;
        }
        if (($c & 0xE0) === 0xC0) {
            $c2 = ord(fread($socket, 1));
            $c3 = ord(fread($socket, 1));

            return (($c & ~0xE0) << 16) + ($c2 << 8) + $c3;
        }
        if (($c & 0xF0) === 0xE0) {
            $c2 = ord(fread($socket, 1));
            $c3 = ord(fread($socket, 1));
            $c4 = ord(fread($socket, 1));

            return (($c & ~0xF0) << 24) + ($c2 << 16) + ($c3 << 8) + $c4;
        }
        // 5 byte length
        $b1 = ord(fread($socket, 1));
        $b2 = ord(fread($socket, 1));
        $b3 = ord(fread($socket, 1));
        $b4 = ord(fread($socket, 1));

        return ($b1 << 24) + ($b2 << 16) + ($b3 << 8) + $b4;
    }

    private function wordsToAssoc(array $sentence): array
    {
        $assoc = [];
        foreach ($sentence as $s) {
            if (str_starts_with($s, '=')) {
                $p = explode('=', substr($s, 1), 2);
                $assoc[$p[0]] = $p[1] ?? '';
            }
        }

        return $assoc;
    }
}
