<?php

declare(strict_types=1);

namespace App\Services\Uploads;

use App\DTOs\Uploads\UploadSessionData;
use App\DTOs\Uploads\UploadInitResult;
use App\DTOs\Uploads\UploadValidatedFile;
use App\DTOs\Uploads\UploadScanResult;
use App\Models\Artwork;
use App\Repositories\Uploads\ArtworkFileRepository;
use App\Repositories\Uploads\UploadSessionRepository;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

final class UploadPipelineService
{
    public function __construct(
        private readonly UploadStorageService $storage,
        private readonly UploadSessionRepository $sessions,
        private readonly UploadValidationService $validator,
        private readonly UploadArchiveValidationService $archiveValidator,
        private readonly UploadHashService $hasher,
        private readonly UploadScanService $scanner,
        private readonly UploadAuditService $audit,
        private readonly UploadDerivativesService $derivatives,
        private readonly ArtworkFileRepository $artworkFiles,
        private readonly UploadTokenService $tokens
    ) {
    }

    public function initSession(int $userId, string $ip): UploadInitResult
    {
        $dir = $this->storage->ensureSection('tmp');
        $filename = Str::uuid()->toString() . '.upload';
        $tempPath = $dir . DIRECTORY_SEPARATOR . $filename;

        File::put($tempPath, '');

        $sessionId = (string) Str::uuid();
        $session = $this->sessions->create($sessionId, $userId, $tempPath, UploadSessionStatus::INIT, $ip);
        $token = $this->tokens->generate($sessionId, $userId);

        $this->audit->log($userId, 'upload_init', $ip, [
            'session_id' => $sessionId,
        ]);

        return new UploadInitResult($session->id, $token, $session->status);
    }

    public function receiveToTmp(UploadedFile $file, int $userId, string $ip): UploadSessionData
    {
        $stored = $this->storage->storeUploadedFile($file, 'tmp');
        $sessionId = (string) Str::uuid();
        $session = $this->sessions->create($sessionId, $userId, $stored->path, UploadSessionStatus::TMP, $ip);
        $this->sessions->updateProgress($sessionId, 10);

        $this->audit->log($userId, 'upload_received', $ip, [
            'session_id' => $sessionId,
            'size' => $stored->size,
        ]);

        return $session;
    }

    public function validateAndHash(string $sessionId): UploadValidatedFile
    {
        $session = $this->sessions->getOrFail($sessionId);
        $validation = $this->validator->validate($session->tempPath);

        if (! $validation->ok) {
            $this->quarantine($session, $validation->reason);
            return new UploadValidatedFile($validation, null);
        }

        $hash = $this->hasher->hashFile($session->tempPath);
        $this->sessions->updateStatus($sessionId, UploadSessionStatus::VALIDATED);
        $this->sessions->updateProgress($sessionId, 30);
        $this->audit->log($session->userId, 'upload_validated', $session->ip, [
            'session_id' => $sessionId,
            'hash' => $hash,
        ]);

        return new UploadValidatedFile($validation, $hash);
    }

    public function validateAndHashArchive(string $sessionId): UploadValidatedFile
    {
        $session = $this->sessions->getOrFail($sessionId);
        $validation = $this->archiveValidator->validate($session->tempPath);

        if (! $validation->ok) {
            $this->quarantine($session, $validation->reason);
            return new UploadValidatedFile($validation, null);
        }

        $hash = $this->hasher->hashFile($session->tempPath);
        $this->sessions->updateStatus($sessionId, UploadSessionStatus::VALIDATED);
        $this->sessions->updateProgress($sessionId, 30);
        $this->audit->log($session->userId, 'upload_archive_validated', $session->ip, [
            'session_id' => $sessionId,
            'hash' => $hash,
        ]);

        return new UploadValidatedFile($validation, $hash);
    }

    public function scan(string $sessionId): UploadScanResult
    {
        $session = $this->sessions->getOrFail($sessionId);
        $result = $this->scanner->scan($session->tempPath);

        if (! $result->ok) {
            $this->quarantine($session, $result->reason);
            return $result;
        }

        $this->sessions->updateStatus($sessionId, UploadSessionStatus::SCANNED);
        $this->sessions->updateProgress($sessionId, 50);
        $this->audit->log($session->userId, 'upload_scanned', $session->ip, [
            'session_id' => $sessionId,
        ]);

        return $result;
    }

    public function processAndPublish(
        string $sessionId,
        string $hash,
        int $artworkId,
        ?string $originalFileName = null,
        ?string $archiveSessionId = null,
        ?string $archiveHash = null,
        ?string $archiveOriginalFileName = null,
        array $additionalScreenshotSessions = []
    ): array
    {
        $session = $this->sessions->getOrFail($sessionId);

        $archiveSession = null;
        if (is_string($archiveSessionId) && trim($archiveSessionId) !== '') {
            $archiveSession = $this->sessions->getOrFail($archiveSessionId);
        }

        $resolvedAdditionalScreenshots = collect($additionalScreenshotSessions)
            ->filter(fn ($payload) => is_array($payload) && is_string($payload['session_id'] ?? null) && is_string($payload['hash'] ?? null))
            ->values()
            ->map(function (array $payload): array {
                return [
                    'session' => $this->sessions->getOrFail((string) $payload['session_id']),
                    'hash' => trim((string) $payload['hash']),
                    'file_name' => is_string($payload['file_name'] ?? null) ? $payload['file_name'] : null,
                ];
            });

        $localCleanup = [];
        $objectCleanup = [];

        try {
            $imageOriginal = $this->derivatives->storeOriginal($session->tempPath, $hash, $originalFileName);
            $localCleanup[] = $imageOriginal['local_path'];
            $objectCleanup[] = $imageOriginal['object_path'];

            $archiveOriginal = null;
            if ($archiveSession && is_string($archiveHash) && trim($archiveHash) !== '') {
                $archiveOriginal = $this->derivatives->storeDownloadOriginal($archiveSession->tempPath, trim($archiveHash), $archiveOriginalFileName);
                $localCleanup[] = $archiveOriginal['local_path'];
                $objectCleanup[] = $archiveOriginal['object_path'];
            }

            $additionalScreenshotOriginals = [];
            foreach ($resolvedAdditionalScreenshots as $index => $payload) {
                $storedScreenshot = $this->derivatives->storeOriginal(
                    $payload['session']->tempPath,
                    $payload['hash'],
                    $payload['file_name']
                );
                $storedScreenshot['variant'] = $this->screenshotVariantName($index + 1);
                $additionalScreenshotOriginals[] = $storedScreenshot;
                $localCleanup[] = $storedScreenshot['local_path'];
                $objectCleanup[] = $storedScreenshot['object_path'];
            }

            $publicAssets = $this->derivatives->generatePublicDerivatives($session->tempPath, $hash);
            foreach ($publicAssets as $asset) {
                $objectCleanup[] = $asset['path'];
            }

            $dimensions = @getimagesize($session->tempPath);
            $width = is_array($dimensions) && isset($dimensions[0]) ? (int) $dimensions[0] : 1;
            $height = is_array($dimensions) && isset($dimensions[1]) ? (int) $dimensions[1] : 1;

            $primaryOriginal = $archiveOriginal ?? $imageOriginal;
            $downloadFileName = $this->resolveDownloadFileName($primaryOriginal['filename'], $primaryOriginal['ext'], $archiveOriginal ? $archiveOriginalFileName : $originalFileName);

            DB::transaction(function () use ($artworkId, $imageOriginal, $archiveOriginal, $additionalScreenshotOriginals, $publicAssets, $primaryOriginal, $downloadFileName, $hash, $width, $height) {
                $this->artworkFiles->upsert($artworkId, 'orig', $primaryOriginal['object_path'], $primaryOriginal['mime'], $primaryOriginal['size']);
                $this->artworkFiles->upsert($artworkId, 'orig_image', $imageOriginal['object_path'], $imageOriginal['mime'], $imageOriginal['size']);

                if ($archiveOriginal) {
                    $this->artworkFiles->upsert($artworkId, 'orig_archive', $archiveOriginal['object_path'], $archiveOriginal['mime'], $archiveOriginal['size']);
                } else {
                    $this->artworkFiles->deleteVariant($artworkId, 'orig_archive');
                }

                $this->artworkFiles->deleteScreenshotVariants($artworkId);
                foreach ($additionalScreenshotOriginals as $screenshot) {
                    $this->artworkFiles->upsert($artworkId, $screenshot['variant'], $screenshot['object_path'], $screenshot['mime'], $screenshot['size']);
                }

                foreach ($publicAssets as $variant => $asset) {
                    $this->artworkFiles->upsert($artworkId, (string) $variant, $asset['path'], $asset['mime'], $asset['size']);
                }

                Artwork::query()->whereKey($artworkId)->update([
                    'file_name' => $downloadFileName,
                    'file_path' => $primaryOriginal['object_path'],
                    'file_size' => $primaryOriginal['size'],
                    'mime_type' => $primaryOriginal['mime'],
                    'hash' => $hash,
                    'file_ext' => $primaryOriginal['ext'],
                    'thumb_ext' => 'webp',
                    'width' => max(1, $width),
                    'height' => max(1, $height),
                ]);
            });

            $this->storage->deleteLocalFile($session->tempPath);
            if ($archiveSession) {
                $this->storage->deleteLocalFile($archiveSession->tempPath);
            }
            foreach ($resolvedAdditionalScreenshots as $payload) {
                $this->storage->deleteLocalFile($payload['session']->tempPath);
            }

            $this->sessions->updateStatus($sessionId, UploadSessionStatus::PROCESSED);
            $this->sessions->updateProgress($sessionId, 100);
            if ($archiveSession) {
                $this->sessions->updateStatus($archiveSession->id, UploadSessionStatus::PROCESSED);
                $this->sessions->updateProgress($archiveSession->id, 100);
            }
            foreach ($resolvedAdditionalScreenshots as $payload) {
                $this->sessions->updateStatus($payload['session']->id, UploadSessionStatus::PROCESSED);
                $this->sessions->updateProgress($payload['session']->id, 100);
            }
            $this->audit->log($session->userId, 'upload_processed', $session->ip, [
                'session_id' => $sessionId,
                'hash' => $hash,
                'artwork_id' => $artworkId,
                'archive_session_id' => $archiveSession?->id,
                'additional_screenshot_session_ids' => $resolvedAdditionalScreenshots->map(fn (array $payload): string => $payload['session']->id)->values()->all(),
            ]);

            return [
                'orig' => $primaryOriginal['object_path'],
                'orig_image' => $imageOriginal['object_path'],
                'orig_archive' => $archiveOriginal['object_path'] ?? null,
                'screenshots' => array_map(static fn (array $screenshot): array => [
                    'variant' => $screenshot['variant'],
                    'path' => $screenshot['object_path'],
                ], $additionalScreenshotOriginals),
                'public' => array_map(static fn (array $asset): string => $asset['path'], $publicAssets),
            ];
        } catch (\Throwable $e) {
            foreach ($localCleanup as $path) {
                $this->storage->deleteLocalFile($path);
            }

            foreach ($objectCleanup as $path) {
                $this->storage->deleteObject($path);
            }

            throw $e;
        }
    }

    public function originalHashExists(string $hash): bool
    {
        return Artwork::query()
            ->where('hash', $hash)
            ->where('artwork_status', 'published')
            ->whereNotNull('published_at')
            ->exists();
    }

    private function quarantine(UploadSessionData $session, string $reason): void
    {
        $newPath = $this->storage->moveToSection($session->tempPath, 'quarantine');
        $this->sessions->updateTempPath($session->id, $newPath);
        $this->sessions->updateStatus($session->id, UploadSessionStatus::QUARANTINED);
        $this->sessions->updateFailureReason($session->id, $reason);
        $this->sessions->updateProgress($session->id, 0);
        $this->audit->log($session->userId, 'upload_quarantined', $session->ip, [
            'session_id' => $session->id,
            'reason' => $reason,
        ]);
    }

    private function resolveDownloadFileName(string $storedFilename, string $ext, ?string $preferredFileName): string
    {
        $name = is_string($preferredFileName) && trim($preferredFileName) !== ''
            ? basename(str_replace('\\', '/', $preferredFileName))
            : $storedFilename;

        $name = preg_replace('/[\x00-\x1F\x7F]/', '', (string) $name) ?? '';
        $name = preg_replace('/\s+/', ' ', $name) ?? '';
        $name = trim((string) $name, ". \t\n\r\0\x0B");

        $baseName = trim((string) pathinfo($name, PATHINFO_FILENAME), ". \t\n\r\0\x0B");
        if ($baseName === '') {
            $baseName = 'artwork';
        }

        $normalizedExt = strtolower(ltrim(trim($ext), '.'));

        return $normalizedExt !== ''
            ? $baseName . '.' . $normalizedExt
            : $baseName;
    }

    private function screenshotVariantName(int $position): string
    {
        return sprintf('shot%02d', max(1, min(99, $position)));
    }
}
