<?php
namespace Misuzu\Messages;

use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Http\Routing\{HttpGet,HttpMiddleware,HttpPost,RouteHandler};
use Syokuhou\IConfig;
use Misuzu\{CSRF,Pagination,Perm,Template};
use Misuzu\Auth\AuthInfo;
use Misuzu\Parsers\Parser;
use Misuzu\Perms\Permissions;
use Misuzu\URLs\{URLInfo,URLRegistry};
use Misuzu\Users\{UsersContext,UserInfo};

class MessagesRoutes extends RouteHandler {
    public const FOLDER_META = [
        'inbox' => [ 'title' => 'Inbox', 'icon' => 'fas fa-inbox fa-fw' ],
        'drafts' => [ 'title' => 'Drafts', 'icon' => 'fas fa-pencil-alt fa-fw' ],
        'sent' => [ 'title' => 'Sent', 'icon' => 'fas fa-paper-plane fa-fw' ],
        'trash' => [ 'title' => 'Trash', 'icon' => 'fas fa-trash-alt fa-fw' ],
    ];

    public function __construct(
        private IConfig $config,
        private URLRegistry $urls,
        private AuthInfo $authInfo,
        private MessagesContext $msgsCtx,
        private UsersContext $usersCtx,
        private Permissions $perms
    ) {}

    private bool $canSendMessages;

    #[HttpMiddleware('/messages')]
    public function checkAccess($response, $request) {
        // should probably be a permission or something too
        if(!$this->authInfo->isLoggedIn())
            return 401;

        $globalPerms = $this->authInfo->getPerms('global');
        if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
            return 403;

        $this->canSendMessages = $globalPerms->check(Perm::G_MESSAGES_SEND)
            && !$this->usersCtx->hasActiveBan($this->authInfo->getUserInfo());

        if($request->getMethod() === 'POST' && $request->isFormContent()) {
            $content = $request->getContent();
            if(!$content->hasParam('_csrfp') || !CSRF::validate((string)$content->getParam('_csrfp')))
                return [
                    'error' => [
                        'name' => 'msgs:verify',
                        'text' => 'Request verification failed! Refresh the page and try again.',
                    ],
                ];

            $response->setHeader('X-CSRFP-Token', CSRF::token());
        }
    }

    private function populateMessage(MessageInfo $messageInfo): object {
        $message = new stdClass;

        $message->info = $messageInfo;
        $message->author_info = $messageInfo->hasAuthorId() ? $this->usersCtx->getUserInfo($messageInfo->getAuthorId(), 'id') : null;
        $message->author_colour = $this->usersCtx->getUserColour($message->author_info);
        $message->recipient_info = $messageInfo->hasRecipientId() ? $this->usersCtx->getUserInfo($messageInfo->getRecipientId(), 'id') : null;
        $message->recipient_colour = $this->usersCtx->getUserColour($message->recipient_info);

        return $message;
    }

    #[HttpGet('/messages')]
    #[URLInfo('messages-index', '/messages', ['folder' => '<folder>', 'page' => '<page>'])]
    public function getIndex($response, $request, string $folderName = '') {
        $folderName = (string)$request->getParam('folder');
        if($folderName === '')
            $folderName = 'inbox';

        if(!array_key_exists($folderName, self::FOLDER_META))
            return 404;

        $folderInbox = $folderName === 'inbox';
        $folderDrafts = $folderName === 'drafts';
        $folderSent = $folderName === 'sent';
        $folderTrash = $folderName === 'trash';

        $selfInfo = $this->authInfo->getUserInfo();
        $msgsDb = $this->msgsCtx->getDatabase();

        $authorInfo = !$folderTrash && $folderSent ? $selfInfo : null;
        $recipientInfo = !$folderTrash && $folderInbox ? $selfInfo : null;
        $sent = $folderTrash ? null : !$folderDrafts;
        $deleted = $folderTrash;

        $pagination = new Pagination($msgsDb->countMessages(
            ownerInfo: $selfInfo,
            authorInfo:  $authorInfo,
            recipientInfo: $recipientInfo,
            sent: $sent,
            deleted: $deleted,
        ), 50, 'page');

        $messageInfos = $msgsDb->getMessages(
            ownerInfo: $selfInfo,
            authorInfo:  $authorInfo,
            recipientInfo: $recipientInfo,
            sent: $sent,
            deleted: $deleted,
            pagination: $pagination,
        );

        $messages = [];
        foreach($messageInfos as $messageInfo)
            $messages[] = $this->populateMessage($messageInfo);

        return Template::renderRaw('messages.index', [
            'can_send_messages' => $this->canSendMessages,
            'folder_name' => $folderName,
            'folder_meta' => self::FOLDER_META,
            'folder_messages' => $messages,
            'folder_pagination' => $pagination,
        ]);
    }

    #[HttpGet('/messages/stats')]
    #[URLInfo('messages-stats', '/messages/stats')]
    public function getStats() {
        $selfInfo = $this->authInfo->getUserInfo();
        $msgsDb = $this->msgsCtx->getDatabase();

        return [
            'unread' => $msgsDb->countMessages(
                ownerInfo: $selfInfo,
                recipientInfo: $selfInfo,
                sent: true,
                deleted: false,
                read: false,
            ),
        ];
    }

    #[HttpPost('/messages/recipient')]
    #[URLInfo('messages-recipient', '/messages/recipient')]
    public function postRecipient($response, $request) {
        if(!$request->isFormContent())
            return 400;
        if(!$this->canSendMessages)
            return 403;

        $content = $request->getContent();
        $name = trim((string)$content->getParam('name'));

        // flappy hacks
        if(str_starts_with(mb_strtolower($name), 'flappyzor'))
            $name = 14;

        $userInfo = null;
        if(!empty($name))
            try {
                $userInfo = $this->usersCtx->getUserInfo($name, 'messaging');
            } catch(InvalidArgumentException $ex) {
            } catch(RuntimeException $ex) {}

        if($userInfo === null)
            return [
                'avatar' => $this->urls->format('user-avatar', [
                    'res' => 200,
                ]),
            ];

        return [
            'id' => $userInfo->getId(),
            'name' => $userInfo->getName(),
            'ban' => $this->usersCtx->hasActiveBan($userInfo),
            'avatar' => $this->urls->format('user-avatar', [
                'user' => $userInfo->getId(),
                'res' => 200,
            ]),
        ];
    }

    #[HttpGet('/messages/compose')]
    #[URLInfo('messages-compose', '/messages/compose', ['recipient' => '<recipient>'])]
    public function getEditor($response, $request) {
        if(!$this->canSendMessages)
            return 403;

        return Template::renderRaw('messages.compose', [
            'recipient' => (string)$request->getParam('recipient'),
        ]);
    }

    #[HttpGet('/messages/([A-Za-z0-9]+)')]
    #[URLInfo('messages-view', '/messages/<message>')]
    public function getView($response, $request, string $messageId) {
        if(strlen($messageId) !== 8)
            return 404;

        $selfInfo = $this->authInfo->getUserInfo();
        $msgsDb = $this->msgsCtx->getDatabase();

        try {
            $messageInfo = $msgsDb->getMessageInfo($selfInfo, $messageId);
        } catch(RuntimeException $ex) {
            return 404;
        }

        if(!$messageInfo->isRead())
            $msgsDb->updateMessage(
                ownerInfo: $selfInfo,
                messageInfo: $messageInfo,
                readAt: time(),
            );

        $message = $this->populateMessage($messageInfo);

        $replyTo = null;
        if($messageInfo->hasReplyToId()) {
            try {
                $replyTo = $this->populateMessage(
                    $msgsDb->getMessageInfo($selfInfo, $messageInfo, true)
                );
            } catch(RuntimeException $ex) {}
        }

        $repliesForInfos = $msgsDb->getMessages(
            ownerInfo: $selfInfo,
            repliesFor: $messageInfo,
            deleted: false,
        );

        $draftInfo = null;
        $repliesFor = [];
        foreach($repliesForInfos as $repliesForInfo) {
            $repliesFor[] = $this->populateMessage($repliesForInfo);

            if(!$repliesForInfo->isSent() && $draftInfo === null)
                $draftInfo = $repliesForInfo;
        }

        return Template::renderRaw('messages.thread', [
            'can_send_messages' => $this->canSendMessages,
            'self_info' => $selfInfo,
            'reply_to' => $replyTo,
            'message' => $message,
            'draft_info' => $draftInfo,
            'replies_for' => $repliesFor,
        ]);
    }

    private function checkCanReceiveMessages(UserInfo|string $userInfo): ?array {
        $globalPerms = $this->perms->getPermissions('global', $userInfo);
        if(!$globalPerms->check(Perm::G_MESSAGES_VIEW))
            return [
                'error' => [
                    'name' => 'msgs:recipient_cannot_recv',
                    'text' => 'This person is not allowed to receive messages.',
                ],
            ];

        return null;
    }

    private function checkMessageFields(string $title, string $body, int $parser): ?array {
        if(!Parser::isValid($parser))
            return [
                'error' => [
                    'name' => 'msgs:invalid_parser',
                    'text' => 'Invalid parser selected.',
                ],
            ];

        $lengths = $this->config->getValues([
            ['title.minLength:i', 1],
            ['title.maxLength:i', 200],
            ['body.minLength:i', 1],
            ['body.maxLength:i', 60000],
        ]);

        $titleLength = mb_strlen(trim($title));

        if($titleLength < $lengths['title.minLength'])
            return [
                'error' => [
                    'name' => 'msgs:title_too_short',
                    'args' => [$lengths['title.minLength'], $titleLength],
                    'text' => sprintf('Title may not be shorter than %d characters. You entered %d characters.', $lengths['title.minLength'], $titleLength),
                ],
            ];

        if($titleLength > $lengths['title.maxLength'])
            return [
                'error' => [
                    'name' => 'msgs:title_too_long',
                    'args' => [$lengths['title.maxLength'], $titleLength],
                    'text' => sprintf('Title may not be longer than %d characters. You entered %d characters.', $lengths['title.maxLength'], $titleLength),
                ],
            ];

        $bodyLength = mb_strlen(trim($body));

        if($bodyLength < $lengths['body.minLength'])
            return [
                'error' => [
                    'name' => 'msgs:body_too_short',
                    'args' => [$lengths['body.minLength'], $bodyLength],
                    'text' => sprintf('Message may not be shorter than %d characters. You entered %d characters.', $lengths['body.minLength'], $bodyLength),
                ],
            ];

        if($bodyLength > $lengths['body.maxLength'])
            return [
                'error' => [
                    'name' => 'msgs:body_too_long',
                    'args' => [$lengths['body.maxLength'], $bodyLength],
                    'text' => sprintf('Message may not be longer than %d characters. You entered %d characters.', $lengths['body.maxLength'], $bodyLength),
                ],
            ];

        return null;
    }

    #[HttpPost('/messages/create')]
    #[URLInfo('messages-create', '/messages/create')]
    public function postCreate($response, $request) {
        if(!$request->isFormContent())
            return 400;
        if(!$this->canSendMessages)
            return 403;

        $content = $request->getContent();
        $recipient = (string)$content->getParam('recipient');
        $replyTo = (string)$content->getParam('reply');
        $title = (string)$content->getParam('title');
        $body = (string)$content->getParam('body');
        $parser = (int)$content->getParam('parser', FILTER_SANITIZE_NUMBER_INT);
        $draft = !empty($content->getParam('draft'));

        $error = $this->checkMessageFields($title, $body, $parser);
        if($error !== null)
            return $error;

        $selfInfo = $this->authInfo->getUserInfo();
        $msgsDb = $this->msgsCtx->getDatabase();

        try {
            $recipientInfo = $this->usersCtx->getUserInfo($recipient, 'messaging');
        } catch(InvalidArgumentException $ex) {
            return [
                'error' => [
                    'name' => 'msgs:recipient_invalid',
                    'text' => 'Name of the recipient was incorrectly formatted.',
                    'jeff' => $recipient,
                ],
            ];
        } catch(RuntimeException $ex) {
            return [
                'error' => [
                    'name' => 'msgs:recipient_not_found',
                    'text' => 'Recipient does not exist.',
                ],
            ];
        }

        $error = $this->checkCanReceiveMessages($recipientInfo);
        if($error !== null)
            return $error;

        $replyToInfo = null;
        if(!empty($replyTo)) {
            try {
                $replyToInfo = $msgsDb->getMessageInfo($selfInfo, $replyTo);
            } catch(RuntimeException $ex) {
                return [
                    'error' => [
                        'name' => 'msgs:reply_not_found',
                        'text' => 'The message you are trying to reply to does not exist.',
                    ],
                ];
            }

            if(!$replyToInfo->isSent())
                return [
                    'error' => [
                        'name' => 'msgs:draft_reply',
                        'text' => 'You cannot reply to a draft.',
                    ],
                ];
        }

        $msgId = XString::random(8);
        $sentAt = $draft ? null : time();

        // own copy
        $msgsDb->createMessage(
            messageId: $msgId,
            ownerInfo: $selfInfo,
            authorInfo: $selfInfo,
            recipientInfo: $recipientInfo,
            title: $title,
            body: $body,
            parser: $parser,
            replyTo: $replyToInfo,
            sentAt: $sentAt
        );

        // recipient copy
        if($sentAt !== null && $recipientInfo->getId() !== $selfInfo->getId())
            $msgsDb->createMessage(
                messageId: $msgId,
                ownerInfo: $recipientInfo,
                authorInfo: $selfInfo,
                recipientInfo: $recipientInfo,
                title: $title,
                body: $body,
                parser: $parser,
                replyTo: $replyToInfo,
                sentAt: $sentAt
            );

        return [
            'id' => $msgId,
            'url' => $this->urls->format('messages-view', ['message' => $msgId]),
        ];
    }

    #[HttpPost('/messages/([A-Za-z0-9]+)')]
    #[URLInfo('messages-update', '/messages/<message>')]
    public function postUpdate($response, $request, string $messageId) {
        if(!$request->isFormContent())
            return 400;
        if(!$this->canSendMessages)
            return 403;

        $content = $request->getContent();
        $title = (string)$content->getParam('title');
        $body = (string)$content->getParam('body');
        $parser = (int)$content->getParam('parser', FILTER_SANITIZE_NUMBER_INT);
        $draft = !empty($content->getParam('draft'));

        $error = $this->checkMessageFields($title, $body, $parser);
        if($error !== null)
            return $error;

        $selfInfo = $this->authInfo->getUserInfo();
        $msgsDb = $this->msgsCtx->getDatabase();

        try {
            $messageInfo = $msgsDb->getMessageInfo($selfInfo, $messageId);
        } catch(RuntimeException $ex) {
            return [
                'error' => [
                    'name' => 'msgs:edit_not_found',
                    'text' => 'The message you are trying to edit does not exist.',
                ],
            ];
        }

        if(!$messageInfo->hasAuthorId() || $messageInfo->getAuthorId() !== $selfInfo->getId())
            return [
                'error' => [
                    'name' => 'msgs:not_author',
                    'text' => 'You are not the author of this message.',
                ],
            ];

        if(!$messageInfo->hasRecipientId())
            return [
                'error' => [
                    'name' => 'msgs:recipient_gone',
                    'text' => 'The recipient of this message no longer exists, it cannot be sent or edited.',
                ],
            ];

        if($messageInfo->isSent())
            return [
                'error' => [
                    'name' => 'msgs:not_draft',
                    'text' => 'You cannot edit a message that has already been sent.',
                ],
            ];

        $error = $this->checkCanReceiveMessages($messageInfo->getRecipientId());
        if($error !== null)
            return $error;

        $sentAt = $draft ? null : time();

        $msgsDb->updateMessage(
            ownerInfo: $selfInfo,
            messageInfo: $messageInfo,
            title: $title,
            body: $body,
            parser: $parser,
            sentAt: $sentAt,
        );

        // recipient copy
        if($sentAt !== null && $messageInfo->getRecipientId() !== $selfInfo->getId())
            $msgsDb->createMessage(
                messageId: $messageId,
                ownerInfo: $messageInfo->getRecipientId(),
                authorInfo: $selfInfo,
                recipientInfo: $messageInfo->getRecipientId(),
                title: $title,
                body: $body,
                parser: $parser,
                replyTo: $messageInfo->getReplyToId(),
                sentAt: $sentAt
            );

        return [
            'id' => $messageId,
            'url' => $this->urls->format('messages-view', ['message' => $messageId]),
        ];
    }

    #[HttpPost('/messages/mark')]
    #[URLInfo('messages-mark', '/messages/mark')]
    public function postMark($response, $request) {
        if(!$request->isFormContent())
            return 400;

        $content = $request->getContent();
        $type = (string)$content->getParam('type');
        $messages = explode(',', (string)$content->getParam('messages'));

        if($type !== 'read' && $type !== 'unread')
            return [
                'error' => [
                    'name' => 'msgs:unsupported_mark',
                    'text' => 'Attempting to mark message with an unsupported state.',
                ],
            ];

        $selfInfo = $this->authInfo->getUserInfo();
        $msgsDb = $this->msgsCtx->getDatabase();

        foreach($messages as $messageId)
            $msgsDb->updateMessage(
                ownerInfo: $selfInfo,
                messageInfo: $messageId,
                readAt: $type === 'read' ? time() : null,
            );

        return [];
    }

    #[HttpPost('/messages/delete')]
    #[URLInfo('messages-delete', '/messages/delete')]
    public function postDelete($response, $request) {
        if(!$request->isFormContent())
            return 400;

        $content = $request->getContent();

        $messages = (string)$content->getParam('messages');
        if($messages === '')
            return [
                'error' => [
                    'name' => 'msgs:empty',
                    'text' => 'No messages were supplied.',
                ],
            ];

        $messages = explode(',', $messages);

        $this->msgsCtx->getDatabase()->deleteMessages(
            $this->authInfo->getUserInfo(),
            $messages
        );

        return [];
    }

    #[HttpPost('/messages/restore')]
    #[URLInfo('messages-restore', '/messages/restore')]
    public function postRestore($response, $request) {
        if(!$request->isFormContent())
            return 400;

        $content = $request->getContent();

        $messages = (string)$content->getParam('messages');
        if($messages === '')
            return [
                'error' => [
                    'name' => 'msgs:empty',
                    'text' => 'No messages were supplied.',
                ],
            ];

        $messages = explode(',', $messages);

        $this->msgsCtx->getDatabase()->restoreMessages(
            $this->authInfo->getUserInfo(),
            $messages
        );

        return [];
    }

    #[HttpPost('/messages/nuke')]
    #[URLInfo('messages-nuke', '/messages/nuke')]
    public function postNuke($response, $request) {
        if(!$request->isFormContent())
            return 400;

        $content = $request->getContent();

        $messages = (string)$content->getParam('messages');
        if($messages === '')
            return [
                'error' => [
                    'name' => 'msgs:empty',
                    'text' => 'No messages were supplied.',
                ],
            ];

        $messages = explode(',', $messages);

        $this->msgsCtx->getDatabase()->nukeMessages(
            $this->authInfo->getUserInfo(),
            $messages
        );

        return [];
    }
}
