<?php

namespace App\Services\Storage;

use Aws\Exception\AwsException;
use Aws\S3\S3Client;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Storage;

class S3StorageDriver implements FileStorageInterface
{
    private S3Client $client;

    public function __construct(private readonly Storage $config)
    {
        StorageLogger::log('SUCCESS', 'S3 driver initializing', $this->baseContext());

        try {
            $this->assertConfigured();
            $this->client = $this->createClient();
            StorageLogger::log('SUCCESS', 'S3 driver initialized', $this->baseContext());
        } catch (\Throwable $e) {
            StorageLogger::log('FAILURE', 'S3 driver initialization failed', $this->baseContext([
                'error' => $e->getMessage(),
            ]));
            throw $e;
        }
    }

    /**
     * @param array<string, mixed> $extra
     * @return array<string, mixed>
     */
    private function baseContext(array $extra = []): array
    {
        return array_merge([
            'driver' => 's3',
            'bucket' => $this->config->s3Bucket,
            'region' => $this->config->s3Region,
            'prefix' => $this->config->s3Prefix !== '' ? $this->config->s3Prefix : null,
        ], $extra);
    }

    private function assertConfigured(): void
    {
        if ($this->config->s3Bucket === '' || $this->config->s3AccessKey === '' || $this->config->s3SecretKey === '') {
            throw new FileStorageException('S3 storage is not configured. Check AWS_BUCKET, AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY.');
        }
    }

    private function createClient(): S3Client
    {
        return new S3Client([
            'version'     => 'latest',
            'region'      => $this->config->s3Region,
            'credentials' => [
                'key'    => $this->config->s3AccessKey,
                'secret' => $this->config->s3SecretKey,
            ],
        ]);
    }

    private function objectKey(string $key): string
    {
        $key = ltrim(str_replace('\\', '/', $key), '/');

        return $this->config->s3Prefix !== ''
            ? $this->config->s3Prefix . '/' . $key
            : $key;
    }

    private function resolveContentType(UploadedFile|string $source, array $options): string
    {
        if (! empty($options['content_type'])) {
            return (string) $options['content_type'];
        }

        if ($source instanceof UploadedFile) {
            return $source->getClientMimeType() ?: 'application/octet-stream';
        }

        $mimeType = mime_content_type($source);

        return $mimeType !== false ? $mimeType : 'application/octet-stream';
    }

    /**
     * @param array<string, mixed> $params
     */
    private function putObject(array $params, string $key, string $operation): void
    {
        StorageLogger::log('SUCCESS', "{$operation} started", $this->baseContext([
            'key'          => $key,
            's3_key'       => $params['Key'] ?? null,
            'content_type' => $params['ContentType'] ?? null,
            'body_size'    => isset($params['Body']) ? strlen((string) $params['Body']) : null,
        ]));

        try {
            $this->client->putObject($params);
            StorageLogger::log('SUCCESS', "{$operation} complete", $this->baseContext([
                'key'    => $key,
                's3_key' => $params['Key'] ?? null,
            ]));
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', "{$operation} failed", $this->baseContext([
                'key'        => $key,
                's3_key'     => $params['Key'] ?? null,
                'aws_code'   => $e->getAwsErrorCode(),
                'aws_message'=> $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 upload failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }

    public function upload(UploadedFile|string $source, string $key, array $options = []): string
    {
        $params = [
            'Bucket'      => $this->config->s3Bucket,
            'Key'         => $this->objectKey($key),
            'ContentType' => $this->resolveContentType($source, $options),
        ];

        if ($source instanceof UploadedFile) {
            if (! $source->isValid()) {
                StorageLogger::log('FAILURE', 'Upload rejected: invalid uploaded file', $this->baseContext([
                    'key'   => $key,
                    'error' => $source->getErrorString() ?: 'Invalid uploaded file',
                ]));
                throw new FileStorageException($source->getErrorString() ?: 'Invalid uploaded file');
            }

            $params['SourceFile'] = $source->getTempName();
            StorageLogger::log('SUCCESS', 'Upload source resolved from uploaded file', $this->baseContext([
                'key'           => $key,
                'original_name' => $source->getClientName(),
                'client_mime'   => $source->getClientMimeType(),
                'size'          => $source->getSize(),
            ]));
        } else {
            if (! is_file($source)) {
                StorageLogger::log('FAILURE', 'Upload rejected: source file not found', $this->baseContext([
                    'key'    => $key,
                    'source' => $source,
                ]));
                throw new FileStorageException("Source file not found: {$source}");
            }

            $params['SourceFile'] = $source;
            StorageLogger::log('SUCCESS', 'Upload source resolved from filesystem path', $this->baseContext([
                'key'    => $key,
                'source' => $source,
                'size'   => filesize($source) ?: 0,
            ]));
        }

        $this->putObject($params, $key, 'Upload');

        return $key;
    }

    public function put(string $key, string $contents, array $options = []): string
    {
        $this->putObject([
            'Bucket'      => $this->config->s3Bucket,
            'Key'         => $this->objectKey($key),
            'Body'        => $contents,
            'ContentType' => $options['content_type'] ?? 'application/octet-stream',
        ], $key, 'Put');

        return $key;
    }

    public function exists(string $key): bool
    {
        StorageLogger::log('SUCCESS', 'Exists check started', $this->baseContext([
            'key'    => $key,
            's3_key' => $this->objectKey($key),
        ]));

        try {
            $exists = $this->client->doesObjectExist(
                $this->config->s3Bucket,
                $this->objectKey($key)
            );

            StorageLogger::log('SUCCESS', 'Exists check complete', $this->baseContext([
                'key'    => $key,
                's3_key' => $this->objectKey($key),
                'exists' => $exists,
            ]));

            return $exists;
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', 'Exists check failed', $this->baseContext([
                'key'         => $key,
                's3_key'      => $this->objectKey($key),
                'aws_code'    => $e->getAwsErrorCode(),
                'aws_message' => $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 exists check failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }

    public function delete(string $key): bool
    {
        StorageLogger::log('SUCCESS', 'Delete started', $this->baseContext([
            'key'    => $key,
            's3_key' => $this->objectKey($key),
        ]));

        try {
            $this->client->deleteObject([
                'Bucket' => $this->config->s3Bucket,
                'Key'    => $this->objectKey($key),
            ]);

            StorageLogger::log('SUCCESS', 'Delete complete', $this->baseContext([
                'key'    => $key,
                's3_key' => $this->objectKey($key),
            ]));

            return true;
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', 'Delete failed', $this->baseContext([
                'key'         => $key,
                's3_key'      => $this->objectKey($key),
                'aws_code'    => $e->getAwsErrorCode(),
                'aws_message' => $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 delete failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }

    public function read(string $key): string
    {
        StorageLogger::log('SUCCESS', 'Read started', $this->baseContext([
            'key'    => $key,
            's3_key' => $this->objectKey($key),
        ]));

        try {
            $result = $this->client->getObject([
                'Bucket' => $this->config->s3Bucket,
                'Key'    => $this->objectKey($key),
            ]);

            $body = (string) $result['Body'];

            StorageLogger::log('SUCCESS', 'Read complete', $this->baseContext([
                'key'       => $key,
                's3_key'    => $this->objectKey($key),
                'size'      => strlen($body),
                'mime_type' => $result['ContentType'] ?? null,
            ]));

            return $body;
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', 'Read failed', $this->baseContext([
                'key'         => $key,
                's3_key'      => $this->objectKey($key),
                'aws_code'    => $e->getAwsErrorCode(),
                'aws_message' => $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 read failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }

    public function downloadToTemp(string $key): string
    {
        $tempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('s3_', true) . '_' . basename($key);

        StorageLogger::log('SUCCESS', 'Download to temp started', $this->baseContext([
            'key'       => $key,
            's3_key'    => $this->objectKey($key),
            'temp_path' => $tempPath,
        ]));

        try {
            $this->client->getObject([
                'Bucket' => $this->config->s3Bucket,
                'Key'    => $this->objectKey($key),
                'SaveAs' => $tempPath,
            ]);

            StorageLogger::log('SUCCESS', 'Download to temp complete', $this->baseContext([
                'key'       => $key,
                's3_key'    => $this->objectKey($key),
                'temp_path' => $tempPath,
                'size'      => is_file($tempPath) ? (filesize($tempPath) ?: 0) : 0,
            ]));

            return $tempPath;
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', 'Download to temp failed', $this->baseContext([
                'key'         => $key,
                's3_key'      => $this->objectKey($key),
                'temp_path'   => $tempPath,
                'aws_code'    => $e->getAwsErrorCode(),
                'aws_message' => $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 download failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }

    public function streamDownload(string $key, ?string $downloadName = null): ResponseInterface
    {
        StorageLogger::log('SUCCESS', 'Stream download started', $this->baseContext([
            'key'           => $key,
            's3_key'        => $this->objectKey($key),
            'download_name' => $downloadName ?: basename($key),
        ]));

        try {
            $result = $this->client->getObject([
                'Bucket' => $this->config->s3Bucket,
                'Key'    => $this->objectKey($key),
            ]);

            $body = (string) $result['Body'];

            StorageLogger::log('SUCCESS', 'Stream download complete', $this->baseContext([
                'key'           => $key,
                's3_key'        => $this->objectKey($key),
                'download_name' => $downloadName ?: basename($key),
                'size'          => strlen($body),
                'mime_type'     => $result['ContentType'] ?? null,
            ]));

            return service('response')
                ->setHeader('Content-Type', $result['ContentType'] ?? 'application/octet-stream')
                ->setHeader(
                    'Content-Disposition',
                    'attachment; filename="' . ($downloadName ?: basename($key)) . '"'
                )
                ->setBody($body);
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', 'Stream download failed', $this->baseContext([
                'key'         => $key,
                's3_key'      => $this->objectKey($key),
                'aws_code'    => $e->getAwsErrorCode(),
                'aws_message' => $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 download failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }

    public function temporaryUrl(string $key, int $ttlSeconds = 900): string
    {
        StorageLogger::log('SUCCESS', 'Presigned URL generation started', $this->baseContext([
            'key'         => $key,
            's3_key'      => $this->objectKey($key),
            'ttl_seconds' => $ttlSeconds,
        ]));

        try {
            $command = $this->client->getCommand('GetObject', [
                'Bucket' => $this->config->s3Bucket,
                'Key'    => $this->objectKey($key),
            ]);

            $request = $this->client->createPresignedRequest($command, "+{$ttlSeconds} seconds");

            StorageLogger::log('SUCCESS', 'Presigned URL generation complete', $this->baseContext([
                'key'         => $key,
                's3_key'      => $this->objectKey($key),
                'ttl_seconds' => $ttlSeconds,
            ]));

            return (string) $request->getUri();
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', 'Presigned URL generation failed', $this->baseContext([
                'key'         => $key,
                's3_key'      => $this->objectKey($key),
                'ttl_seconds' => $ttlSeconds,
                'aws_code'    => $e->getAwsErrorCode(),
                'aws_message' => $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 presigned URL failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }

    public function url(string $key): string
    {
        $url = sprintf(
            'https://%s.s3.%s.amazonaws.com/%s',
            $this->config->s3Bucket,
            $this->config->s3Region,
            rawurlencode($this->objectKey($key))
        );

        StorageLogger::log('SUCCESS', 'Public URL resolved', $this->baseContext([
            'key'    => $key,
            's3_key' => $this->objectKey($key),
        ]));

        return $url;
    }

    public function copy(string $fromKey, string $toKey): bool
    {
        StorageLogger::log('SUCCESS', 'Copy started', $this->baseContext([
            'from_key'    => $fromKey,
            'to_key'      => $toKey,
            'from_s3_key' => $this->objectKey($fromKey),
            'to_s3_key'   => $this->objectKey($toKey),
        ]));

        try {
            $this->client->copyObject([
                'Bucket'     => $this->config->s3Bucket,
                'CopySource' => $this->config->s3Bucket . '/' . $this->objectKey($fromKey),
                'Key'        => $this->objectKey($toKey),
            ]);

            StorageLogger::log('SUCCESS', 'Copy complete', $this->baseContext([
                'from_key' => $fromKey,
                'to_key'   => $toKey,
            ]));

            return true;
        } catch (AwsException $e) {
            StorageLogger::log('FAILURE', 'Copy failed', $this->baseContext([
                'from_key'    => $fromKey,
                'to_key'      => $toKey,
                'aws_code'    => $e->getAwsErrorCode(),
                'aws_message' => $e->getAwsErrorMessage(),
            ]));
            throw new FileStorageException('S3 copy failed: ' . $e->getAwsErrorMessage(), 0, $e);
        }
    }
}
