<?php
namespace Misuzu\Messages;

use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Data\{DbStatementCache,DbTools,IDbConnection};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;

class MessagesDatabase {
    private DbStatementCache $cache;

    public function __construct(private IDbConnection $dbConn) {
        $this->cache = new DbStatementCache($dbConn);
    }

    public function countMessages(
        UserInfo|string|null $ownerInfo = null,
        UserInfo|string|null $authorInfo = null,
        UserInfo|string|null $recipientInfo = null,
        MessageInfo|string|null $repliesFor = null,
        MessageInfo|string|null $replyTo = null,
        ?bool $sent = null,
        ?bool $read = null,
        ?bool $deleted = null
    ): int {
        $hasOwnerInfo = $ownerInfo !== null;
        $hasAuthorInfo = $authorInfo !== null;
        $hasRecipientInfo = $recipientInfo !== null;
        $hasRepliesFor = $repliesFor !== null;
        $hasReplyTo = $replyTo !== null;
        $hasSent = $sent !== null;
        $hasRead = $read !== null;
        $hasDeleted = $deleted !== null;

        $args = 0;
        $query = 'SELECT COUNT(*) FROM msz_messages';
        if($hasOwnerInfo) {
            ++$args;
            $query .= ' WHERE msg_owner_id = ?';
        }
        if($hasAuthorInfo)
            $query .= sprintf(' %s msg_author_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasRecipientInfo)
            $query .= sprintf(' %s msg_recipient_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasRepliesFor)
            $query .= sprintf(' %s msg_reply_to = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasReplyTo) {
            $query .= sprintf(' %s msg_id = ', ++$args > 1 ? 'AND' : 'WHERE');

            if($replyTo instanceof MessageInfo)
                $query .= '?';
            else
                $query .= '(SELECT reply_to FROM msz_messages WHERE msg_id = ?)';
        }
        if($hasSent)
            $query .= sprintf(' %s msg_sent %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $sent ? 'IS NOT' : 'IS');
        if($hasRead)
            $query .= sprintf(' %s msg_read %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $read ? 'IS NOT' : 'IS');
        if($hasDeleted)
            $query .= sprintf(' %s msg_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');
        $query .= ' ORDER BY msg_created DESC';

        $args = 0;
        $stmt = $this->cache->get($query);

        if($hasOwnerInfo)
            $stmt->addParameter(++$args, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
        if($hasAuthorInfo)
            $stmt->addParameter(++$args, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo);
        if($hasRecipientInfo)
            $stmt->addParameter(++$args, $recipientInfo instanceof UserInfo ? $recipientInfo->getId() : $recipientInfo);
        if($hasRepliesFor)
            $stmt->addParameter(++$args, $repliesFor instanceof MessageInfo ? $repliesFor->getId() : $repliesFor);
        if($hasReplyTo)
            $stmt->addParameter(++$args, $replyTo instanceof MessageInfo ? $replyTo->getReplyToId() : $replyTo);

        $stmt->execute();
        $result = $stmt->getResult();

        return $result->next() ? $result->getInteger(0) : 0;
    }

    public function getMessages(
        UserInfo|string|null $ownerInfo = null,
        UserInfo|string|null $authorInfo = null,
        UserInfo|string|null $recipientInfo = null,
        MessageInfo|string|null $repliesFor = null,
        MessageInfo|string|null $replyTo = null,
        ?bool $sent = null,
        ?bool $read = null,
        ?bool $deleted = null,
        ?Pagination $pagination = null
    ): iterable {
        $hasOwnerInfo = $ownerInfo !== null;
        $hasAuthorInfo = $authorInfo !== null;
        $hasRecipientInfo = $recipientInfo !== null;
        $hasRepliesFor = $repliesFor !== null;
        $hasReplyTo = $replyTo !== null;
        $hasSent = $sent !== null;
        $hasRead = $read !== null;
        $hasDeleted = $deleted !== null;
        $hasPagination = $pagination !== null;

        $args = 0;
        $query = 'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages';
        if($hasOwnerInfo) {
            ++$args;
            $query .= ' WHERE msg_owner_id = ?';
        }
        if($hasAuthorInfo)
            $query .= sprintf(' %s msg_author_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasRecipientInfo)
            $query .= sprintf(' %s msg_recipient_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasRepliesFor)
            $query .= sprintf(' %s msg_reply_to = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasReplyTo) {
            $query .= sprintf(' %s msg_id = ', ++$args > 1 ? 'AND' : 'WHERE');

            if($replyTo instanceof MessageInfo)
                $query .= '?';
            else
                $query .= '(SELECT reply_to FROM msz_messages WHERE msg_id = ?)';
        }
        if($hasSent)
            $query .= sprintf(' %s msg_sent %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $sent ? 'IS NOT' : 'IS');
        if($hasRead)
            $query .= sprintf(' %s msg_read %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $read ? 'IS NOT' : 'IS');
        if($hasDeleted)
            $query .= sprintf(' %s msg_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');
        $query .= ' ORDER BY msg_created DESC';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';

        $args = 0;
        $stmt = $this->cache->get($query);

        if($hasOwnerInfo)
            $stmt->addParameter(++$args, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
        if($hasAuthorInfo)
            $stmt->addParameter(++$args, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo);
        if($hasRecipientInfo)
            $stmt->addParameter(++$args, $recipientInfo instanceof UserInfo ? $recipientInfo->getId() : $recipientInfo);
        if($hasRepliesFor)
            $stmt->addParameter(++$args, $repliesFor instanceof MessageInfo ? $repliesFor->getId() : $repliesFor);
        if($hasReplyTo)
            $stmt->addParameter(++$args, $replyTo instanceof MessageInfo ? $replyTo->getReplyToId() : $replyTo);
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }

        $stmt->execute();

        return $stmt->getResult()->getIterator(MessageInfo::fromResult(...));
    }

    public function getMessageInfo(
        UserInfo|string $ownerInfo,
        MessageInfo|string $messageInfoOrId,
        bool $useReplyTo = false
    ): MessageInfo {
        $stmt = $this->cache->get(sprintf(
            'SELECT msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, UNIX_TIMESTAMP(msg_created), UNIX_TIMESTAMP(msg_sent), UNIX_TIMESTAMP(msg_read), UNIX_TIMESTAMP(msg_deleted) FROM msz_messages WHERE msg_id = %s AND msg_owner_id = ?',
            !$useReplyTo || $messageInfoOrId instanceof MessageInfo ? '?' : '(SELECT msg_reply_to FROM msz_messages WHERE msg_id = ?)'
        ));

        if($messageInfoOrId instanceof MessageInfo)
            $stmt->addParameter(1, $useReplyTo ? $messageInfoOrId->getReplyToId() : $messageInfoOrId->getId());
        else
            $stmt->addParameter(1, $messageInfoOrId);
        $stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('Message not found.');

        return MessageInfo::fromResult($result);
    }

    public function createMessage(
        string $messageId,
        UserInfo|string $ownerInfo,
        UserInfo|string|null $authorInfo,
        UserInfo|string|null $recipientInfo,
        string $title,
        string $body,
        int $parser,
        MessageInfo|string|null $replyTo = null,
        DateTime|int|null $sentAt = null,
        DateTime|int|null $readAt = null
    ): MessageInfo {
        $stmt = $this->cache->get('INSERT INTO msz_messages (msg_id, msg_owner_id, msg_author_id, msg_recipient_id, msg_reply_to, msg_title, msg_body, msg_parser, msg_sent, msg_read) VALUES (?, ?, ?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?))');
        $stmt->addParameter(1, $messageId);
        $stmt->addParameter(2, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
        $stmt->addParameter(3, $authorInfo instanceof UserInfo ? $authorInfo->getId() : $authorInfo);
        $stmt->addParameter(4, $recipientInfo instanceof UserInfo ? $recipientInfo->getId() : $recipientInfo);
        $stmt->addParameter(5, $replyTo instanceof MessageInfo ? $replyTo->getId() : $replyTo);
        $stmt->addParameter(6, $title);
        $stmt->addParameter(7, $body);
        $stmt->addParameter(8, $parser);
        $stmt->addParameter(9, $sentAt instanceof DateTime ? $sentAt->getUnixTimeSeconds() : $sentAt);
        $stmt->addParameter(10, $readAt instanceof DateTime ? $readAt->getUnixTimeSeconds() : $readAt);
        $stmt->execute();

        return $this->getMessageInfo($ownerInfo, $messageId);
    }

    public function updateMessage(
        UserInfo|string|null $ownerInfo = null,
        MessageInfo|string|null $messageInfo = null,
        ?string $title = null,
        ?string $body = null,
        ?int $parser = null,
        DateTime|int|null|false $sentAt = false,
        DateTime|int|null|false $readAt = false
    ): void {
        $setQuery = [];
        $setValues = [];
        $whereQuery = [];
        $whereValues = [];

        if($ownerInfo !== null) {
            $whereQuery[] = 'msg_owner_id = ?';
            $whereValues[] = $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo;
        }

        if($messageInfo !== null) {
            $whereQuery[] = 'msg_id = ?';
            $whereValues[] = $messageInfo instanceof MessageInfo ? $messageInfo->getId() : $messageInfo;
        }

        if($title !== null) {
            $setQuery[] = 'msg_title = ?';
            $setValues[] = $title;
        }

        if($body !== null) {
            $setQuery[] = 'msg_body = ?';
            $setValues[] = $body;
        }

        if($parser !== null) {
            $setQuery[] = 'msg_parser = ?';
            $setValues[] = $parser;
        }

        if($sentAt !== false) {
            $setQuery[] = 'msg_sent = FROM_UNIXTIME(?)';
            $setValues[] = $sentAt instanceof DateTime ? $sentAt->getUnixTimeSeconds() : $sentAt;
        }

        if($readAt !== false) {
            $setQuery[] = 'msg_read = FROM_UNIXTIME(?)';
            $setValues[] = $readAt instanceof DateTime ? $readAt->getUnixTimeSeconds() : $readAt;
        }

        if(empty($whereQuery))
            throw new InvalidArgumentException('$ownerInfo or $messageInfo must be specified.');
        if(empty($setQuery))
            return;

        $args = 0;
        $stmt = $this->cache->get(sprintf(
            'UPDATE msz_messages SET %s WHERE %s',
            implode(', ', $setQuery),
            implode(' AND ', $whereQuery)
        ));

        foreach($setValues as $value)
            $stmt->addParameter(++$args, $value);
        foreach($whereValues as $value)
            $stmt->addParameter(++$args, $value);

        $stmt->execute();
    }

    public function deleteMessages(
        UserInfo|string|null $ownerInfo,
        MessageInfo|array|string|null $messageInfos
    ): void {
        $hasOwnerInfo = $ownerInfo !== null;
        $hasMessageInfos = $messageInfos !== null;

        $query = 'UPDATE msz_messages SET msg_deleted = NOW() WHERE msg_deleted IS NULL';
        if($hasOwnerInfo)
            $query .= ' AND msg_owner_id = ?';
        if($hasMessageInfos) {
            if(!is_array($messageInfos))
                $messageInfos = [$messageInfos];

            $query .= sprintf(
                ' AND msg_id IN (%s)',
                DbTools::prepareListString($messageInfos)
            );
        }

        $args = 0;
        $stmt = $this->cache->get($query);

        if($hasOwnerInfo)
            $stmt->addParameter(++$args, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
        if($hasMessageInfos)
            foreach($messageInfos as $messageInfo) {
                if(is_string($messageInfo))
                    $stmt->addParameter(++$args, $messageInfo);
                elseif($messageInfo instanceof MessageInfo)
                    $stmt->addParameter(++$args, $messageInfo->getId());
                else
                    throw new InvalidArgumentException('$messageInfos must be an array of strings or MessageInfo instances.');
            }

        $stmt->execute();
    }

    public function restoreMessages(
        UserInfo|string|null $ownerInfo,
        MessageInfo|array|string|null $messageInfos
    ): void {
        $hasOwnerInfo = $ownerInfo !== null;
        $hasMessageInfos = $messageInfos !== null;

        $query = 'UPDATE msz_messages SET msg_deleted = NULL WHERE msg_deleted IS NOT NULL';
        if($hasOwnerInfo)
            $query .= ' AND msg_owner_id = ?';
        if($hasMessageInfos) {
            if(!is_array($messageInfos))
                $messageInfos = [$messageInfos];

            $query .= sprintf(
                ' AND msg_id IN (%s)',
                DbTools::prepareListString($messageInfos)
            );
        }

        $args = 0;
        $stmt = $this->cache->get($query);

        if($hasOwnerInfo)
            $stmt->addParameter(++$args, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
        if($hasMessageInfos)
            foreach($messageInfos as $messageInfo) {
                if(is_string($messageInfo))
                    $stmt->addParameter(++$args, $messageInfo);
                elseif($messageInfo instanceof MessageInfo)
                    $stmt->addParameter(++$args, $messageInfo->getId());
                else
                    throw new InvalidArgumentException('$messageInfos must be an array of strings or MessageInfo instances.');
            }

        $stmt->execute();
    }

    public function nukeMessages(
        UserInfo|string|null $ownerInfo,
        MessageInfo|array|string|null $messageInfos
    ): void {
        $hasOwnerInfo = $ownerInfo !== null;
        $hasMessageInfos = $messageInfos !== null;

        $query = 'DELETE FROM msz_messages WHERE msg_deleted IS NOT NULL';
        if($hasOwnerInfo)
            $query .= ' AND msg_owner_id = ?';
        if($hasMessageInfos) {
            if(!is_array($messageInfos))
                $messageInfos = [$messageInfos];

            $query .= sprintf(
                ' AND msg_id IN (%s)',
                DbTools::prepareListString($messageInfos)
            );
        }

        $args = 0;
        $stmt = $this->cache->get($query);

        if($hasOwnerInfo)
            $stmt->addParameter(++$args, $ownerInfo instanceof UserInfo ? $ownerInfo->getId() : $ownerInfo);
        if($hasMessageInfos)
            foreach($messageInfos as $messageInfo) {
                if(is_string($messageInfo))
                    $stmt->addParameter(++$args, $messageInfo);
                elseif($messageInfo instanceof MessageInfo)
                    $stmt->addParameter(++$args, $messageInfo->getId());
                else
                    throw new InvalidArgumentException('$messageInfos must be an array of strings or MessageInfo instances.');
            }

        $stmt->execute();
    }
}
