<?php
/*
 *  Copyright (c) 2025 Borlabs GmbH. All rights reserved.
 *  This file may not be redistributed in whole or significant part.
 *  Content of this file is protected by international copyright laws.
 *
 *  ----------------- Borlabs Cookie IS NOT FREE SOFTWARE -----------------
 *
 *  @copyright Borlabs GmbH, https://borlabs.io
 */

declare(strict_types=1);

namespace Borlabs\Cookie\Controller\Admin\Library;

use Borlabs\Cookie\Adapter\WpFunction;
use Borlabs\Cookie\Controller\Admin\ControllerInterface;
use Borlabs\Cookie\Controller\Admin\ExtendedRouteValidationInterface;
use Borlabs\Cookie\Dto\System\RequestDto;
use Borlabs\Cookie\Enum\Library\AutoUpdateIntervalEnum;
use Borlabs\Cookie\Enum\Package\ComponentTypeEnum;
use Borlabs\Cookie\Enum\Package\InstallationStatusEnum;
use Borlabs\Cookie\Exception\GenericException;
use Borlabs\Cookie\Exception\TranslatedException;
use Borlabs\Cookie\Localization\CloudScan\CloudScanDetailsLocalizationStrings;
use Borlabs\Cookie\Localization\GlobalLocalizationStrings;
use Borlabs\Cookie\Localization\Library\LibraryLocalizationStrings;
use Borlabs\Cookie\Repository\CloudScan\CloudScanSuggestionRepository;
use Borlabs\Cookie\Repository\ContentBlocker\ContentBlockerRepository;
use Borlabs\Cookie\Repository\Expression\BinaryOperatorExpression;
use Borlabs\Cookie\Repository\Expression\ContainsLikeLiteralExpression;
use Borlabs\Cookie\Repository\Expression\ListExpression;
use Borlabs\Cookie\Repository\Expression\LiteralExpression;
use Borlabs\Cookie\Repository\Expression\ModelFieldNameExpression;
use Borlabs\Cookie\Repository\Expression\NullExpression;
use Borlabs\Cookie\Repository\Package\PackageRepository;
use Borlabs\Cookie\Repository\Service\ServiceRepository;
use Borlabs\Cookie\Repository\ServiceGroup\ServiceGroupRepository;
use Borlabs\Cookie\Support\Traits\VersionNumberTrait;
use Borlabs\Cookie\System\Config\PluginConfig;
use Borlabs\Cookie\System\Config\Traits\PackageAutoUpdateTimeHelperTrait;
use Borlabs\Cookie\System\Language\Language;
use Borlabs\Cookie\System\Message\MessageManager;
use Borlabs\Cookie\System\Package\PackageAutoUpdateJobService;
use Borlabs\Cookie\System\Package\PackageManager;
use Borlabs\Cookie\System\Package\Traits\SettingsFieldListTrait;
use Borlabs\Cookie\System\Script\BorlabsCookieGlobalsService;
use Borlabs\Cookie\System\Template\Template;
use DateTime;

final class LibraryController implements ControllerInterface, ExtendedRouteValidationInterface
{
    use PackageAutoUpdateTimeHelperTrait;
    use SettingsFieldListTrait;
    use VersionNumberTrait;

    public const CONTROLLER_ID = 'borlabs-cookie-library';

    private BorlabsCookieGlobalsService $borlabsCookieGlobalsService;

    private CloudScanSuggestionRepository $cloudScanSuggestionRepository;

    private ContentBlockerRepository $contentBlockerRepository;

    private GlobalLocalizationStrings $globalLocalizationStrings;

    private Language $language;

    private LibraryLocalizationStrings $libraryLocalizationStrings;

    private MessageManager $messageManager;

    private PackageAutoUpdateJobService $packageAutoUpdateJobService;

    private PackageManager $packageManager;

    private PackageRepository $packageRepository;

    private PluginConfig $pluginConfig;

    private ServiceGroupRepository $serviceGroupRepository;

    private ServiceRepository $serviceRepository;

    private Template $template;

    private WpFunction $wpFunction;

    public function __construct(
        BorlabsCookieGlobalsService $borlabsCookieGlobalsService,
        CloudScanSuggestionRepository $cloudScanSuggestionRepository,
        ContentBlockerRepository $contentBlockerRepository,
        GlobalLocalizationStrings $globalLocalizationStrings,
        Language $language,
        LibraryLocalizationStrings $libraryLocalizationStrings,
        MessageManager $messageManager,
        PackageAutoUpdateJobService $packageAutoUpdateJobService,
        PackageManager $packageManager,
        PackageRepository $packageRepository,
        PluginConfig $pluginConfig,
        ServiceGroupRepository $serviceGroupRepository,
        ServiceRepository $serviceRepository,
        Template $template,
        WpFunction $wpFunction
    ) {
        $this->borlabsCookieGlobalsService = $borlabsCookieGlobalsService;
        $this->cloudScanSuggestionRepository = $cloudScanSuggestionRepository;
        $this->contentBlockerRepository = $contentBlockerRepository;
        $this->globalLocalizationStrings = $globalLocalizationStrings;
        $this->language = $language;
        $this->libraryLocalizationStrings = $libraryLocalizationStrings;
        $this->messageManager = $messageManager;
        $this->packageAutoUpdateJobService = $packageAutoUpdateJobService;
        $this->packageManager = $packageManager;
        $this->packageRepository = $packageRepository;
        $this->pluginConfig = $pluginConfig;
        $this->serviceGroupRepository = $serviceGroupRepository;
        $this->serviceRepository = $serviceRepository;
        $this->template = $template;
        $this->wpFunction = $wpFunction;
    }

    public function refresh(RequestDto $request): string
    {
        try {
            $this->packageManager->updatePackageList();
            $this->messageManager->success($this->libraryLocalizationStrings::get()['alert']['libraryRefreshedSuccessfully']);
        } catch (TranslatedException $exception) {
            $this->messageManager->error($exception->getTranslatedMessage());
        } catch (GenericException $exception) {
            $this->messageManager->error($exception->getMessage());
        }

        return $this->viewOverview($request);
    }

    public function route(RequestDto $request): ?string
    {
        $id = (int) ($request->postData['id'] ?? $request->getData['id'] ?? -1);
        $cloudScanId = $request->postData['cloudScanId'] ?? $request->getData['cloud-scan-id'] ?? null;
        $suggestionId = $request->postData['suggestionId'] ?? $request->getData['suggestionId'] ?? null;

        if ($cloudScanId !== null) {
            $cloudScanId = (int) $cloudScanId;
        }

        if ($suggestionId !== null) {
            $suggestionId = (int) $suggestionId;
        }

        $action = $request->postData['action'] ?? $request->getData['action'] ?? '';

        try {
            if ($action === 'bulk-install') {
                return $this->viewBulkInstall($cloudScanId, $request);
            }

            if ($action === 'details') {
                return $this->viewDetails($id, $request, $suggestionId);
            }

            if ($action === 'refresh') {
                return $this->refresh($request);
            }

            if ($action === 'save') {
                return $this->save($id, $request);
            }

            if ($action === 'save-auto-update-settings') {
                return $this->saveAutoUpdateSettings($request);
            }

            if ($action === 'uninstall') {
                return $this->uninstall($request, $id);
            }
        } catch (TranslatedException $exception) {
            $this->messageManager->error($exception->getTranslatedMessage());
        } catch (GenericException $exception) {
            $this->messageManager->error($exception->getMessage());
        }

        return $this->viewOverview($request);
    }

    public function save(int $id, RequestDto $request): string
    {
        $package = $this->packageRepository->findById($id);

        if ($package === null) {
            $this->messageManager->error(LibraryLocalizationStrings::get()['alert']['packageNotFound']);

            return $this->viewOverview($request);
        }

        $packageSaveSettingsStatus = $this->packageManager->saveSettings($package, $request->postData);

        if ($packageSaveSettingsStatus) {
            $this->messageManager->success($this->globalLocalizationStrings::get()['alert']['savedSuccessfully']);
        } else {
            $this->messageManager->error($this->globalLocalizationStrings::get()['alert']['savedUnsuccessfully']);
        }

        return $this->viewDetails($id, $request);
    }

    public function saveAutoUpdateSettings($request): string
    {
        $postData = $request->postData;
        $defaultPluginConfig = $this->pluginConfig->defaultConfig();
        $pluginConfig = $this->pluginConfig->get();
        $pluginConfig->packageAutoUpdateInterval = AutoUpdateIntervalEnum::hasValue($postData['packageAutoUpdateInterval'] ?? '') ? AutoUpdateIntervalEnum::fromValue($postData['packageAutoUpdateInterval']) : $defaultPluginConfig->packageAutoUpdateInterval;
        $pluginConfig->packageAutoUpdateTimeSpan = isset($postData['packageAutoUpdateTimeSpan']) && preg_match('/([0-1]{1}[0-9]{1}|2[0-3]):00-([0-1]{1}[0-9]{1}|2[0-3]):59/', $postData['packageAutoUpdateTimeSpan']) ? $postData['packageAutoUpdateTimeSpan'] : $defaultPluginConfig->packageAutoUpdateTimeSpan;
        $updateTime = $this->getRandomTimeWithinSpanIgnoringSeconds($pluginConfig->packageAutoUpdateTimeSpan);
        $pluginConfig->packageAutoUpdateTime = $updateTime->format('H:i');

        if (!empty($postData['packageAutoUpdateEmailAddresses'])) {
            $emailAddresses = [];
            $postData['packageAutoUpdateEmailAddresses'] = stripslashes($postData['packageAutoUpdateEmailAddresses']);
            $postData['packageAutoUpdateEmailAddresses'] = preg_split('/\r\n|[\r\n]/', $postData['packageAutoUpdateEmailAddresses']);

            if (!empty($postData['packageAutoUpdateEmailAddresses'])) {
                foreach ($postData['packageAutoUpdateEmailAddresses'] as $emailAddress) {
                    $emailAddress = trim(stripslashes($emailAddress));

                    if (!empty($emailAddress) && filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) {
                        $emailAddresses[$emailAddress] = $emailAddress;
                    }
                }
            }

            $pluginConfig->packageAutoUpdateEmailAddresses = array_values($emailAddresses);
        }

        if (count($pluginConfig->packageAutoUpdateEmailAddresses) === 0) {
            $pluginConfig->packageAutoUpdateEmailAddresses = $defaultPluginConfig->packageAutoUpdateEmailAddresses;
        }

        $pluginConfig->enableEmailNotificationsForUpdatablePackagesWithAutoUpdateDisabled = isset($postData['enableEmailNotificationsForUpdatablePackagesWithAutoUpdateDisabled']) && $postData['enableEmailNotificationsForUpdatablePackagesWithAutoUpdateDisabled'] === '1';
        $pluginConfig->enableEmailNotificationsForUpdatablePackagesWithAutoUpdateEnabled = isset($postData['enableEmailNotificationsForUpdatablePackagesWithAutoUpdateEnabled']) && $postData['enableEmailNotificationsForUpdatablePackagesWithAutoUpdateEnabled'] === '1';
        $this->pluginConfig->save($pluginConfig);
        $this->messageManager->success($this->globalLocalizationStrings::get()['alert']['savedSuccessfully']);

        return $this->viewOverview($request);
    }

    public function uninstall(RequestDto $request, int $id): string
    {
        $package = $this->packageRepository->findById($id);

        if ($package === null) {
            $this->messageManager->error($this->libraryLocalizationStrings->get()['alert']['packageNotFound']);

            return $this->viewOverview($request);
        }

        if ($package->installedAt === null) {
            $this->messageManager->error($this->libraryLocalizationStrings->get()['alert']['packageIsNotInstalled']);

            return $this->viewOverview($request);
        }

        $languages = $this->language->getLanguageList();
        $config = [
            'language' => [],
        ];

        foreach ($languages->list as $language) {
            $config['language'][$language->key] = '1';
        }

        try {
            $uninstallStatusEntries = $this->packageManager->uninstall($package, $config);
            $failed = false;

            foreach ($uninstallStatusEntries as $uninstallStatusEntry) {
                if ($uninstallStatusEntry->status->is(InstallationStatusEnum::fromValue(InstallationStatusEnum::FAILURE))) {
                    $failed = true;

                    if ($uninstallStatusEntry->failureMessage !== null) {
                        $this->messageManager->error($this->libraryLocalizationStrings->get()['alert']['uninstallFailedWithMessage'], [
                            'type' => $uninstallStatusEntry->componentType->getDescription(),
                            'name' => $uninstallStatusEntry->name,
                            'message' => $uninstallStatusEntry->failureMessage,
                        ]);
                    } else {
                        $this->messageManager->error($this->libraryLocalizationStrings->get()['alert']['uninstallFailed'], [
                            'type' => $uninstallStatusEntry->componentType->getDescription(),
                            'name' => $uninstallStatusEntry->name,
                        ]);
                    }
                }
            }

            if (!$failed) {
                $this->messageManager->success($this->libraryLocalizationStrings->get()['alert']['uninstallSuccess'], [
                    'name' => $package->name,
                ]);
            }

            return $this->viewOverview($request);
        } catch (TranslatedException $exception) {
            $this->messageManager->error($exception->getTranslatedMessage());
        } catch (GenericException $exception) {
            $this->messageManager->error($exception->getMessage());
        }

        return $this->viewOverview($request);
    }

    public function validate(RequestDto $request, string $nonce, bool $isValid): bool
    {
        if (isset($request->postData['action'])
            && in_array($request->postData['action'], ['refresh', 'save-auto-update-settings'], true)
            && $this->wpFunction->wpVerifyNonce(self::CONTROLLER_ID . '-' . $request->postData['action'], $nonce)
        ) {
            $isValid = true;
        }

        if (isset($request->getData['action'], $request->getData['cloud-scan-id'])
            && in_array($request->getData['action'], ['bulk-install'], true)
            && $this->wpFunction->wpVerifyNonce(self::CONTROLLER_ID . '-' . $request->getData['cloud-scan-id'] . '-' . $request->getData['action'], $nonce)
        ) {
            $isValid = true;
        }

        if (isset($request->getData['action'], $request->getData['id'])
            && in_array($request->getData['action'], ['uninstall'], true)
            && $this->wpFunction->wpVerifyNonce(self::CONTROLLER_ID . '-' . $request->getData['id'] . '-' . $request->getData['action'], $nonce)
        ) {
            $isValid = true;
        }

        return $isValid;
    }

    public function viewBulkInstall(int $cloudScanId, RequestDto $request): string
    {
        $templateData = [];
        $templateData['controllerId'] = self::CONTROLLER_ID;
        $templateData['localized'] = LibraryLocalizationStrings::get();
        $templateData['localized']['showScan'] = CloudScanDetailsLocalizationStrings::get();
        $templateData['localized']['global'] = $this->globalLocalizationStrings::get();
        $suggestions = $this->cloudScanSuggestionRepository->find(['cloudScanId' => $cloudScanId,]);
        $packageKeys = array_column($suggestions, 'borlabsServicePackageKey');
        $templateData['data']['cloudScanId'] = $cloudScanId;
        $templateData['data']['packages'] = [];

        if (count($packageKeys)) {
            $templateData['data']['packages'] = $this->packageRepository->find([
                new BinaryOperatorExpression(
                    new ModelFieldNameExpression('borlabsServicePackageKey'),
                    'IN',
                    new ListExpression(
                        array_map(
                            fn ($packageKey) => new LiteralExpression($packageKey),
                            $packageKeys,
                        ),
                    ),
                ),
            ], [
                'name' => 'ASC',
            ]);

            // Remove installed packages from the list
            $templateData['data']['packages'] = array_filter($templateData['data']['packages'], function ($package) {
                return $package->installedAt === null;
            });
        }

        $templateData['data']['componentTypes'] = array_column(ComponentTypeEnum::getAll(), 'description', 'value');
        $this->borlabsCookieGlobalsService->addProperty('componentTypes', array_column(ComponentTypeEnum::getAll(), 'description', 'value'));

        return $this->template->getEngine()->render('library/bulk-install.html.twig', $templateData);
    }

    /**
     * @throws \Borlabs\Cookie\Exception\EntryNotFoundException
     * @throws \Borlabs\Cookie\Exception\IncompatibleTypeException
     * @throws \Borlabs\Cookie\Exception\MissingRequiredArgumentException
     */
    public function viewDetails(int $id, RequestDto $request, ?int $suggestionId = null): string
    {
        // Check if package exists
        $package = $this->packageRepository->findById($id);

        if ($package === null) {
            $this->messageManager->error(LibraryLocalizationStrings::get()['alert']['packageNotFound']);

            return $this->viewOverview($request);
        }

        $searchTerm = trim((string) ($request->getData['borlabs-search-term'] ?? null));
        $filter = $request->getData['borlabs-filter'] ?? null;

        $templateData = [];
        $templateData['controllerId'] = self::CONTROLLER_ID;
        $templateData['localized'] = LibraryLocalizationStrings::get();
        $templateData['localized']['showScan'] = CloudScanDetailsLocalizationStrings::get();
        $templateData['localized']['global'] = $this->globalLocalizationStrings::get();
        $templateData['data']['additionalQueryParameters'] = http_build_query([
            'borlabs-filter' => $filter,
            'borlabs-search-term' => $searchTerm,
        ]);
        $templateData['data']['borlabsCookieVersion'] = $this->transformToVersionNumberWithHotfixDto(BORLABS_COOKIE_VERSION);
        $templateData['data']['componentTypes'] = array_column(ComponentTypeEnum::getAll(), 'description', 'value');
        $templateData['data']['job'] = $this->packageAutoUpdateJobService->getJob($package);
        $templateData['data']['languages'] = $this->language->getLanguageList();
        $templateData['data']['package'] = $package;

        foreach ($templateData['data']['package']->components->contentBlockers->list as &$componentData) {
            $this->mergeComponentSettingsWithCustomSettings($componentData, $this->contentBlockerRepository);
        }

        foreach ($templateData['data']['package']->components->services->list as &$componentData) {
            $this->mergeComponentSettingsWithCustomSettings($componentData, $this->serviceRepository, $this->serviceGroupRepository);
        }

        $templateData['data']['selectedLanguages'] = array_column($this->language->getLanguageList()->list, 'key', 'key');

        $this->borlabsCookieGlobalsService->addProperty('componentTypes', array_column(ComponentTypeEnum::getAll(), 'description', 'value'));

        if ($suggestionId !== null) {
            $templateData['data']['suggestion'] = $this->cloudScanSuggestionRepository->findById($suggestionId);
        } else {
            $templateData['data']['suggestion'] = null;
        }

        if ($package->isDeprecated && $package->borlabsServicePackageSuccessorKey !== '') {
            $successorPackage = $this->packageRepository->getByPackageKey($package->borlabsServicePackageSuccessorKey);

            $this->messageManager->error($this->libraryLocalizationStrings->get()['alert']['packageIsDeprecated']);

            if ($successorPackage !== null) {
                $this->messageManager->info($this->libraryLocalizationStrings->get()['alert']['successorPackageAvailable'], [
                    'link' => $this->wpFunction->wpNonceUrl(
                        '?page=borlabs-cookie-library&action=details&id=' . $successorPackage->id,
                        self::CONTROLLER_ID . '-' . $successorPackage->id . '-details',
                    ),
                    'name' => $successorPackage->name,
                ]);
            }
        }

        return $this->template->getEngine()->render('library/details-package.html.twig', $templateData);
    }

    public function viewOverview(RequestDto $request): string
    {
        $searchTerm = trim((string) ($request->postData['searchTerm'] ?? $request->getData['borlabs-search-term'] ?? null));
        $filter = $request->postData['filter'] ?? $request->getData['borlabs-filter'] ?? null;
        $where = [];

        if (strlen($searchTerm) > 1) {
            $where[] = new BinaryOperatorExpression(
                new ModelFieldNameExpression('name'),
                'LIKE',
                new ContainsLikeLiteralExpression(new LiteralExpression($searchTerm)),
            );
        }

        if ($filter && ComponentTypeEnum::hasValue($filter)) {
            $where[] = new BinaryOperatorExpression(
                new ModelFieldNameExpression('type'),
                '=',
                new LiteralExpression($filter),
            );
        } elseif ($filter === 'installed-packages') {
            $where[] = new BinaryOperatorExpression(
                new ModelFieldNameExpression('installedAt'),
                'IS NOT',
                new NullExpression(),
            );
        }

        if ($filter !== 'installed-packages') {
            $where[] = new BinaryOperatorExpression(
                new ModelFieldNameExpression('isDeprecated'),
                '=',
                new LiteralExpression('0'),
            );
        }

        $packages = $this->packageRepository->paginate(
            (int) ($request->getData['borlabs-page'] ?? 1),
            $where,
            [
                'isFeatured' => 'DESC',
                'name' => 'ASC',
            ],
            [],
            24,
            [
                'borlabs-filter' => $filter,
                'borlabs-search-term' => $searchTerm,
            ],
        );

        $templateData = [];
        $templateData['controllerId'] = self::CONTROLLER_ID;
        $templateData['localized'] = LibraryLocalizationStrings::get();
        $templateData['localized']['global'] = $this->globalLocalizationStrings::get();
        $templateData['isUpdateAvailable'] = count($this->packageRepository->getUpdatablePackages()) > 0;
        $templateData['data']['additionalQueryParameters'] = http_build_query([
            'borlabs-filter' => $filter,
            'borlabs-search-term' => $searchTerm,
        ]);
        $templateData['data']['componentTypes'] = array_column(ComponentTypeEnum::getAll(), 'description', 'value');
        $templateData['data']['filter'] = $filter;
        $templateData['data']['pluginConfig'] = $this->pluginConfig->get();
        $templateData['data']['packages'] = $packages;
        $templateData['data']['packageListLastUpdate'] = $this->packageManager->getLastSuccessfulCheckWithApiTimestamp() ? (new DateTime())->setTimestamp($this->packageManager->getLastSuccessfulCheckWithApiTimestamp()) : null;
        $templateData['data']['searchTerm'] = $searchTerm;
        $templateData['options']['packageAutoUpdateIntervals'] = AutoUpdateIntervalEnum::getLocalizedKeyValueList();
        $templateData['options']['packageAutoUpdateTimeSpan'] = $this->getTimeSpanList();

        return $this->template->getEngine()->render('library/overview-packages.html.twig', $templateData);
    }
}
