<?php

declare(strict_types=1);

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Core\SystemResource;

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Package\Resource\Definition\PublicResourceDefinition;
use TYPO3\CMS\Core\Package\VirtualAppPackage;
use TYPO3\CMS\Core\Resource\Exception as FalException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolvePublicResourceException;
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolveSystemResourceException;
use TYPO3\CMS\Core\SystemResource\Exception\CanNotResolveSystemResourceIdentifierException;
use TYPO3\CMS\Core\SystemResource\Exception\InvalidSystemResourceIdentifierException;
use TYPO3\CMS\Core\SystemResource\Identifier\FalResourceIdentifier;
use TYPO3\CMS\Core\SystemResource\Identifier\PackageResourceIdentifier;
use TYPO3\CMS\Core\SystemResource\Identifier\SystemResourceIdentifierFactory;
use TYPO3\CMS\Core\SystemResource\Identifier\UriResourceIdentifier;
use TYPO3\CMS\Core\SystemResource\Type\PackageResource;
use TYPO3\CMS\Core\SystemResource\Type\PublicPackageFile;
use TYPO3\CMS\Core\SystemResource\Type\PublicResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\StaticResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\SystemResourceInterface;
use TYPO3\CMS\Core\SystemResource\Type\UriResource;
use TYPO3\CMS\Core\Utility\PathUtility;

/**
 * This is the heart of system resource handling and
 * the most important API to be used in userland code
 * and throughout the core.
 *
 * This class should be final, but is not, because it
 * needs to be mocked in tests.
 */
#[Autoconfigure(public: true)]
readonly class SystemResourceFactory
{
    public function __construct(
        private SystemResourceIdentifierFactory $identifierFactory,
        private ?StorageRepository $storageRepository,
        private ?ResourceFactory $resourceFactory,
    ) {}

    /**
     * Use this method to obtain a resource that is public,
     * which means, that a URI can be generated for it.
     *
     * Always use it when the goal is to generate a URI,
     * as it checks, whether the resolved resource can/ is
     * indeed published or not and throws an exception otherwise.
     *
     * @throws CanNotResolveSystemResourceException
     * @throws CanNotResolvePublicResourceException
     */
    public function createPublicResource(string $resourceString): PublicResourceInterface
    {
        $resource = $this->createResource($resourceString);
        if (!$resource instanceof PublicResourceInterface || !$resource->isPublished()) {
            throw new CanNotResolvePublicResourceException(sprintf('Resolved resource "%s" is not a public resource. Given resource identifier: "%s"', $resource, $resourceString), 1758098512);
        }
        return $resource;
    }

    /**
     * Use this method, when generating a URI is not required
     * for the resource and only e.g. file contents like for templates
     * is required.
     *
     * @throws CanNotResolveSystemResourceException
     * @throws InvalidSystemResourceIdentifierException
     */
    public function createResource(string $resourceString): StaticResourceInterface
    {
        try {
            return $this->createFromIdentifier($resourceString);
        } catch (CanNotResolveSystemResourceIdentifierException $e) {
            if (str_starts_with($resourceString, Environment::getProjectPath())
                || (PathUtility::isAbsolutePath($resourceString) && file_exists($resourceString))
            ) {
                throw new CanNotResolveSystemResourceException('Absolute paths are not allowed as resource identifiers', 1760618195);
            }
            $resourceString = ltrim($resourceString, '/');
            $falResource = $this->createFromLegacyFalPath($resourceString, $e);
            return $falResource ?? $this->createResourceFromRelativePublicPath($resourceString);
        }
    }

    /**
     * @throws CanNotResolveSystemResourceException
     * @throws InvalidSystemResourceIdentifierException
     */
    private function createResourceFromRelativePublicPath(string $relativePublicPath): SystemResourceInterface
    {
        $absoluteResourcePath = Environment::getPublicPath() . '/' . $relativePublicPath;
        // The file must actually exist, only then we can be sure our
        // following string manipulation is correct. Otherwise, it could be some unexpected
        // string and the manipulation will lead to unexpected results
        // Strip potentially available query string and fragment from the path before checking, though
        try {
            $strippedAbsolutePath = (new Uri($absoluteResourcePath))->getPath();
        } catch (\InvalidArgumentException) {
            $strippedAbsolutePath = null;
        }
        if (!file_exists($strippedAbsolutePath ?? $absoluteResourcePath)) {
            throw new CanNotResolveSystemResourceException(sprintf('Can not resolve relative public path "%s" to a system resource', $relativePublicPath), 1759740281);
        }
        $packageIdentifier = $this->identifierFactory->createFromPackagePath(
            VirtualAppPackage::APP_PACKAGE_KEY,
            substr($absoluteResourcePath, strlen(Environment::getProjectPath()) + 1),
            $relativePublicPath,
        );
        return $this->createFromPackageIdentifier($packageIdentifier);
    }

    /**
     * @throws CanNotResolveSystemResourceException
     * @throws InvalidSystemResourceIdentifierException
     * @throws CanNotResolveSystemResourceIdentifierException
     */
    private function createFromIdentifier(string $potentialIdentifier): StaticResourceInterface
    {
        $identifier = $this->identifierFactory->create($potentialIdentifier);
        return match (get_class($identifier)) {
            UriResourceIdentifier::class => new UriResource($identifier),
            PackageResourceIdentifier::class => $this->createFromPackageIdentifier($identifier),
            FalResourceIdentifier::class => $this->createFromFalIdentifier($identifier),
            default => throw new InvalidSystemResourceIdentifierException(sprintf('Can not resolve "%s" to a system resource. Unknown SystemResourceIdentifier', $potentialIdentifier), 1759393674),
        };
    }

    /**
     * @throws CanNotResolveSystemResourceException
     */
    private function createFromLegacyFalPath(string $resourceString, \Throwable $e): ?StaticResourceInterface
    {
        if (!str_starts_with($resourceString, $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'])) {
            // For legacy resolving, we do not even try resolving any other path than
            // one starting with configured $GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir']
            return null;
        }
        try {
            $potentialFalPath = $resourceString;
            $storageUid = $this->storageRepository?->findBestMatchingStorageByLocalPath($potentialFalPath);
        } catch (\Throwable) {
            $storageUid = 0;
        }
        if ($storageUid <= 0) {
            return null;
        }
        $storage = $this->storageRepository?->findByUid($storageUid);
        if ($storage === null) {
            throw new CanNotResolveSystemResourceException(sprintf('Can not resolve "%s" to a system resource, storage %d does not exist', $resourceString, $storageUid), 1758627596, $e);
        }
        $file = null;
        try {
            $file = $storage->getFile($potentialFalPath);
        } catch (\Throwable) {
        }
        $this->ensureValidFalResource($file, $resourceString);
        return $file;
    }

    /**
     * @throws CanNotResolveSystemResourceException
     */
    private function createFromFalIdentifier(FalResourceIdentifier $resourceUri): PublicResourceInterface
    {
        try {
            $file = $this->resourceFactory?->retrieveFileOrFolderObject($resourceUri->getIdentifier());
            $this->ensureValidFalResource($file, (string)$resourceUri);
            return $file;
        } catch (FalException $e) {
            throw new CanNotResolveSystemResourceException(sprintf('Can not resolve "%s" to a system resource', $resourceUri), 1759397430, $e);
        }
    }

    /**
     * @throws CanNotResolveSystemResourceException
     */
    private function ensureValidFalResource(ProcessedFile|File|Folder|null $falResource, string $resourceUri): void
    {
        if (!$falResource instanceof File || $falResource->getStorage()->getUid() === 0) {
            throw new CanNotResolveSystemResourceException(sprintf('Can not resolve file with URI "%s"', $resourceUri), 1758700078);
        }
    }

    private function createFromPackageIdentifier(PackageResourceIdentifier $packageIdentifier): SystemResourceInterface
    {
        $resourceDefinition = $packageIdentifier->getPackage()->getResources()->definitionForPath($packageIdentifier->getRelativePath());
        if ($resourceDefinition instanceof PublicResourceDefinition) {
            return new PublicPackageFile($packageIdentifier, $resourceDefinition);
        }
        return new PackageResource($packageIdentifier, $resourceDefinition);
    }
}
