<?php

declare(strict_types=1);

namespace PhpWA\Client;

use InvalidArgumentException;
use PhpWA\AppState\AppStateSyncClient;
use PhpWA\AppState\AppStateSyncResult;
use PhpWA\Media\MediaDownloader;
use PhpWA\Media\MediaMessageFactory;
use PhpWA\Media\MediaType;
use PhpWA\Media\MediaUploader;
use PhpWA\Protocol\Jid\Jid;
use PhpWA\Protocol\Proto\Message;
use PhpWA\Runtime\Deadline;
use PhpWA\Storage\PrivateFileArea;

/** Public operations available only while the linked-device session is online. */
final class OnlineClient
{
    /** @var string */ private $accountId;
    /** @var DirectMessageSender */ private $direct;
    /** @var GroupMessageSender */ private $groups;
    /** @var ReadReceiptSender */ private $receipts;
    /** @var MediaUploader */ private $uploader;
    /** @var MediaDownloader */ private $downloader;
    /** @var MediaMessageFactory */ private $messages;
    /** @var HistorySyncWorker */ private $history;
    /** @var AppStateSyncClient */ private $appState;
    /** @var PrivateFileArea */ private $files;
    /** @var ProfilePictureDirectory */ private $profilePictures;

    public function __construct(
        string $accountId,
        DirectMessageSender $direct,
        GroupMessageSender $groups,
        ReadReceiptSender $receipts,
        MediaUploader $uploader,
        MediaDownloader $downloader,
        MediaMessageFactory $messages,
        HistorySyncWorker $history,
        AppStateSyncClient $appState,
        string $mediaWorkingDirectory,
        ProfilePictureDirectory $profilePictures
    ) {
        if ($accountId === '') { throw new InvalidArgumentException('Online client account ID is empty.'); }
        $this->accountId = $accountId; $this->direct = $direct; $this->groups = $groups; $this->receipts = $receipts;
        $this->uploader = $uploader; $this->downloader = $downloader; $this->messages = $messages;
        $this->history = $history; $this->appState = $appState; $this->files = new PrivateFileArea($mediaWorkingDirectory);
        $this->profilePictures = $profilePictures;
    }

    public function sendText(string $to, string $text, Deadline $deadline): OutgoingMessageResult
    {
        return $this->isGroup($to) ? $this->groups->sendText($to, $text, $deadline) : $this->direct->sendText($to, $text, $deadline);
    }

    public function sendMessage(string $to, Message $message, Deadline $deadline): OutgoingMessageResult
    {
        return $this->isGroup($to) ? $this->groups->send($to, $message, $deadline) : $this->direct->send($to, $message, $deadline);
    }

    public function sendImage(string $to, string $path, string $mimetype, ?string $caption, Deadline $deadline): OutgoingMessageResult
    {
        return $this->uploadAndSend($to, $path, MediaType::IMAGE, $mimetype, $caption, null, false, $deadline);
    }

    public function sendVideo(string $to, string $path, string $mimetype, ?string $caption, Deadline $deadline): OutgoingMessageResult
    {
        return $this->uploadAndSend($to, $path, MediaType::VIDEO, $mimetype, $caption, null, false, $deadline);
    }

    public function sendAudio(string $to, string $path, string $mimetype, bool $voiceNote, Deadline $deadline): OutgoingMessageResult
    {
        return $this->uploadAndSend($to, $path, MediaType::AUDIO, $mimetype, null, null, $voiceNote, $deadline);
    }

    public function sendDocument(string $to, string $path, string $mimetype, string $fileName, ?string $caption, Deadline $deadline): OutgoingMessageResult
    {
        return $this->uploadAndSend($to, $path, MediaType::DOCUMENT, $mimetype, $caption, $fileName, false, $deadline);
    }

    public function sendSticker(string $to, string $path, string $mimetype, Deadline $deadline): OutgoingMessageResult
    {
        return $this->uploadAndSend($to, $path, MediaType::STICKER, $mimetype, null, null, false, $deadline);
    }

    /** @param list<string> $messageIds */
    public function markRead(array $messageIds, string $chatJid, ?string $participantJid, int $timestamp, Deadline $deadline): void
    {
        $this->receipts->markRead($messageIds, $chatJid, $participantJid, $timestamp, $deadline);
    }

    /** @param list<string> $messageIds */
    public function markPlayed(array $messageIds, string $chatJid, ?string $participantJid, int $timestamp, Deadline $deadline): void
    {
        $this->receipts->markPlayed($messageIds, $chatJid, $participantJid, $timestamp, $deadline);
    }

    public function runHistorySync(Deadline $deadline, int $maximumEvents = 50): HistorySyncRunResult
    {
        return $this->history->run($deadline, $maximumEvents);
    }

    public function syncAppState(string $collection, Deadline $deadline, bool $forceSnapshot = false): AppStateSyncResult
    {
        return $this->appState->sync($collection, $deadline, $forceSnapshot);
    }

    public function profilePictureUrl(string $jid, Deadline $deadline, bool $preview = true): ?string
    {
        return $this->profilePictures->url($jid, $deadline, $preview);
    }

    public function downloadMedia(
        string $directPath, string $type, string $mediaKey, string $fileSha256,
        string $fileEncSha256, int $fileLength, string $outputPath, Deadline $deadline
    ): void {
        $this->downloader->download($directPath, $type, $mediaKey, $fileSha256, $fileEncSha256, $fileLength, $outputPath, $deadline);
    }

    private function uploadAndSend(
        string $to, string $path, string $type, string $mimetype, ?string $caption,
        ?string $fileName, bool $voiceNote, Deadline $deadline
    ): OutgoingMessageResult {
        $encryptedPath = $this->files->accountPath('upload', $this->accountId . ':' . bin2hex(random_bytes(16)), '.bin');
        try {
            $upload = $this->uploader->uploadFile($path, $encryptedPath, $type, $deadline);
            // Do not send a stanza while a sensitive upload artefact is still
            // left behind locally. A cleanup failure is therefore fail-closed:
            // the remote upload is harmlessly orphaned and no chat message is
            // emitted with ambiguous local state.
            $this->files->delete($encryptedPath);
            $encryptedPath = '';
            if ($type === MediaType::IMAGE) { $message = $this->messages->image($upload, $mimetype, $caption); }
            elseif ($type === MediaType::VIDEO) { $message = $this->messages->video($upload, $mimetype, $caption); }
            elseif ($type === MediaType::AUDIO) { $message = $this->messages->audio($upload, $mimetype, $voiceNote); }
            elseif ($type === MediaType::DOCUMENT) { $message = $this->messages->document($upload, $mimetype, (string) $fileName, $caption); }
            elseif ($type === MediaType::STICKER) { $message = $this->messages->sticker($upload, $mimetype); }
            else { throw new InvalidArgumentException('Unsupported outgoing media type.'); }
            return $this->sendMessage($to, $message, $deadline);
        } finally {
            if ($encryptedPath !== '') {
                try { $this->files->delete($encryptedPath); } catch (\Throwable $ignored) {}
            }
        }
    }

    private function isGroup(string $to): bool
    {
        $jid = Jid::parse($to);
        if (!$jid instanceof Jid) { throw new InvalidArgumentException('Outgoing chat JID is invalid.'); }
        return $jid->isGroup();
    }
}
