<?php

namespace Mautic\PluginBundle\Helper;

use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Cache\ResultCacheOptions;
use Mautic\CoreBundle\Helper\BundleHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\PluginBundle\Entity\Integration;
use Mautic\PluginBundle\Entity\Plugin;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Mautic\PluginBundle\Integration\UnifiedIntegrationInterface;
use Mautic\PluginBundle\Model\PluginModel;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Finder\Finder;
use Twig\Environment;

class IntegrationHelper
{
    /**
     * @var array<string, mixed>
     */
    private array $integrations = [];

    /**
     * @var mixed[]
     */
    private array $available = [];

    /**
     * @var array<string, mixed>
     */
    private array $byFeatureList = [];

    /**
     * @var array<int, mixed>
     */
    private array $byPlugin = [];

    public function __construct(
        private ContainerInterface $container,
        protected EntityManager $em,
        protected PathsHelper $pathsHelper,
        protected BundleHelper $bundleHelper,
        protected CoreParametersHelper $coreParametersHelper,
        protected Environment $twig,
        protected PluginModel $pluginModel,
    ) {
    }

    /**
     * Get a list of integration helper classes.
     *
     * @param array|string $specificIntegrations
     * @param array        $withFeatures
     * @param bool         $alphabetical
     * @param int|null     $pluginFilter
     * @param bool|false   $publishedOnly
     *
     * @return array<AbstractIntegration>
     *
     * @throws \Doctrine\ORM\ORMException
     */
    public function getIntegrationObjects($specificIntegrations = null, $withFeatures = null, $alphabetical = false, $pluginFilter = null, $publishedOnly = false): array
    {
        // Build the service classes
        if ([] === $this->available) {
            // Get currently installed integrations
            $integrationSettings = $this->getIntegrationSettings();

            // And we'll be scanning the addon bundles for additional classes, so have that data on standby
            $plugins = $this->bundleHelper->getPluginBundles();

            // Get a list of already installed integrations
            $integrationRepo = $this->em->getRepository(Integration::class);
            // get a list of plugins for filter
            $installedPlugins = $this->pluginModel->getEntities(
                [
                    'hydration_mode' => 'hydrate_array',
                    'index'          => 'bundle',
                    'result_cache'   => new ResultCacheOptions(Plugin::CACHE_NAMESPACE),
                ]
            );

            $newIntegrations = [];

            // Scan the plugins for integration classes
            foreach ($plugins as $plugin) {
                // Do not list the integration if the bundle has not been "installed"
                if (!isset($plugin['bundle']) || !isset($installedPlugins[$plugin['bundle']])) {
                    continue;
                }

                if (is_dir($plugin['directory'].'/Integration')) {
                    $finder = new Finder();
                    $finder->files()->name('*Integration.php')->in($plugin['directory'].'/Integration')->ignoreDotFiles(true);

                    $id                  = $installedPlugins[$plugin['bundle']]['id'];
                    $this->byPlugin[$id] = [];
                    $pluginReference     = $this->em->getReference(Plugin::class, $id);
                    $pluginNamespace     = str_replace('MauticPlugin', '', $plugin['bundle']);

                    foreach ($finder as $file) {
                        $integrationName = substr($file->getBaseName(), 0, -15);

                        if (!isset($integrationSettings[$integrationName])) {
                            $newIntegration = new Integration();
                            $newIntegration->setName($integrationName)
                                ->setPlugin($pluginReference);
                            $integrationSettings[$integrationName] = $newIntegration;
                            $integrationContainerKey               = strtolower("mautic.integration.{$integrationName}");

                            // Initiate the class in order to get the features supported
                            if ($this->container->has($integrationContainerKey)) {
                                $this->integrations[$integrationName] = $this->container->get($integrationContainerKey);

                                $features = $this->integrations[$integrationName]->getSupportedFeatures();
                                $newIntegration->setSupportedFeatures($features);

                                // Go ahead and stash it since it's built already
                                $this->integrations[$integrationName]->setIntegrationSettings($newIntegration);

                                $newIntegrations[] = $newIntegration;

                                unset($newIntegration);
                            }
                        }

                        /** @var Integration $settings */
                        $settings                          = $integrationSettings[$integrationName];
                        $this->available[$integrationName] = [
                            'isPlugin'    => true,
                            'integration' => $integrationName,
                            'settings'    => $settings,
                            'namespace'   => $pluginNamespace,
                        ];

                        // Sort by feature and plugin for later
                        $features = $settings->getSupportedFeatures();
                        foreach ($features as $feature) {
                            if (!isset($this->byFeatureList[$feature])) {
                                $this->byFeatureList[$feature] = [];
                            }
                            $this->byFeatureList[$feature][] = $integrationName;
                        }
                        $this->byPlugin[$id][] = $integrationName;
                    }
                }
            }

            $coreIntegrationSettings = $this->getCoreIntegrationSettings();

            // Scan core bundles for integration classes
            foreach ($this->bundleHelper->getMauticBundles() as $coreBundle) {
                if (
                    // Skip plugin bundles
                    str_contains($coreBundle['relative'], 'app/bundles')
                    // Skip core bundles without an Integration directory
                    && is_dir($coreBundle['directory'].'/Integration')
                ) {
                    $finder = new Finder();
                    $finder->files()->name('*Integration.php')->in($coreBundle['directory'].'/Integration')->ignoreDotFiles(true);

                    $coreBundleNamespace = str_replace('Mautic', '', $coreBundle['bundle']);

                    foreach ($finder as $file) {
                        $integrationName = substr($file->getBaseName(), 0, -15);

                        if (!isset($coreIntegrationSettings[$integrationName])) {
                            $newIntegration = new Integration();
                            $newIntegration->setName($integrationName);
                            $integrationSettings[$integrationName] = $newIntegration;

                            $integrationContainerKey = strtolower("mautic.integration.{$integrationName}");

                            // Initiate the class in order to get the features supported
                            if ($this->container->has($integrationContainerKey)) {
                                $this->integrations[$integrationName] = $this->container->get($integrationContainerKey);
                                $features                             = $this->integrations[$integrationName]->getSupportedFeatures();
                                $newIntegration->setSupportedFeatures($features);

                                // Go ahead and stash it since it's built already
                                $this->integrations[$integrationName]->setIntegrationSettings($newIntegration);

                                $newIntegrations[] = $newIntegration;
                            } else {
                                continue;
                            }
                        }

                        /** @var Integration $settings */
                        $settings                          = $coreIntegrationSettings[$integrationName] ?? $newIntegration;
                        $this->available[$integrationName] = [
                            'isPlugin'    => false,
                            'integration' => $integrationName,
                            'settings'    => $settings,
                            'namespace'   => $coreBundleNamespace,
                        ];
                    }
                }
            }

            // Save newly found integrations
            if (!empty($newIntegrations)) {
                $integrationRepo->saveEntities($newIntegrations);
                unset($newIntegrations);
            }
        }

        // Ensure appropriate formats
        if (null !== $specificIntegrations && !is_array($specificIntegrations)) {
            $specificIntegrations = [$specificIntegrations];
        }

        if (null !== $withFeatures && !is_array($withFeatures)) {
            $withFeatures = [$withFeatures];
        }

        // Build the integrations wanted
        if (!empty($pluginFilter)) {
            // Filter by plugin
            $filteredIntegrations = $this->byPlugin[$pluginFilter];
        } elseif (!empty($specificIntegrations)) {
            // Filter by specific integrations
            $filteredIntegrations = $specificIntegrations;
        } else {
            // All services by default
            $filteredIntegrations = array_keys($this->available);
        }

        // Filter by features
        if (!empty($withFeatures)) {
            $integrationsWithFeatures = [];
            foreach ($withFeatures as $feature) {
                if (isset($this->byFeatureList[$feature])) {
                    $integrationsWithFeatures = $integrationsWithFeatures + $this->byFeatureList[$feature];
                }
            }

            $filteredIntegrations = array_intersect($filteredIntegrations, $integrationsWithFeatures);
        }

        $returnServices = [];

        // Build the classes if not already
        foreach ($filteredIntegrations as $integrationName) {
            if (!isset($this->available[$integrationName]) || ($publishedOnly && !$this->available[$integrationName]['settings']->isPublished())) {
                continue;
            }

            if (!isset($this->integrations[$integrationName])) {
                $integration             = $this->available[$integrationName];
                $integrationContainerKey = strtolower("mautic.integration.{$integrationName}");

                if ($this->container->has($integrationContainerKey)) {
                    $this->integrations[$integrationName] = $this->container->get($integrationContainerKey);
                    $this->integrations[$integrationName]->setIntegrationSettings($integration['settings']);
                }
            }

            if (isset($this->integrations[$integrationName])) {
                $returnServices[$integrationName] = $this->integrations[$integrationName];
            }
        }

        foreach ($returnServices as $key => $value) {
            if (!$value) {
                unset($returnServices[$key]);
            }
        }

        if (empty($alphabetical)) {
            // Sort by priority
            uasort($returnServices, function ($a, $b): int {
                $aP = (int) $a->getPriority();
                $bP = (int) $b->getPriority();

                return $aP <=> $bP;
            });
        } else {
            // Sort by display name
            uasort($returnServices, function ($a, $b): int {
                $aName = $a->getDisplayName();
                $bName = $b->getDisplayName();

                return strcasecmp($aName, $bName);
            });
        }

        return $returnServices;
    }

    /**
     * Get a single integration object.
     *
     * @return AbstractIntegration|false
     */
    public function getIntegrationObject($name)
    {
        $integrationObjects = $this->getIntegrationObjects($name);

        return $integrationObjects[$name] ?? false;
    }

    /**
     * Gets a count of integrations.
     */
    public function getIntegrationCount($plugin): int
    {
        if (!is_array($plugin)) {
            $plugins = $this->coreParametersHelper->get('plugin.bundles');
            if (array_key_exists($plugin, $plugins)) {
                $plugin = $plugins[$plugin];
            } else {
                // It doesn't exist so return 0

                return 0;
            }
        }

        if (is_dir($plugin['directory'].'/Integration')) {
            $finder = new Finder();
            $finder->files()->name('*Integration.php')->in($plugin['directory'].'/Integration')->ignoreDotFiles(true);

            return iterator_count($finder);
        }

        return 0;
    }

    /**
     * Returns popular social media services and regex URLs for parsing purposes.
     *
     * @param bool $find If true, array of regexes to find a handle will be returned;
     *                   If false, array of URLs with a placeholder of %handle% will be returned
     *
     * @return array
     *
     * @todo Extend this method to allow plugins to add URLs to these arrays
     */
    public function getSocialProfileUrlRegex($find = true)
    {
        if ($find) {
            // regex to find a match
            return [
                'twitter'  => "/twitter.com\/(.*?)($|\/)/",
                'facebook' => [
                    "/facebook.com\/(.*?)($|\/)/",
                    "/fb.me\/(.*?)($|\/)/",
                ],
                'linkedin'  => "/linkedin.com\/in\/(.*?)($|\/)/",
                'instagram' => "/instagram.com\/(.*?)($|\/)/",
                'pinterest' => "/pinterest.com\/(.*?)($|\/)/",
                'klout'     => "/klout.com\/(.*?)($|\/)/",
                'youtube'   => [
                    "/youtube.com\/user\/(.*?)($|\/)/",
                    "/youtu.be\/user\/(.*?)($|\/)/",
                ],
                'flickr' => "/flickr.com\/photos\/(.*?)($|\/)/",
                'skype'  => "/skype:(.*?)($|\?)/",
            ];
        } else {
            // populate placeholder
            return [
                'twitter'    => 'https://twitter.com/%handle%',
                'facebook'   => 'https://facebook.com/%handle%',
                'linkedin'   => 'https://linkedin.com/in/%handle%',
                'instagram'  => 'https://instagram.com/%handle%',
                'pinterest'  => 'https://pinterest.com/%handle%',
                'klout'      => 'https://klout.com/%handle%',
                'youtube'    => 'https://youtube.com/user/%handle%',
                'flickr'     => 'https://flickr.com/photos/%handle%',
                'skype'      => 'skype:%handle%?call',
            ];
        }
    }

    /**
     * Get array of integration entities.
     *
     * @return mixed
     */
    public function getIntegrationSettings()
    {
        return $this->em->getRepository(Integration::class)->getIntegrations();
    }

    public function getCoreIntegrationSettings()
    {
        return $this->em->getRepository(Integration::class)->getCoreIntegrations();
    }

    /**
     * Get the user's social profile data from cache or integrations if indicated.
     *
     * @param \Mautic\LeadBundle\Entity\Lead $lead
     * @param array                          $fields
     * @param bool                           $refresh
     * @param string                         $specificIntegration
     * @param bool                           $persistLead
     * @param bool                           $returnSettings
     *
     * @return array
     */
    public function getUserProfiles($lead, $fields = [], $refresh = false, $specificIntegration = null, $persistLead = true, $returnSettings = false)
    {
        $socialCache     = $lead->getSocialCache();
        $featureSettings = [];
        if ($refresh) {
            // regenerate from integrations
            $now = new DateTimeHelper();

            // check to see if there are social profiles activated
            $socialIntegrations = $this->getIntegrationObjects($specificIntegration, ['public_profile', 'public_activity']);

            /* @var \MauticPlugin\MauticSocialBundle\Integration\SocialIntegration $sn */
            foreach ($socialIntegrations as $integration => $sn) {
                $settings        = $sn->getIntegrationSettings();
                $features        = $settings->getSupportedFeatures();
                $identifierField = $this->getUserIdentifierField($sn, $fields);

                if ($returnSettings) {
                    $featureSettings[$integration] = $settings->getFeatureSettings();
                }

                if ($identifierField && $settings->isPublished()) {
                    $profile = (!isset($socialCache[$integration])) ? [] : $socialCache[$integration];

                    // clear the cache
                    unset($profile['profile'], $profile['activity']);

                    if (in_array('public_profile', $features) && $sn->isAuthorized()) {
                        $sn->getUserData($identifierField, $profile);
                    }

                    if (in_array('public_activity', $features) && $sn->isAuthorized()) {
                        $sn->getPublicActivity($identifierField, $profile);
                    }

                    if (!empty($profile['profile']) || !empty($profile['activity'])) {
                        if (!isset($socialCache[$integration])) {
                            $socialCache[$integration] = [];
                        }

                        $socialCache[$integration]['profile']     = (!empty($profile['profile'])) ? $profile['profile'] : [];
                        $socialCache[$integration]['activity']    = (!empty($profile['activity'])) ? $profile['activity'] : [];
                        $socialCache[$integration]['lastRefresh'] = $now->toUtcString();
                    }
                } elseif (isset($socialCache[$integration])) {
                    // integration is now not applicable
                    unset($socialCache[$integration]);
                }
            }

            if ($persistLead && !empty($socialCache)) {
                $lead->setSocialCache($socialCache);
                $this->em->getRepository(\Mautic\LeadBundle\Entity\Lead::class)->saveEntity($lead);
            }
        } elseif ($returnSettings) {
            $socialIntegrations = $this->getIntegrationObjects($specificIntegration, ['public_profile', 'public_activity']);
            foreach ($socialIntegrations as $integration => $sn) {
                $settings                      = $sn->getIntegrationSettings();
                $featureSettings[$integration] = $settings->getFeatureSettings();
            }
        }

        if ($specificIntegration) {
            return ($returnSettings) ? [[$specificIntegration => $socialCache[$specificIntegration]], $featureSettings]
                : [$specificIntegration => $socialCache[$specificIntegration]];
        }

        return ($returnSettings) ? [$socialCache, $featureSettings] : $socialCache;
    }

    /**
     * @param bool $integration
     *
     * @return array
     */
    public function clearIntegrationCache($lead, $integration = false)
    {
        $socialCache = $lead->getSocialCache();
        if (!empty($integration)) {
            unset($socialCache[$integration]);
        } else {
            $socialCache = [];
        }
        $lead->setSocialCache($socialCache);
        $this->em->getRepository(\Mautic\LeadBundle\Entity\Lead::class)->saveEntity($lead);

        return $socialCache;
    }

    /**
     * Gets an array of the HTML for share buttons.
     */
    public function getShareButtons()
    {
        static $shareBtns = [];

        if (empty($shareBtns)) {
            $socialIntegrations = $this->getIntegrationObjects(null, ['share_button'], true);

            /**
             * @var string              $integration
             * @var AbstractIntegration $details
             */
            foreach ($socialIntegrations as $integration => $details) {
                /** @var Integration $settings */
                $settings = $details->getIntegrationSettings();

                $featureSettings = $settings->getFeatureSettings();
                $apiKeys         = $details->decryptApiKeys($settings->getApiKeys());
                $plugin          = $settings->getPlugin();
                $shareSettings   = $featureSettings['shareButton'] ?? [];

                // add the api keys for use within the share buttons
                $shareSettings['keys']   = $apiKeys;
                $shareBtns[$integration] = $this->twig->render($plugin->getBundle()."/Integration/$integration:share.html.twig", [
                    'settings' => $shareSettings,
                ]);
            }
        }

        return $shareBtns;
    }

    /**
     * Loops through field values available and finds the field the integration needs to obtain the user.
     *
     * @return bool
     */
    public function getUserIdentifierField($integrationObject, $fields)
    {
        $identifierField = $integrationObject->getIdentifierFields();
        $identifier      = (is_array($identifierField)) ? [] : false;
        $matchFound      = false;

        $findMatch = function ($f, $fields) use (&$identifierField, &$identifier, &$matchFound): void {
            if (is_array($identifier)) {
                // there are multiple fields the integration can identify by
                foreach ($identifierField as $idf) {
                    $value = (is_array($fields[$f]) && isset($fields[$f]['value'])) ? $fields[$f]['value'] : $fields[$f];

                    if (!in_array($value, $identifier) && str_contains($f, $idf)) {
                        $identifier[$f] = $value;
                        if (count($identifier) === count($identifierField)) {
                            // found enough matches so break
                            $matchFound = true;
                            break;
                        }
                    }
                }
            } elseif ($identifierField === $f || str_contains($f, $identifierField)) {
                $matchFound = true;
                $identifier = (is_array($fields[$f])) ? $fields[$f]['value'] : $fields[$f];
            }
        };

        $groups = ['core', 'social', 'professional', 'personal'];
        $keys   = array_keys($fields);
        if (0 !== count(array_intersect($groups, $keys)) && count($keys) <= 4) {
            // fields are group
            foreach ($fields as $groupFields) {
                $availableFields = array_keys($groupFields);
                foreach ($availableFields as $f) {
                    $findMatch($f, $groupFields);

                    if ($matchFound) {
                        break;
                    }
                }
            }
        } else {
            $availableFields = array_keys($fields);
            foreach ($availableFields as $f) {
                $findMatch($f, $fields);

                if ($matchFound) {
                    break;
                }
            }
        }

        return $identifier;
    }

    /**
     * Get the path to the integration's icon relative to the site root.
     *
     * @return string
     */
    public function getIconPath($integration)
    {
        $systemPath  = $this->pathsHelper->getSystemPath('root');
        $bundlePath  = $this->pathsHelper->getSystemPath('bundles');
        $pluginPath  = $this->pathsHelper->getSystemPath('plugins');
        $genericIcon = $bundlePath.'/PluginBundle/Assets/img/generic.png';

        if (is_array($integration)) {
            // A bundle so check for an icon
            $icon = $pluginPath.'/'.$integration['bundle'].'/Assets/img/icon.png';
        } elseif ($integration instanceof Plugin) {
            // A bundle so check for an icon
            $icon = $pluginPath.'/'.$integration->getBundle().'/Assets/img/icon.png';
        } elseif ($integration instanceof UnifiedIntegrationInterface) {
            return $integration->getIcon();
        }

        if (file_exists($systemPath.'/'.$icon)) {
            return $icon;
        }

        return $genericIcon;
    }
}
