<?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\Str;
use Illuminate\Support\Facades\File;

final class UploadPipelineService
{
    public function __construct(
        private readonly UploadStorageService $storage,
        private readonly UploadSessionRepository $sessions,
        private readonly UploadValidationService $validator,
        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 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): array
    {
        $session = $this->sessions->getOrFail($sessionId);

        $originalPath = $this->derivatives->storeOriginal($session->tempPath, $hash, $originalFileName);
        $origFilename = basename($originalPath);
        $originalRelative = $this->storage->sectionRelativePath('original', $hash, $origFilename);
        $origMime = File::exists($originalPath) ? File::mimeType($originalPath) : 'application/octet-stream';
        $this->artworkFiles->upsert($artworkId, 'orig', $originalRelative, $origMime, (int) filesize($originalPath));

        $publicAbsolute = $this->derivatives->generatePublicDerivatives($session->tempPath, $hash);
        $publicRelative = [];

        foreach ($publicAbsolute as $variant => $absolutePath) {
            $filename = $hash . '.webp';
            $relativePath = $this->storage->sectionRelativePath($variant, $hash, $filename);
            $this->artworkFiles->upsert($artworkId, $variant, $relativePath, 'image/webp', (int) filesize($absolutePath));
            $publicRelative[$variant] = $relativePath;
        }

        $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;

        $origExt = strtolower(pathinfo($originalPath, PATHINFO_EXTENSION) ?: '');
        $downloadFileName = $origFilename;

        if (is_string($originalFileName) && trim($originalFileName) !== '') {
            $candidate = basename(str_replace('\\', '/', $originalFileName));
            $candidate = preg_replace('/[\x00-\x1F\x7F]/', '', (string) $candidate) ?? '';
            $candidate = trim((string) $candidate);

            if ($candidate !== '') {
                $candidateExt = strtolower((string) pathinfo($candidate, PATHINFO_EXTENSION));
                if ($candidateExt === '' && $origExt !== '') {
                    $candidate .= '.' . $origExt;
                }

                $downloadFileName = $candidate;
            }
        }

        Artwork::query()->whereKey($artworkId)->update([
            'file_name' => $downloadFileName,
            'file_path' => '',
            'file_size' => (int) filesize($originalPath),
            'mime_type' => $origMime,
            'hash' => $hash,
            'file_ext' => $origExt,
            'thumb_ext' => 'webp',
            'width' => max(1, $width),
            'height' => max(1, $height),
        ]);

        $this->sessions->updateStatus($sessionId, UploadSessionStatus::PROCESSED);
        $this->sessions->updateProgress($sessionId, 100);
        $this->audit->log($session->userId, 'upload_processed', $session->ip, [
            'session_id' => $sessionId,
            'hash' => $hash,
            'artwork_id' => $artworkId,
        ]);

        return [
            'orig' => $originalRelative,
            'public' => $publicRelative,
        ];
    }

    public function originalHashExists(string $hash): bool
    {
        return $this->storage->originalHashExists($hash);
    }

    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,
        ]);
    }
}
