<?php

declare(strict_types=1);

namespace apexl\Io\modules\mirrorS3\Service;

use apexl\Io\includes\HookManager;
use apexl\Io\modules\mirror\Dto\UploadedObjectDto;
use apexl\Io\modules\mirror\Exception\UploaderException;
use apexl\Io\modules\mirror\Interface\Uploadable;
use apexl\Io\modules\mirror\Interface\UploaderServiceInterface;
use apexl\Io\modules\mirrorS3\Dto\S3ConfigDto;
use Aws\Exception\AwsException;
use Aws\S3\Exception\S3Exception;
use Aws\S3\ObjectUploader;
use Aws\S3\S3Client;
use Exception;
use Throwable;

final class S3UploaderService implements UploaderServiceInterface
{
    private S3Client $client;
    private string $bucket;

    public function __construct(S3Client $client, S3ConfigDto $s3Config, private readonly HookManager $hookManager)
    {
        $this->bucket = $s3Config->bucket;
        $this->client = $client;
    }

    public function uploadAll(array $toUpload): void
    {
        if (!empty($toUpload)) {
            foreach ($toUpload as $uploadEntity) {
                $this->upload($uploadEntity);
            }
        }
    }

    /**
     * @throws AwsException
     */
    private function ensureBucket(): void
    {
        if (!$this->bucketExists()) {
            $this->createBucket();
        }
    }

    /**
     * @throws AwsException
     */
    private function bucketExists(): bool
    {
        $buckets = $this->client->listBuckets();
        if (!empty($buckets['Buckets'])) {
            foreach ($buckets['Buckets'] as $bucket) {
                if ($bucket['Name'] == $this->bucket) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @throws AwsException
     */
    private function createBucket(): void
    {
        $this->client->createBucket([
            'ACL' => 'public-read',
            'Bucket' => $this->bucket,
        ]);
    }

    public function upload(Uploadable $uploadable): void
    {
        $uploadable->lockUpload();

        $source = $this->getFileContents($uploadable);

        if (!$source) {
            return;
        }

        try {
            $this->ensureBucket();
        } catch (AwsException $e) {
            if ($e->getAwsErrorCode() !== 'BucketAlreadyExists') {
                logger('s3')->error('Error ensuring bucket: {message}', [
                        'message' => $e->getMessage(),
                        'exception' => $e,
                    ]
                );

                return;
            }
        }

        $params = $this->hookManager->processHook(
            self::HOOK__GET_UPLOAD_PARAMS,
            ['ContentType' => $uploadable->getMimeType()],
            $uploadable,
            $this,
        );

        $uploader = new ObjectUploader(
            $this->client,
            $this->bucket,
            $uploadable->getKey(),
            $source,
            'public-read',
            ['params' => $params]
        );
        try {
            $result = $uploader->upload();
            if ($result["@metadata"]["statusCode"] === 200) {
                $uploadable->markUploaded();
            }
        } catch (Exception $e) {
            logger('s3')->error('Error uploading {file}: {error}', [
                'file' => $uploadable->getKey(),
                'error' => $e->getMessage(),
                'exception' => $e,
            ]);
        } finally {
            $uploadable->unlockUpload();
            fclose($source);
        }
    }

    private function getFileContents(Uploadable $uploadEntity)
    {
        try {
            return fopen($uploadEntity->getPath(), 'rb');
        } catch (Throwable $e) {
            //strip locks
            $uploadEntity->unlockUpload();
            logger('s3')->error('Error fetching source for {file}: {error}', [
                'file' => $uploadEntity->getPath(),
                'error' => $e->getMessage(),
                'exception' => $e,
            ]);

            return null;
        }
    }

    public function isUploaded(string $key): bool
    {
        return $this->client->doesObjectExist($this->bucket, $key);
    }

    /**
     * @throws UploaderException
     */
    public function getUploaded(string $key): UploadedObjectDto
    {
        try {
            $object = $this->client->getObject([
                'Bucket' => $this->bucket,
                'Key' => $key,
            ]);

            return new UploadedObjectDto(
                mime: $object['ContentType'],
                stream: $object['Body'],
                length: $object['ContentLength'],
            );
        } catch (S3Exception $e) {
            throw new UploaderException(
                $e->getAwsErrorCode(),
                $e->getAwsErrorMessage(),
                $e->getMessage(),
                $e->getCode(),
            );
        }

    }
}
