<?php

namespace Shm\ShmChat;

use Shm\ShmChat\Exceptions\ChatException;
use Shm\ShmChat\SchemaChat\ShmChannelMessages;
use Shm\ShmChat\SchemaChat\ShmChannelMessagesField;
use Shm\ShmChat\SchemaChat\ShmChannels;
use Shm\ShmChat\SchemaChat\ShmChannelsField;
use Shm\ShmDB\mDB;
use Shm\ShmUtils\ShmDoctor\Utils\StructureHelper;

/**
 * Внутренний движок чатов. Публичный API — {@see ShmChat} и {@see Channel}.
 */
class ShmChatService
{
    public static function getChannel(string $channelId): ?object
    {
        return ShmChannels::structure()->findOne([
            ShmChannelsField::_ID => mDB::id($channelId),
        ]) ?: null;
    }

    public static function requireChannel(string $channelId): object
    {
        $channel = self::getChannel($channelId);

        if (!$channel) {
            throw new ChatException('channel_not_found');
        }

        return $channel;
    }

    /**
     * @param list<Member> $members
     * @param list<Link> $links
     * @param list<Member> $invites
     * @param array<string, mixed> $payload
     */
    public static function createChannel(
        array $members,
        array $links = [],
        string $name = '',
        string $space = '',
        array $invites = [],
        array $payload = [],
        bool $closed = false,
    ): object {
        $memberDocs = [];
        $joinableMembers = [];
        $unreadCounts = [];

        foreach ($members as $member) {
            $memberDocs[] = $member->toMemberDocument();
            $unreadCounts[$member->ownerKey()] = 0;
        }

        foreach ($invites as $member) {
            $joinableMembers[] = $member->toMemberDocument();
        }

        $insert = ShmChannels::structure()->insertOne([
            ShmChannelsField::SPACE => $space,
            ShmChannelsField::NAME => $name,
            ShmChannelsField::PAYLOAD => $payload,
            ShmChannelsField::REFERENCE => array_map(
                static fn(Link $link) => $link->toDocument(),
                $links,
            ),
            ShmChannelsField::MEMBERS => $memberDocs,
            ShmChannelsField::JOINABLE_MEMBERS => $joinableMembers,
            ShmChannelsField::CLOSED => $closed,
            ShmChannelsField::UNREAD_COUNTS => $unreadCounts,
        ]);

        $channel = ShmChannels::structure()->findOne([
            ShmChannelsField::_ID => $insert->getInsertedId(),
        ]);

        if (!$channel) {
            throw new ChatException('channel_not_found');
        }

        return $channel;
    }

    public static function findOrCreateDirect(Member $a, Member $b, ?string $space = null): object
    {
        $existing = self::findDirectChannel($a, $b, $space);

        if ($existing) {
            return $existing;
        }

        return self::createChannel(
            members: [$a, $b],
            space: $space ?? '',
        );
    }

    public static function findDirectChannel(Member $a, Member $b, ?string $space = null): ?object
    {
        $match = [
            ShmChannelsField::MEMBERS__IDX__OWNER => [
                '$all' => [
                    mDB::id($a->owner),
                    mDB::id($b->owner),
                ],
            ],
            '$expr' => [
                '$eq' => [['$size' => '$' . ShmChannelsField::MEMBERS], 2],
            ],
        ];

        if ($space !== null && $space !== '') {
            $match[ShmChannelsField::SPACE] = $space;
        }

        $result = ShmChannels::structure()->aggregate([
            ['$match' => $match],
            ['$limit' => 1],
        ])->toArray();

        $row = $result[0] ?? null;

        if (!$row) {
            return null;
        }

        $id = ShmChannelsField::_ID_GET($row);

        if (!$id) {
            return null;
        }

        $channel = ShmChannels::structure()->findOne([
            ShmChannelsField::_ID => mDB::id($id),
        ]);

        if (!$channel || count(ShmChannelsField::MEMBERS_GET($channel, [])) !== 2) {
            return null;
        }

        $hasA = false;
        $hasB = false;

        foreach (ShmChannelsField::MEMBERS_GET($channel, []) as $member) {
            if ($a->matches($member)) {
                $hasA = true;
            }
            if ($b->matches($member)) {
                $hasB = true;
            }
        }

        if (!$hasA || !$hasB) {
            return null;
        }

        return $channel;
    }

    public static function addMember(string $channelId, Member $member): bool
    {
        $channel = self::requireChannel($channelId);

        foreach (ShmChannelsField::MEMBERS_GET($channel, []) as $existing) {
            if ($member->matches($existing)) {
                return true;
            }
        }

        ShmChannels::structure()->updateOne([
            ShmChannelsField::_ID => mDB::id($channelId),
        ], [
            '$push' => [
                ShmChannelsField::MEMBERS => $member->toMemberDocument(),
            ],
            '$pull' => [
                ShmChannelsField::JOINABLE_MEMBERS => [
                    ShmChannelsField::EL_JOINABLE_MEMBERS__OWNER => mDB::id($member->owner),
                    ShmChannelsField::EL_JOINABLE_MEMBERS__OWNER_COLLECTION => $member->ownerCollection,
                ],
            ],
            '$set' => [
                ShmChannelsField::UNREAD_COUNTS . '.' . $member->ownerKey() => 0,
            ],
        ]);

        return true;
    }

    public static function addLink(string $channelId, Link $link): bool
    {
        self::requireChannel($channelId);

        ShmChannels::structure()->updateOne(
            [ShmChannelsField::_ID => mDB::id($channelId)],
            [
                '$addToSet' => [
                    ShmChannelsField::REFERENCE => $link->toDocument(),
                ],
            ],
        );

        return true;
    }

    public static function removeLink(string $channelId, Link $link): bool
    {
        self::requireChannel($channelId);

        ShmChannels::structure()->updateOne(
            [ShmChannelsField::_ID => mDB::id($channelId)],
            [
                '$pull' => [
                    ShmChannelsField::REFERENCE => $link->toDocument(),
                ],
            ],
        );

        return true;
    }

    /**
     * @param list<Member> $members
     * @param list<Link> $links
     * @return list<object>
     */
    public static function findChannels(
        array $members = [],
        array $links = [],
        ?string $space = null,
        ?bool $closed = null,
    ): array {
        $filter = self::buildChannelSearchFilter($members, $links, $space, $closed);


        return (array) (ShmChannels::structure()->find($filter) ?? []);
    }

    /**
     * @param list<Member> $members
     * @param list<Link> $links
     */
    public static function findChannel(
        array $members = [],
        array $links = [],
        ?string $space = null,
        ?bool $closed = null,
    ): ?object {
        $filter = self::buildChannelSearchFilter($members, $links, $space, $closed);

        return ShmChannels::structure()->findOne($filter) ?: null;
    }

    /**
     * @param list<Member> $members
     * @param list<Link> $links
     * @return array<string, mixed>
     */
    private static function buildChannelSearchFilter(
        array $members,
        array $links,
        ?string $space,
        ?bool $closed,
    ): array {
        if ($members === [] && $links === []) {
            throw new ChatException('find_channels_requires_criteria');
        }

        $conditions = [];

        foreach ($members as $member) {
            $conditions[] = [
                ShmChannelsField::MEMBERS => [
                    '$elemMatch' => [
                        ShmChannelsField::EL_MEMBERS__OWNER => mDB::id($member->owner),
                        ShmChannelsField::EL_MEMBERS__OWNER_COLLECTION => $member->ownerCollection,
                    ],
                ],
            ];
        }

        foreach ($links as $link) {
            $conditions[] = [
                ShmChannelsField::REFERENCE => [
                    '$elemMatch' => [
                        ShmChannelsField::EL_REFERENCE__DOCUMENT_ID => mDB::id($link->documentId),
                        ShmChannelsField::EL_REFERENCE__DOCUMENT_COLLECTION => $link->documentCollection,
                    ],
                ],
            ];
        }

        if ($space !== null && $space !== '') {
            $conditions[] = [ShmChannelsField::SPACE => $space];
        }

        if ($closed !== null) {
            $conditions[] = [ShmChannelsField::CLOSED => $closed];
        }

        if (count($conditions) === 1) {
            return $conditions[0];
        }

        return ['$and' => $conditions];
    }

    public static function closeChannel(string $channelId, ?Member $by = null): bool
    {
        self::requireChannel($channelId);

        ShmChannels::structure()->updateOne([
            ShmChannelsField::_ID => mDB::id($channelId),
        ], ['$set' => [ShmChannelsField::CLOSED => true]]);

        self::sendSystemMessage($channelId, 'Диалог закрыт', $by);

        return true;
    }

    /**
     * @param array<int, mixed> $attachments
     * @param array<string, mixed>|null $payload
     */
    public static function sendMessage(
        string $channelId,
        Member $sender,
        string $text = '',
        ?string $reply = null,
        array $attachments = [],
        mixed $voice = null,
        ?array $payload = null,
        ?string $clientMessageId = null,
    ): object {
        $channel = self::requireChannel($channelId);

        if (!empty(ShmChannelsField::CLOSED_GET($channel))) {
            throw new ChatException('channel_closed');
        }

        return self::insertMessageIdempotent(
            ShmChannelsField::_ID_GET($channel),
            $clientMessageId,
            $sender,
            [
                ShmChannelMessagesField::CHANNEL => ShmChannelsField::_ID_GET($channel),
                ShmChannelMessagesField::CLIENT_MESSAGE_ID => $clientMessageId,
                ShmChannelMessagesField::OWNER => mDB::id($sender->owner),
                ShmChannelMessagesField::OWNER_COLLECTION => $sender->ownerCollection,
                ShmChannelMessagesField::TEXT => $text,
                ShmChannelMessagesField::REPLY => $reply ? mDB::id($reply) : null,
                ShmChannelMessagesField::ATTACHMENTS => $attachments,
                ShmChannelMessagesField::VOICE => $voice,
                ShmChannelMessagesField::TYPE => ShmChannelMessagesField::TYPE_ENUM_REGULAR,
                ShmChannelMessagesField::PAYLOAD => $payload,
            ],
        );
    }

    public static function sendSystemMessage(string $channelId, string $text, ?Member $by = null): object
    {
        $channel = self::requireChannel($channelId);

        $owner = $by?->owner;
        $ownerCollection = $by?->ownerCollection;

        if ($owner === null) {
            $firstMember = ShmChannelsField::MEMBERS_GET($channel, [])[0] ?? null;
            $owner = ShmChannelsField::EL_MEMBERS__OWNER_GET($firstMember);
            $ownerCollection = ShmChannelsField::EL_MEMBERS__OWNER_COLLECTION_GET($firstMember);
        }

        $insert = ShmChannelMessages::structure()->insertOne([
            ShmChannelMessagesField::CHANNEL => mDB::id($channelId),
            ShmChannelMessagesField::OWNER => $owner !== null ? mDB::id($owner) : null,
            ShmChannelMessagesField::OWNER_COLLECTION => $ownerCollection,
            ShmChannelMessagesField::TEXT => $text,
            ShmChannelMessagesField::TYPE => ShmChannelMessagesField::TYPE_ENUM_SYSTEM,
        ]);

        $message = ShmChannelMessages::structure()->findOne([
            ShmChannelMessagesField::_ID => $insert->getInsertedId(),
        ]);

        if (!$message) {
            throw new ChatException('message_not_found');
        }

        return $message;
    }

    public static function markRead(string $channelId, Member $reader, ?string $lastReadMessageId = null): void
    {
        self::requireChannel($channelId);

        ShmChannels::zeroUnreadCounts($channelId, $reader->owner);

        ShmChannelMessages::structure()->updateMany([
            ShmChannelMessagesField::CHANNEL => mDB::id($channelId),
            ShmChannelMessagesField::VIEWED => ['$ne' => true],
            ShmChannelMessagesField::OWNER => ['$ne' => mDB::id($reader->owner)],
        ], [
            '$set' => [
                ShmChannelMessagesField::VIEWED => true,
            ],
        ]);
    }

    public static function assertMember(string $channelId, Member $member): void
    {
        $channel = ShmChannels::findOne([
            ShmChannelsField::_ID => mDB::id($channelId),
            ShmChannelsField::MEMBERS__IDX__OWNER => mDB::id($member->owner),
            ShmChannelsField::MEMBERS__IDX__OWNER_COLLECTION => $member->ownerCollection,
        ]);

        if (!$channel) {
            throw new ChatException('not_channel_member');
        }
    }

    public static function assertMemberOrJoinable(string $channelId, Member $member): void
    {
        $ownerId = mDB::id($member->owner);
        $ownerCollection = $member->ownerCollection;

        $channel = ShmChannels::findOne([
            ShmChannelsField::_ID => mDB::id($channelId),
            '$or' => [
                [
                    ShmChannelsField::MEMBERS__IDX__OWNER => $ownerId,
                    ShmChannelsField::MEMBERS__IDX__OWNER_COLLECTION => $ownerCollection,
                ],
                [
                    ShmChannelsField::JOINABLE_MEMBERS__IDX__OWNER => $ownerId,
                    ShmChannelsField::JOINABLE_MEMBERS__IDX__OWNER_COLLECTION => $ownerCollection,
                ],
            ],
        ]);

        if (!$channel) {
            throw new ChatException('not_channel_member');
        }
    }

    /**
     * @return array{data?: array<int, mixed>, hash: string}
     */
    public static function listChannelsForMember(Member $member, ListQueryInput $query): array
    {
        $pipeline = [
            [
                '$match' => [
                    ShmChannelsField::MEMBERS__IDX__OWNER => mDB::id($member->owner),
                    ShmChannelsField::MEMBERS__IDX__OWNER_COLLECTION => $member->ownerCollection,
                    ShmChannelsField::CLOSED => ['$ne' => true],
                ],
            ],
            [
                '$sort' => [
                    ShmChannelsField::UPDATED_AT => -1,
                ],
            ],
        ];


        return self::aggregateChannelList($pipeline, $query);
    }

    /**
     * @return array{data?: array<int, mixed>, hash: string}
     */
    public static function listClosedChannels(Member $member, ListQueryInput $query): array
    {
        $pipeline = [
            [
                '$match' => [
                    ShmChannelsField::MEMBERS__IDX__OWNER => mDB::id($member->owner),
                    ShmChannelsField::MEMBERS__IDX__OWNER_COLLECTION => $member->ownerCollection,
                    ShmChannelsField::CLOSED => true,
                ],
            ],
            [
                '$sort' => [
                    ShmChannelsField::UPDATED_AT => -1,
                ],
            ],
        ];

        return self::aggregateChannelList($pipeline, $query);
    }

    /**
     * @return array{data?: array<int, mixed>, hash: string}
     */
    public static function listJoinableChannels(Member $member, ListQueryInput $query): array
    {
        $ownerId = mDB::id($member->owner);
        $ownerCollection = $member->ownerCollection;

        $pipeline = [
            [
                '$match' => [
                    ShmChannelsField::JOINABLE_MEMBERS__IDX__OWNER => $ownerId,
                    ShmChannelsField::JOINABLE_MEMBERS__IDX__OWNER_COLLECTION => $ownerCollection,
                    ShmChannelsField::CLOSED => ['$ne' => true],
                    ShmChannelsField::MEMBERS => [
                        '$not' => [
                            '$elemMatch' => [
                                ShmChannelsField::EL_MEMBERS__OWNER => $ownerId,
                                ShmChannelsField::EL_MEMBERS__OWNER_COLLECTION => $ownerCollection,
                            ],
                        ],
                    ],
                ],
            ],
            [
                '$sort' => [
                    ShmChannelsField::UPDATED_AT => -1,
                ],
            ],
        ];

        return self::aggregateChannelList($pipeline, $query);
    }

    /**
     * @return array{data?: array<int, mixed>, hash: string}
     */
    public static function listMessages(string $channelId, ListQueryInput $query): array
    {
        $channel = self::requireChannel($channelId);

        $pipeline = [
            [
                '$match' => [
                    ShmChannelMessagesField::CHANNEL => ShmChannelsField::_ID_GET($channel),
                ],
            ],
            [
                '$sort' => [ShmChannelMessagesField::_ID => -1],
            ],
        ];

        $pipeline = self::applyListPagination($pipeline, $query);

        if ($query->onlyHash) {
            return [
                'hash' => ShmChannelMessages::structure()->aggregateHash($pipeline),
            ];
        }

        $aggregate = ShmChannelMessages::structure()->aggregateWithHash($pipeline);

        return [
            'data' => array_reverse($aggregate['data']),
            'hash' => $aggregate['hash'],
        ];
    }

    public static function togglePinnedMessage(string $messageId, Member $by): bool
    {
        $message = ShmChannelMessages::structure()->findOne([
            ShmChannelMessagesField::_ID => mDB::id($messageId),
        ]);

        if (!$message) {
            throw new ChatException('message_not_found');
        }

        self::assertAdminMember((string) ShmChannelMessagesField::CHANNEL_GET($message), $by);

        ShmChannelMessages::togglePinnedMessage($messageId, $by->ownerKey());

        return true;
    }

    public static function toggleReaction(
        string $channelId,
        string $messageId,
        string $reaction,
        Member $user,
    ): bool {
        self::assertMemberOrJoinable($channelId, $user);

        $message = ShmChannelMessages::structure()->findOne([
            ShmChannelMessagesField::_ID => mDB::id($messageId),
            ShmChannelMessagesField::CHANNEL => mDB::id($channelId),
        ]);

        if (!$message) {
            throw new ChatException('message_not_found');
        }

        $userIdObj = mDB::id($user->owner);
        $messageIdObj = mDB::id($messageId);
        $collection = ShmChannelMessages::structure();

        $existing = $collection->findOne([
            ShmChannelMessagesField::_ID => $messageIdObj,
            ShmChannelMessagesField::REACTIONS => [
                '$elemMatch' => [
                    ShmChannelMessagesField::EL_REACTIONS__USER => $userIdObj,
                    ShmChannelMessagesField::EL_REACTIONS__TYPE => $reaction,
                ],
            ],
        ]);

        if ($existing) {
            $collection->updateOne(
                [ShmChannelMessagesField::_ID => $messageIdObj],
                [
                    '$pull' => [
                        ShmChannelMessagesField::REACTIONS => [
                            ShmChannelMessagesField::EL_REACTIONS__USER => $userIdObj,
                            ShmChannelMessagesField::EL_REACTIONS__TYPE => $reaction,
                        ],
                    ],
                ]
            );
        } else {
            $collection->updateOne(
                [ShmChannelMessagesField::_ID => $messageIdObj],
                [
                    '$pull' => [
                        ShmChannelMessagesField::REACTIONS => [
                            ShmChannelMessagesField::EL_REACTIONS__USER => $userIdObj,
                        ],
                    ],
                ]
            );

            $collection->updateOne(
                [ShmChannelMessagesField::_ID => $messageIdObj],
                [
                    '$push' => [
                        ShmChannelMessagesField::REACTIONS => [
                            ShmChannelMessagesField::EL_REACTIONS__USER => $userIdObj,
                            ShmChannelMessagesField::EL_REACTIONS__TYPE => $reaction,
                            ShmChannelMessagesField::EL_REACTIONS__CREATED_AT => time(),
                        ],
                    ],
                ]
            );
        }

        return true;
    }

    public static function deleteMessage(string $channelId, string $messageId, Member $by): bool
    {
        self::assertMemberOrJoinable($channelId, $by);

        ShmChannelMessages::structure()->updateOne([
            ShmChannelMessagesField::_ID => mDB::id($messageId),
            ShmChannelMessagesField::CHANNEL => mDB::id($channelId),
            ShmChannelMessagesField::OWNER => mDB::id($by->owner),
            ShmChannelMessagesField::OWNER_COLLECTION => $by->ownerCollection,
        ], [
            '$set' => [
                ShmChannelMessagesField::DELETED => mDB::id($by->owner),
            ],
        ]);

        return true;
    }

    public static function assertAdminMember(string $channelId, Member $member): void
    {
        $channel = ShmChannels::findOne([
            ShmChannelsField::_ID => mDB::id($channelId),
            ShmChannelsField::MEMBERS__IDX__OWNER => mDB::id($member->owner),
            ShmChannelsField::MEMBERS__IDX__OWNER_COLLECTION => $member->ownerCollection,
        ]);

        if (!$channel) {
            throw new ChatException('not_channel_member');
        }

        foreach (ShmChannelsField::MEMBERS_GET($channel, []) as $existing) {
            if (!$member->matches($existing)) {
                continue;
            }

            if (!(ShmChannelsField::EL_MEMBERS__IS_ADMIN_GET($existing, false))) {
                throw new ChatException('access_denied');
            }

            return;
        }

        throw new ChatException('not_channel_member');
    }

    public static function resolveMemberDisplayName(Member $member): string
    {
        $structure = StructureHelper::getStructureByCollection($member->ownerCollection);

        if (!$structure) {
            return '';
        }

        return $structure->getChatNameHandler($member->ownerKey());
    }

    /**
     * @param array<string, mixed> $document
     */
    private static function insertMessageIdempotent(
        $channelId,
        ?string $clientMessageId,
        Member $sender,
        array $document,
    ): object {
        if (is_string($clientMessageId) && $clientMessageId !== '') {
            $existing = ShmChannelMessages::findByClientMessageId($channelId, $clientMessageId);
            if ($existing) {
                self::assertMessageOwner($existing, $sender);

                return $existing;
            }
        }

        try {
            $insert = ShmChannelMessages::structure()->insertOne($document);

            $message = ShmChannelMessages::structure()->findOne([
                ShmChannelMessagesField::_ID => $insert->getInsertedId(),
            ]);

            if (!$message) {
                throw new ChatException('message_not_found');
            }

            return $message;
        } catch (ChatException $e) {
            throw $e;
        } catch (\Throwable $e) {
            if (is_string($clientMessageId) && $clientMessageId !== '' && ShmChannelMessages::isDuplicateKeyError($e)) {
                $existing = ShmChannelMessages::findByClientMessageId($channelId, $clientMessageId);
                if ($existing) {
                    self::assertMessageOwner($existing, $sender);

                    return $existing;
                }
            }

            throw $e;
        }
    }

    private static function assertMessageOwner(object $message, Member $sender): void
    {
        if ((string) (ShmChannelMessagesField::OWNER_GET($message, '') ?? '') !== $sender->ownerKey()) {
            throw new ChatException('access_denied');
        }
    }

    /**
     * @param array<int, array<string, mixed>> $pipeline
     * @return array{data?: array<int, mixed>, hash: string}
     */
    private static function aggregateChannelList(array $pipeline, ListQueryInput $query): array
    {
        $pipeline = self::applyListPagination($pipeline, $query);

        if ($query->onlyHash) {
            return [
                'hash' => ShmChannels::structure()->aggregateHash($pipeline),
            ];
        }

        $aggregate = ShmChannels::structure()->aggregateWithHash($pipeline);

        return [
            'data' => $aggregate['data'],
            'hash' => $aggregate['hash'],
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $pipeline
     * @return array<int, array<string, mixed>>
     */
    private static function applyListPagination(array $pipeline, ListQueryInput $query): array
    {
        if ($query->offset > 0) {
            $pipeline[] = [
                '$skip' => $query->offset,
            ];
        }

        $pipeline[] = [
            '$limit' => $query->limit,
        ];

        return $pipeline;
    }
}
