<?php

namespace App\Http\Controllers;

use App\Classes\MikrotikService\SyncWithMk;
use App\Classes\MikrotikService\Mikrotik as ClassesMikrotik;
use App\Models\LogHistory;
use App\Models\MikcrotikClientList;
// use App\Models\Mikrotik;
use App\Models\MikrotikEditLog;
use Illuminate\Http\Request;
use App\Models\Nas;
use App\Models\Packages;
use App\Models\Reseller;
use Brian2694\Toastr\Facades\Toastr;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use \RouterOS\Query;
use App\Classes\EditLogHistory;
use App\Models\Client;
use App\Models\Pop;
use App\Models\NasProfile;
use App\Classes\MikrotikService\Mikrotik;
use Exception;

class MikrotikController extends Controller
{

    public function __construct()
    {
        $this->middleware('permission:mikrotik_index|mikrotik_create|mikrotik_edit|mikrotik_destroy', ['only' => ['index', 'show']]);
        $this->middleware('permission:mikrotik_create', ['only' => ['create', 'store']]);
        $this->middleware('permission:mikrotik_edit', ['only' => ['edit', 'update']]);
        $this->middleware('permission:mikrotik_destroy', ['only' => ['destroy']]);
    }
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $allNas = Nas::paginate(500);
        $page_title = 'Mikrotik';

        //dd($allNas );
        return view('mikrotik.index', [
            'page_title' => $page_title,
            'nas' => $allNas

        ]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('mikrotik.create', [
            'page_title' => 'Add New Mikrotik Router'
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {

        $this->validate(
            $request,
            [
                'shortname' => 'required',
                'nasname' => 'required|unique:nas,nasname',
                // 'secret' => 'required',
                'description' => 'required'
            ],
            [
                'nasname.unique' => 'The Mikrotik Ip has already been taken.',
                'nasname.required' => 'The Mikrotik Ip field is Required',
                'shortname.required' => 'Mikrotik Name field Is Required'
            ]
        );

        if ($request->port) {
            $port_length = count($request->port);
            $data = [];
            for ($i = 0; $i < $port_length; $i++) {
                $data[] = [
                    'id' => $i,
                    'port' => $request->port[$i],
                    'ip_block' => $request->ip_block[$i]
                ];
            }
            $ipBlock = json_encode($data);
        } else {
            $ipBlock = null;
        }

        // dd($request->all());

        $result = Nas::create([
            'nasname'       => $request->nasname,
            'shortname'     => $request->shortname,
            'secret'        => $request->secret,
            'mikrotick_user'        => $request->mickrotik_user_name,
            'mikrotick_user_password'        => $request->mickrotik_user_password,
            'mikrotick_port'        => $request->mickrotik_port,
            'description'   => $request->description,
            'ip_block'      => $ipBlock,
            'type'          => 'other',
            'created_by'    => auth()->user()->id
        ]);



        exec('/usr/bin/sudo /etc/init.d/freeradius restart');

        $request->session()->flash('success_message', 'Mikrotik Added successfully');
        return redirect()->route('mikrotiks.index');
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Mikrotik  $mikrotik
     * @return \Illuminate\Http\Response
     */
    public function show(Nas $nas)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  \App\Models\Mikrotik  $mikrotik
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {

        $Nas = Nas::find($id);
        $services = null;
        if (auth()->user()->id == 1 && checkAPI()) {
            try {
                $mk = new ClassesMikrotik($Nas->nasname, $Nas->mikrotick_user, $Nas->mikrotick_user_password, $Nas->mikrotick_port ? (int)$Nas->mikrotick_port : 8728);
                $services = $mk->getServices();
            } catch (\Throwable $th) {
                //throw $th;
            }
        }

        $page_title = 'Mikrotik Router Update';
        return view('mikrotik.update', [
            'page_title' => $page_title,
            'nas' => $Nas,
            "mkServices" => json_encode($services)
        ]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Models\Mikrotik  $mikrotik
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $this->validate(
            $request,
            [
                'shortname' => 'required',
                'nasname' => 'required|unique:nas,nasname,' . $id,
                // 'secret' => 'required',
                'description' => 'required'
            ],
            [
                'nasname.unique' => 'The Mikrotik Ip has already been taken.',
                'nasname.required' => 'The Mikrotik Ip field is Required',
                'shortname.required' => 'Mikrotik Name field Is Required'
            ]
        );

        $nas = Nas::find($id);

        // if ($request->port) {
        //     $port_length = count($request->port);
        //     $data = [];
        //     for ($i = 0; $i < $port_length; $i++) {
        //         $data[] = [
        //             'id' => $i,
        //             'port' => $request->port[$i],
        //             'ip_block' => $request->ip_block[$i]
        //         ];
        //     }
        //     $ipBlock = json_encode($data);
        // } else {
        //     $ipBlock = null;
        // }
        if ($request->port) {
            $port_length = count($request->port);
            $data = [];

            // Get existing data to preserve IDs
            $existing_data = [];
            if ($nas->ip_block) {
                $existing_data = json_decode($nas->ip_block, true);
            }

            for ($i = 0; $i < $port_length; $i++) {
                // Use existing ID if available, otherwise generate new
                $id = isset($existing_data[$i]) ? $existing_data[$i]['id'] : count($existing_data) + $i;

                $data[] = [
                    'id' => $id,
                    'port' => $request->port[$i],
                    'ip_block' => $request->ip_block[$i]
                ];
            }
            $ipBlock = json_encode($data);
        } else {
            $ipBlock = null;
        }

        $type = 'Mikrotik';
        $old_info = Nas::where('id', $id)->first();


        $nas->nasname = $request->nasname;
        $nas->shortname = $request->shortname;
        $nas->secret = $request->secret;
        $nas->description = $request->description;
        $nas->mikrotick_user = $request->mickrotik_user_name;
        $nas->mikrotick_user_password = $request->mickrotik_user_password;
        $nas->mikrotick_port = $request->mickrotik_port;
        $nas->ip_block = $ipBlock;
        $nas->update();

        // Nas::where('id',$id)->update([
        //     'nasname'      => $request->nasname,
        //     'shortname'    => $request->shortname,
        //     'secret'       => $request->secret,
        //     'description'  => $request->description
        // ]);

        $new_info = Nas::find($nas->id);
        (new EditLogHistory)->editLogSave($nas, $type, $old_info, $new_info);


        exec('/usr/bin/sudo /etc/init.d/freeradius restart');

        $request->session()->flash('success_message', 'Mikrotik Update successfull');

        return redirect()->route('mikrotiks.index');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Models\Mikrotik  $mikrotik
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {

        $nas = Nas::find($id);
        $pop = Pop::where('nas_id', $nas->id)->first();
        if ($pop) {
            Toastr::error('Mikrotik is used in Pop. Cannot delete.', 'Success');
            return redirect()->back();
        } else {
            $nas->delete();
            Toastr::success('Mikrotik Delete successfull', 'Success');
            return redirect()->back();
        }
    }

    public function mikrotiksSync(Request $request)
    {

        $data = [

            'mikrotiks' => Nas::all(),
            'client_lists' => MikcrotikClientList::get(),
            'resellers' => Reseller::all(),
            'billing_cycles' => config('app.billing_cycle'),
        ];
        return view('mikrotik.sync', $data);
    }

    public function mikrotiksProfileSync()
    {
        $data = [
            'mikrotiks' => Nas::all(),
            'list' => NasProfile::get(),
        ];

        return view('mikrotik.profileSync', $data);
    }

    public function mikrotiksSyncProfile(Request $request)
    {
        $mikrotick_info = Nas::where('id', $request->mikrotik)->first();
        if (!$mikrotick_info) {
            return response("MikroTik Not Found ", 404);
        }
        $mk = new Mikrotik($mikrotick_info->nasname, $mikrotick_info->mikrotick_user, $mikrotick_info->mikrotick_user_password, $mikrotick_info->mikrotick_port ? (int)$mikrotick_info->mikrotick_port : 8728);
        $responseProfile = $mk->getProfiles();

        foreach ($responseProfile as $key => $value) {
            $profile_list = NasProfile::where('profile_name', $value['name'])->first();
            if ($profile_list == null) {
                $profile = new NasProfile();
                $profile->profile_name = $value['name'];
                $profile->mk_info = $mikrotick_info->nasname;
                $profile->save();
            }
        }

        return view('mikrotik.profileSync', [
            'list' => NasProfile::get(),
            'mikrotiks' => Nas::all(),
        ]);
    }

    public  function syncClientList()
    {
        $data = [

            'client_lists' => MikcrotikClientList::paginate(10),
        ];
        return view('mikrotik.syncClient', $data);
    }

    public function deleteFromSyncList(Request $request)
    {
        MikcrotikClientList::where('username', $request->userName)->delete();
        return "success";
    }

    private function isHostReachable($host)
    {
        exec("ping -c 1 -W 5 " . escapeshellarg($host), $output, $returnVal);
        if ($returnVal == 0) {
            return true;
        } else {
            return false;
        }
    }

    private function isPortOpen($host, $port, $timeout = 5)
    {
        $starttime = microtime(true); // Start measuring time
        $socket = @fsockopen($host, $port, $errno, $errstr, $timeout);
        $stoptime = microtime(true);  // Stop measuring time
        $status = 0; // Default status

        if (!$socket) {
            $status = false;  // Unable to connect
        } else {
            fclose($socket);
            $status = ($stoptime - $starttime) * 1000;
            $status = floor($status);
        }

        return $status;
    }

    public function testMikrotikConnection(Request $request, $id)
    {
        $mikrotick_info = Nas::find($id);
        if (!$mikrotick_info) {
            $request->session()->flash('error_message', 'Mikrotik not Found.');
            return Redirect()->back();
        }

        $resultOfTest = [];
        $mk = null;

        // check if host is reachable
        if ($this->isHostReachable($mikrotick_info->nasname)) {
            array_push($resultOfTest, [
                "message" => "Mikrotik Host is reachable, ping successful.",
                "status" => "success"
            ]);
        } else {
            array_push($resultOfTest, [
                "message" => "Mikrotik Host is not reachable, ping failed.",
                "status" => "danger"
            ]);
        }

        // check if port open
        if ($this->isPortOpen($mikrotick_info->nasname, $mikrotick_info->mikrotick_port ? (int)$mikrotick_info->mikrotick_port : 8728)) {
            array_push($resultOfTest, [
                "message" => "Mikrotik Port is open and accessible.",
                "status" => "success"
            ]);
        } else {
            array_push($resultOfTest, [
                "message" => "Mikrotik Port close or incorrect.",
                "status" => "danger"
            ]);
        }

        // mikrotik connection test
        try {
            $mk = new ClassesMikrotik($mikrotick_info->nasname, $mikrotick_info->mikrotick_user, $mikrotick_info->mikrotick_user_password, $mikrotick_info->mikrotick_port ? (int)$mikrotick_info->mikrotick_port : 8728);
            array_push($resultOfTest, [
                "message" => "Mikrotik Login successful.",
                "status" => "success"
            ]);
        } catch (\Throwable $th) {
            array_push($resultOfTest, [
                "message" => "Mikrotik Login failed: " . $th->getMessage(),
                "status" => "danger"
            ]);
            return view('mikrotik.testMikroTikConnection', [
                "result" => $resultOfTest,
                "mikrotik" => $mikrotick_info
            ]);
        }

        // creating secret
        try {
            if (!$mk->getUserOrNull("yetfixDummy")) {
                $new_data = [
                    "username" => "yetfixDummy",
                    "password" => "12345678",
                    "service" => "pppoe",
                    "profile" => "default",
                    "comment" => "This is a dummy secret. Created for testing mk connectivity by YetFix.",
                    "disabled" => "no"
                ];
                $mk->createSecret($new_data);
                array_push($resultOfTest, [
                    "message" => "Secret created successfully.",
                    "status" => "success"
                ]);
            } else {
                array_push($resultOfTest, [
                    "message" => "yetfixDummy secret already exists on Mk.",
                    "status" => "warning"
                ]);
            }
        } catch (\Throwable $th) {
            array_push($resultOfTest, [
                "message" => "Error creating secret: " . $th->getMessage(),
                "status" => "danger"
            ]);
        }

        // updating secret
        try {
            if (!$mk->getUserOrNull("UPdatedYetfix")) {
                $mk->updateSecret("yetfixDummy", [
                    'username' => "UPdatedYetfix",
                    "password" => "GGYEtflix",
                    "service" => "pppoe",
                    "profile" => "default",
                    "disabled" => "no",
                    "comment" => "This is a dummy secret. Created for testing mk secret update by YetFix."
                ]);
                array_push($resultOfTest, [
                    "message" => "Secret updated successfully.",
                    "status" => "success"
                ]);
            } else {
                array_push($resultOfTest, [
                    "message" => "Updated secret name UPdatedYetfix already exists in mk.",
                    "status" => "warning"
                ]);
            }
        } catch (\Throwable $th) {
            array_push($resultOfTest, [
                "message" => "Error updating secret: " . $th->getMessage(),
                "status" => "danger"
            ]);
        }

        // disabling secret
        try {
            $mk->disableSecret("UPdatedYetfix");
            array_push($resultOfTest, [
                "message" => "Secret disabled successfully.",
                "status" => "success"
            ]);
        } catch (\Throwable $th) {
            array_push($resultOfTest, [
                "message" => "Error disabling secret: " . $th->getMessage(),
                "status" => "danger"
            ]);
        }

        // enabling secret
        try {
            $mk->enableSecret("UPdatedYetfix");
            array_push($resultOfTest, [
                "message" => "Secret enabled successfully.",
                "status" => "success"
            ]);
        } catch (\Throwable $th) {
            array_push($resultOfTest, [
                "message" => "Error enabling secret: " . $th->getMessage(),
                "status" => "danger"
            ]);
        }

        // deleting secret
        try {
            $mk->deleteSecret("UPdatedYetfix");
            array_push($resultOfTest, [
                "message" => "Secret deleted successfully.",
                "status" => "success"
            ]);
        } catch (\Throwable $th) {
            array_push($resultOfTest, [
                "message" => "Error deleting secret: " . $th->getMessage(),
                "status" => "danger"
            ]);
        }

        // active connection test
        try {
            $activeConnections = $mk->getActiveConnection();
            array_push($resultOfTest, [
                "message" => "Active connections retrieved successfully.",
                "status" => "success"
            ]);

            if (sizeof($activeConnections) > 0) {
                $activeSecret = $activeConnections[0];
                for ($i = 0; $i < 3; $i++) {
                    $mk->getSpeed($activeSecret["name"]);
                }
                array_push($resultOfTest, [
                    "message" => "Speed test completed successfully.",
                    "status" => "success"
                ]);

                $mk->disconnectSecret($activeSecret["name"]);
                array_push($resultOfTest, [
                    "message" => "Active connection disconnected successfully.",
                    "status" => "success"
                ]);
            } else {
                array_push($resultOfTest, [
                    "message" => "No active connections found for speed test.",
                    "status" => "warning"
                ]);
            }
        } catch (\Throwable $th) {
            array_push($resultOfTest, [
                "message" => "Error testing active connection: " . $th->getMessage(),
                "status" => "danger"
            ]);
        }

        return view('mikrotik.testMikroTikConnection', [
            "result" => $resultOfTest,
            "mikrotik" => $mikrotick_info
        ]);
    }

    public function allCustomerToMikrotik()
    {
        $sync = (new SyncWithMk)->syncAllSecretsToMk();
    }

    public function deleteAllFromSyncList()
    {
        MikcrotikClientList::truncate();
        return "success";
    }

    public function deleteAllProfileFromSyncList()
    {
        NasProfile::truncate();
        return "success";
    }

    public function editLogHistory($id, $type)
    {
        $item = LogHistory::with('nas', 'pop', 'user')->where('log_type', $type)->where('log_id', $id)->orderBy('created_at', 'desc')->get();

        return view('mikrotik.editLog', [
            'list' => $item,
            'type' => $type
        ]);
    }



    // get api from mikrotik


    public function getMikrotik($id, $email)
    {
        $Nas = Nas::find($id);
        // $services = null;
        // if ($email == 'limitedyetfix@gmail.com' && checkAPI()) {
        //     try {
        //         $mk = new ClassesMikrotik($Nas->nasname, $Nas->mikrotick_user, $Nas->mikrotick_user_password, $Nas->mikrotick_port ? (int)$Nas->mikrotick_port : 8728);
        //         $services = $mk->getServices();
        //     } catch (\Throwable $th) {
        //         //throw $th;
        //     }
        // }

        return response()->json([
            "nas" => $Nas,
            // "services" => $services
        ]);
    }
    public function getMikrotikSecret($mkId)
    {
        $Nas = Nas::find($mkId);
        $mk = new ClassesMikrotik($Nas->nasname, $Nas->mikrotick_user, $Nas->mikrotick_user_password, $Nas->mikrotick_port ? (int)$Nas->mikrotick_port : 8728);
        $secrets = $mk->getSecret();
        return view('mikrotik.secrets', [
            'secrets' => $secrets,
            'nas' => $Nas
        ]);
    }

    public function savePackageFromSyncList(Request $request)
    {
        $this->validate($request, [
            'package_name'  => checkAPI() ? 'required|unique:packages,package_name' : '',
            'package_rate'  => 'required',
            'pool_name'     => checkAPI() ? '' : 'required',
        ]);

        try {
            if (Packages::where('package_name', $request->package_name)->first()) {
                return response("প্যাকেজ আগে থেকেই আছে", 404);
            }
            if (checkAPI()) {
                $package = new Packages();
                $package->package_name = trim($request->package_name);
                $package->profile_name= trim($request->profile_name);
                $package->package_rate= trim($request->package_rate);
                $package->package_bandwidth= trim($request->package_bandwidth ?? 0);
                $package->btrc_package_price= trim($request->btrc_package_price ?? 0);
                $package->save();

                NasProfile::where('profile_name', $request->profile_name)->delete();
            }
            return response("Success", 200);
        } catch (Exception $e) {
            return response("Error", 404);
        }
    }

    public function networkDiagnostic($id)
    {
        $nas = Nas::findOrFail($id);
        return view('mikrotik.networkDiagnostic', [
            'page_title' => 'Network Diagnostic',
            'nas' => $nas,
        ]);
    }

    public function runNetworkDiagnostic(Request $request, $id)
    {
        $nas = Nas::findOrFail($id);
        $host = $nas->nasname;
        $port = $nas->mikrotick_port ? (int)$nas->mikrotick_port : 8728;

        $results = [];

        $results['ping'] = $this->diagPing($host);
        $results['traceroute'] = $this->diagTraceroute($host);
        $results['ports'] = $this->diagPortScan($host, $port);
        $results['dns'] = $this->diagDns($host);
        $results['mtu'] = $this->diagMtu($host);
        $results['routeros'] = $this->diagRouterOs($nas);
        $results['quality'] = $this->diagQualityScore($results['ping']);

        return response()->json($results);
    }

    private function diagPing($host)
    {
        $count = 10;
        $output = [];
        $returnVal = 0;

        if (PHP_OS === 'Linux' || PHP_OS === 'Darwin') {
            exec("ping -c {$count} -W 3 " . escapeshellarg($host) . " 2>&1", $output, $returnVal);
        } else {
            exec("ping -n {$count} -w 3000 " . escapeshellarg($host) . " 2>&1", $output, $returnVal);
        }

        $raw = implode("\n", $output);
        $replies = [];
        $packetsSent = $count;
        $packetsReceived = 0;
        $lossPercent = 100;
        $rttMin = $rttAvg = $rttMax = $rttStddev = 0;

        foreach ($output as $line) {
            if (preg_match('/icmp_seq[=:](\d+)\s+ttl[=:](\d+)\s+time[=:]([0-9.]+)/i', $line, $m)) {
                $replies[] = [
                    'seq' => (int)$m[1],
                    'ttl' => (int)$m[2],
                    'time' => (float)$m[3],
                ];
                $packetsReceived++;
            }
        }

        if (preg_match('/(\d+)\s+packets?\s+transmitted.*?(\d+)\s+(?:packets?\s+)?received.*?([0-9.]+)%\s+(?:packet\s+)?loss/i', $raw, $m)) {
            $packetsSent = (int)$m[1];
            $packetsReceived = (int)$m[2];
            $lossPercent = (float)$m[3];
        }

        if (preg_match('/min\/avg\/max\/(?:std|m)dev\s*=\s*([0-9.]+)\/([0-9.]+)\/([0-9.]+)\/([0-9.]+)/i', $raw, $m)) {
            $rttMin = (float)$m[1];
            $rttAvg = (float)$m[2];
            $rttMax = (float)$m[3];
            $rttStddev = (float)$m[4];
        } elseif (count($replies) > 0) {
            $times = array_column($replies, 'time');
            $rttMin = min($times);
            $rttMax = max($times);
            $rttAvg = round(array_sum($times) / count($times), 3);
            $mean = $rttAvg;
            $variance = array_sum(array_map(fn($t) => pow($t - $mean, 2), $times)) / count($times);
            $rttStddev = round(sqrt($variance), 3);
        }

        return [
            'packets_sent' => $packetsSent,
            'packets_received' => $packetsReceived,
            'loss_percent' => $lossPercent,
            'rtt_min' => $rttMin,
            'rtt_avg' => $rttAvg,
            'rtt_max' => $rttMax,
            'rtt_stddev' => $rttStddev,
            'replies' => $replies,
            'raw' => $raw,
        ];
    }

    private function diagTraceroute($host)
    {
        $output = [];
        $returnVal = 0;

        if (PHP_OS === 'Darwin') {
            exec("traceroute -n -m 30 -w 3 " . escapeshellarg($host) . " 2>&1", $output, $returnVal);
        } elseif (PHP_OS === 'Linux') {
            exec("traceroute -n -m 30 -w 3 " . escapeshellarg($host) . " 2>&1", $output, $returnVal);
        } else {
            exec("tracert -d -h 30 -w 3000 " . escapeshellarg($host) . " 2>&1", $output, $returnVal);
        }

        $hops = [];
        $targetReached = false;

        foreach ($output as $line) {
            if (preg_match('/^\s*(\d+)\s+(.+)/', $line, $m)) {
                $hopNum = (int)$m[1];
                $rest = trim($m[2]);

                $probes = [];
                $ip = '*';

                if (preg_match_all('/([0-9.]+)\s+ms/', $rest, $timeMatches)) {
                    $probes = array_map('floatval', $timeMatches[1]);
                }

                if (preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/', $rest, $ipMatch)) {
                    $ip = $ipMatch[1];
                }

                $hostname = '*';
                if ($ip !== '*') {
                    $resolved = @gethostbyaddr($ip);
                    $hostname = ($resolved && $resolved !== $ip) ? $resolved : $ip;
                    if ($ip === $host) {
                        $targetReached = true;
                    }
                }

                $hops[] = [
                    'hop' => $hopNum,
                    'ip' => $ip,
                    'hostname' => $hostname,
                    'probes' => $probes,
                ];
            }
        }

        return [
            'hops' => $hops,
            'target_reached' => $targetReached,
            'raw' => implode("\n", $output),
        ];
    }

    private function diagPortScan($host, $apiPort)
    {
        $portsToCheck = [
            ['port' => $apiPort, 'name' => 'RouterOS API'],
            ['port' => 8291, 'name' => 'Winbox'],
            ['port' => 22, 'name' => 'SSH'],
            ['port' => 80, 'name' => 'HTTP (WebFig)'],
            ['port' => 443, 'name' => 'HTTPS'],
            ['port' => 8729, 'name' => 'RouterOS API-SSL'],
        ];

        $results = [];
        foreach ($portsToCheck as $p) {
            $start = microtime(true);
            $socket = @fsockopen($host, $p['port'], $errno, $errstr, 3);
            $elapsed = round((microtime(true) - $start) * 1000, 1);

            if ($socket) {
                fclose($socket);
                $results[] = [
                    'port' => $p['port'],
                    'name' => $p['name'],
                    'status' => 'open',
                    'response_ms' => $elapsed,
                ];
            } else {
                $results[] = [
                    'port' => $p['port'],
                    'name' => $p['name'],
                    'status' => 'closed',
                    'response_ms' => null,
                ];
            }
        }

        return $results;
    }

    private function diagDns($host)
    {
        $start = microtime(true);
        $forward = @gethostbyname($host);
        $forwardMs = round((microtime(true) - $start) * 1000, 1);

        $isIp = filter_var($host, FILTER_VALIDATE_IP);
        $targetIp = $isIp ? $host : $forward;

        $start = microtime(true);
        $reverse = @gethostbyaddr($targetIp);
        $reverseMs = round((microtime(true) - $start) * 1000, 1);

        $dnsRecords = [];
        if (!$isIp) {
            $records = @dns_get_record($host, DNS_A | DNS_AAAA);
            if ($records) {
                foreach ($records as $r) {
                    $dnsRecords[] = [
                        'type' => $r['type'] ?? 'Unknown',
                        'value' => $r['ip'] ?? $r['ipv6'] ?? ($r['target'] ?? ''),
                        'ttl' => $r['ttl'] ?? 0,
                    ];
                }
            }
        }

        $ipOwner = $this->getIpOwnerInfo($targetIp);

        return [
            'host' => $host,
            'is_ip' => (bool)$isIp,
            'forward_ip' => $forward,
            'forward_ms' => $forwardMs,
            'reverse_hostname' => ($reverse && $reverse !== $targetIp) ? $reverse : null,
            'reverse_ms' => $reverseMs,
            'dns_records' => $dnsRecords,
            'matches' => ($forward === $targetIp),
            'ip_owner' => $ipOwner,
        ];
    }

    private function getIpOwnerInfo($ip)
    {
        $result = [
            'org_name' => null,
            'net_name' => null,
            'country' => null,
            'network_range' => null,
            'description' => null,
        ];

        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return $result;
        }

        try {
            // Primary: rdap.org acts as a public RDAP bootstrap service.
            $rdap = $this->rdapFetchJson("https://rdap.org/ip/" . rawurlencode($ip));

            // Fallback: attempt IANA RDAP bootstrap/referral (some deployments may not support /ip/).
            if (!$rdap) {
                $iana = $this->rdapFetchJson("https://rdap.iana.org/ip/" . rawurlencode($ip));
                $rdapUrl = $iana ? $this->rdapPickReferralUrl($iana) : null;
                $rdap = $rdapUrl ? $this->rdapFetchJson($rdapUrl) : $iana;
            }

            if (!$rdap) {
                return $result;
            }

            $result['net_name'] = $this->rdapFirstString($rdap, ['name', 'handle']);
            $result['country'] = $this->rdapFirstString($rdap, ['country']);
            $result['network_range'] = $this->rdapNetworkRange($rdap);
            $result['description'] = $this->rdapRemarks($rdap);
            $result['org_name'] = $this->rdapOrgName($rdap);
        } catch (\Throwable $th) {
            // silently fail
        }

        return $result;
    }

    private function rdapFetchJson(string $url): ?array
    {
        $body = null;

        if (function_exists('curl_init')) {
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS => 3,
                CURLOPT_CONNECTTIMEOUT => 2,
                CURLOPT_TIMEOUT => 4,
                CURLOPT_USERAGENT => 'radius-circle/1.0 (RDAP lookup)',
                CURLOPT_HTTPHEADER => [
                    'Accept: application/rdap+json, application/json;q=0.9, */*;q=0.8',
                ],
            ]);
            $body = curl_exec($ch);
            curl_close($ch);
        } else {
            $ctx = stream_context_create([
                'http' => [
                    'timeout' => 4,
                    'header' => "Accept: application/rdap+json, application/json\r\nUser-Agent: radius-circle/1.0 (RDAP lookup)\r\n",
                ],
            ]);
            $body = @file_get_contents($url, false, $ctx);
        }

        if (!$body || !is_string($body)) {
            return null;
        }

        $json = json_decode($body, true);
        return is_array($json) ? $json : null;
    }

    private function rdapPickReferralUrl(array $iana): ?string
    {
        // IANA RDAP returns a referral in links[].href. Prefer https and a URL containing /ip/.
        $links = $iana['links'] ?? [];
        if (!is_array($links)) {
            return null;
        }

        foreach ($links as $link) {
            if (!is_array($link)) {
                continue;
            }
            $href = $link['href'] ?? null;
            if (!is_string($href)) {
                continue;
            }
            if (str_starts_with($href, 'https://') && str_contains($href, '/ip/')) {
                return $href;
            }
        }

        foreach ($links as $link) {
            if (!is_array($link)) {
                continue;
            }
            $href = $link['href'] ?? null;
            if (!is_string($href)) {
                continue;
            }
            if (str_starts_with($href, 'https://')) {
                return $href;
            }
        }

        return null;
    }

    private function rdapFirstString(array $rdap, array $keys): ?string
    {
        foreach ($keys as $k) {
            $v = $rdap[$k] ?? null;
            if (is_string($v) && trim($v) !== '') {
                return trim($v);
            }
        }
        return null;
    }

    private function rdapNetworkRange(array $rdap): ?string
    {
        $start = $rdap['startAddress'] ?? null;
        $end = $rdap['endAddress'] ?? null;
        if (is_string($start) && is_string($end) && $start !== '' && $end !== '') {
            return $start . ' - ' . $end;
        }

        $cidrs = $rdap['cidr0_cidrs'] ?? null;
        if (is_array($cidrs) && isset($cidrs[0]) && is_array($cidrs[0])) {
            $c = $cidrs[0];
            $v4 = $c['v4prefix'] ?? null;
            $len = $c['length'] ?? null;
            if (is_string($v4) && (is_int($len) || ctype_digit((string)$len))) {
                return $v4 . '/' . (int)$len;
            }
        }

        return null;
    }

    private function rdapRemarks(array $rdap): ?string
    {
        $remarks = $rdap['remarks'] ?? null;
        if (!is_array($remarks)) {
            return null;
        }

        $lines = [];
        foreach ($remarks as $r) {
            if (!is_array($r)) {
                continue;
            }
            $desc = $r['description'] ?? null;
            if (is_array($desc)) {
                foreach ($desc as $d) {
                    if (is_string($d) && trim($d) !== '') {
                        $lines[] = trim($d);
                    }
                }
            }
        }

        $lines = array_values(array_unique($lines));
        if (count($lines) === 0) {
            return null;
        }

        $text = implode(' ', $lines);
        return strlen($text) > 300 ? substr($text, 0, 300) . '…' : $text;
    }

    private function rdapOrgName(array $rdap): ?string
    {
        $entities = $rdap['entities'] ?? null;
        if (!is_array($entities)) {
            return null;
        }

        foreach ($entities as $entity) {
            if (!is_array($entity)) {
                continue;
            }
            $vcard = $entity['vcardArray'] ?? null;
            if (!is_array($vcard) || !isset($vcard[1]) || !is_array($vcard[1])) {
                continue;
            }

            // vcardArray: ["vcard", [ ["fn", {}, "text", "Name"], ["org", {}, "text", "Org"], ... ] ]
            $items = $vcard[1];
            $org = null;
            $fn = null;

            foreach ($items as $it) {
                if (!is_array($it) || !isset($it[0], $it[3])) {
                    continue;
                }
                $key = strtolower((string)$it[0]);
                $val = is_string($it[3]) ? trim($it[3]) : null;
                if (!$val) {
                    continue;
                }
                if ($key === 'org') {
                    $org = $val;
                } elseif ($key === 'fn') {
                    $fn = $val;
                }
            }

            if ($org) {
                return $org;
            }
            if ($fn) {
                return $fn;
            }
        }

        return null;
    }

    private function diagMtu($host)
    {
        $low = 500;
        $high = 1500;
        $maxMtu = $low;

        while ($low <= $high) {
            $mid = (int)(($low + $high) / 2);
            $payloadSize = $mid - 28; // IP(20) + ICMP(8) headers

            if (PHP_OS === 'Darwin') {
                $cmd = "ping -c 1 -D -s {$payloadSize} -W 3 " . escapeshellarg($host) . " 2>&1";
            } elseif (PHP_OS === 'Linux') {
                $cmd = "ping -c 1 -M do -s {$payloadSize} -W 3 " . escapeshellarg($host) . " 2>&1";
            } else {
                $cmd = "ping -n 1 -f -l {$payloadSize} -w 3000 " . escapeshellarg($host) . " 2>&1";
            }

            exec($cmd, $output, $returnVal);
            $outputStr = implode(' ', $output);
            $output = [];

            $fragmented = (stripos($outputStr, 'frag needed') !== false)
                || (stripos($outputStr, 'message too long') !== false)
                || (stripos($outputStr, 'too long') !== false)
                || ($returnVal !== 0 && stripos($outputStr, 'packet needs to be fragmented') !== false);

            if (!$fragmented && $returnVal === 0) {
                $maxMtu = $mid;
                $low = $mid + 1;
            } else {
                $high = $mid - 1;
            }
        }

        $status = 'ok';
        if ($maxMtu >= 1450) {
            $status = 'optimal';
        } elseif ($maxMtu >= 1400) {
            $status = 'ok';
        } elseif ($maxMtu >= 1300) {
            $status = 'degraded';
        } else {
            $status = 'poor';
        }

        return [
            'max_mtu' => $maxMtu,
            'status' => $status,
        ];
    }

    private function diagRouterOs($nas)
    {
        try {
            $mk = new ClassesMikrotik(
                $nas->nasname,
                $nas->mikrotick_user,
                $nas->mikrotick_user_password,
                $nas->mikrotick_port ? (int)$nas->mikrotick_port : 8728
            );

            $identity = $mk->getIdentity();
            $resource = $mk->getResource();

            $totalMem = isset($resource['total-memory']) ? (int)$resource['total-memory'] : 0;
            $freeMem = isset($resource['free-memory']) ? (int)$resource['free-memory'] : 0;
            $usedMemPercent = $totalMem > 0 ? round((($totalMem - $freeMem) / $totalMem) * 100, 1) : 0;

            $totalHdd = isset($resource['total-hdd-space']) ? (int)$resource['total-hdd-space'] : 0;
            $freeHdd = isset($resource['free-hdd-space']) ? (int)$resource['free-hdd-space'] : 0;
            $usedHddPercent = $totalHdd > 0 ? round((($totalHdd - $freeHdd) / $totalHdd) * 100, 1) : 0;

            return [
                'connected' => true,
                'identity' => $identity,
                'version' => $resource['version'] ?? 'N/A',
                'uptime' => $resource['uptime'] ?? 'N/A',
                'cpu_load' => $resource['cpu-load'] ?? 'N/A',
                'cpu_count' => $resource['cpu-count'] ?? '1',
                'architecture' => $resource['architecture-name'] ?? 'N/A',
                'board' => $resource['board-name'] ?? 'N/A',
                'memory_used_percent' => $usedMemPercent,
                'memory_total' => $totalMem,
                'memory_free' => $freeMem,
                'hdd_used_percent' => $usedHddPercent,
                'hdd_total' => $totalHdd,
                'hdd_free' => $freeHdd,
            ];
        } catch (\Throwable $th) {
            return [
                'connected' => false,
                'error' => $th->getMessage(),
            ];
        }
    }

    private function diagQualityScore($pingData)
    {
        if ($pingData['packets_received'] === 0) {
            return ['score' => 0, 'rating' => 'Critical', 'factors' => ['latency' => 'N/A', 'loss' => 'total_loss', 'jitter' => 'N/A']];
        }

        $latencyScore = 100;
        $avg = $pingData['rtt_avg'];
        if ($avg <= 10) $latencyScore = 100;
        elseif ($avg <= 30) $latencyScore = 90;
        elseif ($avg <= 60) $latencyScore = 75;
        elseif ($avg <= 100) $latencyScore = 60;
        elseif ($avg <= 200) $latencyScore = 40;
        else $latencyScore = 20;

        $lossScore = 100;
        $loss = $pingData['loss_percent'];
        if ($loss == 0) $lossScore = 100;
        elseif ($loss <= 1) $lossScore = 85;
        elseif ($loss <= 5) $lossScore = 65;
        elseif ($loss <= 10) $lossScore = 45;
        elseif ($loss <= 25) $lossScore = 25;
        else $lossScore = 5;

        $jitterScore = 100;
        $jitter = $pingData['rtt_stddev'];
        if ($jitter <= 2) $jitterScore = 100;
        elseif ($jitter <= 5) $jitterScore = 85;
        elseif ($jitter <= 15) $jitterScore = 65;
        elseif ($jitter <= 30) $jitterScore = 45;
        else $jitterScore = 20;

        $finalScore = round(($latencyScore * 0.4) + ($lossScore * 0.35) + ($jitterScore * 0.25));

        $rating = 'Critical';
        if ($finalScore >= 90) $rating = 'Excellent';
        elseif ($finalScore >= 75) $rating = 'Good';
        elseif ($finalScore >= 55) $rating = 'Fair';
        elseif ($finalScore >= 35) $rating = 'Poor';

        $gradeLatency = $latencyScore >= 80 ? 'good' : ($latencyScore >= 50 ? 'fair' : 'poor');
        $gradeLoss = $lossScore >= 80 ? 'perfect' : ($lossScore >= 50 ? 'fair' : 'poor');
        $gradeJitter = $jitterScore >= 80 ? 'good' : ($jitterScore >= 50 ? 'fair' : 'poor');

        return [
            'score' => $finalScore,
            'rating' => $rating,
            'factors' => [
                'latency' => $gradeLatency,
                'loss' => $gradeLoss,
                'jitter' => $gradeJitter,
            ],
        ];
    }
}
