<?php
/**
 * @package     MefisAssetBridge
 * @subpackage  MefisAssetBridgeCore_Api_Endpoints
 *
 * @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 MefisAssetBridgeCore\Api\Endpoints\Asset;

use MefisAssetBridgeCore\Api\ApiClient;
use MefisAssetBridgeCore\Api\Storage\S3Storage;
use MefisAssetBridgeCore\Cache\CoreCache;
use MefisAssetBridgeCore\DTO\ThumbnailDto;
use MefisAssetBridgeCore\Traits\Encryptor;
use MefisAssetBridgeCore\Auth\AuthenticatedUser;
use MefisAssetBridgeCore\Api\Response\ApiResponse;
use MefisAssetBridgeCore\Interfaces\EndpointInterface;
use MefisAssetBridgeCore\Log;

/**
 * SaveAsset API Endpoint
 */
class SaveAsset implements EndpointInterface
{
    use Encryptor;

    /** @var ApiClient API client instance */
    private ApiClient $ApiClient;

    /** @var AuthenticatedUser|null Authenticated user */
    private ?AuthenticatedUser $user;

    private string $uuid;
    private string $pathHash;
    private string $_masterKey;
    private bool $verifySSL;
    private ?CoreCache $cache;
    /** @var array Required parameters */
    private array $requiredParams = [
        'yootheme_id',
        'name',
        'asset_type',
        'data',
        'version',
        'domain',
        'cms_type',
        'cms_version',
        'modified',
        'category',
        'thumbnail'
    ];

    private string $validationError = '';

    public function __construct(string $uuid, ApiClient $ApiClient, ?AuthenticatedUser $user, string $pathHash = '', string $masterKey = '', bool $verifySSL = true, ?CoreCache $cache = null)
    {
        $this->ApiClient = $ApiClient;
        $this->user      = $user;
        $this->uuid      = $uuid;
        $this->pathHash  = $pathHash;
        $this->verifySSL = $verifySSL;
        $this->cache     = $cache;

        if(empty($masterKey)) {
            throw new \RuntimeException('Master key is required to save asset');
        }
        $this->_masterKey = $masterKey;
        $this->setEncryptorKey($masterKey);
    }

    /**
     * Validate request parameters
     *
     * @param array $params Request parameters
     * @return bool True if parameters are valid
     */
    public function validateParams(array $params): bool
    {

        foreach ($this->requiredParams as $param) {
            if (! isset($params[$param])) {
                $this->validationError = sprintf('Missing required parameter: %s', $param);
                return false;
            }
        }

        // Check if data is valid
        if (! is_array($params['data']) && ! is_object($params['data'])) {
            $this->validationError = 'Layout data must be an array or object';
            return false;
        }

        return true;
    }

    /**
     * Send request to save a asset
     *
     * @param array $params Request parameters
     * @return ApiResponse
     */
    public function sendRequest(array $params): ApiResponse
    {

        // Validate required parameters
        if (! $this->validateParams($params)) {
            return ApiResponse::error($this->validationError);
        }

        unset($params['data']['name'], $params['data']['modified'], $params['data']['version']);
        $this->ApiClient->setUser($this->user);

        // Step 1: JSON encode the data
        $jsonEncodedData = json_encode($params['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        // Step 2: Compress if available
        $isCompressed = false;
        if (function_exists('gzencode')) {
            $compressedData = gzencode($jsonEncodedData, 9);
            if ($compressedData !== false) {
                $jsonEncodedData = $compressedData;
                $isCompressed = true;
            }
        }

        // Step 3: Encrypt (returns base64)
        $encryptedData = $this->encrypt($jsonEncodedData);
        if ($encryptedData === null) {
            return ApiResponse::error('Failed to encrypt asset data');
        }

        // Step 4: Convert to binary
        $binary = base64_decode($encryptedData, true);
        if ($binary === false) {
            return ApiResponse::error('Encryption produced invalid base64 output');
        }

        // Step 5: Create temporary file with appropriate extension
        $fileExtension = $isCompressed ? '.gz.enc' : '.enc';
        $tmpDataFile = tempnam(sys_get_temp_dir(), 'mefis_data_') . $fileExtension;
        if (file_put_contents($tmpDataFile, $binary) === false) {
            return ApiResponse::error('Failed to create temporary data file');
        }

        $thumbnail = new ThumbnailDto($params['thumbnail'] ?? [], $this->_masterKey);

        if (!$thumbnail->isValid()) {
            return ApiResponse::error('Invalid thumbnail');
        }
        // if over 500kb return error
        if($thumbnail->isTooBig(500 * 1024)) {
            return ApiResponse::error('Thumbnail size exceeds the maximum allowed size of 500KB');
        }
        $requestData = [
            'id'                => $params['id'] ?? null, // Optional ID for existing assets
            'yootheme_id'       => $params['yootheme_id'],
            'name'              => $params['name'],
            'version'           => $params['version'],
            'domain'            => $params['domain'],
            'path'              => $this->pathHash,
            'asset_type'        => $params['asset_type'],
            'cms_type'          => $params['cms_type'],
            'cms_version'       => $params['cms_version'],
            'modified_yootheme' => $params['modified'],
            'is_compressed'     => $isCompressed ? 1 : 0, // Flag to indicate compression
        ];

        // handle saving category
        if (!empty($params['category'])) {
            // if id exist this is existing category
            if (!empty($params['category']['id'])) {
                $requestData['category_id'] = intval($params['category']['id']);

            } else {
                // no id but we have value
                if (!empty($params['category']['value'])) {
                    $requestData['category_name'] = trim($params['category']['value']);
                }
            }
        }

        if(empty($requestData['category_id']) && empty($requestData['category_name'])) {
            throw new \RuntimeException('Category is missing or invalid.');
        }


        // Step 6: Send the request to create asset and get presigned POST URLs
        $response = $this->ApiClient->post('assets', $requestData);

        // Check if asset creation was successful
        if (!$response->isSuccess()) {
            // Cleanup temporary files
            if (file_exists($tmpDataFile)) {
                @unlink($tmpDataFile);
            }
            return $response;
        }

        $responseData = $response->getData();

        // Validate response contains upload credentials
        if (!isset($responseData['upload']['thumbnail']) || !isset($responseData['upload']['data'])) {
            // Cleanup temporary files
            if (file_exists($tmpDataFile)) {
                @unlink($tmpDataFile);
            }
            return ApiResponse::error('Server did not provide upload credentials');
        }

        $assetId = $responseData['assets'][0]['asset_id'] ?? null;
        if (!$assetId) {
            // Cleanup temporary files
            if (file_exists($tmpDataFile)) {
                @unlink($tmpDataFile);
            }
            return ApiResponse::error('Server did not provide asset ID');
        }

        // Step 7: Upload files to S3 using presigned POST
        $s3Storage = new \MefisAssetBridgeCore\Api\Storage\S3Storage($this->verifySSL);

        // Get thumbnail binary content
        $thumbnailBinary = file_get_contents($thumbnail->getTemporaryFilePath());
        if ($thumbnailBinary === false) {
            return ApiResponse::error('Failed to read thumbnail file');
        }

        // Upload thumbnail
        $thumbnailUpload = $s3Storage->upload(
            $responseData['upload']['thumbnail']['url'],
            $responseData['upload']['thumbnail']['fields'],
            $thumbnailBinary
        );

        // Log thumbnail upload result
        if (!$thumbnailUpload->isSuccess()) {
            Log::getInstance()->error(
                'S3 thumbnail upload failed: ' . $thumbnailUpload->getMessage(),
                's3-upload'
            );
        } else {
            Log::getInstance()->debug(
                'S3 thumbnail uploaded successfully',
                's3-upload'
            );
        }

        // Upload data file
        $dataUpload = $s3Storage->upload(
            $responseData['upload']['data']['url'],
            $responseData['upload']['data']['fields'],
            $binary
        );

        // Log data upload result
        if (!$dataUpload->isSuccess()) {
            Log::getInstance()->error(
                'S3 data upload failed: ' . $dataUpload->getMessage(),
                's3-upload'
            );
        } else {
            Log::getInstance()->debug(
                'S3 data uploaded successfully',
                's3-upload'
            );
        }

        // Cleanup temporary data file
        if (file_exists($tmpDataFile)) {
            @unlink($tmpDataFile);
        }

        // Step 8: Notify server about upload results, including ETag for integrity verification
        $uploadResults = [
            'files' => [
                [
                    'type'   => 'thumb',
                    'status' => $thumbnailUpload->isSuccess() ? 1 : 0,
                    'etag'   => $thumbnailUpload->isSuccess() ? ($thumbnailUpload->getData()['eTag'] ?? null) : null,
                ],
                [
                    'type'   => 'data',
                    'status' => $dataUpload->isSuccess() ? 1 : 0,
                    'etag'   => $dataUpload->isSuccess() ? ($dataUpload->getData()['eTag'] ?? null) : null,
                ],
            ],
        ];

        // Step 9: Send storage status to server
        $storageResponse = $this->ApiClient->patch(
            'assets/' . $assetId . '/storage',
            $uploadResults
        );

        // Remove upload credentials before returning
        unset($responseData['upload']);

        // Cache encrypted binaries for the saved asset.
        // asset_uuid must be present in the server response; silently skipped when absent.
        $savedUuid = $responseData['assets'][0]['uuid'] ?? null;
        $userUuid  = $params['userUuid'] ?? '';
        if ($this->cache !== null && $savedUuid !== null && $userUuid !== '') {
            if ($dataUpload->isSuccess()) {
                $this->cache->set($userUuid, $binary, $savedUuid . '.data');
            }
            if ($thumbnailUpload->isSuccess()) {
                $this->cache->set($userUuid, $thumbnailBinary, $savedUuid . '.thumb');
            }
        }

        // If both uploads succeeded and storage confirmation succeeded, return success
        if ($thumbnailUpload->isSuccess() && $dataUpload->isSuccess() && $storageResponse->isSuccess()) {
            $responseData['assets'][0]['thumbnail'] = true;

            return ApiResponse::success(
                $responseData,
                'Asset saved successfully'
            );
        }

        // If uploads failed, return error
        if (!$thumbnailUpload->isSuccess() && !$dataUpload->isSuccess()) {
            return ApiResponse::error('Failed to upload both thumbnail and data to S3');
        }

        // If only one upload failed
        if (!$thumbnailUpload->isSuccess()) {
            return ApiResponse::error('Failed to upload thumbnail to S3');
        }

        if (!$dataUpload->isSuccess()) {
            $responseData['assets'][0]['thumbnail'] = true;
            return ApiResponse::error('Failed to upload data to S3');
        }

        // If storage confirmation failed but uploads succeeded
        return ApiResponse::error('Files uploaded but failed to confirm with server');
    }

    public function setMasterKey(string $masterKey)
    {
        $this->setEncryptorKey($masterKey);
    }
}
