<?php

namespace Shm\ShmChat;

use Shm\ShmChat\Classes\ChannelReference;
use Shm\ShmChat\Classes\ChatMember;
use Shm\ShmChat\Classes\CreateChannelInput;
use Shm\ShmChat\Classes\FindChannelsInput;
use Shm\ShmChat\Classes\ListQueryInput;
use Shm\ShmChat\Classes\SendMessageInput;
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;
use Shm\ShmUtils\ShmInit;

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

    public static function createChannel(CreateChannelInput $input): object
    {
        $members = [];
        $unreadCounts = [];

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

        $insert = ShmChannels::structure()->insertOne([
            'space' => $input->space,
            'name' => $input->name,
            'type' => $input->type,
            'payload' => $input->payload,
            'reference' => array_map(
                static fn (ChannelReference $ref) => $ref->toDocument(),
                $input->references,
            ),
            'members' => $members,
            'closed' => $input->closed,
            'unreadCounts' => $unreadCounts,
        ]);

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

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

        return $channel;
    }

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

        if ($existing) {
            return $existing;
        }

        return self::createChannel(CreateChannelInput::make(
            [$a, $b],
            $space,
            '',
            'default',
        ));
    }

    public static function findDirectChannel(ChatMember $a, ChatMember $b, ?string $space = null): ?object
    {
        $match = [
            'type' => 'default',
            '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, ChatMember $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(),
            ],
            '$set' => [
                'unreadCounts.' . $member->ownerKey() => 0,
            ],
        ]);

        return true;
    }

    public static function addReference(string $channelId, ChannelReference $ref): bool
    {
        self::requireChannel($channelId);

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

        return true;
    }

    public static function removeReference(string $channelId, ChannelReference $ref): bool
    {
        self::requireChannel($channelId);

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

        return true;
    }

    /**
     * @return list<object>
     */
    public static function findChannels(FindChannelsInput $input): array
    {
        $filter = self::buildChannelSearchFilter($input);

        return ShmChannels::structure()->find($filter)->toArray();
    }

    public static function findChannel(FindChannelsInput $input): ?object
    {
        $filter = self::buildChannelSearchFilter($input);

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

    /**
     * @return list<object>
     */
    public static function findChannelsByReference(
        ChannelReference $ref,
        ?string $type = null,
        ?bool $closed = null,
    ): array {
        $input = FindChannelsInput::byReference($ref);
        $input->type = $type;
        $input->closed = $closed;

        return self::findChannels($input);
    }

    public static function findChannelByReference(
        ChannelReference $ref,
        ?string $type = null,
        ?bool $closed = null,
    ): ?object {
        $input = FindChannelsInput::byReference($ref);
        $input->type = $type;
        $input->closed = $closed;

        return self::findChannel($input);
    }

    /**
     * @return array<string, mixed>
     */
    private static function buildChannelSearchFilter(FindChannelsInput $input): array
    {
        if ($input->members === [] && $input->references === []) {
            throw new ChatException('find_channels_requires_criteria');
        }

        $conditions = [];

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

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

        if ($input->type !== null) {
            $conditions[] = ['type' => $input->type];
        }

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

        if ($input->closed !== null) {
            $conditions[] = ['closed' => $input->closed];
        }

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

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

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

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

        $channelType = $channel->type ?? 'default';

        $text = 'Диалог закрыт';
        if ($channelType === 'support') {
            $text = 'Обращение закрыто';
        }

        self::sendSystemMessage($channelId, $text, $by);

        return true;
    }

    public static function sendMessage(SendMessageInput $input): object
    {
        $channel = self::requireChannel($input->channelId);

        if (!empty($channel->closed)) {
            throw new ChatException('channel_closed');
        }

        return self::insertMessageIdempotent(
            $channel->_id,
            $input->clientMessageId,
            $input->sender,
            [
                'channel' => $channel->_id,
                'clientMessageId' => $input->clientMessageId,
                'owner' => mDB::id($input->sender->owner),
                'ownerCollection' => $input->sender->ownerCollection,
                'text' => $input->text,
                'reply' => $input->reply ? mDB::id($input->reply) : null,
                'attachments' => $input->attachments,
                'voice' => $input->voice,
                'type' => 'regular',
                'payload' => $input->payload,
            ],
        );
    }

    public static function sendSystemMessage(string $channelId, string $text, ?ChatMember $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, ChatMember $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, ChatMember $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, ChatMember $member): void
    {
        $channel = ShmChannels::findOne([
            '_id' => mDB::id($channelId),
            'members.owner' => mDB::id($member->owner),
            'members.ownerCollection' => $member->ownerCollection,
        ]);

        if ($channel) {
            return;
        }

        if (!ShmChannels::hasJoinablePipelineHandler()) {
            throw new ChatException('not_channel_member');
        }

        $pipeline = ShmChannels::getJoinablePipelineHandler();
        $pipeline[] = [
            '$match' => [
                '_id' => mDB::id($channelId),
            ],
        ];

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

        if (count($result) === 0) {
            throw new ChatException('not_channel_member');
        }
    }

    /**
     * @return array{data?: array<int, mixed>, hash: string}
     */
    public static function listChannelsForMember(ChatMember $member, ListQueryInput $query): array
    {
        $pipeline = [
            [
                '$match' => [
                    'members.owner' => mDB::id($member->owner),
                    'members.ownerCollection' => $member->ownerCollection,
                ],
            ],
            [
                '$sort' => [
                    'closed' => 1,
                    'updated_at' => -1,
                ],
            ],
        ];

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

    /**
     * @return array{data?: array<int, mixed>, hash: string}
     */
    public static function listJoinableChannels(ListQueryInput $query): array
    {
        if (!ShmChannels::hasJoinablePipelineHandler()) {
            if (ShmInit::isDev()) {
                error_log('[ShmChat] joinChannelList: joinable pipeline handler is not configured. Call ShmChannels::setJoinablePipelineHandler().');
            }

            if ($query->onlyHash) {
                return ['hash' => ''];
            }

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

        $pipeline = ShmChannels::getJoinablePipelineHandler();
        $pipeline[] = [
            '$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, ChatMember $by): bool
    {
        $message = ShmChannelMessages::structure()->findOne([
            '_id' => mDB::id($messageId),
        ]);

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

        self::assertMemberWithRoles((string) $message->channel, $by, ['admin', 'owner', 'support']);

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

        return true;
    }

    public static function toggleReaction(
        string $channelId,
        string $messageId,
        string $reaction,
        ChatMember $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, ChatMember $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;
    }

    /**
     * @param array<int, string> $roles
     */
    public static function assertMemberWithRoles(string $channelId, ChatMember $member, array $roles): 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;
            }

            $role = $existing->role ?? 'member';

            if (!in_array($role, $roles, true)) {
                throw new ChatException('access_denied');
            }

            return;
        }

        throw new ChatException('not_channel_member');
    }

    public static function resolveMemberDisplayName(ChatMember $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,
        ChatMember $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, ChatMember $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;
    }
}
