<?php

/* * ********************************************************************
 * ProxmoxVPS product developed. (Jan 16, 2019)
 * *
 *
 *  CREATED BY MODULESGARDEN       ->       http://modulesgarden.com
 *  CONTACT                        ->       contact@modulesgarden.com
 *
 *
 * This software is furnished under a license and may be used and copied
 * only  in  accordance  with  the  terms  of such  license and with the
 * inclusion of the above copyright notice.  This software  or any other
 * copies thereof may not be provided or otherwise made available to any
 * other person.  No title to and  ownership of the  software is  hereby
 * transferred.
 *
 *
 * ******************************************************************** */

namespace ModulesGarden\ProxmoxAddon\App\Services;

use MGProvision\Proxmox\v2\models\Kvm;
use MGProvision\Proxmox\v2\models\Lxc;
use ModulesGarden\ProxmoxAddon\App\Models\NodeSetting;
use ModulesGarden\ProxmoxAddon\App\Models\Whmcs\Hosting;
use ModulesGarden\ProxmoxAddon\Core\Models\ModuleSettings\Model as Settings;

/**
 * Description of LoadBalancerService
 *
 * @author Pawel Kopec <pawelk@modulesgardne.com>
 */
class LoadBalancerService
{
    protected $nodesResurces = [];
    private $serverId;

    /**
     *
     * @var \MGProvision\Proxmox\v2\Api
     */
    private $api;
    private $usage = false;
    private $filter = [];
    private $excludeVmid;
    private $vmsWeight;
    private $ramWeight;
    private $diskWeight;
    private $cpuWeight;

    function __construct($serverId)
    {
        $this->serverId = $serverId;
    }

    public function getExcludeVmid()
    {
        return $this->excludeVmid;
    }

    public function setExcludeVmid($excludeVmid)
    {
        $this->excludeVmid = $excludeVmid;
        return $this;
    }

    public function findByNodes(array  $nodes){
        $this->filter['nodes'] = $nodes;
        return $this;
    }
    /**
     * @return LoadBalancerService
     */
    public function findByVmCreate()
    {
        //Node settings
        $this->nodesResurces = [];
        $query               = NodeSetting::ofServer($this->serverId);
        if($this->filter['nodes']){
            $query->ofNodes($this->filter['nodes']);
        }
        $query->ofSetting('vmCreate')->ofValue(1);
        foreach ($query->get() as $entity)
        {
            $this->nodesResurces[] = $this->format($entity);
        }
        return clone $this;
    }

    private function format(NodeSetting $entity)
    {
        $nodesResurce = [
            'node'       => $entity->node,
            'vmCreate'   => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('vmCreate')->value('value'),
            'vmsMax'     => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('vmsMax')->value('value'),
            'vmsWeight'  => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('vmsWeight')->value('value'),
            'vmsUsed'    => 0,
            'vmsFree'    => null,
            'cpuMax'     => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('cpuMax')->value('value'),
            'cpuWeight'  => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('cpuWeight')->value('value'),
            'cpuUsed'    => 0,
            'cpuFree'    => null,
            'diskUsed'   => 0,
            'diskMax'    => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('diskMax')->value('value'),
            'diskWeight' => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('diskWeight')->value('value'),
            'diskFree'   => null,
            'ramMax'     => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('ramMax')->value('value'),
            'ramWeight'  => NodeSetting::ofServer($this->serverId)->ofNode($entity->node)->ofSetting('ramWeight')->value('value'),
            'ramUsed'    => 0,
            'ramFree'    => 0,
        ];
        if ($nodesResurce['diskMax'])
        {
            $nodesResurce['diskMax'] = $nodesResurce['diskMax'] * pow(1024, 3);
        }
        if ($nodesResurce['ramMax'])
        {
            $nodesResurce['ramMax'] = $nodesResurce['ramMax'] * pow(1024, 3);
        }
        return $nodesResurce;
    }

    /**
     * @return LoadBalancerService
     */
    public function findByNode($node, $vmCreate = true)
    {
        $this->filter['node'] = $node;
        $this->usage = false;
        //Node settings
        $this->nodesResurces = [];
        $query               = NodeSetting::ofServer($this->serverId)
            ->ofNode($node)
            ->ofSetting('vmCreate');
        if ($vmCreate)
        {
            $query->ofValue(1);
        }
        if(!$query->count()){
            $entity = new NodeSetting();
            $entity->node = $node;
            $entity->server_id = $this->serverId;
            $entity->setting =  'vmsMax';
            $entity->value = "";
            $this->nodesResurces[] = $this->format($entity);
        }
        foreach ($query->get() as $entity)
        {
            $this->nodesResurces[] = $this->format($entity);
        }

        return clone $this;
    }

    /**
     * @return LoadBalancerService
     */
    public function findNotUser($userId)
    {
        $this->filter['notUserId'] = $userId;
        foreach ($this->nodesResurces as $k => $nodeResurce)
        {
            if ($this->hasNode($userId, $nodeResurce['node']))
            {
                unset($this->nodesResurces[$k]);
            }
        }
        return clone $this;
    }

    private function hasNode($userId, $node)
    {
        $total = Hosting::ofUserId($userId)
            ->ofServerId($this->serverId)
            ->active()
            ->ofCustomFieldNode($node)
            ->count();
        try
        {
            $total += Hosting::ofUserId($userId)
                ->ofServerId($this->serverId)
                ->active()
                ->ofvServerNode($node)
                ->count();
        }
        catch (\Exception $ex)
        {//Table does not exist
        }
        return $total > 0;
    }

    /**
     * @return LoadBalancerService
     */
    public function findByRam($bytes)
    {
        $this->filter['ram'] = $bytes;
        $this->usage();
        foreach ($this->nodesResurces as $k => $nodeResurce)
        {
            if (!is_null($nodeResurce['ramFree']) && $nodeResurce['ramFree'] < $bytes)
            {
                unset($this->nodesResurces[$k]);
            }
        }
        return clone $this;
    }

    private function usage()
    {
        if ($this->usage)
        {
            return true;
        }
        if (!$this->api instanceof \MGProvision\Proxmox\v2\Api)
        {
            throw new \Exception("API instance is not initialized");
        }
        $nodes = [];
        foreach ($this->api->get("/nodes") as $node)
        {
            if ($node['status'] != "online")
            {
                continue;
            }
            $nodes[] = $node['node'];
        }
        foreach ($this->nodesResurces as $k => &$nodeResurce)
        {
            if (!in_array($nodeResurce['node'], $nodes))
            {
                continue;
            }
            //kvm
            $qemu = $this->api->get("/nodes/{$nodeResurce['node']}/qemu");
            foreach ($qemu as $record)
            {
                if ($record['template'] || ($this->excludeVmid && $this->excludeVmid == $record['vmid']) || !$this->hasVmid($record['vmid']))
                {
                    continue;
                }
                $vm = new Kvm($nodeResurce['node'], $record['vmid']);
                $vm->setApi($this->api);
                foreach ($vm->getHardDisks() as $hardDisk)
                {
                    $nodeResurce['diskUsed'] += $hardDisk->getBytes();
                }
                $nodeResurce['ramUsed'] += $record['maxmem'];
                $nodeResurce['vmsUsed']++;
                $nodeResurce['cpuUsed'] += $record['cpus'];
            }
            unset($qemu, $record, $vm);
            //lxc
            $lxc = $this->api->get("/nodes/{$nodeResurce['node']}/lxc");
            foreach ($lxc as $record)
            {
                if ($record['template'] || ($this->excludeVmid && $this->excludeVmid == $record['vmid']) || !$this->hasVmid($record['vmid']))
                {
                    continue;
                }
                $vm = new Lxc($nodeResurce['node'], $record['vmid']);
                $vm->setApi($this->api);
                foreach ($vm->getMounPoints()->fetch() as $mounPoint)
                {
                    /*@var $mounPoint MountPoint */
                    $nodeResurce['diskUsed'] += $mounPoint->getBytes();
                }
                $nodeResurce['ramUsed'] += $record['maxmem'];
                $nodeResurce['vmsUsed']++;
                $nodeResurce['cpuUsed'] += $record['cpus'];
            }
            unset($lxc, $record, $vm);
        }
        //calculate free resurces
        foreach ($this->nodesResurces as $k => &$nodeResurce)
        {
            $nodeResurce['ramFree']  = $nodeResurce['ramMax'] - $nodeResurce['ramUsed'];
            $nodeResurce['vmsFree']  = $nodeResurce['vmsMax'] - $nodeResurce['vmsUsed'];
            $nodeResurce['cpuFree']  = $nodeResurce['cpuMax'] - $nodeResurce['cpuUsed'];
            $nodeResurce['diskFree'] = $nodeResurce['diskMax'] - $nodeResurce['diskUsed'];
        }
        $this->usage = true;
    }

    private function hasVmid($vmid)
    {
        $total = Hosting::ofServerId($this->serverId)
            ->ofStatus(['Active', 'Suspended'])
            ->ofCustomFielVmid($vmid)
            ->count();
        try
        {
            $total += Hosting::ofServerId($this->serverId)
                ->active()
                ->ofvServerVmid($vmid)
                ->count();
        }
        catch (\Exception $ex)
        {//Table does not exist
        }
        return $total > 0;
    }

    /**
     * @return LoadBalancerService
     */
    public function findByDisk($bytes)
    {
        $this->filter['disk'] = $bytes;
        $this->usage();
        foreach ($this->nodesResurces as $k => $nodeResurce)
        {
            if (!is_null($nodeResurce['diskFree']) && $nodeResurce['diskFree'] < $bytes)
            {
                unset($this->nodesResurces[$k]);
            }
        }
        return clone $this;
    }

    /**
     * @return LoadBalancerService
     */
    public function findByCpu($cpu)
    {
        $this->filter['cpu'] = $cpu;
        $this->usage();
        foreach ($this->nodesResurces as $k => $nodeResurce)
        {
            if (!is_null($nodeResurce['cpuFree']) && $nodeResurce['cpuFree'] < $cpu)
            {
                unset($this->nodesResurces[$k]);
            }
        }
        return clone $this;
    }

    /**
     * @return LoadBalancerService
     */
    public function findByVms($number)
    {
        $this->filter['vms'] = $number;
        $this->usage();
        foreach ($this->nodesResurces as $k => $nodeResurce)
        {
            if (!is_null($nodeResurce['vmsFree']) && $nodeResurce['vmsFree'] < $number)
            {
                unset($this->nodesResurces[$k]);
            }
        }
        return clone $this;
    }

    public function getApi()
    {
        return $this->api;
    }

    public function setApi(\MGProvision\Proxmox\v2\Api $api)
    {
        $this->api = $api;
        return $this;
    }

    public function getNodesResurces()
    {
        $this->usage();
        return $this->nodesResurces;
    }

    public function count()
    {
        return count($this->nodesResurces);
    }

    public function isEmpty()
    {
        return empty($this->nodesResurces);
    }

    public function nodeLowLoad()
    {
        $this->usage();
        $weight           = Settings::where("setting", 'vmsWeight')->value("value");
        $this->vmsWeight  = $weight ? $weight : 1;
        $weight           = Settings::where("setting", 'cpuWeight')->value("value");
        $this->cpuWeight  = $weight ? $weight : 1;
        $weight           = Settings::where("setting", 'ramWeight')->value("value");
        $this->ramWeight  = $weight ? $weight : 1;
        $weight           = Settings::where("setting", 'diskWeight')->value("value");
        $this->diskWeight = $weight ? $weight : 1;

        $nodes = [];
        //By ram
        $nodes = $this->nodesUsage($nodes, $this->orderByRamUsage(), $this->ramWeight);
        //By cpu
        $nodes = $this->nodesUsage($nodes, $this->orderByCpuUsage(), $this->cpuWeight);
        //By Disk
        $nodes = $this->nodesUsage($nodes, $this->orderByDiskUsage(), $this->diskWeight);
        //By Vms
        $nodes = $this->nodesUsage($nodes, $this->orderByVmsUsage(), $this->vmsWeight);

        if (empty($nodes))
        {
            throw new \Exception("Load Balancer: Cannot find node with free resources");
        }

        asort($nodes);
        $nodes = array_reverse($nodes);

        return key($nodes);
    }

    private function nodesUsage($nodes, $values, $weight = 1)
    {
        foreach (array_reverse($values) as $index => $nodeName)
        {
            if (!array_key_exists($nodeName, $nodes))
            {
                $nodes[$nodeName] = 0;
            }

            $nodes[$nodeName] += ($index + 1) * $weight; // we don't want zero value so we add "1"
        }


        return $nodes;
    }

    /**
     * @return LoadBalancerService
     */
    public function orderByRamUsage()
    {
        return $this->orderBy('ramUsed');
    }

    private function orderBy($column)
    {
        $this->usage();
        $column = array_column($this->nodesResurces, $column);
        array_multisort($column, SORT_ASC, $this->nodesResurces);
        $nodes = [];
        foreach ($column as $k => $v)
        {
            $nodes[] = $this->nodesResurces[$k]['node'];
        }
        return $nodes;
    }

    public function orderByCpuUsage()
    {
        return $this->orderBy('ramUsed');
    }

    public function orderByDiskUsage()
    {
        return $this->orderBy('vmsUsed');
    }

    public function orderByVmsUsage()
    {
        return $this->orderBy('vmsUsed');
    }


}
