<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Policy;

use Exception;
use Piwik\Tracker\Cache;
use Piwik\Plugin\Manager;
use Piwik\Policy\Exceptions\CompliancePolicyNotFoundException;
use Piwik\Settings\Interfaces\PolicyComparisonInterface;
use Piwik\Settings\Interfaces\SettingValueInterface;
use Piwik\Settings\Interfaces\Traits\Getters\ConfigGetterTrait;
use Piwik\Settings\Interfaces\Traits\Getters\CustomGetterTrait;
use Piwik\Settings\Interfaces\Traits\Getters\MeasurableGetterTrait;
use Piwik\Settings\Interfaces\Traits\Getters\OptionGetterTrait;
use Piwik\Settings\Interfaces\Traits\Getters\SystemGetterTrait;
use Piwik\Settings\Measurable\MeasurableProperty;
use Piwik\Settings\Measurable\MeasurableSetting;
use Piwik\Settings\Plugin\SystemConfigSetting;
use Piwik\Settings\Plugin\SystemSetting;
use Piwik\Settings\Setting;
use Piwik\Tracker\Config\ThirdPartyCookies;

class PolicyManager
{
    public const SETTING_TYPE_SYSTEM = 'system';
    public const SETTING_TYPE_MEASURABLE = 'measurable';
    public const SETTING_TYPE_CUSTOM = 'custom';
    public const SETTING_TYPE_OPTION = 'option';
    public const SETTING_TYPE_CONFIG = 'config';

    // TODO: In Matomo 6, get*Name methods will change visibility from protected to public,
    //  so we will need to replace the method names here
    private static $settingTypesMap = [
        self::SETTING_TYPE_SYSTEM     => [
            'trait' => SystemGetterTrait::class,
            'method' => 'getSystemSettingShortName',
        ],
        self::SETTING_TYPE_MEASURABLE => [
            'trait' => MeasurableGetterTrait::class,
            'method' => 'getMeasurableSettingShortName',
        ],
        self::SETTING_TYPE_CUSTOM     => [
            'trait' => CustomGetterTrait::class,
            'method' => 'getCustomSettingShortName',
        ],
        self::SETTING_TYPE_OPTION     => [
            'trait' => OptionGetterTrait::class,
            'method' => 'getOptionSettingShortName',
        ],
        self::SETTING_TYPE_CONFIG     => [
            'trait' => ConfigGetterTrait::class,
            'method' => 'getConfigSettingShortName',
        ],
    ];

    /**
     * @return array<class-string<CompliancePolicy>>
     */
    public static function getAllPolicies(): array
    {
        return [
            CnilPolicy::class,
        ];
    }

    /**
     * @return array<array<string, string>>
     */
    public static function getAllPoliciesDetails(): array
    {
        $policies = static::getAllPolicies();
        return array_map(function ($policyClass) {
            return $policyClass::getDetails();
        }, $policies);
    }

    /**
     * @return class-string<CompliancePolicy>|null
     */
    public static function getPolicyByName(string $policyName): ?string
    {
        $policies = static::getAllPolicies();
        foreach ($policies as $policyClass) {
            if ($policyName === $policyClass::getName()) {
                return $policyClass;
            }
        }

        return null;
    }

    /**
     * @return array<class-string<PolicyComparisonInterface<mixed>&SettingValueInterface<mixed>>>
     */
    protected static function getAllSettings(?string $settingType = null): array
    {
        $settings = Manager::getInstance()->findMultipleComponents('Settings', SettingValueInterface::class);
        $underPolicy = [];

        // Add core specific settings
        $settings[] = ThirdPartyCookies::class;

        foreach ($settings as $setting) {
            if (!is_a($setting, PolicyComparisonInterface::class, true)) {
                continue;
            }
            if ($settingType && !in_array(self::$settingTypesMap[$settingType]['trait'], class_uses($setting), true)) {
                continue;
            }

            $underPolicy[] = $setting;
        }

        return $underPolicy;
    }

    /**
     * @param class-string<CompliancePolicy> $policyClass
     * @return array<class-string<PolicyComparisonInterface<mixed>&SettingValueInterface<mixed>>>
     */
    public static function getAllControlledSettings(string $policyClass, ?int $idSite = null, ?string $settingType = null): array
    {
        $settings = static::getAllSettings($settingType);
        $underPolicy = [];

        foreach ($settings as $setting) {
            if (!$setting::isControlledBySpecificPolicy($policyClass, $idSite)) {
                continue;
            }

            $underPolicy[] = $setting;
        }

        return $underPolicy;
    }

    /**
     * @param class-string<CompliancePolicy> $policyClass
     * @throws CompliancePolicyNotFoundException when $policyClass is not a valid policy
     */
    public static function isPolicyActive(string $policyClass, ?int $idSite = null): bool
    {
        self::checkPolicyIsValid($policyClass);
        return $policyClass::isActive($idSite);
    }

    /**
     * @param class-string<CompliancePolicy> $policyClass
     * @throws \Exception when $policyClass is not a valid policy
     */
    public static function isPolicyConfigControlled(string $policyClass): bool
    {
        self::checkPolicyIsValid($policyClass);
        return $policyClass::isConfigControlled();
    }

    /**
     * @param class-string<CompliancePolicy> $policyClass
     * @return array<array<string>> of [['id' => (string) 'ID', 'title' => (string) 'TITLE', 'note' => (string) 'NOTE']]
     * @throws CompliancePolicyNotFoundException when $policyClass is not a valid policy
     */
    public static function getAllUnknownSettings(string $policyClass): array
    {
        self::checkPolicyIsValid($policyClass);
        return $policyClass::getUnknownSettings();
    }

    /**
     * @param class-string<CompliancePolicy> $policyClass
     * @throws CompliancePolicyNotFoundException when $policyClass is not a valid policy

     */
    private static function checkPolicyIsValid(string $policyClass): void
    {
        if (!is_a($policyClass, CompliancePolicy::class, true)) {
            throw new CompliancePolicyNotFoundException('Invalid compliance policy.');
        }
    }

    /**
     * @param class-string<CompliancePolicy> $policyClass
     * @throws CompliancePolicyNotFoundException when $policyClass is not a valid policy
     */
    public static function setPolicyActiveStatus(string $policyClass, bool $isActive, ?int $idSite = null): void
    {
        self::checkPolicyIsValid($policyClass);
        $policyClass::setActiveStatus($idSite, $isActive);
        if (!is_null($idSite)) {
            Cache::deleteCacheWebsiteAttributes($idSite);
        }
        Cache::deleteTrackerCache();
    }

    /**
     * Sets the enforcement state of multiple policy-controlled settings at once.
     *
     * @param class-string<CompliancePolicy> $policyClass
     * @param array<string, bool|int|string> $settingIdToEnforced Map of policy setting id => whether to enforce it
     * @throws CompliancePolicyNotFoundException when $policyClass is not a valid policy
     * @throws Exception when a setting id is unknown or the setting cannot be toggled
     */
    public static function setPolicySettingEnforcedStatuses(string $policyClass, array $settingIdToEnforced, ?int $idSite = null): void
    {
        self::checkPolicyIsValid($policyClass);

        $toggleableSettingsById = [];
        foreach (self::getAllControlledSettings($policyClass, $idSite) as $settingClass) {
            if (!$settingClass::isExternallyManagedByPolicyPage()) {
                $toggleableSettingsById[$settingClass::getPolicySettingId()] = $settingClass;
            }
        }

        // validate everything upfront: a failure mid-write would leave a partial
        // change behind without the tracker caches ever being invalidated
        $normalised = [];
        foreach ($settingIdToEnforced as $settingId => $enforced) {
            if (!array_key_exists($settingId, $toggleableSettingsById)) {
                throw new Exception(sprintf('The setting "%s" is unknown or cannot be toggled', $settingId));
            }

            $enforced = filter_var($enforced, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
            if (is_null($enforced)) {
                throw new Exception(sprintf('Invalid enforcement value for the setting "%s"', $settingId));
            }

            $normalised[$settingId] = $enforced;
        }

        foreach ($normalised as $settingId => $enforced) {
            $toggleableSettingsById[$settingId]::setEnforced($enforced, $idSite);
        }

        if (!is_null($idSite)) {
            Cache::deleteCacheWebsiteAttributes($idSite);
        }
        Cache::deleteTrackerCache();
    }

    public static function storePolicySettingValuesInTrackerCache(array &$cacheContent, int $idSite): array
    {
        $settings = static::getAllSettings();
        foreach ($settings as $setting) {
            $cacheContent[$setting] = $setting::getInstance($idSite)->getValue();
        }
        return $cacheContent;
    }

    /**
     * Return setting type from a given Setting instance, including subclasses
     */
    public static function getSettingTypeFromSettingClass(Setting $setting): ?string
    {
        $map = [
            MeasurableSetting::class   => self::SETTING_TYPE_MEASURABLE,
            MeasurableProperty::class  => self::SETTING_TYPE_MEASURABLE,
            SystemSetting::class       => self::SETTING_TYPE_SYSTEM,
            SystemConfigSetting::class => self::SETTING_TYPE_CONFIG,
        ];

        foreach ($map as $class => $type) {
            if ($setting instanceof $class) {
                return $type;
            }
        }

        return null;
    }

    /**
     * For a given setting name, return an information on policies that may control the setting and its required value.
     *
     * @return array<string, array{
     *      requiredValue: mixed
     *  }>
     * @throws Exception
     */
    public static function getCompliancePoliciesControllingASetting(string $settingName, ?int $idSite = null, ?string $settingType = null): array
    {
        if (!$settingType || !in_array($settingType, array_keys(self::$settingTypesMap), true)) {
            return [];
        }

        $policies = static::getAllPolicies();
        $settings = [];

        foreach ($policies as $policyClass) {
            $controlledSettings = self::getAllControlledSettings($policyClass, $idSite, $settingType);

            foreach ($controlledSettings as $controlledSetting) {
                if ($settingName !== call_user_func([$controlledSetting, self::$settingTypesMap[$settingType]['method']])) {
                    continue;
                }
                if (!$controlledSetting::isEnforced($idSite)) {
                    continue;
                }
                $settings[$policyClass::getName()] = [
                    'requiredValue' => $controlledSetting::getPolicyRequirements()[$policyClass],
                ];
            }
        }

        return $settings;
    }
}
