<?php

declare(strict_types=1);

/**
 * APO-194 Card 4 — Files action service.
 *
 * Wraps MyronFileRepository so BOTH the admin form path (multipart $_FILES)
 * AND the new API path (JSON-content base64 envelope) produce identical
 * file rows + identical bytes-on-disk. The service accepts either an
 * already-decoded MyronDecodedContent + metadata (API path) OR a $_FILES
 * array + $_POST metadata (admin path) — they funnel into the same
 * repository call.
 */
final class MyronFilesActionService
{
    public function __construct(
        private readonly MyronFileRepository $repository,
        private readonly MyronAiIntegrationRepository $aiIntegrations,
        private readonly MyronAuthRepository $authRepository,
    ) {
    }

    /**
     * Upload a file from a multipart-style $_FILES array.
     *
     * @param array<string, mixed> $uploadedFile  $_FILES shape
     * @param array<string, mixed> $metadata      $_POST shape (alt_text, description, caption, …)
     * @return array<string, mixed>               the created file row
     */
    public function uploadFromMultipart(array $uploadedFile, array $metadata, MyronActionCallerContext $caller, bool $metadataRequired = false): array
    {
        return (array) $this->repository->uploadFile(
            $uploadedFile,
            $this->callerUserId($caller),
            $metadata,
            $metadataRequired
        );
    }

    /**
     * Upload a file from a JSON-content envelope.
     *
     * @param array<string, mixed> $envelope canonical envelope shape
     * @param array<string, mixed> $metadata operator-supplied metadata (alt_text, description, caption, …)
     * @return array<string, mixed>          the created file row
     */
    public function uploadFromEnvelope(
        array $envelope,
        array $metadata,
        MyronActionCallerContext $caller,
        ?MyronJsonContentEnvelope $decoder = null,
        bool $metadataRequired = false
    ): array {
        $decoder ??= new MyronJsonContentEnvelope($this->defaultTempDir());
        $decoded  = $decoder->decode($envelope);
        try {
            return (array) $this->repository->uploadFile(
                $decoded->asUploadedFileArray(),
                $this->callerUserId($caller),
                $metadata,
                $metadataRequired
            );
        } finally {
            $decoded->cleanup();
        }
    }

    /**
     * @param array<string, mixed> $metadata
     * @return array{fileId: string}
     */
    public function updateMetadata(string $fileId, array $metadata, MyronActionCallerContext $caller): array
    {
        $fileId = trim($fileId);
        if ($fileId === '') {
            throw new InvalidArgumentException('fileId is required.');
        }
        $this->repository->updateMetadata($fileId, $metadata);
        return ['fileId' => $fileId];
    }

    /**
     * @param array<string, mixed> $uploadedFile
     * @param array<string, mixed> $metadata
     * @return array<string, mixed>
     */
    public function replaceFromMultipart(string $fileId, array $uploadedFile, array $metadata, MyronActionCallerContext $caller): array
    {
        $fileId = trim($fileId);
        if ($fileId === '') {
            throw new InvalidArgumentException('fileId is required.');
        }

        return (array) $this->repository->replaceFilePayload($fileId, $uploadedFile, $metadata);
    }

    /**
     * @param array<string, mixed> $envelope
     * @param array<string, mixed> $metadata
     * @return array<string, mixed>
     */
    public function replaceFromEnvelope(
        string $fileId,
        array $envelope,
        array $metadata,
        MyronActionCallerContext $caller,
        ?MyronJsonContentEnvelope $decoder = null
    ): array {
        $fileId = trim($fileId);
        if ($fileId === '') {
            throw new InvalidArgumentException('fileId is required.');
        }

        $decoder ??= new MyronJsonContentEnvelope($this->defaultTempDir());
        $decoded = $decoder->decode($envelope);
        try {
            return (array) $this->repository->replaceFilePayload(
                $fileId,
                $decoded->asUploadedFileArray(),
                $metadata
            );
        } finally {
            $decoded->cleanup();
        }
    }

    /**
     * @return array{fileId: string, deleted: bool}
     */
    public function delete(string $fileId, MyronActionCallerContext $caller): array
    {
        $fileId = trim($fileId);
        if ($fileId === '') {
            throw new InvalidArgumentException('fileId is required.');
        }
        $this->repository->deleteFile($fileId);
        return ['fileId' => $fileId, 'deleted' => true];
    }

    private function defaultTempDir(): string
    {
        return dirname(__DIR__, 2) . '/tmp/uploads';
    }

    private function callerUserId(MyronActionCallerContext $caller): string
    {
        if ($caller->callerType === 'human-session') {
            return $this->requireUploadCapableUserId($caller->callerId, 'authenticated user');
        }

        if ($caller->callerType === 'ai-bearer') {
            if ($caller->callerId === 'dev-token') {
                throw new RuntimeException('File uploads require an AI integration with operator attribution.');
            }

            $integration = $this->aiIntegrations->findById($caller->callerId);
            if ($integration === null || $integration->isRevoked()) {
                throw new RuntimeException('File uploads require an active AI integration.');
            }

            return $this->requireUploadCapableUserId($integration->createdByUserId, 'AI integration creator');
        }

        throw new RuntimeException('File uploads require a caller with uploader attribution.');
    }

    private function requireUploadCapableUserId(string $userId, string $label): string
    {
        $userId = trim($userId);
        if ($userId === '') {
            throw new RuntimeException($label . ' is not available for file upload attribution.');
        }

        $user = $this->authRepository->findUserById($userId);
        if (
            !is_array($user)
            || ($user['is_active'] ?? false) !== true
            || (!myronUserCanAccessContent($user) && !myronUserCanAccessAdministration($user))
        ) {
            throw new RuntimeException($label . ' is not an active content or administration user.');
        }

        return $userId;
    }
}
