<?php
/**
 * @package     MefisAssetBridge
 * @subpackage  Joomla_Administrator_Component
 *
 * @copyright   Copyright (C) 2025 Goran Usljebrka. All rights reserved.
 * @license     Proprietary — No resale or incorporation into other projects without explicit written permission. see LICENSE.txt
 */

namespace Mefi\Component\MefisAssetBridge\Administrator\Controller;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Response\JsonResponse;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use MefisAssetBridgeCore\Auth\AuthenticatedUser;
use MefisAssetBridgeCore\Cache\CoreCache;
use Mefi\Component\MefisAssetBridge\Administrator\Helper\AssetPreviewHelper;
use Mefi\Component\MefisAssetBridge\Administrator\Traits\CoreBridgeTrait;

/**
 * Assets Controller
 *
 * @since  1.0.0
 */
class AssetsController extends BaseController
{
    use CoreBridgeTrait;
    protected $userDto = null;
    protected $MEDIA_CACHE_DIR;
    protected $MEDIA_CACHE_DIR_PREVIEW;
    protected $MEDIA_CACHE_URL;
    protected $MEDIA_CACHE_URL_PREVIEW;
    protected ?CoreCache $coreCache = null;

    /**
     * Constructor.
     *
     * @param   array                $config   An optional associative array of configuration settings.
     * @param   MVCFactoryInterface  $factory  The factory.
     * @param   CMSApplication       $app      The JApplication for the dispatcher
     * @param   Input                $input    Input
     *
     * @since   1.0.0
     */
    public function __construct($config = [], $factory = null, $app = null, $input = null)
    {
        // necessary to populate cache dirs
        $this->initCoreCache();
        $this->coreCache = new CoreCache($this->MEDIA_CACHE_DIR);
        parent::__construct($config, $factory, $app, $input);

        if (! $this->isAppRegistered()) {
            $this->setRedirect(Route::_('index.php?option=com_mefisassetbridge&view=problems', false), Text::_('COM_MEFISASSETBRIDGE_APP_NOT_REGISTERED'), 'error');
            return;
        }

    }

    public function getAsset()
    {

        $app    = $this->app;
        $input  = $app->input;
        $id     = (int) $input->getInt('id', 0);
        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if ($id <= 0) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_ID'), true);
            $app->close();
            return;
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
            $app->close();
            return;
        }

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);

            $assets = $this->coreGetAsset(['id' => $id]);

            // $message = Text::_('COM_MEFISASSETBRIDGE_GET_ASSET_SUCCESS');

            echo new JsonResponse(
                $assets,
                null,
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }
    public function getAssetData()
    {

        $app    = $this->app;
        $input  = $app->input;
        $id     = (int) $input->getInt('id', 0);
        $assetUuid = trim($input->getString('uuid', '')); // Asset UUID for cache
        $modifiedAtZulu = $input->getString('modifiedAt', false);
        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if ($id <= 0) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_ID'), true);
             $app->close();
            return;
        }

        // Asset UUID is required for cache structure
        if (empty($assetUuid)) {
            echo new JsonResponse(null, 'Asset UUID is required', true);
             $app->close();
            return;
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
             $app->close();
            return;
        }

        // try converting zulu to timestamp, if fails, ignore
        $modifiedAtTimestamp = null;
        if ($modifiedAtZulu) {
            try {
                // expected format 2025-11-08T17:48:37.000Z
                $dt = new \DateTime($modifiedAtZulu);
                $dt->setTimezone(new \DateTimeZone('UTC'));
                // should modified timestamp be in local server timezone?
                $modifiedAtTimestamp = $dt->getTimestamp();
            } catch (\Exception $e) {}
        }

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $userUuid      = $this->userDto->getUser()->getUserUuid();

            // Validate userUuid
            if (empty($userUuid)) {
                throw new \RuntimeException('User UUID is not available. Please re-login.');
            }

            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);
            $apiFactory->setCache($this->coreCache);

            $data = $this->coreGetAssetData($id, [
                'userUuid'   => $userUuid,
                'assetUuid'  => $assetUuid,
                'modifiedAt' => $modifiedAtTimestamp,
            ]);

            echo new JsonResponse(
                $data,
                Text::_('COM_MEFISASSETBRIDGE_GET_ASSET_DATA_SUCCESS'),
                false);
                 $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }
    public function getAssets()
    {

        $app   = $this->app;
        $input = $app->input;

        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
             $app->close();
            return;
        }

        $params = [
            'isElement'   => $input->getBool('isElement', false) ? true : false,
            'search'   => trim($input->getString('search', '')),
            'sort'     => trim($input->getString('sort', '')),
            'type'     => trim($input->getString('type', '')),
            'origin'   => $input->getInt('origin', 0),
            'category' => $input->get('category', []),
            'meta'     => $input->getBool('meta', false) ? true : false,
            'page'     => (int) $input->getInt('page', 1),
            'limit'    => (int) $input->getInt('limit', 20),
        ];

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);

            $assets = $this->coreGetAssets($params);

            // $message = Text::_('COM_MEFISASSETBRIDGE_GET_ASSET_SUCCESS');
            // probably decode images later, so keep it
            // if(isset($assets['total']) && isset($assets['decoded']) && $assets['total'] != $assets['decoded'])
            // {
            //     if($assets['decoded'] == 0)
            //     {
            //         $message = Text::sprintf('COM_MEFISASSETBRIDGE_GET_ASSET_NOT_DECODED_AT_ALL', $assets['total']);
            //     }
            //     else
            //     {
            //         $message = Text::sprintf('COM_MEFISASSETBRIDGE_GET_ASSET_NOT_DECODED', ($assets['total']-$assets['decoded']), $assets['total']);
            //     }
            // }
            echo new JsonResponse(
                $assets,
                // $message,
                null,
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }

    public function getAssetThumbnail()
    {

        $app            = $this->app;
        $input          = $app->input;
        $id             = (int) $input->getInt('id', 0);
        $assetUuid      = trim($input->getString('uuid', '')); // Asset UUID for cache
        $modifiedAtZulu = $input->getString('modifiedAt', false);
        $format         = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if ($id <= 0) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_ID'), true);
             $app->close();
            return;
        }

        // Asset UUID is required for cache structure
        if (empty($assetUuid)) {
            echo new JsonResponse(null, 'Asset UUID is required', true);
             $app->close();
            return;
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
             $app->close();
            return;
        }

        // try converting zulu to timestamp, if fails, ignore
        $modifiedAtTimestamp = null;
        if ($modifiedAtZulu) {
            try {
                // expected format 2025-11-08T17:48:37.000Z
                $dt = new \DateTime($modifiedAtZulu);
                $dt->setTimezone(new \DateTimeZone('UTC'));
                // should modified timestamp be in local server timezone?
                $modifiedAtTimestamp = $dt->getTimestamp();
            } catch (\Exception $e) {}
        }

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $userUuid      = $this->userDto->getUser()->getUserUuid();

            // Validate userUuid
            if (empty($userUuid)) {
                throw new \RuntimeException('User UUID is not available. Please re-login.');
            }

            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);
            $apiFactory->setCache($this->coreCache);

            $thumbnail = $this->coreGetAssetThumbnail($id, [
                'userUuid'   => $userUuid,
                'assetUuid'  => $assetUuid,
                'modifiedAt' => $modifiedAtTimestamp,
            ]);

            echo new JsonResponse(
                $thumbnail,
                Text::_('COM_MEFISASSETBRIDGE_GET_ASSET_THUMBNAIL_SUCCESS'),
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }

    public function saveAsset()
    {

        $app   = $this->app;
        $input = $app->input;
        try {
            $rawData   = file_get_contents('php://input');
            $assetData = json_decode($rawData, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \RuntimeException('Invalid asset data format: ' . json_last_error_msg());
            }
            if (! isset($assetData['payload'])) {
                throw new \RuntimeException('Missing payload');
            }
            if (! isset($assetData['payload']['element']) || ! isset($assetData['payload']['id'])) {
                throw new \RuntimeException('Invalid payload');
            }
            if (! isset($assetData['payload']['thumbnail'])) {
                throw new \RuntimeException('Invalid payload');
            }

            $params = $assetData['payload'];

        } catch (\Exception $e) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_DATA'), true);
             $app->close();
            return;
        }

        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
             $app->close();
            return;
        }

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);
            $apiFactory->setCache($this->coreCache);

            $params['userUuid'] = $this->userDto->getUser()->getUserUuid();
            $assets = $this->coreSaveAsset($params);

            $this->deleteUserPreviewCache($params['userUuid']);
            echo new JsonResponse(
                $assets,
                Text::_('COM_MEFISASSETBRIDGE_SAVE_ASSET_SUCCESS'),
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }

    public function changeAssetThumbnail()
    {

        $app   = $this->app;
        $input = $app->input;

        try {
            $rawData   = file_get_contents('php://input');
            $assetData = json_decode($rawData, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \RuntimeException('Invalid asset data format: ' . json_last_error_msg());
            }
            if (! isset($assetData['id'])) {
                throw new \RuntimeException('Missing id');
            }

            if (! isset($assetData['thumbnail'])) {
                throw new \RuntimeException('Invalid payload');
            }

            if (empty($assetData['uuid'])) {
                throw new \RuntimeException('Asset UUID is required');
            }

            $params = [
                'id'        => (int) $assetData['id'],
                'uuid'      => trim($assetData['uuid']),
                'thumbnail' => $assetData['thumbnail'],
            ];

        } catch (\Exception $e) {
            // $this->logger->error('Error in changeAssetThumbnail: ' . $e->getMessage());
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_DATA'), true);
             $app->close();
            return;
        }

        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
             $app->close();
            return;
        }

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);
            $apiFactory->setCache($this->coreCache);

            $params['userUuid']  = $this->userDto->getUser()->getUserUuid();
            $params['assetUuid'] = $params['uuid'];
            $assets = $this->coreChangeAssetThumbnail($params);

            $this->deleteUserPreviewCache($params['userUuid']);
            echo new JsonResponse(
                $assets,
                Text::_('COM_MEFISASSETBRIDGE_SAVE_ASSET_SUCCESS'),
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
            $app->close();
            return;
        }
    }
    public function renameAsset()
    {

        $app    = $this->app;
        $input  = $app->input;
        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        try {
            $rawData = file_get_contents('php://input');
            $i       = json_decode($rawData, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \RuntimeException('Invalid asset data format: ' . json_last_error_msg());
            }
            if (empty($i['name'])) {
                throw new \RuntimeException('Invalid asset name');
            }
            if (empty($i['id']) || intval($i['id']) <= 0) {
                throw new \RuntimeException('Invalid asset id');
            }
            $id   = (int) $i['id'];
            $name = (string) $i['name'];

            // trim $name to 191 chars
            $name = substr($name, 0, 191);

        } catch (\Exception $e) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_DATA'), true);
            $app->close();
            return;
        }

        if (empty($name)) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_NAME'), true);
            $app->close();
            return;
        }

        if ($id <= 0) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_ID'), true);
            $app->close();
            return;
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
            $app->close();
            return;
        }
        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);

            $assets = $this->coreRenameAsset($id, $name);

            echo new JsonResponse(
                $assets,
                null,
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }
    public function changeAssetCategory()
    {

        $app   = $this->app;
        $input = $app->input;
        try {
            $rawData = file_get_contents('php://input');
            $payload = json_decode($rawData, true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \RuntimeException('Invalid asset data format: ' . json_last_error_msg());
            }

            if (! isset($payload['id'])) {
                throw new \RuntimeException('Missing Asset ID');
            }

            if (! isset($payload['category'])) {
                throw new \RuntimeException('Missing Asset Category');
            }

            $params = [
                'id'       => (int) $payload['id'],
                'category' => $payload['category'],
            ];

        } catch (\Exception $e) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_DATA'), true);
             $app->close();
            return;
        }

        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
             $app->close();
            return;
        }

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);

            $assets = $this->coreChangeAssetCategory($params);

            $this->deleteUserPreviewCache($this->userDto->getUser()->getUserUuid());
            echo new JsonResponse(
                $assets,
                Text::_('COM_MEFISASSETBRIDGE_SAVE_ASSET_SUCCESS'),
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }
    public function deleteAsset()
    {

        $app    = $this->app;
        $input  = $app->input;
        $id     = (int) $input->getInt('id', 0);
        $uuid   = trim($input->getString('uuid', ''));
        $format = trim($input->getString('format', 'html'));
        if ($format !== 'json') {
            $app->close();
        }

        if ($id <= 0) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_ID'), true);
             $app->close();
            return;
        }

        if (empty($uuid)) {
            echo new JsonResponse(null, 'Asset UUID is required', true);
             $app->close();
            return;
        }

        if (! Session::checkToken('request')) {
            echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true);
            $app->close();
        }

        // Check if user is authenticated
        if (! $this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
             $app->close();
            return;
        }

        try {

            $apiFactory    = $this->getApiFactory();
            $this->userDto = $this->getDataFromSession();
            $masterKey     = $this->userDto->getMasterKey();
            $apiFactory->setUser(new AuthenticatedUser($this->userDto));
            $apiFactory->setMasterKey($masterKey);
            $apiFactory->setCache($this->coreCache);

            // Delete asset from server
            $assets = $this->coreDeleteAsset($id);

            // Delete cached files (thumb + data)
            $userUuid = $this->userDto->getUser()->getUserUuid();
            if (!empty($userUuid)) {
                $this->coreCache->deleteAsset($userUuid, $uuid);
            }

            echo new JsonResponse(
                $assets,
                null,
                false);
             $app->close();
            return;

        } catch (\Exception $e) {
            echo new JsonResponse(null, $e->getMessage(), true);
             $app->close();
            return;
        }
    }

    /**
     * Delete all cached preview files for a specific user.
     *
     * @param   string  $userUuid  The authenticated user's UUID.
     */
    private function deleteUserPreviewCache(string $userUuid): void
    {
        if (empty($userUuid)) {
            return;
        }

        $userDir = $this->MEDIA_CACHE_DIR_PREVIEW . DIRECTORY_SEPARATOR . $userUuid;
        if (!is_dir($userDir)) {
            return;
        }

        $files = glob($userDir . DIRECTORY_SEPARATOR . '*');
        if ($files) {
            foreach ($files as $file) {
                if (is_file($file)) {
                    unlink($file);
                }
            }
        }

        // Remove the now-empty user subdirectory so no trace remains on disk.
        @rmdir($userDir);
    }

    /**
     * Delete the cached preview file for a specific capture session.
     *
     * Called by the JavaScript layer when the Save Asset modal is closed so
     * that stale preview files are not left on disk indefinitely.
     * Validates the captureId against the session-stored value to prevent one
     * user from clearing another user's files.
     */
    public function clearPreview()
    {
        $app     = $this->app;
        $input   = $app->input;
        $session = Factory::getSession();

        if (!$this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
            $app->close();
            return;
        }

        $captureId = preg_replace('/[^a-f0-9]/', '', trim($input->getString('captureId', '')));

        if (empty($captureId)) {
            echo new JsonResponse(['cleared' => false], null, false);
            $app->close();
            return;
        }

        // Cross-check against the session-issued captureId to prevent arbitrary deletions.
        $sessionCaptureId = (string) $session->get('mefisAssetBridge.previewCaptureId', '');

        // Derive the UUID from the authenticated user — no need to store it
        // separately; it is already in mefisAssetBridge.auth.
        $this->userDto = $this->getDataFromSession();
        $userUuid      = $this->userDto ? (string) $this->userDto->getUser()->getUserUuid() : '';

        if (!hash_equals($sessionCaptureId, $captureId) || empty($userUuid)) {
            echo new JsonResponse(['cleared' => false], null, false);
            $app->close();
            return;
        }

        // Delete ALL files in the user's preview directory — this includes the
        // .json metadata file AND every remote asset (images, etc.) that was
        // downloaded during the setPreview call.  Leaving those files on a
        // shared server would be a security risk.
        $this->deleteUserPreviewCache($userUuid);

        // Clear the session captureId so the preview view cannot be loaded
        // again with this captureId after it has been explicitly invalidated.
        $session->set('mefisAssetBridge.previewCaptureId', null);

        echo new JsonResponse(['cleared' => true], null, false);
        $app->close();
    }

    /**
     * Check whether a specific preview capture is ready to serve.
     * Returns { ready: true/false, captureId: string }.
     */
    public function getPreviewStatus()
    {
        $app   = $this->app;
        $input = $app->input;

        if (!$this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
            $app->close();
            return;
        }

        $captureId = preg_replace('/[^a-f0-9]/', '', trim($input->getString('captureId', '')));

        if (empty($captureId)) {
            echo new JsonResponse(['ready' => false, 'captureId' => null], null, false);
            $app->close();
            return;
        }

        $this->userDto = $this->getDataFromSession();
        $userUuid = $this->userDto->getUser()->getUserUuid();

        if (empty($userUuid)) {
            echo new JsonResponse(['ready' => false, 'captureId' => $captureId], null, false);
            $app->close();
            return;
        }

        $filePath = $this->MEDIA_CACHE_DIR_PREVIEW
            . DIRECTORY_SEPARATOR . $userUuid
            . DIRECTORY_SEPARATOR . $captureId . '.json';

        $ready = file_exists($filePath) && is_readable($filePath);

        echo new JsonResponse(['ready' => $ready, 'captureId' => $captureId], null, false);
        $app->close();
    }

    /**
     * Prepare the html2canvas preview for a given asset.
     *
     * Stores the asset JSON in a per-user, per-capture file so that concurrent
     * saves by the same user cannot overwrite each other's preview data.
     * Returns { success: true, captureId: string } on success.
     */
    public function setPreview()
    {
        $app     = $this->app;
        $input   = $app->input;
        $session = Factory::getSession();

        // Authenticate first — we need the user UUID before touching any files.
        if (!$this->isAuthenticated()) {
            echo new JsonResponse([], 'NOT_AUTHENTICATED', true);
            $app->close();
            return;
        }

        $this->userDto = $this->getDataFromSession();
        $userUuid = $this->userDto->getUser()->getUserUuid();

        if (empty($userUuid)) {
            echo new JsonResponse(null, 'User UUID not available', true);
            $app->close();
            return;
        }

        $masterKey = (string) $this->userDto->getMasterKey();
        if (empty($masterKey)) {
            echo new JsonResponse(null, 'Master key not available. Please log in again.', true);
            $app->close();
            return;
        }

        $this->setEncryptorKey($masterKey);

        // given permission to cache remote files (canDownload=true)
        $canDownload    = $input->getBool('cache', false);
        // should we download remote files? (html2canvas needs local copies of remote images)
        $shouldDownload = (bool) $input->getBool('download', true);

        // Per-user preview directory: preview_cache/{userUuid}/
        $userPreviewDir     = $this->MEDIA_CACHE_DIR_PREVIEW . DIRECTORY_SEPARATOR . $userUuid;
        $userPreviewUrlBase = $this->MEDIA_CACHE_URL_PREVIEW . '/' . $userUuid;

        // Acquire an exclusive per-user advisory lock to prevent concurrent
        // setPreview calls from deleting each other's cache directories mid-flight.
        // The lock file sits next to (not inside) the user dir so it survives
        // deleteUserPreviewCache().
        if (!is_dir($this->MEDIA_CACHE_DIR_PREVIEW)) {
            mkdir($this->MEDIA_CACHE_DIR_PREVIEW, 0755, true);
        }
        $lockFile   = $this->MEDIA_CACHE_DIR_PREVIEW . DIRECTORY_SEPARATOR . $userUuid . '.lock';
        $lockHandle = fopen($lockFile, 'c');
        if ($lockHandle === false || !flock($lockHandle, LOCK_EX | LOCK_NB)) {
            if ($lockHandle !== false) {
                fclose($lockHandle);
            }
            echo new JsonResponse(null, 'A preview capture is already in progress for your account. Please wait for it to complete.', true);
            $app->close();
            return;
        }

        try {
            // Flush stale files for this user before writing new ones.
            $this->deleteUserPreviewCache($userUuid);

            if (!is_dir($userPreviewDir)) {
                mkdir($userPreviewDir, 0755, true);
                @file_put_contents($userPreviewDir . DIRECTORY_SEPARATOR . 'index.php', '<?php http_response_code(403); exit;' . PHP_EOL);
            }

            // Generate a per-capture random token (128-bit) used to:
            //  1. Name the preview data file on disk.
            //  2. Embed in the iframe URL so the view can load the correct file.
            //  3. Echo back in IFRAME_READY so the parent can reject stale signals.
            $captureId = bin2hex(random_bytes(16));

            $rawData   = file_get_contents('php://input');
            $assetData = json_decode($rawData, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \RuntimeException('Invalid asset data format: ' . json_last_error_msg());
            }

            // Simple status: true or false (with message if false)
            $downloadStatus = ['status' => true];

            // IF shouldDownload=1 AND canDownload=0 → ERROR
            if ($shouldDownload && !$canDownload) {
                echo new JsonResponse(null, 'Download requested but caching permission not granted', true);
                $app->close();
                return;
            }

            // IF shouldDownload=1 AND canDownload=1 → Download / rewrite remote asset URLs
            if ($shouldDownload && $canDownload) {
                $analysis = AssetPreviewHelper::analyzeAssetData($assetData);

                if (!$analysis['feasible']) {
                    echo new JsonResponse(null, $analysis['reason'], true);
                    $app->close();
                    return;
                }

                $result = AssetPreviewHelper::processAssetDataWithTimeout(
                    $assetData,
                    $userPreviewDir,
                    $userPreviewUrlBase,
                    20
                );

                if (!$result['success']) {
                    echo new JsonResponse(null, $result['reason'], true);
                    $app->close();
                    return;
                }

                $assetData = $result['data'];
            }

            // Persist asset data to a named, AES-256 encrypted file.
            $previewFile = $userPreviewDir . DIRECTORY_SEPARATOR . $captureId . '.json';
            if (file_put_contents($previewFile, $this->encrypt(json_encode($assetData))) === false) {
                throw new \RuntimeException('Failed to write preview data file');
            }

            // Store the captureId and download status in the session so the view
            // can re-validate which file to serve (prevents URL-guessing by other tabs).
            // The user UUID is NOT stored separately — it is already available in
            // mefisAssetBridge.auth and derived from there wherever needed.
            $session->set('mefisAssetBridge.previewCaptureId', $captureId);
            $session->set('mefisAssetBridge.previewcache',     $canDownload);
            $session->set('mefisAssetBridge.previewdownload',  $shouldDownload);
            $session->set('mefisAssetBridge.previewstatus',    json_encode($downloadStatus));

            // Flush the session to disk before sending the response so the
            // iframe's immediate GET request sees the updated session state.
            $session->close();

            echo new JsonResponse(['success' => true, 'captureId' => $captureId], null, false);
            $app->close();

        } catch (\Exception $e) {
            echo new JsonResponse(null, Text::_('COM_MEFISASSETBRIDGE_INVALID_ASSET_DATA'), true);
            $app->close();
        } finally {
            // Always release the lock — even on early return, timeout, or exception.
            flock($lockHandle, LOCK_UN);
            fclose($lockHandle);
        }
    }

    private function processAssetDataForCaching($data, $cacheDir, $cacheUrl)
    {
        return AssetPreviewHelper::processAssetDataForCaching($data, $cacheDir, $cacheUrl);
    }

    private function cacheRemoteAsset($url, $cacheDir, $cacheUrl)
    {
        return AssetPreviewHelper::cacheRemoteAsset($url, $cacheDir, $cacheUrl);
    }

}
