<?php

namespace Shm\ShmChat;

use Shm\ShmChat\Exceptions\ChatException;
use Shm\ShmChat\SchemaChat\ShmChannelMessages;
use Shm\ShmChat\SchemaChat\ShmChannels;
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([
            '_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([
            'space' => $space,
            'name' => $name,
            'payload' => $payload,
            'reference' => array_map(
                static fn(Link $link) => $link->toDocument(),
                $links,
            ),
            'members' => $memberDocs,
            'joinableMembers' => $joinableMembers,
            'closed' => $closed,
            'unreadCounts' => $unreadCounts,
        ]);

        $channel = ShmChannels::structure()->findOne([
            '_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 = [
            'members.owner' => [
                '$all' => [
                    mDB::id($a->owner),
                    mDB::id($b->owner),
                ],
            ],
            '$expr' => [
                '$eq' => [['$size' => '$members'], 2],
            ],
        ];

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

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

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

        if (!$row) {
            return null;
        }

        $id = is_array($row) ? ($row['_id'] ?? null) : ($row->_id ?? null);

        if (!$id) {
            return null;
        }

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

        if (!$channel || count($channel->members ?? []) !== 2) {
            return null;
        }

        $hasA = false;
        $hasB = false;

        foreach ($channel->members ?? [] 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 ($channel->members ?? [] as $existing) {
            if ($member->matches($existing)) {
                return true;
            }
        }

        ShmChannels::structure()->updateOne([
            '_id' => mDB::id($channelId),
        ], [
            '$push' => [
                'members' => $member->toMemberDocument(),
            ],
            '$pull' => [
                'joinableMembers' => [
                    'owner' => mDB::id($member->owner),
                    'ownerCollection' => $member->ownerCollection,
                ],
            ],
            '$set' => [
                'unreadCounts.' . $member->ownerKey() => 0,
            ],
        ]);

        return true;
    }

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

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

        return true;
    }

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

        ShmChannels::structure()->updateOne(
            ['_id' => mDB::id($channelId)],
            [
                '$pull' => [
                    '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[] = [
                'members' => [
                    '$elemMatch' => [
                        'owner' => mDB::id($member->owner),
                        'ownerCollection' => $member->ownerCollection,
                    ],
                ],
            ];
        }

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

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

        if ($closed !== null) {
            $conditions[] = ['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([
            '_id' => mDB::id($channelId),
        ], ['$set' => ['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($channel->closed)) {
            throw new ChatException('channel_closed');
        }

        return self::insertMessageIdempotent(
            $channel->_id,
            $clientMessageId,
            $sender,
            [
                'channel' => $channel->_id,
                'clientMessageId' => $clientMessageId,
                'owner' => mDB::id($sender->owner),
                'ownerCollection' => $sender->ownerCollection,
                'text' => $text,
                'reply' => $reply ? mDB::id($reply) : null,
                'attachments' => $attachments,
                'voice' => $voice,
                'type' => 'regular',
                '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 = $channel->members[0] ?? null;
            $owner = $firstMember?->owner ?? null;
            $ownerCollection = $firstMember?->ownerCollection ?? null;
        }

        $insert = ShmChannelMessages::structure()->insertOne([
            'channel' => mDB::id($channelId),
            'owner' => $owner !== null ? mDB::id($owner) : null,
            'ownerCollection' => $ownerCollection,
            'text' => $text,
            'type' => 'system',
        ]);

        $message = ShmChannelMessages::structure()->findOne([
            '_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([
            'channel' => mDB::id($channelId),
            'viewed' => ['$ne' => true],
            'owner' => ['$ne' => mDB::id($reader->owner)],
        ], [
            '$set' => [
                'viewed' => true,
            ],
        ]);
    }

    public static function assertMember(string $channelId, Member $member): void
    {
        $channel = ShmChannels::findOne([
            '_id' => mDB::id($channelId),
            'members.owner' => mDB::id($member->owner),
            'members.ownerCollection' => $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([
            '_id' => mDB::id($channelId),
            '$or' => [
                [
                    'members.owner' => $ownerId,
                    'members.ownerCollection' => $ownerCollection,
                ],
                [
                    'joinableMembers.owner' => $ownerId,
                    'joinableMembers.ownerCollection' => $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' => [
                    'members.owner' => mDB::id($member->owner),
                    'members.ownerCollection' => $member->ownerCollection,
                    'closed' => ['$ne' => true],
                ],
            ],
            [
                '$sort' => [
                    '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' => [
                    'members.owner' => mDB::id($member->owner),
                    'members.ownerCollection' => $member->ownerCollection,
                    'closed' => true,
                ],
            ],
            [
                '$sort' => [
                    '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' => [
                    'joinableMembers.owner' => $ownerId,
                    'joinableMembers.ownerCollection' => $ownerCollection,
                    'closed' => ['$ne' => true],
                    'members' => [
                        '$not' => [
                            '$elemMatch' => [
                                'owner' => $ownerId,
                                'ownerCollection' => $ownerCollection,
                            ],
                        ],
                    ],
                ],
            ],
            [
                '$sort' => [
                    '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' => [
                    'channel' => $channel->_id,
                ],
            ],
            [
                '$sort' => ['_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([
            '_id' => mDB::id($messageId),
        ]);

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

        self::assertAdminMember((string) $message->channel, $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([
            '_id' => mDB::id($messageId),
            '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([
            '_id' => $messageIdObj,
            'reactions' => [
                '$elemMatch' => [
                    'user' => $userIdObj,
                    'type' => $reaction,
                ],
            ],
        ]);

        if ($existing) {
            $collection->updateOne(
                ['_id' => $messageIdObj],
                [
                    '$pull' => [
                        'reactions' => [
                            'user' => $userIdObj,
                            'type' => $reaction,
                        ],
                    ],
                ]
            );
        } else {
            $collection->updateOne(
                ['_id' => $messageIdObj],
                [
                    '$pull' => [
                        'reactions' => [
                            'user' => $userIdObj,
                        ],
                    ],
                ]
            );

            $collection->updateOne(
                ['_id' => $messageIdObj],
                [
                    '$push' => [
                        'reactions' => [
                            'user' => $userIdObj,
                            'type' => $reaction,
                            'created_at' => time(),
                        ],
                    ],
                ]
            );
        }

        return true;
    }

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

        ShmChannelMessages::structure()->updateOne([
            '_id' => mDB::id($messageId),
            'channel' => mDB::id($channelId),
            'owner' => mDB::id($by->owner),
            'ownerCollection' => $by->ownerCollection,
        ], [
            '$set' => [
                'deleted' => mDB::id($by->owner),
            ],
        ]);

        return true;
    }

    public static function assertAdminMember(string $channelId, Member $member): void
    {
        $channel = ShmChannels::findOne([
            '_id' => mDB::id($channelId),
            'members.owner' => mDB::id($member->owner),
            'members.ownerCollection' => $member->ownerCollection,
        ]);

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

        foreach ($channel->members ?? [] as $existing) {
            if (!$member->matches($existing)) {
                continue;
            }

            if (!($existing->isAdmin ?? 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([
                '_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) ($message->owner ?? '') !== $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;
    }
}
