<?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;
    }

    /**
     * Fetch all active connections from MikroTik and create a map keyed by username
     * @param Mikrotik $mikrotik
     * @return array Map of username => connection array
     */
    public function getActiveConnectionsMap(Mikrotik $mikrotik): array
    {
        $mkActiveConnections = $mikrotik->getActiveConnection();
        $activeConnectionsMap = [];
        foreach ($mkActiveConnections as $connection) {
            if (is_array($connection) && isset($connection["name"])) {
                $activeConnectionsMap[$connection["name"]] = $connection;
            }
        }
        return $activeConnectionsMap;
    }

    public function syncClientHandler($client, Mikrotik $mikrotik, $mkSecret, $isOnline)
    {
        // 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
        ];


        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, $mk->getActiveConnection($client->userid));
    }

    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;
        }

        // Fetch all active connections once and create a map for quick lookup
        $activeConnectionsMap = $this->getActiveConnectionsMap($mk);

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

            try {
                $checkIfExist = array_key_exists($user->userid, $userMap);
                $activeConnection = $activeConnectionsMap[$user->userid] ?? null;

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

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

        // Only fetch NAS IDs and client counts - minimal memory footprint before forking
        $nasClientCounts = DB::table('clients')
            ->join('pops', 'clients.pop_id', '=', 'pops.id')
            ->whereNotNull('pops.nas_id')
            ->where('clients.client_approval', 'approved')
            ->groupBy('pops.nas_id')
            ->select('pops.nas_id', DB::raw('COUNT(*) as client_count'))
            ->pluck('client_count', 'nas_id')
            ->toArray();

        $nasProcesses = [];

        // First level: Create one process per NAS
        foreach ($nasClientCounts as $nasId => $clientCount) {
            $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 NAS {$nasId} ({$clientCount} clients) in PID " . getmypid() . "\n";

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

                    // Fetch NAS once for this process
                    $nas = Nas::find($nasId);
                    if (!$nas) {
                        echo "❌ NAS {$nasId} not found\n";
                        exit(1);
                    }

                    // Connect to MikroTik once and fetch all secrets for this NAS
                    $mk = $this->mikrotikProvider($nas);
                    $mkSecrets = $mk->getSecret();
                    $userMap = [];
                    foreach ($mkSecrets as $user) {
                        if (is_array($user) && isset($user["name"])) {
                            $userMap[$user["name"]] = $user;
                        }
                    }

                    // Get all client IDs for this NAS (minimal memory)
                    $clientIds = DB::table('clients')
                        ->join('pops', 'clients.pop_id', '=', 'pops.id')
                        ->where('pops.nas_id', $nasId)
                        ->where('clients.client_approval', 'approved')
                        ->pluck('clients.id')
                        ->toArray();

                    // Calculate group sizes using arrays (more memory efficient than collections)
                    $totalClients = count($clientIds);
                    $groupSize = (int) ceil($totalClients / $this->groupsPerNas);
                    $clientIdGroups = array_chunk($clientIds, max(1, $groupSize));

                    // Free memory
                    unset($clientIds);

                    $groupProcesses = [];

                    // Second level: Create child processes within this NAS process
                    foreach ($clientIdGroups as $groupIndex => $clientIdGroup) {
                        $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} (" . count($clientIdGroup) . " clients) in PID " . getmypid() . "\n";

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

                                // Small delay to avoid thundering herd on routers
                                $baseDelay = 50_000; // 50ms base
                                $staggerDelay = 25_000; // 25ms per group
                                usleep(random_int(
                                    $baseDelay + ($groupIndex * $staggerDelay),
                                    ($baseDelay + 100_000) + ($groupIndex * $staggerDelay)
                                ));

                                // Re-establish MikroTik connection in child process
                                $nas = Nas::find($nasId);
                                $mk = $this->mikrotikProvider($nas);

                                // Re-fetch active connections in this group process to ensure fresh data
                                $activeConnectionsMap = $this->getActiveConnectionsMap($mk);

                                // Process clients in small chunks to limit memory usage
                                $chunkSize = 100;
                                foreach (array_chunk($clientIdGroup, $chunkSize) as $chunkIds) {
                                    // Load only the chunk we need with required relations
                                    $clients = Client::with(['pop', 'packages', 'clientsinfo'])
                                        ->whereIn('id', $chunkIds)
                                        ->get();

                                    foreach ($clients as $user) {
                                        try {
                                            $start = microtime(true);

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

                                            $end = microtime(true);
                                            $duration = $end - $start;
                                            echo "Synced {$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;
                                        }
                                    }

                                    // Free memory after each chunk
                                    unset($clients);
                                }
                            } 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);
        }
    }
}
