<?php
/**********************************************************************
 * ProxmoxVPS developed. (26.03.19)
 * *
 *
 *  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\Servers\ProxmoxVps\App\Http\Actions;

use MGProvision\Proxmox\v2\models\Kvm;
use MGProvision\Proxmox\v2\models\Node;
use MGProvision\Proxmox\v2\repository\ClusterResourcesRepository;
use ModulesGarden\ProxmoxAddon as addon;
use ModulesGarden\ProxmoxAddon\App\Configuration\Addon\Update\AddonUpgradeService;
use ModulesGarden\ProxmoxAddon\App\Models\VmIpAddress;
use ModulesGarden\ProxmoxAddon\App\Services\ApiService;
use ModulesGarden\ProxmoxAddon\App\Services\Utility;
use ModulesGarden\ProxmoxAddon\App\Services\Vps\HostingService;
use ModulesGarden\ProxmoxAddon\App\Services\Vps\NetworkService;
use ModulesGarden\ProxmoxAddon\App\Services\Vps\ProductService;
use ModulesGarden\ProxmoxAddon\App\Services\Vps\UserService;
use ModulesGarden\ProxmoxAddon\Core\Queue\Models\Job;
use ModulesGarden\Servers\ProxmoxVps\App\Enum\CustomField;
use ModulesGarden\Servers\ProxmoxVps\App\Helpers\AppParams;
use ModulesGarden\Servers\ProxmoxVps\App\Helpers\ProxmoxAddonValidator;
use ModulesGarden\Servers\ProxmoxVps\Core\App\Controllers\Instances\AddonController;
use ModulesGarden\Servers\ProxmoxVps\Core\Models\Whmcs\Server;
use ModulesGarden\Servers\ProxmoxVps\Core\UI\Traits\WhmcsParams;
use ModulesGarden\Servers\ProxmoxVps\App\Enum\ConfigurableOption;

class CreateAccount extends AddonController
{
    use WhmcsParams;
    use ProductService;
    use ApiService;
    use UserService;
    use HostingService;
    use addon\App\Traits\Vps\SnippetTrait;

    private $errors = [];

    /**
     * @var AddonUpgradeService
     */
    private $addonUpgradeService;

    /**
     * @var Kvm|null
     */
    private $templateVm;

    public function execute($params = null)
    {
        if(!ProxmoxAddonValidator::isInstalled()){
            return ProxmoxAddonValidator::failAsString();
        }
        (new AppParams())->initFromWhmcsParams();
        $this->addonUpgradeService = new AddonUpgradeService();
        $this->addonUpgradeService->run();
        try
        {
            //init nameservers
            $this->initNameServers();
            //Validate
            $this->validate();
            //Clone validate
            if ($this->configuration()->isQemu())
            {
                $this->cloneValidate();
            }
            //Check free resources
            $this->checkResources();
            //update hostname
            if($params['customfields']['proxmoxHostname']){
                $this->hosting()->domain = $params['customfields']['proxmoxHostname'];
                $this->hosting()->save();
            }
            //throw errors
            if ($this->errors)
            {
                throw new \Exception(implode(", ", $this->errors));
            }
            //Create User
            if (!$this->isUser())
            {
                $this->userCreate();
            }elseif($this->isUser() ){
                $this->createUserIfNotExist($this->getUser());
            }
            //Assing ip
            if (!Utility::isIpManagerProxmoxVPSIntegration())
            {
                if ($this->isCustomIpRequest() && $this->hasCustomIp())
                {
                    $this->addCustomIp();
                }
                else
                {
                    $networkService = new NetworkService();
                    list($requestIPv4, $requestIPv6) = $networkService->getIpAddressesRequest();
                    $node = $this->getNode()->getNode();
                    $networkService->hasIp($requestIPv4, $requestIPv6, $node)
                        ->addIp($requestIPv4, $requestIPv6, $node);
                    $this->customFieldUpdate(CustomField::NODE, $node);

                }
            }
            //automaticaly configure administartor username for window
            if($this->templateVm instanceof Kvm &&
                $this->configuration()->isAgent() &&
                $this->configuration()->isAgentServicePassword() &&
                !$this->configuration()->isAgentTemplateUser() &&
                preg_match('/w/', $this->templateVm->config()['ostype'])){
                $ciuser =  $this->templateVm->config()['ciuser'];
                $this->hosting()->update(['username' => $ciuser ?: 'Administrator' ]);
            }
            elseif($this->configuration()->isQemu() && $this->configuration()->getCiuser()){
                $this->hosting()->update(['username' =>$this->configuration()->getCiuser() ]);
            }
                        //hosting limit
            $this->saveUsageLimit();
            //Create task
            if ($this->configuration()->isLxc())
            {
                $taskName = addon\App\Jobs\Vps\CreateLxcJob::class;
            }
            else
            {
                if ($this->configuration()->isQemu())
                {
                    $taskName   = addon\App\Jobs\Vps\CreateQemuJob::class;
                    $osTemplate = $this->getWhmcsConfigOption(ConfigurableOption::OS_TEMPLATE, $this->configuration()->getOsTemplate());
                    if (!empty($osTemplate) && $osTemplate != "0")
                    {
                        $taskName = addon\App\Jobs\Vps\CloneQemuJob::class;
                    }
                }
            }
            $arguments = [
                'hostingId' => $this->getWhmcsParamByKey('serviceid')
            ];
            if($this->configuration()->isQemu() && $this->hasSnippet()){
                $parent = addon\Core\Helper\queue(addon\App\Jobs\Vps\CreateSnippet::class, $arguments, null, "hosting", $this->getWhmcsParamByKey("serviceid"));
            }
            addon\Core\Helper\queue($taskName, $arguments, $parent->id, "hosting", $this->getWhmcsParamByKey("serviceid"));
            return "success";
        }
        catch (\Exception $ex)
        {
            if (addon\App\Models\ModuleSettings::isDebug())
            {
                logModuleCall(
                    'ProxmoxVps',
                    __CLASS__,
                    [],
                    null,
                    $ex->getMessage() . " " . $ex->getTraceAsString()
                );
            }
            return $ex->getMessage();
        }
    }

    private function validate()
    {
        //vmid
        if ($this->getWhmcsCustomField(CustomField::VMID))
        {
            $this->errors[] = "Custom Field \"VMID\" is not empty";
        }
        //node
        if ($this->getWhmcsCustomField(CustomField::NODE))
        {
            $this->errors[] = "Custom Field \"Node\" is not empty";
        }
        if (!in_array($this->configuration()->getVirtualization(), ["qemu", "lxc"]))
        {
            $this->errors[] = 'Unknown virtualization type: ' . $this->configuration()->getVirtualization();
        }
        //Network bridge
        if (!$this->configuration()->getBridge())
        {
            $this->errors[] = "''Bridge' - is empty";
        }
        //lxc
        if ($this->configuration()->isLxc())
        {
            //OS Template
            if (!$this->getWhmcsConfigOption(ConfigurableOption::OS_TEMPLATE, $this->configuration()->getOsTemplate()) )
            {
                $this->errors[] = "'OS Template' - is empty";
            }
            //Storage
            if (!$this->configuration()->getStorage())
            {
                $this->errors[] = "'Default Storage' - is empty";
            }
            $taskName = addon\App\Jobs\Vps\CreateLxcJob::class . '@handle';
        }
        else
        {
            if ($this->configuration()->isQemu())
            {
                //searchdomain
                if ($this->configuration()->getSearchdomain() && !preg_match('/^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$/', $this->configuration()->getSearchdomain()))
                {
                    $this->errors[] = sprintf('DNS Domain \'%s\' is invalid', $this->configuration()->getSearchdomain());
                }
                //SSH Key
                if ($this->getWhmcsParamByKey('customfields')['sshkeys'] && !preg_match('/ssh\-/', $this->getWhmcsParamByKey('customfields')['sshkeys']))
                {
                    $this->errors[] = ' SSH Public Key is invalid';
                }
                $taskName   = addon\App\Jobs\Vps\CreateQemuJob::class . '@handle';
                $osTemplate = $this->getWhmcsConfigOption(ConfigurableOption::OS_TEMPLATE, $this->configuration()->getOsTemplate() );
                //clone
                if (!empty($osTemplate) && $osTemplate != "0")
                {
                    $taskName = addon\App\Jobs\Vps\CloneQemuJob::class . '@handle';
                }
            }
        }
        $query = Job::where("job", $taskName)->whereIn("status", ['waiting', "running", ""])->where("rel_id", $this->getWhmcsParamByKey("serviceid"))->where("rel_type", "hosting")
                    ->orWhere("job", $taskName)->where("status", 'error')->where("rel_id", $this->getWhmcsParamByKey("serviceid"))->where("rel_type", "hosting")->where("retry_count","<",100);
        if ( $query->count() )
        {
            $this->errors[] = "Job 'create' already exist";
        }
        //Name servers
        if ($this->configuration()->isLxc() || $this->configuration()->isCloudInitServiceNameservers())
        {
            $ns = [];
            for ($i = 1; $i <= 2; $i++)
            {
                $n = trim($this->hosting()->{"ns{$i}"});
                if (!empty($n) && !filter_var($n, FILTER_VALIDATE_IP))
                {
                    $n = gethostbyname($n);
                }
                if (!empty($n) && !filter_var($n, FILTER_VALIDATE_IP))
                {
                    $ns[] = "'{$n}'";
                }
            }
            if (!empty($ns))
            {
                $this->errors[] = sprintf(" Name Server value: %s does not look like a valid IP Address", implode(", ", $ns));
            }
        }
        //Disk size
        $diskSize = $this->getWhmcsConfigOption(ConfigurableOption::DISK_SIZE);
        if ($diskSize && $this->configuration()->getDiskUnit() == "mb" && $diskSize < 1024)
        {
            $this->errors[] = sprintf("Disk size of %sMB cannot be  created", $diskSize);
        }

        $rate = $this->getWhmcsConfigOption(addon\App\Enum\Vps\ConfigurableOption::NETWORK_RATE) ? $this->getWhmcsConfigOption(addon\App\Enum\Vps\ConfigurableOption::NETWORK_RATE) :$this->configuration()->getRate();
        if ($rate && !is_numeric($rate))
        {
            $this->errors[] = sprintf("Network Rate - invalid format: %s ", $rate) ;
        }

    }

    private function initNameServers()
    {
        $this->setHostingId($this->getWhmcsParamByKey("serviceid"));
        if ($this->configuration()->isServerNameservers())
        {
            $server               = Server::select('id', 'nameserver1ip', 'nameserver2ip')->findOrFail($this->getWhmcsParamByKey('serverid'));
            $this->hosting()->ns1 = trim($server->nameserver1ip);
            $this->hosting()->ns2 = trim($server->nameserver2ip);
            $this->hosting()->save();
        }
        else
        {
            if ($this->configuration()->isLxc() || $this->configuration()->isCloudInitServiceNameservers())
            {
                $domain =  $this->getWhmcsCustomField(CustomField::HOSTNAME, $this->getWhmcsParamByKey('domain'));
                $update = [];
                for ($i = 1; $i <= 2; $i++)
                {
                    $n = trim($this->hosting()->{"ns{$i}"});
                    if (!empty($n) && !filter_var($n, FILTER_VALIDATE_IP))
                    {
                        $host = preg_match('/\./', $n) ? $n : "{$n}.{$domain}";
                        $n    = gethostbyname($host);
                        if (!$n || !filter_var($n, FILTER_VALIDATE_IP))
                        {
                            throw new \Exception(sprintf("Nameserver '%s' cannot be resolved", $host));
                        }
                    }
                    if (!empty($n) && filter_var($n, FILTER_VALIDATE_IP))
                    {
                        $update['ns' . $i]           = $n;
                        $this->hosting()->{"ns{$i}"} = $n;
                    }
                }
                if ($update)
                {
                    $this->hosting()->update($update);
                }
            }
        }
    }

    private function isCustomIpRequest()
    {
        //if there is custom ip on dedicated ip or assinged ips
        $model = $this->getWhmcsParamByKey('model');
        if (!$model)
        {
            $model = \WHMCS\Service\Service::where("id", $this->getWhmcsParamByKey('serviceid'))->first();
        }
        if (!$model->dedicatedip && !$model->assignedips)
        {
            return false;
        }
        $ips = $this->getCustomIp();
        if (!$ips)
        {
            return false;
        }
        foreach ($ips as $k => $ip)
        {
            if (!filter_var((string)$ip, FILTER_VALIDATE_IP))
            {
                throw new \Exception(sprintf("IP Address '%s' is not valid", $ip));
            }
        }
        return true;
    }

    private function getCustomIp()
    {
        $ips   = [];
        $model = $this->getWhmcsParamByKey('model');
        if (!$model)
        {
            $model = \WHMCS\Service\Service::where("id", $this->getWhmcsParamByKey('serviceid'))->first();
        }
        if ($model->dedicatedip)
        {
            $ips[] = $model->dedicatedip;
        }
        if ($model->assignedips)
        {
            $ex = explode(PHP_EOL, $model->assignedips);
            foreach ($ex as $ip)
            {
                if (!$ip)
                {
                    continue;
                }
                $ips[] = str_replace(["\r", "\n"], ["", ""], $ip);
            }
        }
        return array_diff($ips, [""]);
    }

    private function hasCustomIp()
    {
        $nodeName       = $this->getNode()->getNode();
        $ipAddresses    = $this->getCustomIp();
        $virtualization = $this->configuration()->isQemu() ? "KVM" : "LXC";
        $ipCollections  = addon\App\Models\IpAddress::where('hosting_id', '0')
            ->where('private', '0')
            ->whereIn('ip', $ipAddresses)
            ->whereIn('sid', [$this->getWhmcsParamByKey('serverid'), '0'])
            ->whereIn('visualization', [$virtualization, 'Auto'])
            ->whereIn('node', [$nodeName, '0', ""]);
        $count          = $ipCollections->count();
        return $count == count($ipAddresses);
    }

    private function addCustomIp()
    {
        $nodeName       = $this->getNode()->getNode();
        $ipAddresses    = $this->getCustomIp();
        $virtualization = $this->configuration()->isQemu() ? "KVM" : "LXC";
        addon\App\Models\IpAddress::where('hosting_id', '0')
            ->where('private', '0')
            ->whereIn('ip', $ipAddresses)
            ->whereIn('sid', [$this->getWhmcsParamByKey('serverid'), '0'])
            ->whereIn('visualization', [$virtualization, 'Auto'])
            ->whereIn('node', [$nodeName, '0', ""])
            ->update(["hosting_id" => $this->getWhmcsParamByKey('serviceid'), 'last_check' => Utility::timeStamp()]);
        //get ips
        $collection = addon\App\Models\IpAddress::where('hosting_id', $this->getWhmcsParamByKey('serviceid'))
            ->whereNotIn('ip', function ($query)
            {
                $query->select('ip')->from((new VmIpAddress)->getTable());
            });
        foreach ($collection->get() as $ip)
        {
            /*@var $ip VmIpAddress */
            $newIp                = new VmIpAddress();
            $ipData               = $ip->toArray();
            $ipData['hosting_id'] = $this->getWhmcsParamByKey('serviceid');
            $ipData['server_id']  = $this->getWhmcsParamByKey('serverid');
            $newIp->fill($ipData)->save();
        }
        return $this;
    }

    private function checkResources()
    {
        if (!$this->configuration()->isCheckResources())
        {
            return;
        }
        //get free space
        $storage = $this->configuration()->isQemu() ? $this->configuration()->getDiskStorage() : $this->configuration()->getStorage();
        $resurces      = $this->getNode()->getFreeSpace($storage);
        //Memory
        if ($this->getWhmcsConfigOption(ConfigurableOption::MEMORY))
        {
            $memoryBytes = $this->getWhmcsConfigOption(ConfigurableOption::MEMORY);
            Utility::unitFormat($memoryBytes, $this->configuration()->getMemoryUnit(), 'bytes');
        }
        else
        {
            $memoryBytes = $this->configuration()->getMemory();
            Utility::unitFormat($memoryBytes, "mb", 'bytes');
        }
        //Validation
        if ($memoryBytes && $resurces['memory'] && $memoryBytes > $resurces['memory'])
        {
            $this->errors[] = sprintf("Unable to allocate %s of Memory. Memory available: %s",
                addon\App\Libs\Format::convertBytes($memoryBytes), addon\App\Libs\Format::convertBytes($resurces['memory']));
        }
        //SWAP
        if ($this->configuration()->isLxc())
        {
            if ($this->getWhmcsConfigOption(ConfigurableOption::SWAP))
            {
                $swapBytes = $this->getWhmcsConfigOption(ConfigurableOption::SWAP);
                Utility::unitFormat($swapBytes, strtolower($this->configuration()->getSwapUnit()), 'bytes');
            }
            else
            {
                $swapBytes = $this->configuration()->getSwap();
                Utility::unitFormat($swapBytes, "mb", 'bytes');
            }
            //Validation
            if ($swapBytes && $resurces['swap'] && $swapBytes > $resurces['swap'])
            {
                $this->errors[] = sprintf("Unable to allocate %s of SWAP. SWAP available: %s",
                    addon\App\Libs\Format::convertBytes($swapBytes), addon\App\Libs\Format::convertBytes($resurces['swap']));
            }
        }
        //Disk
        if ($this->getWhmcsConfigOption(ConfigurableOption::DISK_SIZE))
        {
            $diskBytes = $this->getWhmcsConfigOption(ConfigurableOption::DISK_SIZE);
            Utility::unitFormat($diskBytes, $this->configuration()->getDiskUnit(), 'bytes');
        }
        else
        {
            $diskBytes = $this->configuration()->getDiskSize();
            Utility::unitFormat($diskBytes, "gb", 'bytes');
        }
        //Validation
        if ($diskBytes && $resurces['storage'] && $diskBytes > $resurces['storage'])
        {
            $this->errors[] = sprintf("Unable to allocate %s of Disk Space. Disk Space available: %s",
                addon\App\Libs\Format::convertBytes($diskBytes), addon\App\Libs\Format::convertBytes($resurces['storage']));
        }
    }

    private function cloneValidate()
    {

        $osTemplate    = $this->getWhmcsConfigOption(ConfigurableOption::OS_TEMPLATE, $this->configuration()->getOsTemplate() );
        if (empty($osTemplate) && $osTemplate == "0")
        {
            return;
        }
        //Support for configurable options i.e vmname|OS Name
        if (is_string($osTemplate) && !preg_match('/\//', $osTemplate))
        {
            $templateNode      = $this->getNode()->getNode();
            $clusterRepository = new ClusterResourcesRepository();
            $clusterRepository->setApi($this->api());
            if(!$this->configuration()->isOsTemplatesInAllNodes()){
                $clusterRepository->findByNodes([$templateNode]);
            }
            $clusterRepository->findKvmTemplate();
            foreach ($clusterRepository->fetch() as $resurce)
            {
                if ($resurce->getName() == $osTemplate )
                {
                    $templateVmid = $resurce->getVmid();
                    $templateNode = $resurce->getNode();
                    break;
                }
            }
            if (!$templateVmid)
            {
                $this->errors[] = sprintf("Unable to find KVM template: %s on node: %s", $osTemplate, $this->getNode()->getNode());
                return;
            }
            //Support for configurable options like nodename/vmid|OS Name
        }
        else
        {
            if (preg_match('/\//', $osTemplate))
            {
                list($templateNode, $templateVmid) = explode("/", $osTemplate);
            }
        }
        if (!in_array($templateNode, (array)$this->api()->get_node_list()))
        {
            $this->errors[] = sprintf("Unable to find node %s on cluster %s", $templateNode, $this->api()->getPveHostname());
            return;
        }
        //template exist on node?
        $node = new Node($templateNode);
        $node->setApi($this->api());
        if (!$node->hasKvm($templateVmid))
        {
            $this->errors[] = sprintf("KVM template VMID '%s' does not exist on  node '%s'", $templateVmid, $templateNode);
            return;
        }
        //Disk size validation
        $this->templateVm = new Kvm($templateNode, $templateVmid);
        $this->templateVm->setApi($this->api());
        //Disk
        $diskSize = $this->configuration()->getDiskSize();
        if ($this->getWhmcsConfigOption(ConfigurableOption::DISK_SIZE))
        {
            $diskSize = $this->getWhmcsConfigOption(ConfigurableOption::DISK_SIZE);
            Utility::unitFormat($diskSize, $this->configuration()->getDiskUnit(), 'bytes');
        }else{
             Utility::unitFormat($diskSize, "gb", 'bytes');
        }
        if ($this->templateVm->getMasterHardDisk()->getBytes() > $diskSize)
        {
            $this->errors[] = sprintf('Downgrading disk size  is restricted. Request Disk size: %s, VM disk size: %s', addon\App\Libs\Format::convertBytes($diskSize), addon\App\Libs\Format::convertBytes($this->templateVm->getMasterHardDisk()->getBytes()));
        }
    }

}
