<?php

namespace idoit\Module\License;

use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use idoit\Component\Helper\Unserialize;
use idoit\Module\License\Crypt\DefaultEncryption;
use idoit\Module\License\Entity\License;
use idoit\Module\License\Event\License\LegacyLicenseRemovedEvent;
use idoit\Module\License\Event\License\LicenseAddedEvent;
use idoit\Module\License\Exception\InsufficientLicenseObjectsException;
use idoit\Module\License\Exception\LegacyLicenseExistingException;
use idoit\Module\License\Exception\LegacyLicenseExpiredException;
use idoit\Module\License\Exception\LegacyLicenseInstallException;
use idoit\Module\License\Exception\LegacyLicenseInvalidKeyException;
use idoit\Module\License\Exception\LegacyLicenseInvalidTypeException;
use idoit\Module\License\Exception\LegacyLicenseParseException;
use idoit\Module\License\Exception\LicenseDecryptionException;
use idoit\Module\License\Exception\LicenseExistsException;
use idoit\Module\License\Exception\LicenseInvalidException;
use idoit\Module\License\Exception\LicenseParseException;
use idoit\Module\License\Exception\LicenseServerAuthenticationException;
use idoit\Module\License\Exception\LicenseServerConnectionException;
use idoit\Module\License\Exception\LicenseServerNoLicensesException;
use isys_application;
use isys_cmdb_dao;
use isys_component_dao_mandator;
use isys_component_database;
use isys_settings;
use isys_statistics_dao;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;

class LicenseService
{
    const LEGACY_LICENSE_ID     = 'license_id';
    const LEGACY_LICENSE_PARENT = 'license_parent_id';
    const LEGACY_LICENSE_TYPE   = 'license_type';
    const LEGACY_LICENSE_MANDATOR = 'license_mandator';
    const LEGACY_LICENSE_EXPIRES = 'license_expires';

    const C__LICENCE__OBJECT_COUNT = 0x001;
    const C__LICENCE__DB_NAME = 0x002;
    const C__LICENCE__CUSTOMER_NAME = 0x003;
    const C__LICENCE__REG_DATE = 0x004;
    const C__LICENCE__RUNTIME = 0x005;
    const C__LICENCE__EMAIL = 0x006;
    const C__LICENCE__KEY = 0x007;
    const C__LICENCE__TYPE = 0x008;
    const C__LICENCE__DATA = 0x009;
    const C__LICENCE__CONTRACT = 0x010;
    const C__LICENCE__MAX_CLIENTS = 0x011;
    const C__LICENCE__IS_EVALUATION = 0x012;

    const C__LICENCE_TYPE__SINGLE = 0;
    const C__LICENCE_TYPE__HOSTING = 1;
    const C__LICENCE_TYPE__HOSTING_SINGLE = 2;
    const C__LICENCE_TYPE__BUYERS_LICENCE = 3;
    const C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING = 4;
    const C__LICENCE_TYPE__NEW__IDOIT = 5;
    const C__LICENCE_TYPE__NEW__ADDON = 6;

    const LICENCE_ERROR_EXISTS = -6;
    const LICENCE_ERROR_SYSTEM = -100;
    const LICENCE_ERROR_INVALID_TYPE = -10;
    const LICENCE_ERROR_KEY = -5;

    const LEGACY_LICENSE_TYPES = [
        self::C__LICENCE_TYPE__SINGLE,
        self::C__LICENCE_TYPE__HOSTING,
        self::C__LICENCE_TYPE__HOSTING_SINGLE,
        self::C__LICENCE_TYPE__BUYERS_LICENCE,
        self::C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING
    ];

    const LEGACY_LICENSE_TYPES_HOSTING = [
        self::C__LICENCE_TYPE__HOSTING,
        self::C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING
    ];

    const LICENSE_TYPES        = [
        self::C__LICENCE_TYPE__NEW__IDOIT,
        self::C__LICENCE_TYPE__NEW__ADDON
    ];

    const HTTP_STATUS_POSITIVE = [
        Response::HTTP_OK,
        Response::HTTP_NOT_FOUND
    ];

    const DEFAULT_DATE_FORMAT = '%Y-%m-%d';

    /**
     * @var Client
     */
    private $client;

    /**
     * @var \isys_component_database
     */
    private $databaseSystem;

    /**
     * @var DefaultEncryption
     */
    private $encryption;

    /**
     * @var null|string
     */
    private $encryptionToken;

    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    /**
     * @var string
     */
    private $licenseServer = 'https://lizenzen.i-doit.com';

    /**
     * LicenseService constructor.
     *
     * @param \isys_component_database $databaseSystem
     * @param DefaultEncryption        $encryption
     * @param EventDispatcherInterface $eventDispatcher
     * @param Client                   $client
     * @param string                   $encryptionToken
     */
    public function __construct(
        \isys_component_database $databaseSystem,
        DefaultEncryption $encryption,
        EventDispatcherInterface $eventDispatcher,
        Client $client,
        $encryptionToken = null
    ) {
        $this->databaseSystem = $databaseSystem;
        $this->encryption = $encryption;
        $this->eventDispatcher = $eventDispatcher;
        $this->client = $client;
        $this->encryptionToken = $encryptionToken;
    }

    /**
     * @return EventDispatcherInterface
     */
    public function getEventDispatcher()
    {
        return $this->eventDispatcher;
    }

    /**
     * @param string $licenseServer
     */
    public function setLicenseServer($licenseServer)
    {
        $this->licenseServer = rtrim($licenseServer, '/');
    }

    /**
     * @return string|null
     */
    public function getEncryptionToken()
    {
        return $this->encryptionToken;
    }

    /**
     * @param string $encryptionToken
     */
    public function setEncryptionToken($encryptionToken)
    {
        $this->encryptionToken = $encryptionToken;
    }

    /**
     * Sets available license objects for tenant, will throw exception when amount is unvailable
     *
     * @param array $licenseObjects
     * @param int[] $tenantIds
     *
     * @return bool
     *
     * @throws InsufficientLicenseObjectsException
     */
    public function setLicenseObjectsForTenants(array $licenseObjects, array $tenantIds)
    {
        $this->databaseSystem->begin();

        foreach ($tenantIds as $tenantId) {
            $sql = "UPDATE isys_mandator SET isys_mandator__license_objects = " . (int) $licenseObjects[$tenantId] . " WHERE isys_mandator__id = " . (int) $tenantId . ";";

            $this->databaseSystem->query($sql);
        }

        $totalLicenseObjects = $this->databaseSystem->retrieveArrayFromResource(
            $this->databaseSystem->query(
                "SELECT SUM(isys_mandator__license_objects) as totalLicenseObjects  FROM isys_mandator WHERE isys_mandator__active = 1 LIMIT 1;"
            )
        )[0]['totalLicenseObjects'];

        $objectsAvailable = $this->getTotalObjects();

        if ($objectsAvailable < $totalLicenseObjects) {
            $this->databaseSystem->rollback();

            throw new InsufficientLicenseObjectsException('Insufficient license objects '. abs($objectsAvailable - $totalLicenseObjects) .' missing.');
        }

        return $this->databaseSystem->commit();
    }

    /**
     * Calculated objects for given count of tenants
     *
     * @param int $dividend
     * @param int $divisor
     * @param int $multiple
     *
     * @return int[]
     */
    public static function calculateObjectDivisions($dividend, $divisor, $multiple)
    {
        $values = [];

        while ($dividend > 0 && $divisor > 0) {
            $a = round($dividend / $divisor / $multiple) * $multiple;
            $dividend -= $a;
            $divisor--;
            $values[] = $a;
        }

        return $values;
    }

    /**
     * Retrieve an array of licensed add-ons, for legacy static and new licenses dynamic
     *
     * @return array
     */
    public function getLicensedAddOns()
    {
        $addOns = [
            'iso27001' => [
                'label' => 'ISMS',
                'licensed' => false
            ],
            'viva' =>  [
                'label' => 'VIVA',
                'licensed' => false
            ],
            'viva2' =>  [
                'label' => 'VIVA2',
                'licensed' => false
            ],
            'cmk2' =>  [
                'label' => 'Check_MK 2',
                'licensed' => false
            ],
            'document' =>  [
                'label' => 'Documents',
                'licensed' => false
            ],
            'rfc' => [
                'label' => 'RFC',
                'licensed' => false
            ],
            'analytics' =>  [
                'label' => 'Analytics',
                'licensed' => false
            ],
            'forms' =>  [
                'label' => 'Forms',
                'licensed' => false
            ],
            'sectornord_ocs' =>  [
                'label' => 'OCS (Sector Nord AG)',
                'licensed' => false
            ],
            'becon_trigger' => [
                'label' => 'Trigger (becon)',
                'licensed' => false
            ],
            'med_tec' => [
                'label' => 'MedTec (becon)',
                'licensed' => false
            ],
            'synetics_flows' => [
                'label' => 'Flows',
                'licensed' => false
            ]
        ];

        $newLicenses = $this->getLicenses(true);

        foreach ($newLicenses as $newLicense) {
            if ($newLicense->isIdoitLicense()) {
                continue;
            }

            $productIdentifier = $newLicense->getProductIdentifier();

            if ($productIdentifier === null) {
                continue;
            }

            $addOns[$productIdentifier] = [
                'licensed' => true,
                'label' => $newLicense->getProductName() ?: $productIdentifier
            ];
        }

        $oldLicenses = $this->getLegacyLicenses(true);

        foreach ($oldLicenses as $oldLicense) {
            foreach ($oldLicense[self::C__LICENCE__DATA] as $addOn => $licensed) {
                if (isset($addOns[$addOn]) && $addOns[$addOn]['licensed'] === false) {
                    $addOns[$addOn]['licensed'] = true;
                }
            }
        }

        return $addOns;
    }

    /**
     * Retrieve used license objects by all (active) tenants
     *
     * @param bool $onlyActiveTenants
     *
     * @return int
     */
    public function getUsedLicenseObjects($onlyActiveTenants)
    {
        $tenants = $this->getTenants($onlyActiveTenants);

        $usedObjects = 0;

        foreach ($tenants as $tenant) {
            try {
                $tenantDatabase = $this->retrieveConnectionForTenant($tenant);
                $statisticsDao = new isys_statistics_dao($tenantDatabase, isys_cmdb_dao::instance($tenantDatabase));
                $usedObjects += $statisticsDao->count_objects();
            } catch (\Exception $exception) {
                continue;
            }

            unset($tenantDatabase, $statisticsDao);
        }

        return $usedObjects;
    }

    /**
     * Retrieve connection for tenant
     *
     * @param array $tenant
     *
     * @return isys_component_database
     *
     * @throws \Exception
     */
    public function retrieveConnectionForTenant(array $tenant)
    {
        global $g_db_system;

        // Create connection to mandator DB.
        return isys_component_database::get_database(
            $g_db_system['type'],
            $tenant["isys_mandator__db_host"],
            $tenant["isys_mandator__db_port"],
            $tenant["isys_mandator__db_user"],
            isys_component_dao_mandator::getPassword($tenant),
            $tenant["isys_mandator__db_name"]
        );
    }

    /**
     * Checks if a license, either legacy or new, is still valid
     *
     * @param $license
     *
     * @return bool
     */
    public function isLicenseValid($license)
    {
        $now = Carbon::now();

        if ($license instanceof License) {
            return (
                $now < $license->getValidityTo()
            );
        }

        if (
            $license[self::C__LICENCE__TYPE] == self::C__LICENCE_TYPE__BUYERS_LICENCE ||
            $license[self::C__LICENCE__TYPE] == self::C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING
        ) {
            return true;
        }

        if (
            isset(
                $license[self::C__LICENCE__REG_DATE],
                $license[self::C__LICENCE__RUNTIME]
            )
        ) {
            $days = (int) round(abs((($license[self::C__LICENCE__RUNTIME] / 60 / 60 / 24))));

            $registrationDate = Carbon::createFromTimestamp($license[self::C__LICENCE__REG_DATE]);

            $endDate = $registrationDate->copy()->addDays($days);

            return $now < $endDate;
        }

        return false;
    }

    /**
     * Retrieve legacy license, optional only valid ones
     *
     * @param bool $validate
     *
     * @return array
     */
    public function getLegacyLicenses($validate = false): array
    {
        $sql = 'SELECT * FROM isys_licence WHERE isys_licence__type IN (' . implode(', ', self::LEGACY_LICENSE_TYPES) . ');';

        $rawLicenses = $this->databaseSystem->retrieveArrayFromResource($this->databaseSystem->query($sql));

        $licenses = [];

        foreach ($rawLicenses as $rawLicense) {
            $license = Unserialize::toArray($rawLicense['isys_licence__data']);

            if ($validate === true && $this->isLicenseValid($license) === false) {
                continue;
            }

            $license[self::LEGACY_LICENSE_ID] = $rawLicense['isys_licence__id'];
            $license[self::LEGACY_LICENSE_PARENT] = $rawLicense['isys_licence__isys_licence__id'];
            $license[self::LEGACY_LICENSE_TYPE] = $rawLicense['isys_licence__type'];
            $license[self::LEGACY_LICENSE_MANDATOR] = $rawLicense['isys_licence__isys_mandator__id'];
            $license[self::LEGACY_LICENSE_EXPIRES] = $rawLicense['isys_licence__expires'];

            $licenses[$license[self::LEGACY_LICENSE_ID]] = $license;
        }

        return $licenses;
    }

    /**
     * Retrieve new licenses, optional only valid ones
     *
     * @param bool $validate
     *
     * @return License[]
     */
    public function getLicenses($validate = false)
    {
        $sql = 'SELECT * FROM isys_licence WHERE isys_licence__type IN (' . implode(', ', self::LICENSE_TYPES) . ');';

        $rawLicenses = $this->databaseSystem->retrieveArrayFromResource($this->databaseSystem->query($sql));

        $licenses = [];

        foreach ($rawLicenses as $license) {
            try {
                $newLicense = $this->generateLicenseFromKey($license['isys_licence__data']);
            } catch (LicenseDecryptionException $exception) {
                continue;
            }

            if ($validate === true && $this->isLicenseValid($newLicense) === false) {
                continue;
            }

            $licenses[$license['isys_licence__id']] = $newLicense;
        }

        return $licenses;
    }

    /**
     * Given string with multiple encrypted licenses will return Licenses
     *
     * @param string $encryptedLicenses
     *
     * @return License[]
     *
     * @throws LicenseParseException
     */
    public function parseEncryptedLicenses($encryptedLicenses)
    {
        $decryptedLicenses = [];

        if (
            preg_match_all('/===== LICENSE BEGIN =====\s*(\S*)\s*===== LICENSE END =====/m', $encryptedLicenses, $licenses) === false ||
            (!is_array($licenses[1]) || !$licenses[1])
        ) {
            throw new LicenseParseException('No parsable licenses found');
        }

        foreach ($licenses[1] as $licenseKey) {
            try {
                $decryptedLicenses[] = $this->generateLicenseFromKey($licenseKey);
            } catch (LicenseDecryptionException $exception) {
                continue;
            }
        }

        return $decryptedLicenses;
    }

    /**
     * Generate license object from encrypted key
     *
     * @param string $licenseKey
     *
     * @return License
     *
     * @throws LicenseDecryptionException
     */
    public function generateLicenseFromKey($licenseKey)
    {
        $decryptedLicense = $this->encryption->decrypt(trim($licenseKey), $this->encryptionToken);

        if ($decryptedLicense === null) {
            throw new LicenseDecryptionException('Could not decrypt given license');
        }

        $licenseData = json_decode($decryptedLicense, true);

        if (!is_array($licenseData)) {
            throw new LicenseDecryptionException('Could not decrypt given license');
        }

        $license = new License();
        $license->setId($licenseData['id']);
        $license->setExternalIdentifier($licenseData['external_identifier']);
        $license->setValidityFrom($licenseData['validity_from']);
        $license->setValidityTo($licenseData['validity_to']);
        $license->setObjects($licenseData['objects']);
        $license->setTenants($licenseData['tenants']);
        $license->setProductType($licenseData['product_type']);
        $license->setProductName($licenseData['product_name']);
        $license->setProductIdentifier($licenseData['product_identifier']);
        $license->setActive($licenseData['active']);
        $license->setEvaluation($licenseData['evaluation']);
        $license->setCreated($licenseData['created']);
        $license->setEnvironment($licenseData['environment']);
        $license->setLicenseIdentifier(sha1($decryptedLicense));
        $license->setDecrypted($licenseKey);

        return $license;
    }

    /**
     * Get total available license objects from valid legacy and new licenses
     *
     * @return int
     */
    public function getTotalObjects()
    {
        $objects = 0;

        $newLicenses = $this->getLicenses(true);
        $oldLicenses = $this->getLegacyLicenses(true);
        $now = Carbon::now();

        foreach ($newLicenses as $license) {
            if ($now < $license->getValidityFrom()) {
                continue;
            }

            $objects += $license->getObjects();
        }

        foreach ($oldLicenses as $oldLicense) {
            if (!isset($oldLicense[self::C__LICENCE__REG_DATE]) &&
                !empty($oldLicense[self::LEGACY_LICENSE_PARENT]) &&
                isset($oldLicenses[$oldLicense[self::LEGACY_LICENSE_PARENT]][self::C__LICENCE__OBJECT_COUNT]) &&
                $oldLicenses[$oldLicense[self::LEGACY_LICENSE_PARENT]][self::C__LICENCE__OBJECT_COUNT] > 0
            ) {
                continue;
            }

            $registrationDate = Carbon::createFromTimestamp($oldLicense[self::C__LICENCE__REG_DATE]);

            if ($now < $registrationDate) {
                continue;
            }

            $objects += (int) $oldLicense[self::C__LICENCE__OBJECT_COUNT];
        }

        return $objects;
    }

    /**
     * Retrieve tenants, filtered by active and ids
     *
     * @param bool  $active
     *
     * @param int[] $ids
     *
     * @return array
     */
    public function getTenants($active = true, array $ids = [])
    {
        $sql = "SELECT * FROM isys_mandator WHERE isys_mandator__active = " . (int) $active;

        if (!empty($ids)) {
            $sql .= ' AND isys_mandator__id IN (' . implode(', ', $ids) . ')';
        }

        $sql .= ";";

        return $this->databaseSystem->retrieveArrayFromResource($this->databaseSystem->query(
            $sql
        ));
    }

    /**
     * Update object devisions for active tenants
     *
     * @return void
     */
    public function updateObjectDivision()
    {
        $activeTenants = $this->getTenants();

        $totalObjects = $this->getTotalObjects();

        if (!isys_settings::get('admin.active_license_distribution', 1)) {
            return;
        }

        $objectCounts = static::calculateObjectDivisions($totalObjects, count($activeTenants), 1);

        $licenseObjects = [];
        $tenantIds = [];

        foreach ($activeTenants as $index => $tenant) {
            $licenseObjects[$tenant['isys_mandator__id']] = $objectCounts[$index];
            $tenantIds[] = $tenant['isys_mandator__id'];
        }

        try {
            $this->setLicenseObjectsForTenants($licenseObjects, $tenantIds);
        } catch (InsufficientLicenseObjectsException $exception) {
            $this->setLicenseObjectsForTenants(array_map(function ($value) {
                $value = 0;
            }, $licenseObjects), $tenantIds);
        }

        $this->resetDeactivatedTenants();
    }

    /**
     * Resets licensed objects from deactivated tenants
     *
     * @return void
     */
    public function resetDeactivatedTenants()
    {
        $nonActiveTenants = $this->getTenants(false);

        foreach ($nonActiveTenants as $tenant) {
            $sql = "UPDATE isys_mandator SET isys_mandator__license_objects = 0 WHERE isys_mandator__id = " . (int) $tenant['isys_mandator__id'] . ";";

            $this->databaseSystem->query($sql) && $this->databaseSystem->commit();
        }
    }

    /**
     * Checks if given amount of license objects is still available
     *
     * @param int $licenseObjects
     *
     * @return bool
     */
    public function areLicenseObjectsAvailable($licenseObjects)
    {
        $totalObjects = $this->getTotalObjects();
        $usedObjects = 0;

        $activeTenants = $this->getTenants(true);

        foreach ($activeTenants as $tenant) {
            $usedObjects += (int) $tenant['isys_mandator__license_objects'];
        }

        return ($usedObjects + $licenseObjects) <= $totalObjects;
    }

    /**
     * Checks if given tenant is licensed
     *
     * @param int $tenantId
     *
     * @return bool
     */
    public function isTenantLicensed($tenantId): bool
    {
        $isTenantLicensed = false;

        $tenants = $this->getTenants();

        $allowedTenantCount = $this->getTotalTenants();

        $usedObjects = 0;

        $totalObjects = $this->getTotalObjects();
        $reservedObjects = 0;

        foreach ($tenants as $tenant) {
            if ($tenant['isys_mandator__id'] == $tenantId) {
                try {
                    $tenantDatabase = $this->retrieveConnectionForTenant($tenant);
                } catch (\Exception $exception) {
                    return false;
                }

                $statisticsDao = new isys_statistics_dao($tenantDatabase, isys_cmdb_dao::instance($tenantDatabase));

                try {
                    $usedObjects = $statisticsDao->count_objects();
                } catch (\Exception $exception) {
                    return false;
                }

                $isTenantLicensed = $usedObjects < ((int) $tenant['isys_mandator__license_objects']);

                if ($isTenantLicensed === false) {
                    return $isTenantLicensed;
                }
            }

            $reservedObjects += (int) $tenant['isys_mandator__license_objects'];
        }

        return \count($tenants) <= $allowedTenantCount && $isTenantLicensed && ($totalObjects >= $reservedObjects);
    }

    /**
     * Calculates total available tenants
     *
     * @return int
     */
    public function getTotalTenants()
    {
        $tenants = 0;

        $newLicenses = $this->getLicenses(true);
        $oldLicenses = $this->getLegacyLicenses(true);
        $now = Carbon::now();

        foreach ($newLicenses as $license) {
            if ($now < $license->getValidityFrom()) {
                continue;
            }

            $tenants += $license->getTenants();
        }

        foreach ($oldLicenses as $oldLicense) {
            // Don't count licenses from hosting licenes
            if (!empty($oldLicense[self::LEGACY_LICENSE_PARENT]) || !isset($oldLicense[self::C__LICENCE__REG_DATE])) {
                continue;
            }

            $registrationDate = Carbon::createFromTimestamp($oldLicense[self::C__LICENCE__REG_DATE]);

            if ($now < $registrationDate) {
                continue;
            }

            switch ($oldLicense[self::LEGACY_LICENSE_TYPE]) {
                case self::C__LICENCE_TYPE__HOSTING:
                case self::C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING:
                    $tenants += 50;
                    break;
                default:
                    ++$tenants;
                    break;
            }
        }

        return $tenants;
    }

    /**
     * Returns the earliest expiring license
     *
     * Either returns false when no license can be retrieved, or returns legacy license array, or returns new license entity
     *
     * @return false|License|array
     */
    public function getEarliestExpiringLicense()
    {
        $license = $this->databaseSystem->retrieveArrayFromResource($this->databaseSystem->query("SELECT * FROM isys_licence WHERE NOW() < isys_licence__expires AND isys_licence__type != " . self::C__LICENCE_TYPE__NEW__ADDON . " ORDER BY isys_licence__expires ASC LIMIT 1;"));

        if (empty($license[0])) {
            return false;
        }

        if (in_array($license[0]['isys_licence__type'], self::LEGACY_LICENSE_TYPES, false)) {
            try {
                $parsedlicense = Unserialize::toArray($license[0]['isys_licence__data']);

                $parsedlicense[self::LEGACY_LICENSE_ID] = $license[0]['isys_licence__id'];
                $parsedlicense[self::LEGACY_LICENSE_PARENT] = $license[0]['isys_licence__isys_licence__id'];
                $parsedlicense[self::LEGACY_LICENSE_TYPE] = $license[0]['isys_licence__type'];

                $licenses[$license[self::LEGACY_LICENSE_ID]] = $parsedlicense;
            } catch (\isys_exception_licence $exception) {
                return false;
            }

            return $parsedlicense;
        }

        if (in_array($license[0]['isys_licence__type'], self::LICENSE_TYPES, false)) {
            try {
                return $this->generateLicenseFromKey($license[0]['isys_licence__data']);
            } catch (LicenseDecryptionException $exception) {
                return false;
            }
        }
    }

    /**
     * Installs a parsed legacy license
     *
     * @param array $license
     *
     * @throws LegacyLicenseInstallException
     * @throws LegacyLicenseExpiredException
     * @throws LegacyLicenseExistingException
     */
    public function installLegacyLicense(array $license)
    {
        // Unlimited licenses
        if ($license[self::C__LICENCE__OBJECT_COUNT] == 0) {
            $licenseData = [
                self::C__LICENCE__OBJECT_COUNT => 99999999,
                self::C__LICENCE__DB_NAME => $license[self::C__LICENCE__DB_NAME],
                self::C__LICENCE__CUSTOMER_NAME => $license[self::C__LICENCE__CUSTOMER_NAME],
                self::C__LICENCE__REG_DATE => $license[self::C__LICENCE__REG_DATE],
                self::C__LICENCE__RUNTIME => $license[self::C__LICENCE__RUNTIME],
                self::C__LICENCE__EMAIL => $license[self::C__LICENCE__EMAIL],
                self::C__LICENCE__TYPE => $license[self::C__LICENCE__TYPE],
                self::C__LICENCE__CONTRACT => $license[self::C__LICENCE__CONTRACT],
                self::C__LICENCE__MAX_CLIENTS => $license[self::C__LICENCE__MAX_CLIENTS],
                self::C__LICENCE__DATA => $license[self::C__LICENCE__DATA],
            ];

            $licenseData[self::C__LICENCE__KEY] = sha1(serialize($licenseData));

            $license = $licenseData;
        }

        $sql = "SELECT * FROM isys_licence WHERE isys_licence__key = '" . $this->databaseSystem->escape_string($license[self::C__LICENCE__KEY]) . "' LIMIT 1";
        $existenceCheck = $this->databaseSystem->query($sql);

        // Throw error, since licence is already installed.
        if ($this->databaseSystem->num_rows($existenceCheck) > 0) {
            throw new LegacyLicenseExistingException(isys_application::instance()->container->get('language')
                ->get("LC__LICENCE__INSTALL__FAIL_EXISTS"), self::LICENCE_ERROR_EXISTS);
        }

        if (!$this->isLicenseValid($license)) {
            throw LegacyLicenseExpiredException::defaultLicenseExpiredError();
        }

        // Default behaviour: Licence does not exist. So install it:
        $sql = "INSERT INTO isys_licence " . "SET " .
            "isys_licence__expires = '%s', " . "isys_licence__data = '%s', " . "isys_licence__type = '%s', " . "isys_licence__datetime = NOW(), " .
            "isys_licence__key = '%s' ";

        if (
            $license[self::C__LICENCE__TYPE] == self::C__LICENCE_TYPE__BUYERS_LICENCE ||
            $license[self::C__LICENCE__TYPE] == self::C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING
        ) {
            $registrationDate = Carbon::createFromTimestamp($license[self::C__LICENCE__REG_DATE]);

            $expires = $registrationDate->modify("+99 years");
        } else {
            $days = (int) round(abs((($license[self::C__LICENCE__RUNTIME] / 60 / 60 / 24))));

            $registrationDate = Carbon::createFromTimestamp($license[self::C__LICENCE__REG_DATE]);

            $expires = $registrationDate->copy()->modify("+{$days} days");
        }



        $licenseInstallation = $this->databaseSystem->query(sprintf(
            $sql,
            $expires->format(Carbon::ATOM),
            $this->databaseSystem->escape_string(serialize($license)),
            $license[self::C__LICENCE__TYPE],
            $license[self::C__LICENCE__KEY]
        )) && $this->databaseSystem->commit();

        if (!$licenseInstallation) {
            throw new LegacyLicenseInstallException('License could not be installed');
        }

        $this->eventDispatcher->dispatch(
            new \idoit\Module\License\Event\License\LegacyLicenseAddedEvent(),
            \idoit\Module\License\Event\License\LegacyLicenseAddedEvent::NAME
        );

        // Include the admin-center functions.
        include_once isys_application::instance()->app_path . '/admin/src/functions.inc.php';

        try {
            // @see ID-8908 Remove token, once a license file was successfully installed.
            saveLicenseToken('');
        } catch (\Throwable $e) {
            // Nothing to do here
        }

        $this->deleteLicenses([LicenseService::C__LICENCE_TYPE__NEW__IDOIT, LicenseService::C__LICENCE_TYPE__NEW__ADDON]);
    }

    /**
     * Installs a new license if not already existing and given a valid license
     *
     * @param License $license
     *
     * @return bool
     *
     * @throws LicenseExistsException
     * @throws LicenseInvalidException
     */
    public function installLicense(License $license)
    {
        if (!$this->isLicenseValid($license)) {
            throw new LicenseInvalidException('License was not valid');
        }

        $existing = $this->databaseSystem->retrieveArrayFromResource($this->databaseSystem->query("SELECT isys_licence__id FROM isys_licence WHERE isys_licence__key = '{$this->databaseSystem->escape_string($license->getLicenseIdentifier())}' LIMIT 1"));

        if (!empty($existing)) {
            throw new LicenseExistsException('License already exists');
        }

        $licenseType = $license->isIdoitLicense() ? self::C__LICENCE_TYPE__NEW__IDOIT : self::C__LICENCE_TYPE__NEW__ADDON;

        $sql = "INSERT INTO isys_licence (isys_licence__type, isys_licence__expires, isys_licence__key, isys_licence__data, isys_licence__datetime) VALUES (". $licenseType .", '" . $license->getValidityTo()->format(\DateTime::ATOM) . "', '{$this->databaseSystem->escape_string($license->getLicenseIdentifier())}', '{$this->databaseSystem->escape_string($license->getDecrypted())}', NOW())";

        if ($this->databaseSystem->query($sql) && $this->databaseSystem->commit()) {
            $this->eventDispatcher->dispatch(
                new LicenseAddedEvent($license),
                LicenseAddedEvent::NAME
            );

            // @see ID-8908 Remove 'old' licenses, once a token was successfully installed.
            $this->deleteLicenses([
                LicenseService::C__LICENCE_TYPE__SINGLE,
                LicenseService::C__LICENCE_TYPE__HOSTING,
                LicenseService::C__LICENCE_TYPE__HOSTING_SINGLE,
                LicenseService::C__LICENCE_TYPE__BUYERS_LICENCE,
                LicenseService::C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING
            ]);

            $this->getEventDispatcher()->dispatch(
                new LegacyLicenseRemovedEvent(),
                LegacyLicenseRemovedEvent::NAME
            );

            return true;
        }


        return false;
    }

    /**
     * Returns parsed license from given file
     *
     * @param string $file
     *
     * @return false|array
     *
     * @throws LegacyLicenseExpiredException
     * @throws LegacyLicenseInvalidKeyException
     * @throws LegacyLicenseInvalidTypeException
     * @throws LegacyLicenseParseException
     */
    public function parseLicenseFile($file)
    {
        $packedLicense = file_get_contents($file);

        $unpackedLicense = null;

        // Unpack license file
        try {
            if (!strstr($packedLicense, 'i:') || !strstr($packedLicense, 'a:') || !strstr($packedLicense, 's:')) {
                $unpackedLicense = @gzuncompress($packedLicense);
            }
        } catch (\Exception $exception) {
            $unpackedLicense = $packedLicense;
        }

        $unserializedLicense = null;

        if ($unpackedLicense !== null) {
            // Unserialize unpacked license file
            try {
                $unserializedLicense = Unserialize::toArray($unpackedLicense);
            } catch (\Exception $exception) {
                throw new LegacyLicenseParseException($exception->getMessage(), self::LICENCE_ERROR_SYSTEM);
            }
        }

        if ($unserializedLicense !== null && isset($unserializedLicense[self::C__LICENCE__TYPE])) {

            if (!in_array($unserializedLicense[self::C__LICENCE__TYPE], [
                    self::C__LICENCE_TYPE__HOSTING_SINGLE,
                    self::C__LICENCE_TYPE__SINGLE,
                    self::C__LICENCE_TYPE__HOSTING,
                    self::C__LICENCE_TYPE__BUYERS_LICENCE,
                    self::C__LICENCE_TYPE__BUYERS_LICENCE_HOSTING
                ])
            ) {
                throw new LegacyLicenseInvalidTypeException("Invalid licence type", self::LICENCE_ERROR_INVALID_TYPE);
            }

            $licenseCopy = $unserializedLicense;
            unset($licenseCopy[self::C__LICENCE__KEY]);
            $licenseKey = sha1(serialize($licenseCopy));

            if ($licenseKey != $unserializedLicense[self::C__LICENCE__KEY]) {
                throw new LegacyLicenseInvalidKeyException('Invalid license key', self::LICENCE_ERROR_KEY);
            }

            if (!$this->isLicenseValid($unserializedLicense)) {
                throw LegacyLicenseExpiredException::defaultLicenseExpiredError();
            }

            return $unserializedLicense;
        }

        return false;
    }

    /**
     * Retrieve licenses from license server with given token
     * Log responses into isys_licence_communication
     *
     * @param bool $keepLicenseOnError
     *
     * @return string
     * @throws GuzzleException
     * @throws LicenseServerAuthenticationException
     * @throws LicenseServerConnectionException
     * @throws LicenseServerNoLicensesException
     */
    public function getLicensesFromServer(bool $keepLicenseOnError = false): string
    {
        $requestOptions = ['headers' => ['X-API-KEY' => $this->encryptionToken]];
        $licenseServerUrl = isys_application::instance()->getEnv('LICENSE_SERVER_URL') ?? $this->licenseServer;
        // Use global i-doit proxy if active
        if (isys_settings::get('proxy.active', false)) {
            $requestOptions = array_merge($requestOptions, ['curl'  => [
                CURLOPT_PROXY => isys_settings::get('proxy.host'),
                CURLOPT_PROXYPORT => isys_settings::get('proxy.port')
            ]]);

            if (isys_settings::get('proxy.username', false)) {
                $requestOptions['curl'][CURLOPT_PROXYUSERPWD] = isys_settings::get('proxy.username') . ':' . isys_settings::get('proxy.password');
            }
        }

        try {
            $response = $this->client->request('GET', $licenseServerUrl . '/api/license/contact', $requestOptions);
        } catch (ConnectException $exception) {
            $this->logLicenseServerResponse(Response::HTTP_BAD_GATEWAY, 0);
            throw new LicenseServerConnectionException("Could not connect to license server. Please make sure the server can connect to {$licenseServerUrl}");
        } catch (RequestException $exception) {
            switch ($exception->getCode()) {
                case Response::HTTP_UNAUTHORIZED:
                case Response::HTTP_FORBIDDEN:
                    $this->logLicenseServerResponse($exception->getCode(), 0);
                    throw new LicenseServerAuthenticationException('Authentication failed, please check your weblicense token');

                case Response::HTTP_NOT_FOUND:
                    $this->logLicenseServerResponse($exception->getCode(), 0);
                    if (!$keepLicenseOnError) {
                        $this->deleteLicenses([self::C__LICENCE_TYPE__NEW__IDOIT, self::C__LICENCE_TYPE__NEW__ADDON]);
                    }
                    throw new LicenseServerNoLicensesException('Authentication successful, but no valid licenses found');

                default:
                    $this->logLicenseServerResponse($exception->getCode(), 0);
                    throw $exception;
            }
        }

        $responseContent = $response->getBody()->getContents();

        $this->logLicenseServerResponse(Response::HTTP_OK, substr_count($responseContent, '===== LICENSE BEGIN ====='));
        $this->deleteLicenses([self::C__LICENCE_TYPE__NEW__IDOIT, self::C__LICENCE_TYPE__NEW__ADDON]);

        return $responseContent;
    }

    /**
     * Deletes licenses by their types
     *
     * @param int[] $types
     *
     * @return bool
     */
    public function deleteLicenses($types)
    {
        return $this->databaseSystem->query('DELETE FROM isys_licence WHERE isys_licence__type IN (' . implode(', ', $types) . ')') && $this->databaseSystem->commit();
    }

    /**
     * Logs a simple license server response with Response::HTTP_* status code and retrieved license count
     *
     * @param int $status
     * @param int $licenseCount
     *
     * @return bool
     */
    private function logLicenseServerResponse($status, $licenseCount)
    {
        return $this->databaseSystem->query('INSERT INTO isys_licence_communication (status, licenses_count) VALUES (' . $status . ', ' . $licenseCount . ')') && $this->databaseSystem->commit();
    }

    /**
     * Retrieves last license server communcation log
     *
     * @return array|null
     */
    public function getLastLicenseServerCommuncation()
    {
        $result = $this->databaseSystem->retrieveArrayFromResource($this->databaseSystem->query('SELECT * FROM isys_licence_communication WHERE TRUE ORDER BY created DESC LIMIT 1;'));

        if (is_array($result) && isset($result[0])) {
            return $result[0];
        }

        return null;
    }

    /**
     * Retrieves last license server communcation log
     *
     * @return array|null
     */
    public function getLastSuccessfulLicenseServerCommuncation()
    {
        $result = $this->databaseSystem->retrieveArrayFromResource($this->databaseSystem->query('SELECT * FROM isys_licence_communication WHERE status = 200 ORDER BY created DESC LIMIT 1;'));

        if (is_array($result) && isset($result[0])) {
            return $result[0];
        }

        return null;
    }

    /**
     * @return bool
     */
    public function isEvaluation(): bool
    {
        return $this->getTrialLicense() !== null;
    }

    /**
     * @return array|null
     */
    public function getTrialLicense(): ?array
    {
        // @todo Cache it to prevent performance issues!

        /** @var License|false $serverEvaluationLicense */
        $serverEvaluationLicense = reset(array_filter($this->getLicenses(true), function (License $license) {
            return $license->isEvaluation() && $license->isIdoitLicense();
        }));

        if ($serverEvaluationLicense instanceof License) {
            return $serverEvaluationLicense->toLegacy();
        }

        $legacyEvaluationLicense = reset(array_filter($this->getLegacyLicenses(true), fn ($e) => $e[self::C__LICENCE__IS_EVALUATION]));

        return $legacyEvaluationLicense ?: null;
    }

    public function refresh():bool
    {
        try {
            $webLicensesString = $this->getLicensesFromServer();
            foreach ($this->parseEncryptedLicenses($webLicensesString) as $license) {
                $this->installLicense($license);
            }

            return true;
        } catch (\Throwable $e) {
        }

        return false;
    }
}
