<?php

namespace Opentalent\OtCore\Website;

use Doctrine\DBAL\DBALException;
use Opentalent\OtCore\Exception\InvalidWebsiteConfigurationException;
use Opentalent\OtCore\Exception\NoSuchRecordException;
use Opentalent\OtCore\Exception\NoSuchWebsiteException;
use Opentalent\OtCore\Utility\RouteNormalizer;
use Opentalent\OtCore\Utility\UrlUtils;
use Psr\Http\Message\UriInterface;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Site\Entity\Site;

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

    public function injectConnectionPool(ConnectionPool $connectionPool)
    {
        $this->connectionPool = $connectionPool;
    }

    /**
     * Get an all of the OtWebsite records
     *
     */
    public function getAll(bool $withRestrictions = true): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
        $q = $queryBuilder
            ->select('*')
            ->from('ot_websites');
        if ($withRestrictions) {
            $q->where($q->expr()->eq('deleted', 0));
        }
        return $q->execute()
                 ->fetchAll();
    }

    /**
     * Get an OtWebsite by uid
     *
     * @throws NoSuchWebsiteException
     */
    public function getWebsiteByUid(int $uid, bool $withRestrictions = true): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
        $q = $queryBuilder
            ->select('*')
            ->from('ot_websites')
            ->where($queryBuilder->expr()->eq('uid', $uid));
        if ($withRestrictions) {
            $q->andWhere($q->expr()->eq('deleted', 0));
        }
        $website = $q->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
     * @throws DBALException
     */
    public function getWebsiteByOrganizationId(int $organizationId, bool $withRestrictions = true): array
    {
        $queryBuilder = $this->connectionPool->getQueryBuilderForTable('ot_websites');
        $q = $queryBuilder
            ->select('*')
            ->from('ot_websites')
            ->where($queryBuilder->expr()->eq('organization_id', $organizationId));
        if ($withRestrictions) {
            $q->andWhere($q->expr()->eq('deleted', 0));
        }
        $website = $q->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();
        }
        $q = $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));
        if ($withRestrictions) {
            $q->andWhere($q->expr()->eq('w.deleted', 0));
        }
        $website = $q->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 identifier ' . $identifier);
        }
        return $website;
    }

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

    /**
     * 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();
        }
        $q = $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));
        if ($withRestrictions) {
            $q->andWhere($q->expr()->eq('w.deleted', 0));
        }
        $rootUid = $q->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
     * @param string|null $identifier
     * @return Site
     * @throws InvalidWebsiteConfigurationException
     * @throws NoSuchRecordException
     */
    public function generateWebsiteConfiguration(array $website, string $identifier = null): Site
    {
        $rootUid = $this->getWebsiteRootUid($website['uid'], false);

        $identifier = $identifier ?? $website['config_identifier'];

        return new Site(
            $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' => [],
            ]
        );
    }

    /**
     * Returns the typo3 site matching the current request (FE only!)
     *
     * @return array
     * @throws NoSuchWebsiteException
     */
    public function getCurrentWebsiteFromFEGlobals(): array
    {
        $request = $GLOBALS['TYPO3_REQUEST'];
        $ot_website = $GLOBALS['TYPO3_REQUEST']->getAttribute('ot_website');
        if ($ot_website) {
            return $ot_website;
        } else {
            $site = $request->getAttribute('site');
            return $this->getWebsiteByConfigIdentifier($site->getIdentifier());
        }
    }

    /**
     * Returns the typo3 site matching the current request (FE only!)
     *
     * @return int
     * @throws NoSuchWebsiteException
     */
    public function getCurrentRootpageUidFromFEGlobals(): int
    {
        $website = $this->getCurrentWebsiteFromFEGlobals();
        return $this->getWebsiteRootUid($website['uid']);
    }

    /**
     * Returns the current site's rootpage URI (FE only)
     *
     * @return string
     * @throws InvalidWebsiteConfigurationException
     * @throws NoSuchWebsiteException
     */
    public function getCurrentSiteRootUriFromFeGlobals(): string
    {
        $website = $this->getCurrentWebSiteFromFEGlobals();
        return $this->resolveWebsiteBaseUri($website);
    }

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

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

        $domain = UrlUtils::normalizeDomain($uri->getHost());
        $path = UrlUtils::normalizePath($uri->getPath());

        if ($devMode) {
            preg_match("/([\w\-]+)(?:\/.*)?/", $path, $m);
            $q = $q->where($queryBuilder->expr()->eq('subdomain', $queryBuilder->expr()->literal($m[1])));
        } else {
            preg_match("/([\w\-]+)\.opentalent\.fr/", $domain, $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($domain)));
            }
        }

        if ($withRestrictions) {
            $q = $q->andWhere($queryBuilder->expr()->eq('deleted', 0));
        }

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

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

        return $website;
    }

    /**
     * @param UriInterface $uri
     * @param bool $devMode
     * @param array|null $website
     * @return int
     * @throws NoSuchWebsiteException
     */
    public function matchUriToPage(array $otWebsite, UriInterface $uri, bool $devMode=false, bool $withRestrictions = true): int
    {
        $tail = $uri->getPath();
        if ($devMode) {
            $tail = preg_replace("/\/?[\w\-]+\/?(.*)/", "/$1", $tail);
        }
        if ($tail != "/") {
            $tail = rtrim($tail, '/');
        }

        $q = $this->connectionPool->getQueryBuilderForTable('pages');
        if (!$withRestrictions) {
            $q->getRestrictions()->removeAll();
        }
        return $q
            ->select('uid')
            ->from('pages')
            ->where($q->expr()->eq('ot_website_uid', $otWebsite['uid']))
            ->andWhere($q->expr()->eq('slug', $q->expr()->literal($tail)))
            ->execute()
            ->fetchColumn(0);
    }

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

        if ($identifier !== null) {
            $filename = $configs_directory . $identifier . "/config.yaml";
            try {
                $yamlConfig = Yaml::parseFile($filename);
            } catch (ParseException $e) {
                throw new \RuntimeException("No configuration file found for identifier " . $identifier);
            }

            if ($yamlConfig['rootPageId'] === $rootUid) {
                return [$filename, $yamlConfig];
            } else {
                throw new \RuntimeException("The configuration file with identifier " . $identifier . " is attached to another root uid");
            }
        }

        $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 (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 (ParseException $e) {
                continue;
            }
        }

        return [null, []];
    }

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

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

    /**
     * Find the site configuration identifier of the given website
     * @param int $rootUid
     * @return string|null   Config identifier
     */
    public function findConfigIdentifierFor(int $rootUid): ?string
    {
        $path = $this->findConfigFilePathFor($rootUid);
        if ($path) {
            return basename(dirname($path));
        } else {
            return null;
        }
    }
}
