<?php

namespace Opentalent\OtCore\Website;

use Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException;
use Opentalent\OtCore\Exception\NoSuchWebsiteException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Site\Entity\Site;

/**
 * Repository for the ot_websites table
 *
 * @package Opentalent\OtCore\Website
 */
class OtWebsiteRepository
{
    /**
     * @var \TYPO3\CMS\Core\Database\ConnectionPool
     */
    private \TYPO3\CMS\Core\Database\ConnectionPool $connectionPool;

    public function injectConnectionPool(\TYPO3\CMS\Core\Database\ConnectionPool $connectionPool)
    {
        $this->connectionPool = $connectionPool;
    }

    /**
     * Get an OtWebsite by uid
     *
     * @throws NoSuchWebsiteException
     */
    public function getWebsiteByUid(int $uid, bool $withRestrictions = true): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
        if (!$withRestrictions) {
            $queryBuilder->getRestrictions()->removeAll();
        }
        $website = $queryBuilder
            ->select('*')
            ->from('ot_websites')
            ->where($queryBuilder->expr()->eq('uid', $uid))
            ->execute()
            ->fetch();
        if (!isset($website['uid'])) {
            throw new NoSuchWebsiteException('No website found with uid ' . $uid);
        }
        return $website;
    }

    /**
     * Get the OtWebsite of the given organization
     *
     * @throws NoSuchWebsiteException
     */
    public function getWebsiteByOrganizationId(int $organizationId, bool $withRestrictions = true): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
        if (!$withRestrictions) {
            $queryBuilder->getRestrictions()->removeAll();
        }
        $website = $queryBuilder
            ->select('*')
            ->from('ot_websites')
            ->where($queryBuilder->expr()->eq('organization_id', $organizationId))
            ->execute()
            ->fetch();
        if (!isset($website['uid'])) {
            throw new NoSuchWebsiteException('No website found for organization ' . $organizationId);
        }
        return $website;
    }

    /**
     * Get the OtWebsite of the given page
     *
     * @throws NoSuchWebsiteException
     */
    public function getWebsiteByPageUid(int $pageUid, bool $withRestrictions = true): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
        if (!$withRestrictions) {
            $queryBuilder->getRestrictions()->removeAll();
        }
        $website = $queryBuilder
            ->select('w.*')
            ->from('ot_websites', 'w')
            ->innerJoin('w', 'pages', 'p', $queryBuilder->expr()->eq('p.ot_website_uid', 'w.uid'))
            ->where($queryBuilder->expr()->eq('p.uid', $pageUid))
            ->execute()
            ->fetch();
        if (!isset($website['uid'])) {
            throw new NoSuchWebsiteException('No website found for page ' . $pageUid);
        }
        return $website;
    }

    /**
     * Get the OtWebsite by its config identifier
     *
     * @throws NoSuchWebsiteException
     */
    public function getWebsiteByConfigIdentifier(string $identifier): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
        $website = $queryBuilder
            ->select('*')
            ->from('ot_websites')
            ->where($queryBuilder->expr()->eq('config_identifier', $queryBuilder->expr()->literal($identifier)))
            ->execute()
            ->fetch();
        if (!isset($website['uid'])) {
            throw new NoSuchWebsiteException('No website found for page ' . $pageUid);
        }
        return $website;
    }

    /**
     * Get the root page uid of the given OtWebsite
     *
     * @param int $websiteUid
     * @param bool $withRestrictions
     * @return int
     */
    public function getWebsiteRootUid(int $websiteUid, bool $withRestrictions = true): int {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
        if (!$withRestrictions) {
            $queryBuilder->getRestrictions()->removeAll();
        }
        return $queryBuilder
            ->select('uid')
            ->from('pages')
            ->where($queryBuilder->expr()->eq('ot_website_uid', $websiteUid))
            ->andWhere($queryBuilder->expr()->eq('is_siteroot', 1))
            ->execute()
            ->fetchColumn(0);
    }

    /**
     * Try to find the root page uid of the organization's website and return it.
     * Throw a Opentalent\OtAdmin\NoSuchWebsiteException exception if the website does not exist.
     *
     * @param int $organizationId
     * @param bool $withRestrictions
     * @return int
     * @throws NoSuchWebsiteException
     */
    public function findRootUidForOrganization(int $organizationId, bool $withRestrictions = true): int
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
        if (!$withRestrictions) {
            $queryBuilder->getRestrictions()->removeAll();
        }
        $rootUid = $queryBuilder
            ->select('p.uid')
            ->from('pages', 'p')
            ->innerJoin('p', 'ot_websites', 'w', $queryBuilder->expr()->eq('p.ot_website_uid', 'w.uid'))
            ->where($queryBuilder->expr()->eq('w.organization_id', $organizationId))
            ->andWhere($queryBuilder->expr()->eq('p.is_siteroot', 1))
            ->execute()
            ->fetchColumn(0);
        if (!$rootUid) {
            throw new NoSuchWebsiteException("No website found for organization " . $organizationId);
        }
        return $rootUid;
    }

    /**
     * Retrieves the current full domain of the given website.
     *
     * @param array $website
     * @return string
     * @throws InvalidWebsiteConfigurationException
     */
    public function resolveWebsiteDomain(array $website): string
    {
        if ($website['custom_domain']) {
            return $website['custom_domain'];
        } else if ($website['subdomain']) {
            return $website['subdomain'] . '.opentalent.fr';
        }
        throw new InvalidWebsiteConfigurationException("No domain defined for website " . $website['uid']);
    }

    /**
     * Get the website current base uri
     *
     * @param array $website
     * @return string
     * @throws InvalidWebsiteConfigurationException
     */
    public function resolveWebsiteBaseUri(array $website): string
    {
        return 'https://' . $this->resolveWebsiteDomain($website);
    }

    /**
     * Generate an array as it would be loaded from the site.yaml configuration
     * file of the given website
     *
     * @param array $website
     * @return Site
     * @throws InvalidWebsiteConfigurationException
     */
    public function generateWebsiteConfiguration(array $website): Site
    {
        $rootUid = $this->getWebsiteRootUid($website['uid']);

        return new Site(
            $website['config_identifier'],
            $rootUid,
            [
                'base' => $this->resolveWebsiteBaseUri($website),
                'baseVariants' => [0 => [
                                'base' => $website['subdomain'] . '/',
                                'condition' => 'applicationContext == "Development"',
                                        ],
                                ],
                'errorHandling' => [0 => ['errorCode' => '404',
                                            'errorHandler' => 'PHP',
                                            'errorPhpClassFQCN' => 'Opentalent\\OtTemplating\\Page\\ErrorHandler',
                                         ],
                                    1 => ['errorCode' => '403',
                                            'errorHandler' => 'PHP',
                                            'errorPhpClassFQCN' => 'Opentalent\\OtTemplating\\Page\\ErrorHandler',
                                         ],
                                    ],
                'flux_content_types' => '',
                'flux_page_templates' => '',
                'languages' => [0 => [
                                        'title' => 'Fr',
                                        'enabled' => true,
                                        'base' => '/',
                                        'typo3Language' => 'fr',
                                        'locale' => 'fr_FR',
                                        'iso-639-1' => 'fr',
                                        'navigationTitle' => 'Fr',
                                        'hreflang' => 'fr-FR',
                                        'direction' => 'ltr',
                                        'flag' => 'fr',
                                        'languageId' => '0',
                                     ],
                              ],
                'rootPageId' => $rootUid,
                'routes' => [],
            ]
        );
    }

    /**
     * Try to retrieve the website matching the given Uri
     *
     * @param \Psr\Http\Message\UriInterface $uri
     * @param bool $devMode
     * @return Site
     * @throws NoSuchWebsiteException
     */
    public function matchUriToWebsite(\Psr\Http\Message\UriInterface $uri, bool $devMode=false): Site
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');

        $q = $queryBuilder
            ->select('*')
            ->from('ot_websites');

        if ($devMode) {
            preg_match("/(\w+)(?:\/.*)?/", $uri->getPath(), $m);
            $q = $q->where($queryBuilder->expr()->eq('subdomain', $queryBuilder->expr()->literal($m[1])));
        } else {
            preg_match("/([\w\-]+)\.opentalent\.fr/", $uri->getHost(), $m);
            if (count($m) > 0) {
                $q = $q->where($queryBuilder->expr()->eq('subdomain', $queryBuilder->expr()->literal($m[1])));
            } else {
                $q = $q->where($queryBuilder->expr()->eq('custom_domain', $queryBuilder->expr()->literal($uri->getHost())));
            }
        }

        $website = $q->execute()
                    ->fetch();

        if (!isset($website['uid'])) {
            throw new NoSuchWebsiteException('No website found for this URI: ' . $uri);
        }

        return $this->generateWebsiteConfiguration($website);
    }

    /**
     * Try to find the config file of the website in the less resource-consuming way
     * and parse it.
     *
     * @param int $rootUid
     * @return array   Path of the configuration file and parsed configuration of the website
     */
    public function findConfigFileAndContentFor(int $rootUid): array
    {
        $configs_directory = $_ENV['TYPO3_PATH_APP'] . "/config/sites/";
        $candidates = array_filter(
            scandir($configs_directory),
            function ($x) { return $x != '.' && $x != '..'; }
        );

        // try to filter by directory name
        foreach ($candidates as $subdir) {
            if (preg_match('/\.*_' . $rootUid . '$/', $subdir)) {
                $filename = $configs_directory . $subdir . '/config.yaml';
                try {
                    $yamlConfig = Yaml::parseFile($filename);

                    if ($yamlConfig['rootPageId'] === $rootUid) {
                        return [$filename, $yamlConfig];
                    }
                } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
                    continue;
                }
            }
        }

        // it wasn't found the easy way, let's look to each file... :(
        foreach ($candidates as $subdir) {
            $filename = $configs_directory . $subdir . '/config.yaml';
            try {
                $yamlConfig = Yaml::parseFile($filename);
                if ($yamlConfig['rootPageId'] === $rootUid) {
                    return [$filename, $yamlConfig];
                }
            } catch (\Symfony\Component\Yaml\Exception\ParseException $e) {
                continue;
            }
        }

        return [null, []];
    }

    /**
     * Similar to findConfigFileAndContentFor(), but only returns the parsed configuration
     *
     * @param int $rootUid
     * @return array   Configuration of the website
     */
    public function findConfigFor(int $rootUid): array
    {
        $pathAndConfig = $this->findConfigFileAndContentFor($rootUid);
        return $pathAndConfig[1];
    }

    /**
     * Similar to findConfigFileAndContentFor(), but only returns the config file path
     * @param int $rootUid
     * @return string   Path of the config file of the given website
     */
    public function findConfigFilePathFor(int $rootUid): string
    {
        $pathAndConfig = $this->findConfigFileAndContentFor($rootUid);
        return $pathAndConfig[0];
    }
}
