<?php

namespace Snaatch\Typo3\Controller;

use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
use Snaatch\Typo3\Utility\ConfigurationUtility;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Resource\Index\MetaDataRepository;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class FilesController
{
  protected array $extensionConfiguration;

  /**
   * @throws ExtensionConfigurationPathDoesNotExistException
   * @throws ExtensionConfigurationExtensionNotConfiguredException
   */
  public function __construct()
  {
    $this->extensionConfiguration = ConfigurationUtility::getExtensionConfiguration();
  }

  /**
   * This method is called by the AJAX endpoint.
   *
   * @param ServerRequestInterface $request
   * @param ResponseInterface|null $response
   * @return ResponseInterface
   * @noinspection
   */
  public function downloadFiles(ServerRequestInterface $request, ?ResponseInterface $response = null): ResponseInterface
  {
    // prepare
    $storage = $this->getStorage();

    // Needed for TYPO3 10
    if (!is_object($response)) {
      $response = new JsonResponse();
    }

    // destructure
    $json_body = $this->getJSONRequest($request)->getParsedBody();

    // if $json_body->folder is not set, return the list of folders and ask the frontend to select one
    if (!isset($json_body->folder)) {
      if ($this->extensionConfiguration['subfolder']) {
        $user_upload_folder = $storage->getFolder($this->extensionConfiguration['subfolder']);
      } else {
        $user_upload_folder = $storage->getRootLevelFolder();
      }

      // https://api.typo3.org/9.5/class_t_y_p_o3_1_1_c_m_s_1_1_core_1_1_resource_1_1_folder.html#a1e73145dd29bea65c91a53ef5f446af5
      $set_subfolder = $this->extensionConfiguration['subfolder'];

      $subfolders = $user_upload_folder->getSubfolders(
        0,
        0,
        1, // TYPO3\CMS\Core\Resource\Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS = 1
        true // recursive
      );

      $subfolder_names = array_map(function ($subfolder) {
        return $subfolder->getName();
      }, $subfolders);


      // If subfolder setting is set, filter out parent folders of the subfolder to ensure that the folder tree
      // starts with the configured subfolder and all parents are hidden
      if (!empty($set_subfolder)) {
        $extracted_subfolder = '/' . preg_replace('/[^a-zA-Z]/', '', strrchr(rtrim($set_subfolder, '/'), '/')) . '/';

        $filterCallback = function ($folder, $subfolder_name) use ($set_subfolder, $extracted_subfolder) {
          // Check if the folder contains the desired string
          if (strpos($folder, $set_subfolder) === 0) {
            // If yes, return an array with the adjusted key-value pair
            $adjusted_key = $extracted_subfolder . trim(str_replace($set_subfolder, '', $folder), '/');
            return [$adjusted_key . '/' => $subfolder_name];
          }

          return [];
        };

        // Use array_map with array_filter to filter out folders that do not contain the desired string
        $subfolder_names = array_reduce(
          array_map($filterCallback, array_keys($subfolder_names), $subfolder_names),
          'array_merge',
          []
        );
      }

      $response->getBody()->write(json_encode([
        'subject' => 'folder-missing',
        'folders' => $subfolder_names
      ]));

      return $response;
    }

    $destination_path = $json_body->folder;

    $files = $json_body->files;

    if ($files) {
      $importedFiles = $this->pullFiles($files, $destination_path);
      $response->getBody()->write(json_encode(['files' => $importedFiles]));
    }
    return $response;
  }

  public function getJSONRequest(ServerRequestInterface $request): ServerRequestInterface
  {
    if (!str_contains($request->getHeaderLine('Content-Type'), 'application/json')) {
      return $request;
    }

    return $request->withParsedBody(json_decode($request->getBody()->getContents()));
  }

  private function pullFiles($files, $destination_path): array
  {
    $importedFiles = [];
    $reservedFilenames = [];
    foreach ($files as $file) {
      $metadata = $file->metadata ?? new \stdClass();

      // set upload filename and upload folder
      $name = (string)$this->getObjectValue($metadata, 'name', 'imported-file');
      $extension = (string)$this->getObjectValue($metadata, 'extension', '');

      $rawUrl = $this->getObjectValue($file->links ?? null, 'raw');
      $downloadUrl = $this->getObjectValue($file->links ?? null, 'download');
      $url = $rawUrl ?: $downloadUrl;

      if (!$extension && is_string($url) && $url !== '') {
        $pathFromUrl = parse_url($url, PHP_URL_PATH);
        $extension = strtolower((string)pathinfo((string)$pathFromUrl, PATHINFO_EXTENSION));
      }

      if (!$extension) {
        $extension = 'bin';
      }

      $filename = $this->getNonUtf8Filename($name, $extension);
      $filename = $this->buildUniqueFilename($filename, $destination_path, $reservedFilenames);
      $reservedFilenames[] = $filename;

      if (!$url) {
        $this->throwError('No valid source URL found for file "' . $filename . '"', 4);
      }

      // upload file
      $importedFilePath = $this->saveFile($filename, $url, $destination_path);
      if (!$importedFilePath) {
        $this->throwError('Copying file "' . $filename . '" to path "' . '" failed.' . "(URL: " . $url . ")", 4);
      } else {
        $importedFilePath =         $this->getCharactersAfterSubstring($importedFilePath, $this->getStorage()->getConfiguration()['basePath']);

        $importedFile = $this->getStorage()->getFile($importedFilePath);
        if ($importedFile) {
          // import file to FAL
          $importedFileUid = $importedFile->getUid();
          $importedFiles[] = $importedFileUid;

            // fix error with negative createdAtTimestamp
            $createdAtTimestamp = (int)$this->getObjectValue($metadata, 'createdAtTimestamp', 0);
            if ($createdAtTimestamp < 0) {
                $createdAtTimestamp = 0;
            }

            $updatedAtTimestamp = (int)$this->getObjectValue($metadata, 'updatedAtTimestamp', 0);
            if ($updatedAtTimestamp < 0) {
              $updatedAtTimestamp = 0;
            }

          // set meta data
          $additionalFields = array(
            'title' => (string)$this->getObjectValue($metadata, 'name', ''),
            'description' => (string)$this->getObjectValue($metadata, 'description', ''),
            'alternative' => (string)$this->getObjectValue($metadata, 'alt', ''),
            'keywords' => (string)$this->getObjectValue($metadata, 'keywordsAsString', ''),
            'caption' =>  (string)$this->getObjectValue($metadata, 'title', ''),
            // 'creator_tool' => 'MS Paint',
            'download_name' => (string)$this->getObjectValue($metadata, 'name', ''),
            'creator' => (string)$this->getObjectValue($metadata, 'creator', ''),
            //'publisher' => 'Hardcoded publisher',
            'source' => (string)$this->getObjectValue($metadata, 'source', ''),
            'copyright' => (string)$this->getObjectValue($metadata, 'copyrightNotice', ''),
            'location_country' => (string)$this->getObjectValue($metadata, 'country', ''),
            'location_region' => (string)$this->getObjectValue($metadata, 'state', ''),
            'location_city' => (string)$this->getObjectValue($metadata, 'city', ''),
            //'latitude' => 50.266667,
            //'longitude' => 10.966667,
            'ranking' => (string)$this->getObjectValue($metadata, 'rating', ''),
            'content_creation_date' => $createdAtTimestamp, // Unix timestamp
            'content_modification_date' => $updatedAtTimestamp,
            //'language' => 'German'
          );

          $metaDataRepository = GeneralUtility::makeInstance(MetaDataRepository::class);
          $metaDataRepository->update($importedFileUid, $additionalFields);
        }
      }
    }
    return $importedFiles;
  }

  /**
   * @param $filename
   * @param $url
   * @return false|string
   */
  private function saveFile($filename, $url, $destination_path)
  {
    $absFileIdentifier = $this->combinePaths($this->uploadPath(), $destination_path) . $filename;
    $destination_path = $this->combinePaths($this->uploadPath(), $destination_path);

    if ($this->isExecutableExtension($filename)) {
      $this->throwError('Wrong upload file extension. Is not allowed to use php,js,js,exe,doc,xls,sh: "' . $filename, 5);
    }

    // create folder if not exists
    if (!is_dir($destination_path)) {
      mkdir($destination_path, 0777, true);
    }

    # SNM: Use TYPO3 Core API to download file (Uses TYPO3 request handler and configured proxy)
    $uploaded = false;
    $fileContent = GeneralUtility::getUrl($url);
    if ($fileContent !== false) {
        $uploaded = file_put_contents($absFileIdentifier, $fileContent);
    }

    return $uploaded ? $absFileIdentifier : false;
  }

  private function uploadPath()
  {
    try {
      $storage = $this->getStorage();

      $storageBasePath = $storage->getConfiguration()['basePath'];

      // correct beginning and trailing slashes
      if (!str_ends_with($storageBasePath, '/')) {
        $storageBasePath = $storageBasePath . '/';
      }
      if (str_starts_with($storageBasePath, '/')) {
        $storageBasePath = substr($storageBasePath, 1);
      }

      if ($this->extensionConfiguration['subfolder']) {
        //remove first slash of subfolder
        if (str_starts_with($this->extensionConfiguration['subfolder'], '/')) {
          $extensionSubfolder = substr($this->extensionConfiguration['subfolder'], 1);
        }
        $storageBasePath .= $extensionSubfolder;
      }

      if (!str_ends_with($storageBasePath, '/')) {
        $storageBasePath = $storageBasePath . '/';
      }

      return GeneralUtility::getFileAbsFileName($storageBasePath);
    } catch (Exception $error) {
      $this->throwError($error->getMessage(), $error->getCode());
    }
  }

  private function getStorage()
  {
    $storageUid = (int)$this->extensionConfiguration['filestorage_id'];
    if (!($storageUid > 0)) {
      $storageUid = 1;
    }

    $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
    return $resourceFactory->getStorageObject($storageUid);
  }

  private function throwError($message, $num)
  {
    throw new RuntimeException($message, TYPO3_EXT_NUM_SNAATCH + $num);
  }

  protected function isExecutableExtension($filename): bool
  {
    $notSupportedImages = array(
      'php',
      'js',
      'cgi',
      'exe',
      'doc',
      'xls',
      'sh'
    );
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
    return in_array($ext, $notSupportedImages);
  }

  protected function getNonUtf8Filename($filename, $extension): string
  {
    $filename = mb_strtolower($filename, 'UTF-8');
    $filename = str_replace(
      array(
        'ä',
        'ö',
        'ü',
        'ß',
        ' - ',
        ' + ',
        '_',
        ' / ',
        '/'
      ),
      array(
        'ae',
        'oe',
        'ue',
        'ss',
        '-',
        '-',
        '-',
        '-',
        '-'
      ),
      $filename
    );
    $filename = str_replace(' ', '-', $filename);
    $filename = preg_replace('/[^a-z0-9_-]/iU', '', $filename);
    $filename = trim($filename);
    return $filename . '.' . $extension;
  }
  private function combinePaths($path1, $path2)
  {
    $lastFolder = basename(rtrim($path1, '/'));

    // Check if the lastFolder is found in the second path to combine
    if (strpos($path2, $lastFolder) === false) {
      $combined_path = rtrim($path1, '/') . $path2;
    } else {
      // Use only the remaining part of $path2
      $remainingPath2 = substr($path2, strpos($path2, $lastFolder) + strlen($lastFolder));

      // Combine the paths
      $combined_path = rtrim($path1, '/') . $remainingPath2;
    }

    // Normalize the path by removing any duplicated slashes
    $combined_path = preg_replace('#/+#', '/', $combined_path);

    return $combined_path;
  }

  private function buildUniqueFilename(string $filename, string $destinationPath, array $reservedFilenames): string
  {
    $extension = pathinfo($filename, PATHINFO_EXTENSION);
    $baseName = pathinfo($filename, PATHINFO_FILENAME);
    $candidate = $filename;
    $suffix = 1;

    while (in_array($candidate, $reservedFilenames, true) || $this->fileExistsInDestination($destinationPath, $candidate)) {
      $candidate = $baseName . '-' . $suffix;
      if ($extension !== '') {
        $candidate .= '.' . $extension;
      }
      $suffix++;
    }

    return $candidate;
  }

  private function fileExistsInDestination(string $destinationPath, string $filename): bool
  {
    $absolutePath = $this->combinePaths($this->uploadPath(), $destinationPath) . $filename;
    return file_exists($absolutePath);
  }

  private function getObjectValue($object, string $property, $default = null)
  {
    if (is_object($object) && property_exists($object, $property)) {
      return $object->{$property};
    }

    return $default;
  }

  private function getCharactersAfterSubstring($inputString, $substring)
  {
    // Find the position of the substring in the input string
    $position = strpos($inputString, $substring);

    // Check if the substring is found in the input string
    if ($position !== false) {
      // Return the characters after the substring
      return substr($inputString, $position + strlen($substring));
    }

    // If the substring is not found, return the original string
    return $inputString;
  }
}
