<?php

namespace App\Classes\MikrotikService;

use App\Models\Client;
use App\Models\CompanyInformation;
use App\Models\Nas;
use App\Models\Pop;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

class SyncWithMk
{
    /**
     * Number of parallel groups to process clients for each NAS
     * @var int
     */
    private $groupsPerNas = 4; // Default value, can be changed via setter

    private $time;
    function __construct()
    {
        $this->time = collect(json_decode(siteinfo()->settings))->where('type', 'expire_time')->first()->value ?? '00:00:00';
    }

    /**
     * Set the number of parallel groups per NAS
     * @param int $count Number of groups (minimum 1)
     * @return self
     */
    public function setGroupsPerNas(int $count): self
    {
        $this->groupsPerNas = max(1, $count); // Ensure at least 1 group
        return $this;
    }

    /**
     * Reset all connections for forked child processes
     */
    private function resetConnectionsForChildProcess()
    {
        try {
            // Reset DB connections
            DB::disconnect();
            DB::purge();

            // Aggressively reset Redis connections - MUST happen before any cache/Redis operations
            try {
                // Try to disconnect existing connection
                Redis::connection()->disconnect();
            } catch (\Throwable $e) {
                // Ignore disconnect errors - connection might already be broken
            }

            // Purge all Redis connections from the manager
            Redis::purge();

            // Recreate the Redis manager from scratch
            app()->forgetInstance('redis');
            app()->forgetInstance('redis.connection');

            // Clear cache manager
            app()->forgetInstance('cache');
            app()->forgetInstance('cache.store');

            // Force reconnect DB
            DB::reconnect();

            // Small delay to ensure clean separation
            usleep(10000); // 10ms
        } catch (\Throwable $e) {
            // Log but don't fail
            echo "Warning: Error resetting connections: " . $e->getMessage() . "\n";
        }
    }

    public function mikrotikProvider(Nas $nas, $staticIp = false)
    {
        $mkIp = $nas->nasname;
        $mkUser = $nas->mikrotick_user;
        $mkPass = $nas->mikrotick_user_password;
        $mkPort = $nas->mikrotick_port;

        if ($staticIp) {
            return new MikrotikStaticIP($mkIp, $mkUser, $mkPass, $mkPort ? (int)$mkPort : 8728);
        }

        return new Mikrotik($mkIp, $mkUser, $mkPass, $mkPort ? (int)$mkPort : 8728);
    }

    public function isExpire($client)
    {
        $timeComponents = explode(':', $this->time);
        $expireDateTime = Carbon::parse($client->expire_date)
            ->addDays($client->payment_dadeline);

        if (globalPermission('expire_before_expire_date')) {
            if ($this->time != "00:00:00") $expireDateTime->setTime($timeComponents[0], $timeComponents[1], $timeComponents[2]);
        } else {
            if ($this->time != "00:00:00") $expireDateTime->setTime($timeComponents[0], $timeComponents[1], $timeComponents[2])->addDay();
        }

        return $expireDateTime < Carbon::now();
    }

    public function isDisable($client)
    {
        if ($client->clients_status == "active") {
            return false;
        } elseif ($client->clients_status == "expired") {
            if ($client->pop->experity_check == "Yes") {
                return false;
            } elseif ($client->experity_check == "No") {
                return false;
            } else {
                return $this->isExpire($client);
            }
        } else {
            return true;
        }
    }


    public function isInformationDiffer($mkClientInfo, $newClientInfo, $key)
    {
        $isKeyEmptyOrNull = (isset($mkClientInfo[$key]) == false || $mkClientInfo[$key] == '');

        if ($isKeyEmptyOrNull xor $newClientInfo[$key] == null) return true;
        else if (
            !$isKeyEmptyOrNull && isset($newClientInfo[$key]) &&
            $mkClientInfo[$key] != $newClientInfo[$key]
        ) return true;
    }

    public function isClientInformationDiffer($mkClientInfo, $newClientInfo)
    {
        $differenceCheck = ($mkClientInfo["disabled"] == 'false' xor $newClientInfo["disabled"] == 'no')
            || $mkClientInfo["password"] !== $newClientInfo["password"]
            || $mkClientInfo["service"] !== $newClientInfo["service"]
            || $mkClientInfo["profile"] !== $newClientInfo["profile"]
            || $this->isInformationDiffer($mkClientInfo, $newClientInfo, "comment")
            || (globalPermission("static-ip-address") && $this->isInformationDiffer($mkClientInfo, $newClientInfo, "remote-address"))
            || (globalPermission("client-mac-binding") && $this->isInformationDiffer($mkClientInfo, $newClientInfo, "caller-id"));

        return $differenceCheck;
    }


    public function syncClientHandler($client, Mikrotik $mikrotik, $mkSecret)
    {
        // echo "syncClientHandler: " . $client->userid . "\n";
        $disableStatus = $this->isDisable($client) &&  $this->isDisable(Client::with("pop")->find($client->id));

        if ($disableStatus && globalPermission("show-message-to-expire-customer") && $client->isStatic == false) {
            $this->expireProfileChange($mikrotik, $client, $mkSecret != null);

            if ($disableStatus) {
                try {
                    $mikrotik->disconnectSecret($client->userid);
                } catch (\Exception $err) {
                }
            }
            return;
        }


        // working with static ip
        if ($client->isStatic) {
            $clientInterface = null;
            foreach (json_decode($client->pop->nas->ip_block) as $array) {
                if ((int)$array->id === (int)$client->ethernet_port) {
                    $clientInterface = $array->port;
                    break;
                }
            }

            $mkStatic = $this->mikrotikProvider($client->pop->nas, true);
            $uploadSpeed = (int)$client->packages->speed_up / 8;
            $downloadSpeed = (int)$client->packages->speed_down / 8;
            $mkStatic->createStaticIP([
                "ip_address" => $client->ip_address,
                "max-limit" => "{$uploadSpeed}M/{$downloadSpeed}M",
                "action" => $disableStatus ? "reject" : "accept",
                "interface" => $clientInterface,
                "mac_address" => $client->mac,
                "comment" => str_replace("\n", " ", $client->clientsinfo->remarks)
            ]);

            return;
        }


        $userData = [
            "username" => $client->userid,
            "password" => $client->password,
            "service" => "pppoe",
            "profile" => $client->packages->profile_name,
            "comment" => str_replace("\n", " ", $client->clientsinfo->remarks),
            "disabled" =>  $disableStatus ? "yes" : "no",
            "remote-address" => $client->ip_address,
            "caller-id" => $client->mac
        ];

        $isOnline = (function () use ($mikrotik, $client) {
            try {
                // if (globalPermission("clientManageThroughNodeJs")) {
                //     return !empty(activeFromRedis($client->userid));
                // }
                return $mikrotik->getActiveConnection($client->userid);
            } catch (\Throwable $th) {
                return null;
            }
        })();

        if (
            $mkSecret &&
            ($this->isClientInformationDiffer($mkSecret, $userData) ||
                ($isOnline && $disableStatus))
        ) {
            $userData['profile'] = key_exists($userData['profile'], $mikrotik->profileMap) ? $userData['profile'] : $mkSecret['profile'];
            $mikrotik->updateSecret($client->userid, $userData);

            if ($isOnline && $disableStatus) {
                try {
                    $mikrotik->disconnectSecret($client->userid);
                } catch (\Exception $err) {
                }
            }
        } else if ($mkSecret == null) {
            $mikrotik->createSecret($userData);
        }
        // echo "syncClientHandler Done: " . $client->userid . "\n";
    }

    public function syncSingleClient($clientId)
    {
        if (!checkAPI()) return;

        $client = Client::with("packages", "pop.nas", "clientsinfo")->find($clientId);
        if ($client->client_approval != "approved") {
            return;
        }

        $mk = $this->mikrotikProvider($client->pop->nas);
        $mkSecret = $mk->getUserOrNull($client->userid);
        return $this->syncClientHandler($client, $mk, $mkSecret);
    }

    public function syncSinglePop($popId)
    {
        if (!checkAPI()) return;

        $clients = Client::with("packages")->where("pop_id", $popId)->get();
        $pop = Pop::with("nas")->find($popId);

        $mk = $this->mikrotikProvider($pop->nas);

        $mkSecrets = $mk->getSecret();
        $userMap = [];
        foreach ($mkSecrets as $user) {
            if (!is_array($user) || !array_key_exists('name', $user)) {
                continue;
            }
            $userMap[$user["name"]] = $user;
        }



        foreach ($clients as $user) {
            if ($user->client_approval != "approved") {
                continue;
            }

            try {
                // if (globalPermission("clientManageThroughNodeJs")) {
                //     $this->syncClientHandler($user, $mk, secretFromRedis($user->userid));
                //     continue;
                // }


                $checkIfExist = array_key_exists($user->userid, $userMap);
                $this->syncClientHandler($user, $mk, $checkIfExist ? $userMap[$user->userid] : null);
            } catch (Exception $err) {
                continue;
            }
        }
    }

    public function syncAllSecretsToMk()
    {
        if (!checkAPI()) return;

        // if (globalPermission("clientManageThroughNodeJs")) {
        //     $companyInfo = \App\Models\CompanyInformation::latest()->first();
        //     $port = $companyInfo->hyper_port;
        //     $url = $companyInfo->hyper_host;
        //     $response = Http::post("http://{$url}:{$port}/sync-all-secrets-to-mk");
        //     Log::info('Sync all secrets to MK response:', $response->json());
        //     return;
        // }

        $clients = Client::with("pop.nas", "pop", "packages")->get();

        // Group clients by NAS to avoid multiple concurrent connections to the same router
        $clientsByNas = $clients->groupBy(function ($client) {
            return optional($client->pop->nas)->id ?? ('pop-' . $client->pop_id);
        });

        $nasProcesses = [];

        // First level: Create one process per NAS
        foreach ($clientsByNas as $nasId => $nasClients) {
            $nasPid = pcntl_fork();

            if ($nasPid == -1) {
                echo "❌ Could not fork process for NAS {$nasId}\n";
                continue;
            } elseif ($nasPid === 0) {
                // We are in the NAS process
                echo "Starting NAS process for {$nasId} in PID " . getmypid() . "\n";

                try {
                    // Reset all connections for this forked process
                    $this->resetConnectionsForChildProcess();

                    // Split clients into groups for parallel processing within this NAS
                    $clientCount = $nasClients->count();
                    $groupSize = ceil($clientCount / $this->groupsPerNas);
                    $clientGroups = [];

                    // Create the specified number of groups
                    for ($i = 0; $i < $this->groupsPerNas; $i++) {
                        if ($i === $this->groupsPerNas - 1) {
                            // Last group gets all remaining clients
                            $clientGroups[] = $nasClients->skip($groupSize * $i);
                        } else {
                            $clientGroups[] = $nasClients->skip($groupSize * $i)->take($groupSize);
                        }
                    }

                    $groupProcesses = [];

                    // Second level: Create child processes within this NAS process
                    foreach ($clientGroups as $groupIndex => $clientGroup) {
                        $groupPid = pcntl_fork();

                        if ($groupPid == -1) {
                            echo "❌ Could not fork group process {$groupIndex} for NAS {$nasId}\n";
                            continue;
                        } elseif ($groupPid === 0) {
                            // We are in a group process
                            echo "Starting group {$groupIndex} for NAS {$nasId} in PID " . getmypid() . "\n";

                            try {
                                // Reset all connections for this forked process
                                $this->resetConnectionsForChildProcess();

                                // Small delay to avoid thundering herd on routers
                                // Stagger delays across groups: each group adds 25ms to min/max
                                $baseDelay = 50_000; // 50ms base
                                $staggerDelay = 25_000; // 25ms per group
                                usleep(random_int(
                                    $baseDelay + ($groupIndex * $staggerDelay),
                                    ($baseDelay + 100_000) + ($groupIndex * $staggerDelay)
                                ));

                                // Group clients by POP within this group
                                $popGroups = $clientGroup->groupBy('pop_id');

                                foreach ($popGroups as $pop_id => $pop_users) {
                                    $pop = $pop_users->first()->pop;
                                    $nas = $pop->nas;

                                    try {
                                        $mk = $this->mikrotikProvider($nas);

                                        $mkSecrets = $mk->getSecret();
                                        $userMap = [];
                                        foreach ($mkSecrets as $user) {
                                            $userMap[$user["name"]] = $user;
                                        }


                                        foreach ($pop_users as $user) {
                                            if ($user->client_approval != "approved") {
                                                continue;
                                            }
                                            try {
                                                $start = microtime(true);

                                                // if (globalPermission("clientManageThroughNodeJs")) {
                                                //     $secret = secretFromRedis($user->userid);
                                                //     $this->syncClientHandler($user, $mk, $secret);
                                                //     continue;
                                                // }

                                                $checkIfExist = array_key_exists($user->userid, $userMap);
                                                $this->syncClientHandler($user, $mk, $checkIfExist ? $userMap[$user->userid] : null);



                                                $end = microtime(true);
                                                $duration = $end - $start;
                                                echo "Command execution time for {$nas->nasname} {$user->userid} (NAS {$nasId}, group {$groupIndex}): " . round($duration * 1000, 2) . " ms\n";
                                            } catch (Exception $err) {
                                                echo "Error syncing user {$user->userid} (NAS {$nasId}, group {$groupIndex}): " . $err->getMessage() . "\n";
                                                continue;
                                            }
                                        }
                                    } catch (Exception $err) {
                                        echo "Error processing POP {$pop_id} (NAS {$nasId}, group {$groupIndex}): " . $err->getMessage() . "\n";
                                        continue;
                                    }
                                }
                            } catch (\Throwable $e) {
                                echo "Error in group {$groupIndex} worker for NAS {$nasId}: " . $e->getMessage() . "\n";
                            }
                            exit(0);
                        }

                        // NAS process tracks its group processes
                        $groupProcesses[$groupPid] = true;
                    }

                    // NAS process waits for its group processes to complete
                    while (pcntl_wait($status) > 0) {
                        // Continue waiting until all group processes are done
                    }
                    exit(0);
                } catch (\Throwable $e) {
                    echo "Error in NAS {$nasId} main process: " . $e->getMessage() . "\n";
                    exit(1);
                }
            }

            // Main process tracks NAS processes
            $nasProcesses[$nasPid] = true;
        }

        // Main process waits for all NAS processes to complete
        while (pcntl_wait($status) > 0) {
            // Continue waiting until all NAS processes are done
        }
    }

    public function expireProfileChange(Mikrotik $mikrotik, $client, bool $exist)
    {
        $expireProfileName = cache()->remember('expire_profile', 60 * 60, function () {
            $companyInfo = CompanyInformation::latest()->first();
            return $companyInfo->expire_profile;
        });

        $userData = [
            "username" => $client->userid,
            "password" => $client->password,
            "service" => "pppoe",
            "profile" => $expireProfileName,
            "disabled" => "no",
            "remote-address" => "",
            "caller-id" => $client->mac
        ];

        if ($exist) {
            $mikrotik->updateSecret($client->userid, $userData);
        } else {
            $mikrotik->createSecret($userData);
        }
    }
}
